Object-Oriented Programming - Inheritance

Contents

  • Inheritance
    • Method Resolution Order
  • The Super() Method

Inheritance

객체지향적 패턴을 사용하는 큰 이유중 하나는 바로 클래스의 상속이다. 자연에서 볼 수 있는 taxonomy 를 그대로 구현하여 사용자 입장에서도 직관적인 해석이 가능하며, 코드의 reusability 측면에서 진가를 발휘한다.

class Student:

    reduce_health = 0.9

    def __init__(self, first, last, health):
        self.first = first
        self.last = last
        self.health = health
        self.email = first + '.' + last + '@korea.ac.kr'

    def fullname(self):
        return f'{self.first} {self.last}'

    def allnighter(self):
        self.health = self.health * self.reduce_health
        return self.health

class Phd(Student):

    reduce_health = 0.8

stu_1 = Student('Tom', 'Lee', 100)
phd_1 = Phd('Ian', 'Goodfellow', 100)

>>> print(phd_1.health)
100
>>> phd_1.allnighter()
80.0

먼저 부모 클래스로 이전 예시에 쓰던 Student 클래스를 정의했다. Student 클래스는 크게 학생의 범주를 구현한 것이므로, 그 아래에 학부생, 석사과정생, 박사과정생 등으로 자식 클래스를 선언 할 수 있을 것이다. class Phd(Student) 는 박사과정생을 구현한 클래스로, 부모 클래스에선 볼 수 없었던 인자값을 받는다. 즉, 자식 클래스는 부모를 인자로 받음으로써 부모 클래스의 모든 메서드와 class variables, instance variables 를 상속받는다.

class Phd(Student) 클래스엔 __init__ 메서드도, 일반 메서드도 없이 reduce_health 라는 class variable 하나만 정의되어있다. 그럼에도 phd_1 = Phd('Ian', 'Goodfellow', 100) 라는 Phd 클래스로 인스턴스가 생성되며, 부모 클래스에 정의되어있는 메서드인 allnighter() 도 작동하는 것을 볼 수 있다. 하지만 이때 Phd 클래스와 Student 클래스의 reduce_health 값은 다르다. phd_1 인스턴스는 Phd 클래스로 선언된 인스턴스이기 때문에, reduce_health 인자는 Phd 의 값을 쓴다.

Method Resolution Order

위 예시에선 하나의 부모 클래스와 하나의 자식 클래스만 선언했기에 상속의 흐름을 이해하는데 어렵지 않다. 하지만 나중에 학생을 학부, 석.박으로, 단과대별로, 나이별로 자식 클래스를 선언한다면 어떨까? 코드의 가독성은 떨어지고 특정 인스턴스가 어디서 어떤 클래스로부터 특정 attribute 를 상속받았는지 파악하기 어려울 것이다. 이때 사용할 수 있는 파이썬의 builtin 함수 help() 를 사용하면 된다.

help(Phd) or help(phd_1):

help() 함수는 클래스, 혹은 인스턴스를 인자로 받으며, 해당 객체의 메서드와 attribute 들이 어떤 순서로 선언되는지 확인 할 수 있다. 이 순서를 Method Resolution Order 라고 하며, 위 예시의 phd_1 인스턴스는 Phd, Student, builtins.object 순서로 객체들이 선언되는 것을 볼 수 있다.

  • Data and other attributes defined here: 부분을 보면 Phd 클래스에서 먼저 정의된 attribute 을 볼 수 있으며, reduce_health=0.8 로 선언된 것을 볼 수 있다.

  • Methods inherited from Student: 를 보면 부모 클래스에서 어떤 메서드들을 상속받았는지도 볼 수 있다. 이때 재밌는 점은 __init__ 메서드도 상속받는 것을 볼 수 있다. 이말인즉슨, Phd 클래스에 __init__ 메서드가 없기 때문에 인스턴스 생성을 위해 부모 클래스의 생성 메서드를 사용한 것이다.
  • Data descriptors inherited from Student: 를 보면 부모 클래스에서 어떤 attribute 들이 선언되었는지 확인 할 수 있다. phd_1.__dict__ 를 출력해보면 부모 클래스에서 선언된 값들인 {'first': 'Ian', 'last': 'Goodfellow', 'health': 80.0, 'email': 'Ian.Goodfellow@korea.ac.kr'} 가 출력된다.

The Super() method

이전 예시에선 자식 클래스에 따로 __init__ 메서드가 없었어도 자동으로 부모 클래스의 __init__ 메서드를 불러와 인스턴스를 생성하였다. 하지만 이런 방식으로 자식 클래스의 인스턴스를 정의하다 보면 자식과 부모 클래스에서 상속받는 attribute 들이 꼬일 수 있다. 이럴 때 쓰는 메서드가 바로 super().__init__ 이다.

class Student:

    reduce_health = 0.9

    def __init__(self, first, last, health):
        self.first = first
        self.last = last
        self.health = health
        self.email = first + '.' + last + '@korea.ac.kr'

    def fullname(self):
        return f'{self.first} {self.last}'

    def allnighter(self):
        self.health = self.health * self.reduce_health
        return self.health

class Phd(Student):
    def __init__(self, first, last, health, prog_lang):
        super().__init__(first, last, health)
        self.prog_lang = prog_lang

>>> print(phd_1.prog_lang)
python

이 전 예시와 달리 자식 클래스인 Phd 에도 __init__ 메서드를 정의하였다. 이때 자식 클래스의 __init__ 메서드엔 부모 클래스에서 정의되는 모든 attribute 과 자식 클래스에서 추가적으로 정의하고픈 attribute 를 정의 한 뒤, 그 아래에 super().__init__ 메서드를 추가적으로 정의하여 부모 클래스에게서 상속받을 attribute 를 인자로 넘긴다.

super().__init__ 은 부모 클래스에서 상속받고싶은 인자들을 정의하는 메서드이다. 직접 부모 클래스의 이름으로 생성자 메서드를 호출 할 수 있지만 (ex. Student.__init__), 추후 여러개의 상속클래스를 정의한다면 반드시 super().__init__ 를 사용해야 한다.

class Student:

    reduce_health = 0.9

    def __init__(self, first, last, health):
        self.first = first
        self.last = last
        self.health = health
        self.email = first + '.' + last + '@korea.ac.kr'

    def fullname(self):
        return f'{self.first} {self.last}'

    def allnighter(self):
        self.health = self.health * self.reduce_health
        return self.health

class Phd(Student):
    def __init__(self, first, last, health, prog_lang):
        super().__init__(first, last, health)
        self.prog_lang = prog_lang

class Professor(Student):
    def __init__(self, first, last, health, students=None):
        super().__init__(first, last, health)
        if students is None:
            self.students = []
        else:
            self.students = students

    def add_student(self,stu):
        if stu not in self.students:
            self.students.append(stu)

    def remove_student(self,stu):
        if stu in self.students:
            self.students.remove(stu)

    def print_student(self):
        for stu in self.students:
            print(stu.fullname())

stu_1 = Student('Tom', 'Lee', 100)
phd_1 = Phd('Ian', 'Goodfellow', 100, 'python')
prof_1 = Professor('John', 'Doe', 1000,[stu_1, phd_1])
prof_1.remove_student(stu_1)
>>> prof_1.print_student()
Ian Goodfellow

위 예시는 부모 클래스 Student 와 자식 클래스 Phd, Professor 을 정의한 예시이다. Professor 클래스는 자신이 관리하는 학생들을 attribute 로 정의하고, 학생을 추가하는 메서드와 삭제하는 메서드, 그리고 출력하는 메서드가 있다. 이때 주목할 점은 바로 Professor 클래스의 student 인자이다.

student 인자는 학생 인스턴스를 인자로 받기 때문에 리스트로 정의된다. 이때 그냥 self.students = students 로 하지 않고 먼저 students=None 로 정의 한 뒤, if else 문으로 빈 리스트를 생성한다. 보기에는 필요 이상의 코드같지만, 클래스에서 mutable datatype (ex. 리스트) 를 절대로 기존 attribute 로 설정하면 안된다. 위와 같이 먼저 None 으로 설정 한 뒤, 상황에 맞게 자신을 리턴해줘야 한다.