21. 상속과 다형성




1 클래스 상속


1-1 클래스 상속과 이름 공간의 관계

  • 상속의 이유
    • 코드의 재사용
    • 상속받은 자식 클래스는 상속을 해준 부모 클래스의 모든 기능을 그대로 사용
    • 자식 클래스는 필요한 기능만을 정의하거나 기존의 기능을 변경(재정의, Override)할 수 있음
In [2]:
class Person:
    def __init__(self, name, phone=None):
        self.name = name
        self.phone = phone
        
    def __str__(self):
        return '<Person {0} {1}>'.format(self.name, self.phone)
In [15]:
class Employee(Person):                    # 괄호 안에 쓰여진 클래스는 슈퍼클래스를 의미한다.
    def __init__(self, name, phone, position, salary):
        Person.__init__(self, name, phone) # Person클래스의 생성자 호출 --> Unbound Method Call
        #super().__init__(name, phone)   # --> Bound Method Call
        self.position = position
        self.salary = salary
  • 이름 공간의 포함관계
    • 자식 클래스 > 부모 클래스

inheritance

In [16]:
p1 = Person('홍길동', 1498)
print(p1.name)
print(p1)

print()

m1 = Employee('손창희', 5564, '대리', 200)
m2 = Employee('김기동', 8546, '과장', 300)
print(m1.name, m1.position)  # 슈퍼클래스와 서브클래스의 멤버를 하나씩 출력한다.
print(m1)
print(m2.name, m2.position)
print(m2)
홍길동
<Person 홍길동 1498>

손창희 대리
<Person 손창희 5564>
김기동 과장
<Person 김기동 8546>

inheritance

1-2 생성자 호출

  • 서브 클래스의 생성자는 슈퍼 클래스의 생성자를 자동으로 호출하지 않는다.
In [5]:
class Super:
    def __init__(self):
        print('Super init called')

class Sub(Super):
    def __init__(self):
        print('Sub init called')
    
s = Sub()
Sub init called
  • 서브 클래스의 생성자에서 슈퍼 클래스의 생성자를 명시적으로 호출해야 한다.
In [19]:
class Super:
    def __init__(self):
        print('Super init called')
        
class Sub(Super):
    def __init__(self):
        Super.__init__(self)   # 명시적으로 슈퍼클래스의 생성자를 호출한다.
        #super().__init__() # 또는 왼쪽처럼 Bound 메소드 호출 
        print('Sub init called')
    
s = Sub()
Super init called
Sub init called
  • 서브 클래스에 생성자가 정의되어 있지 않은 경우에는 슈퍼 클래스의 생성자가 호출된다.
In [20]:
class Super:
    def __init__(self):
        print('Super init called')
        
class Sub(Super):
    pass

s = Sub()
Super init called

1-3 메쏘드의 대치 (메소드 오버라이드 - Override)

  • 서브 클래스에서 슈퍼 클래스에 정의된 메소드를 재정의하여 대치하는 기능
In [8]:
class Person:
    def __init__(self, name, phone=None):
        self.name = name
        self.phone = phone
        
    def __str__(self):
        return '<Person %s %s>' % (self.name, self.phone)
    
class Employee(Person):
    def __init__(self, name, phone, position, salary):
        Person.__init__(self, name, phone)
        self.position = position
        self.salary = salary
        
p1 = Person('gslee', 5284)
m1 = Employee('kslee', 5224, 'President', 500)

print(p1)
print(m1)
<Person gslee 5284>
<Person kslee 5224>
In [9]:
class Employee(Person):
    def __init__(self, name, phone, position, salary):
        Person.__init__(self, name, phone)
        self.position = position
        self.salary = salary
        
    def __str__(self):
        return '<Employee %s %s %s %s>' % (self.name, self.phone, self.position, self.salary)
    
p1 = Person('gslee', 5284)
m1 = Employee('kslee', 5224, 'President', 500)

print(p1)
print(m1)
<Person gslee 5284>
<Employee kslee 5224 President 500>

1-4 다형성(Polymorphism)

  • 상속 관계 내의 다른 클래스들의 인스턴스들이 같은 멤버 함수 호출에 대해 각각 다르게 반응하도록 하는 기능

    • 연산자 오버로딩도 다형성을 지원하는 중요한 기술
      • 예를 들어, a와 b의 객체 형에 따라 a + b의 + 연산자 행동 방식이 변경되는 것
  • 다형성의 장점

    • 적은 코딩으로 다양한 객체들에게 유사한 작업을 수행시킬 수 있음
    • 프로그램 작성 코드 량이 줄어든다.
    • 코드의 가독성을 높혀준다.
  • 파이썬에서 다형성의 장점

    • 형 선언이 없다는 점에서 파이썬에서는 다형성을 적용하기가 더욱 용이하다.
    • 실시간으로 객체의 형이 결정되므로 단 하나의 메소드에 의해 처리될 수 있는 객체의 종류에 제한이 없다.
      • 즉, 다른 언어보다 코드의 양이 더욱 줄어든다.
In [21]:
class Animal:
    def cry(self):
        print('...')
        
class Dog(Animal):
    def cry(self):
        print('멍멍')
        
class Duck(Animal):
    def cry(self):
        print('꽥꽥')
        
class Fish(Animal):
    pass

for each in (Dog(), Duck(), Fish()):
    each.cry()
멍멍
꽥꽥
...

2 내장 자료형과 클래스의 통일


  • 내장 자료형(list, dict, tuple, string)을 상속하여 사용자 클래스를 정의하는 것
    • 내장 자료형과 사용자 자료형의 차이를 없에고 통일된 관점으로 모든 객체를 다룰 수 있는 방안
  • 클래스 정의는 새로운 자료형의 정의임

2-1 리스트 서브 클래스 만들기

In [22]:
a = list()
print(a)
print(dir(a))
[]
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
  • 아래 예제는 내장 자료형인 list를 상속하여 뺄셈 연산(-)을 추가함
In [23]:
class MyList(list):
    def __sub__(self, other):   # '-' 연산자 중복 함수 정의
        for x in other:
            if x in self:
                self.remove(x)     # 각 항목을 하나씩 삭제한다.
        return self

L = MyList([1, 2, 3, 'spam', 4, 5])
print(L)
print()

L = L - ['spam', 4]
print(L)
[1, 2, 3, 'spam', 4, 5]

[1, 2, 3, 5]

1) Stack 클래스 정의 예

  • 슈퍼 클래스로 list 클래스를 지정
  • 즉, list 클래스를 확장하여 Stack 클래스를 정의함
In [24]:
class Stack(list):  # 클래스 정의
    push = list.append
    
s = Stack()         # 인스턴스 생성

s.push(4)
s.push(5)
print(s)
print()

s = Stack([1,2,3])
s.push(4)
s.push(5)
print(s)
print()

print(s.pop())       # 슈퍼 클래스인 리스트 클래스의 pop() 메소드 호출
print(s.pop())
print(s)
[4, 5]

[1, 2, 3, 4, 5]

5
4
[1, 2, 3]

2) Queue 클래스 정의 예

  • 슈퍼 클래스로 역시 list를 지닌다.
  • 즉, list 클래스를 확장하여 Queue 클래스를 정의함
In [25]:
class Queue(list):
    enqueue = list.append
    
    def dequeue(self):
        return self.pop(0)
    
q = Queue()
q.enqueue(1)      # 데이터 추가
q.enqueue(2)
print(q)

print(q.dequeue()) # 데이터 꺼내기
print(q.dequeue())
[1, 2]
1
2

2-2 사전 서브 클래스 만들기

In [26]:
a = dict()
print(a)
print(dir(a))
{}
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
  • 아래 예제는 keys() 메소드를 정렬된 키값 리스트를 반환하도록 재정의한다.
In [27]:
class MyDict(dict):
    def keys(self):
        K = list(dict.keys(self)) # 언바운드 메소드 호출 --> K = list(self.keys()) 라고 호출하면 무한 재귀 호출
        K.sort()
        return K

d = {'b':1, 'c':2, 'a':3}
print(d.keys())
print(list(d.keys()))
print()

d2 = MyDict({'b':1, 'c':2, 'a':3})
print(d2.keys())
dict_keys(['b', 'c', 'a'])
['b', 'c', 'a']

['a', 'b', 'c']

3 상속 관계에 있는 클래스들의 정보 획득


3-1 객체가 어떤 클래스에 속해 있는지 확인하기

In [31]:
print(int)
<class 'int'>
  • 객체의 자료형 비교 방법 I (전통적 방법)
In [29]:
print(type(123) == int)
print(type(123) == type(0))
a = 12345678
print(type(a) == type(0))
True
True
True
  • 객체의 자료형 비교 방법 II (새로운 방법)
    • isinstance() 내장 함수와 기본 객체 클래스 사용
In [30]:
print(isinstance(123, int))
True
  • 서브 클래스의 인스턴스는 슈퍼 클래스의 인스턴스이기도 하다.
In [33]:
class A:
    pass

class B:
    def f(self):
        pass
    
class C(B):
    pass

def check(obj):
    print(obj, '=>', end=" ")
    if isinstance(obj, A):
        print('A', end="")
    if isinstance(obj, B):
        print('B', end="")
    if isinstance(obj, C):
        print('C', end="")
    print()
    
a = A()
b = B()
c = C()

check(a)
check(b)
check(c)
<__main__.A object at 0x10c2f9c50> => A
<__main__.B object at 0x10c2dae48> => B
<__main__.C object at 0x10c2f9ac8> => BC

3-2 클래스 간의 상속 관계 알아내기

  • issubclass() 내장 함수 활용
In [34]:
class A:
    pass

class B:
    def f(self):
        pass
    
class C(B):
    pass

def check(obj):
    print(obj, '=>', end=" ")
    if issubclass(obj, A):
        print('A', end="")
    if issubclass(obj, B):
        print('B', end="")
    if issubclass(obj, C):
        print('C', end="")
    print()
    
check(A)
check(B)
check(C)
<class '__main__.A'> => A
<class '__main__.B'> => B
<class '__main__.C'> => BC

참고 문헌: 파이썬(열혈강의)(개정판 VER.2), 이강성, FreeLec, 2005년 8월 29일