[ Python ] OOP(6): 상속

#1 상속이란?

부모클래스가 자식 클래스에게 속성을 넘겨주는 것이다. 새로운 클래스를 만들 때 기존의 클래스가 가진 속성들을 그대로 사용할 수 있다. 기존의 클래스를 부모클래스, 새로운 클래스는 자식클래스이다. 상위클래스와 하위클래스라고도 한다.

#2 상속 문법

자식 클래스 정의하기

새로운 클래스를 정의할 때 기존 클래스를 상속받을 수 있다. 새로운 클래스(자식 클래스)의 이름 옆에 부모 클래스의 이름을 기재한다.

# 부모클래스
class SuperClass:
    def method_super(self):
        print("Super method")

# 자식클래스: 괄호로 상속받을 클래스 이름 추가
class SubClass(SuperClass):
    pass

자식 객체 사용하기

자식 객체를 생성하면 자신의 메서드 뿐만 아니라 부모의 메서드도 호출할 수 있다.

i = SubClass()

i.method_super()  # 자식객체는 부모 클래스의 메서드 호출 가능

자식객체와 부모클래스의 관계

자식객체는 자식 클래스 뿐만 아니라 부모클래스의 자료형이기도 하다.

print(
    isinstance(i, SubClass), isinstance(i, SuperClass)
)  # 자식객체는 자식클래스는 물론, 부모클래스의 자료형이기도하다
print(issubclass(SubClass, SuperClass), issubclass(SuperClass, SubClass))

전체 코드

위의 내용을 전부 합친 코드는 다음과 같다.

# 부모클래스
class SuperClass:
    def method_super(self):
        print("Super method")

# 자식클래스: 괄호로 상속받을 클래스 이름 추가
class SubClass(SuperClass):
    pass

i = SubClass()

i.method_super()  # 자식객체는 부모 클래스의 메서드 호출 가능

print(
    isinstance(i, SubClass), isinstance(i, SuperClass)
)  # 자식객체는 자식클래스는 물론, 부모클래스의 자료형이기도하다
print(issubclass(SubClass, SuperClass), issubclass(SuperClass, SubClass))

#3 상속을 사용하는 이유

코드의 중복을 줄이기 위함이다. 여러 클래스가 존재할 때 공통되는 속성을 묶어서 추상화된 클래스를 선언하고 자식 클래스가 부모 클래스를 상속하면 별도로 함수를 정의할 필요 없이 부모 클래스의 메서드 등을 사용할 수 있다.

class Animal:
    def walk(self):
        print("걸어간다.")

class Duck(Animal):
    def speak(self):
        print("꽥꽥")

class Dog(Animal):
    def speak(self):
        print("멍멍")

duck1 = Duck()
dog1 = Dog()

duck1.walk()  # 걸어간다.
dog1.walk()  # 걸어간다.

예시에서는 DuckDogAnimal을 상속한다. 따라서 각각의 객체는 walk()를 정의하지 않고도 호출해서 사용할 수 있다.

#4 클래스들 간의 관계

is-a 관계(상속관계)

클래스가 다른 클래스를 문법적으로 상속하면 상속관계다.

class Animal:
    def walk(self):
        print("걸어간다.")

class Duck(Animal):
    def speak(self):
        print("꽥꽥")

class Dog(Animal):
    def speak(self):
        print("멍멍")

duck1 = Duck()
dog1 = Dog()

duck1.walk()  # 걸어간다.
dog1.walk()  # 걸어간다.

has-a 관계(구성관계)

클래스가 다른 클래스를 상속하지는 않지만 내부적으로 다른 클래스의 인스턴스를 생성해서 소유할 때 구성관계라고 한다.

class Student:
    def study(self):
        print("공부합니다.")

class Teacher:
    def teach(self):
        print("가르칩니다.")

# 구성관계: 다른 클래스의 객체들을 인스턴스 내부에서 소유한다
class Lesson:
    def __init__(self):
        self.teacher = Teacher()
        self.student = Student()

    def run(self):
        self.teacher.teach()
        self.student.study()

l = Lesson()
l.run()

has-a 관계(집합관계)

상속관계도 아니고 구성관계도 아니지만 외부에서 생성된 인스턴스를 받아서 사용하는 관계를 조합관계라고한다.

class Student:
    def study(self):
        print("공부합니다.")

class Teacher:
    def teach(self):
        print("가르칩니다.")

# 집합관계: 다른 클래스의 객체들을 외부에서 받는다
class Lesson:
    def __init__(self, teacher, student):
        self.teacher = teacher
        self.student = student

    def run(self):
        self.teacher.teach()
        self.student.study()

teacher = Teacher()
student = Student()

l = Lesson(teacher, student)
l.run()

#5 초기화시 주의사항

부모 클래스의 인스턴스 사용법

자식 클래스의 초기화 메서드에서 부모클래스의 초기화 메서드를 호출하지 않는다면 부모클래스의 인스턴스 변수를 사용할 수 없다.

class SuperClass:

    # 클래스변수
    super_cls_var = "A class variable of SuperClass"

    def __init__(self):
        self.super_inst_var = "An instance variable of SuperClass"

    def do_super(self):
        print(
            "In SuperClass :", SuperClass.super_cls_var
        )  # 클래스명으로 클래스변수에 접근
        print("In SuperClass :", self.super_inst_var)  # 인스턴스변수에 접근

class SubClass(SuperClass):
    def __init__(self):
        # 부모 클래스 이름 사용
        # self를 넣어줘야 합니다.
        SuperClass.__init__(
            self
        )  # 부모클래스의 클래스변수와 인스턴스 변수에 접근하려면 부모클래스의 인스턴스도 초기화해야함

    def do_sub(self):
        print("In SubClass :", SuperClass.super_cls_var)
        print("In SubClass :", self.super_inst_var)
        self.do_super()
        SuperClass.do_super(self)

i = SubClass()

i.do_super()
i.do_sub()

super()

상속관계에서 자식 클래스가 부모클래스의 초기화 메서드 등 부모클래스를 호출해야할 때 이름을 직접 입력하기보다는 super()를 사용하는 것이 편리하다.

class SuperClass:
    def __init__(self):
        self.super_inst_var = "An instance variable of SuperClass"

    def do_super(self):
        print("I'm super.")

class SubClass(SuperClass):
    def __init__(self):
        # 부모 클래스 이름 대신에 super() 사용
        # self 불필요
        super().__init__()

    def do_sub(self):
        super().do_super()
        SuperClass.do_super(self)
        super().do_super()  # 뒤에서 배울 super()로 부모 클래스의 메써드 실행

s = SubClass()

s.do_sub()

#6 메서드 재정의

정의

자식클래스가 부모 클래스의 메서드의 내용을 변경하여 동일한 이름으로 정의한 것이다.

용도

코드의 재사용성을 높이기 위해서다. 기존 클래스에서 일부를 추가하거나 변경하면서도 기존 클래스에 영향을 주지 않는다.

문법

부모클래스에서 오버라이딩 할 메서드가 존재해야한다. 부모클래스에서 메서드를 구현할 수도 있지만 별개로 구현하지 않고 assert이나 raise를 통해 자식 클래스에게 구현해야한다는 것을 알릴 수도 있다.

class Shape:
    def __init__(self, x, y):
        # 도형의 중심
        self.x = x
        self.y = y

    # # 추상적인 상위 클래스에서는 구현할 수 없는 기능
    # # 자식 클래스들은 구현해야 한다는 안내
    def area(self):
        # raise로 예외를 발생
        # raise NotImplementedError(f"In {type(self)},  area() is not implemented")
        assert False, f"In {type(self)},  area() is not implemented"

    def print_area(self):
        print(f"넓이는 {self.area():.2f} 입니다.")

raise의 사용법이다.

raise NotImplementedError(f"In {type(self)},  area() is not implemented")

assert의 사용법이다.

assert False, f"In {type(self)},  area() is not implemented"

파이썬의 경우 스크립트 모드에서 실행시 -o 명령어를 추가하면 assert를 무시한다.

python -o file.py

자식 클래스는 오버라이딩할 메서드를 재정의해야한다.

class Box(Shape):
    def __init__(self, x, y, width, height):
        super().__init__(x, y)
        self.width = width
        self.height = height

        # 메서드 재정의
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, x, y, r):
        super().__init__(x, y)
        self.r = r

        # 메서드 재정의
    def area(self):
        return 3.14 * self.r**2

다형성

클래스의 다형성이란 같은 이름의 메서드나 연산자를 다른 클래스에서 다르게 구현한 것이다. 즉, 서로 다른 클래스에서 같은 이름의 메서드를 호출하더라도 그 결과가 서로 다르게 반환될 수 있도록 하는 것이다. 메서드 오버로딩은 다형성의 예다. 메서드 오버로딩과 추상 클래스, 인터페이스도 다형성의 예이다.

#7 다중 상속

하나의 클래스가 두개 이상의 클래스를 상속하는 것이다.

class A:
    def __init__(self):
        print("A")

class B:
    def __init__(self):
        print("B")

class C(A, B):
    def __init__(self):
        # super().__init__()
        # 부모 클래스 이름으로 직접 초기화할 경우 인수로 self를 넣어야함
        A.__init__(self)
        B.__init__(self)

        print("C")

c = C()

자식 클래스가 부모 클래스 각각에 접근하려면 super()보다는 부모클래스의 이름을 활용한다.

class C(A, B):
    def __init__(self):
        # super().__init__()
        # 부모 클래스 이름으로 직접 초기화할 경우 인수로 self를 넣어야함
        A.__init__(self)
        B.__init__(self)

        print("C")

다이아몬드 상속

자식 클래스가 다중 상속을 하고 다중 상속 된 부모클래스가 공통의 클래스를 상속하 때의 관계다.

class A:
    def __init__(self):
        print("A")

class B(A):
    def __init__(self):
        print("B in")
        super().__init__()  # D가 B와 C를 다중상속 -> mro에 따라 이번 행의 super()는 C를 가리킴
        print("B out")

class C(A):
    def __init__(self):
        print("C in")
        super().__init__()  # D가 B와 C를 다중상속 -> mro에 따라 이번 행의 super()는 A를 가리킴
        print("C out")

class D(B, C):
    def __init__(self):
        print("D in")
        super().__init__()
        print("D out")

print(
    D.mro()
)  # MRO 확인 - 다중상속의 경우 D가 부모클래스에서 메서드를 참조할 때 순서 출력
d = D()

다이아몬드 상속관계에서 자식 클래스가 메서드를 찾는 순서가 문제다. 우선순위는 MRO를 따른다.

#8 object 클래스

파이썬에서는 모든 클래스는 object 클래스의 하위클래스다.

# class MyClass(object):
class MyClass:
    pass

issubclass(MyClass, object)

__init__(), __str__() 모두 object 클래스에서 정의된 메서드로 다른 클래스는 이를 정의할 때 메서드 오버라이딩을 한 것이다.

dir(object)

#9 private 변수

name mangling

파이썬에는 자바나 C와 달리 접근제한자가 없다. 따라서 파이썬에서는 변수가 접근제한이 있다는 것을 표현할 때 관례상 name mangling을 한다.

# 클래스 A 선언하고
# 메서드: 말하기, 외로움 구현하기
class A:
    # Name mangling -> 클래스 외부에서는 사용할 수 없는 메서드이나 클래스 내부에서는 사용 가능
    def __speak(self):
        print("Don't call me!")

    def lonely(self):
        self.__speak()

class B(A):
    pass

a = A()
# a.__speak()  # 이름 사용 불가
# a.lonely()

b = B()
# b.__speak()  # 자식 클래스도 사용 불가

dir(
    a
)  # name mangling의 효과: 메서드의 이름을 변경해서 못 찾게 막아버림 -> 자바와 C 등은 private으로 가제로 못 찾게 하나 파이썬은 그런 문법이 없어서 비슷하게 구현한 것임

접근제한을 두려는 메서드 앞에 더블 언더스코어 하나를 둔다.

def __speak(self):
        pass

데이터 숨기기

데이터를 숨기고 싶은 경우가 있다.

class MyMind:
    def __init__(self):
        # 파이썬은 데이터를 숨기는 문법이 없어서 관례로 언더바 하나일 때 외부에서 접근 불가하다는 것을 약속함
        self._secret = "나는 산타를 믿어요"

    def get_secret(self):  # getter
        return self._secret

    def set_secret(self, new_secret):  # setter
        self._secret = new_secret
        # 주로 변화로 인한 2차적인 파급효과가 있는 경우에 사용

m = MyMind()

# m._secret = "헬로" # 문법적으로 할 수는 있으나 하지 말 것

변수의 경우 싱글 언더스코어 하나를 통해 표현한다.

self._secret = "나는 산타를 믿어요"

숨긴 변수로부터 데이터를 가져오거나 수정하려고 할 때 getter, setter를 사용한다. getter와 setter는 데코레이터를 통해 정의한다.

class MyMind:
    # 생성자
    def __init__(self):
        self._secret = "나는 산타를 믿어요"

    # 게터
    @property
    def secret(self):
        return self._secret

    # 세터
    @secret.setter
    def secret(self, new_secret):
        print("setter called")
        self._secret = new_secret

    # 게터와 세터를 통해 속성에 접근하는 것처럼 다룰 수 있음

m = MyMind()
print(m.secret)  # getter called
m.secret = "없음"  # setter called
print(m.secret)