본문 바로가기
programming/python

[PY] 객체지향 - 상속 (inheritance)

by AteN 2022. 11. 24.

OOP에 대한 설명은 상속, 캡슐화, 다양성과 같은 다양한 용어로 설명하고 있으며, 기본적인 설명을 필요하다 

 

상속

함수를 정의하고 여러 곳에서 호출하게 만들면 소스 코드를 복사해서 붙여 넣기 하는 수고들 피할 수 있다. 함수와 마찬가지로 상속 (inheritance)은 클래스에 적용할 수 있는 코드 재사용 기법이다. 즉, 클래스 들을 부모-자식(parent-chlid) 관계로 만들어 자식 클래스가 부모 클래스의 매소드 사본을 상속 받는 방식으로  여러 클래스들에 걸쳐 매소드를 복제하지 않아도 된다.

프로그램에 추가된 상속된 클래스들이 이루는 거미줄처럼 얽힌 관계는 복잡성을 가중시키기 때문에, 상속은 과대평가나 위험하다고 생각하는 프로그래머도 많다. 확실히 상속은 남용할 여지가 많지만 상속 기겁을 제한적으로 사용하면 코드를 구성할 때 상당한 시간을 절약할 수 있다. 

 

1 class ParentClass:
2     def printHello(self):
        print('Hello, world!')

3 class ChildClass(ParentClass):
    def someNewMethod(self):
        print('ParentClass objects don't have this method.')

4 class GrandchildClass(ChildClass):
    def anotherNewMethod(self):
        print('Only GrandchildClass objects have this method.')

print('Create a ParentClass object and call its methods:')
parent = ParentClass()
parent.printHello()

print('Create a ChildClass object and call its methods:')
child = ChildClass()
child.printHello()
child.someNewMethod()

print('Create a GrandchildClass object and call its methods:')
grandchild = GrandchildClass()
grandchild.printHello()
grandchild.someNewMethod()
grandchild.anotherNewMethod()

print('An error:')
Create a ParentClass object and call its methods:
Hello, world!
Create a ChildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Create a GrandchildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Only GrandchildClass objects have this method.
An error:
Traceback (most recent call last):
  File "inheritanceExample.py", line 35, in <module>
    parent.someNewMethod() # ParentClass objects don't have this method.
AttributeError: 'ParentClass' object has no attribute 'someNewMethod'

이렇게 1. ParentClass, 2. ChildClass, 4. GrandChildClass라는 클래스 3개를 만들었다. 

ChildClass는 ParentClass의 하위 클래스 (subclass)로, ParentClass와 동일한 메소드를 모두 가지게 된다. 이것을 일컬어 ChildClass가 ParentClass로부터 메소드를 상속받았다고 말한다. GrandChildClass 역시 ChildClass의 하위클래스이므로, 따라서 ChildClass와 그 부모인 ParentClass의 모든 메소드를 똑같이 가지게 된다. 

여기서 자식 클래스는 하위 클래스 (subclass) 또는 파생 클래스 (derived class)라고 부르며, 부모 클래스는 상위 클래스 (super class) 또는 기반 클래스(base class)라고 한다.

 

이 기법을 사용하면 printHello() 메소드의 코드를 ChildClass와 GrandChildClass에 사실상 복사해서 붙여 넣은 것과 유사한 효과를 얻는다. 하지만  printHello()  코드를 변경하면  ChildClass와 GrandChildClass까지 갱신한다. 이는 함수 내부에서 코드를 변경하면 함수 호출 부분도 모두 갱신되는 상황과 같다. 이는 어느 클래스나 항상 기반 클래스는 알지만 하위 클래스는 알지 못한다는 사실을 반영한다. 

일반적으로 부모-자식 클래스는 'is a' 관계라고 부른다. ChildClass 객체는 ParentClass 객체에 대해  'is a' 관계 관계다.  ChildClass 객체는 ParentClass 가 가진 모든 메소드를 가지고, 자신이 정의한 메소드를 추가로 포함하기 때문이다. 이 관계는 일방향성이라서 ParentClass객체는 ChildClass 객체에 대해서 'is a' 관계가 아니다. ParentClass 객체가 ChildClass 객체에만 정의되어 있는 someNewMethod()를 호출할려고 하면 파이썬은 AttributeError를 발생시킨다. 

 

메소드 오버라이드

자식 클래스는 부모클래스의 모든 메소드를 상속받는다. 그러나 자식 클래스는 자체 코드로 구현한 자체 메소드를 제공함으로써 상속된 메소드를 오버라이드(override)할 수 있다. 자식 클래스에서 오버라이드한 메소드는 부모 클래스 메소드와 이름이 같다. 

class TTTBoard:
    def getBoardStr(board):
        """Return a text-representation of the board."""
        return f'''
          {board['1']}|{board['2']}|{board['3']}  1 2 3
          -+-+-
          {board['4']}|{board['5']}|{board['6']}  4 5 6
          -+-+-
          {board['7']}|{board['8']}|{board['9']}  7 8 9'''
class MiniBoard(TTTBoard):
    def getBoardStr(self):
        """Return a tiny text-representation of the board."""
        # Change blank spaces to a '.'
        for space in ALL_SPACES:
            if self._spaces[space] == BLANK:
                self._spaces[space] = '.'

        boardStr = f'''
          {self._spaces['1']}{self._spaces['2']}{self._spaces['3']} 123
          {self._spaces['4']}{self._spaces['5']}{self._spaces['6']} 456
          {self._spaces['7']}{self._spaces['8']}{self._spaces['9']} 789'''

        # Change '.' back to blank spaces.
        for space in ALL_SPACES:
            if self._spaces[space] == '.':
                self._spaces[space] = BLANK
        return boardStr

TTTBoard 클래스의 getBoradStr(0 메소드와 마찬가지로 MiniBoard의 getBoardStr() 메소드는 print() 함수로 전달하기 위한 틱택토 말판의 다중해 문자열을 만든다. 그러나 이 문자열은 x와 0 표시 사이의 선을 무시하고 마침표를 사용하여 공백 한 칸을 나타내기 때문에 크기가 휠씬 작다. 

다음과 같이 오버라이드하여 이름이 같은 함수를 사용하고 MiniBoard()와 TTTBoard()에 대한 과정을 전달할 수있을 것이다. 하지만 상속을 하지 안으면 매스도 내부에 if-else 문이 폭발적으로 늘어나고 코드의 복잡성이 급격히 증가할 것이다. 하위 클래스를 사용하고 매소드를 오버라이드하는 방법으로, 사용 방식이 달라질 대만다 클래스를 분리함으로써 훨신 더 멋지게 구성할 수 있을 것이다. 

 

super() 함수 

자식 클래스의 오버라이드된 메소드는 부모 클래스의 메소드와 유사한 경우가 많다. 분명히 상속은 코드 재사용 기법이지만, 매소드를 오버라이드하면 자식 클래스의 매소드 구현부는 부모 클래스의 메소드 구현부와 중복된 부분이 많아질 수 잇다. 이런 중복 코드를 방직하기 위해, 내장 super() 함수를 통해 오버라이드된 메소드가 부모 클래스에 속한 원래의 메소드를 호출하게 만들 수 있다. 

class HintBoard(TTTBoard):
    def getBoardStr(self):
        """Return a text-representation of the board with hints."""
1         boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard.

        xCanWin = False
        oCanWin = False
2         originalSpaces = self._spaces # Backup _spaces.
        for space in ALL_SPACES: # Check each space:
            # Simulate X moving on this space:
            self._spaces = copy.copy(originalSpaces)
            if self._spaces[space] == BLANK:
                self._spaces[space] = X
            if self.isWinner(X):
                xCanWin = True
            # Simulate O moving on this space:
3             self._spaces = copy.copy(originalSpaces)
            if self._spaces[space] == BLANK:
                self._spaces[space] = O
            if self.isWinner(O):
                oCanWin = True
        if xCanWin:
            boardStr += '\nX can win in one more move.'
        if oCanWin:
            boardStr += '\nO can win in one more move.'
        self._spaces = originalSpaces
        return boardStr

모든 오버라이드된 메소드가 super()를 사용할 필요는 없다. 자식 클래스에서 오버라이드하는 메소드가 부모 클래스에서 오버라이드된 메소드와 완전히 다른 행동을 한다면 super()를 사용해 오버라이된 메소드를 부를 필요는 없다. super() 함수는 다중 상속. 즉, 어떤 클래스가 둘 이상의 부모 메소드를 가질 대 더 유용한다. 

 

다중 상속

대다수 프로그래밍 언어에서는 클래스가 기껏해야 하나의 부모 클래스만 갖도록 제한을 받는다. 파이썬에서는 다중 상속 (multi ingeritance)이라는 기능을 제공해서 여러 부모 클래스를 지원한다. 예를 들어 flyInTheAir() 매소드가 있는 Airplane 클래스와 floatOnWater() 메소드가 있는 Ship 클래스가 있다고 가정해보자. 그러면 Class 문에 쉼표로 구분해서 열거하는 방식으로 Airplane, Ship 모두 상속받는 FlyingBoat 클래스를 만들 수 있다 

class Airplane:
    def flyInTheAir(self):
        print('Flying...')

class Ship:
    def floatOnWater(self):
        print('Floating...')

class FlyingBoat(Airplane, Ship):
    pass
>>> from flyingboat import *
>>> seaDuck = FlyingBoat()
>>> seaDuck.flyInTheAir()
Flying...
>>> seaDuck.floatOnWater()
Floating...

부모 클래스의 메소드 이름이 뚜렷하게 다르고 겹치지 않는 이상 다중 상속은 직관적이다. 이러한 종류의 클래스를 믹스인(mixin)이라고 한다. 

그러나 메소드 이름을 공유하는 여러 복잡한 클래스에 사용할 경우는 어떤일이 벌어질까?

class HybridBoard(HintBoard, MiniBoard):
    pass

다음과 같이 부모클래스인 MiniBoard와 HintBoard 모두 getBoardStr()라는 메소드를 가지고 있다고 가정한다. 

gameBoard = HybridBoard()

이때 HybridBoard는 어떤 부모 클래스를 상속 받을 까? 이때 분명히 상속을 받아 두가지 모두를 수행하는 것처러 실행 될 수 있다 

이런 일이 일어나는 이유는 파이썬의 메소드 결정 순서 (Method Resolution Order, MRO)와 super() 함수가 실제로 어떻게 작동하는지 이해해야 한다 

 

메소드 결정 순서 

아래의 그림과 같이 TTTBoard, HintBoard, MiniBoard는 정의된 getBoardStr() 메소드를 , HybridBoard는 상속된 getBoardStr() 메소드를 가지고 있다. 

 

>>> from tictactoe_oop import *
>>> HybridBoard.mro()
[<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>]

mro() 메소드의 반환값을 살펴보면 HybridBoard에서 메소드가 호출되는 경우 파이썬 먼저 HybridBoardmㄹ래스에서 해당 메소드를 확인한다는 사실을 알 수 있다. 해당 메소드가 없을 경우, 파이썬은 HintBoard 클래스, 그런 다음 MiniBoard, 마지막으로 TTTBoard 클래스를 확인한다. 모든 MRO 리스트의 끝에는 파이썬에서 모든 클래스의 상위 클래스인 내장 Object 클래스가 존재한다 

 

단일 상속의 경우 MRO를 결정하기는 쉽다. 부모 클래스의 연쇄만 만들면 된다. 다중 상속의 경우는 더 까다롭다. 파이선의 MRO C3 알고리즘(상속에서 메소드 결정 순서를 제공하는 것을 목표로 하는 알고리즘)을 따르는데, 다은과 같은 두가지 규약을 기억하면 된다.

  • 파이썬은 부모 클래스에 앞서 자식 클래스 먼저 확인한다
  • 파이썬은 Class 문의 왼쪽에서 오른쪽의 순서로 상속된 클래스를 확인한다. 

이처럼 다중 삭속은 적은 양의 코드가 많은 기능을 하게 도와주지만, 지나치게 복잡하고 이해하기 어려운 코드를 만들 가능성이 높다. 단일 상속이나 믹스인 클래스, 아니면 아예 상속을 쓰지 않는 편이 낫다. 이런 기법을 통해 크로그램의 수준이 월등히 높이지기도 한다. 

 

캡슐화 

캡슐화 (encapsulation)라는 단어는 일반적이만 연관이 있는 두가지 정의로 설명된다. 첫번째는 정의는 연관된 데이터와 코드를 하나의 단위로 묶는 것이다. 캡슐화란 한 상자에 포장하는 것을 의미한다. 이는 본직적으로 클래스가 하는 일이다. 클래스는 관련된 속성과 매서드를 결합한다. 예를 들어 WizCoin 클래스는 각 Knuts, sickles, galleons을 나타내기 위한 정수 세 걔를 단일 WizCoin 객체에 캡슐화한다 

두 번째 정의는 객체가 어떻게 작동하는지에 대한 복잡한 구현 세부사항을 감추는 정보 은닉 (information hiding) 기술이라는 것이다. 함수는 블랙박스처럼 작용한다. 예를 들어 math.sprt() 함수가 숫자의 제급근을 계산하는 방법은 숨겨져 있다. 프로그래머는 그저 함수가 자신에게 전달된 숫자의 제곱근을 반환한다는 정도만 알면된다.

 

다형성

다형성(polymorphism)을 통해 어떤 타입의 객체를 다른 타입의 객체로 취급할 수 있다. 에를 들어 len() 함수는 전달된 인수의 길이를 반환한다. len()에 문자열을 넘겨서 문자가 몇 개인지 알 수 있지만, len()에 리스트나 딕셔너리를 넘겨서 각각 몇 개의 아이템이나 키-값 쌍을 갖는지도 알 수 있다. 이런 형태의 다형성은 여러 타입의 객체를 다룰 수 있기 때문에 제네릭 함수(generic function) 또는 파라미터 다형성이라고한다.

다형성은 애드훅 다형성(ad hoc polymorphism) 또는 연산자 오버로드 (operator overload)를 의미하기도 하는데, 여기서 (+나 * 같은) 연산자는 연산 대상이 되는 객체의 타입의 따라 행위 (behavior)가 달라질 수 있다. 에를 들어 + 연산자의 경우, 두 개의 정수나 부동소수 값에 대해 연산할 때는 수학적인 덧셈을 하지만, 두 개의 문자열에 대해 연산할 때는 문자열 연결을 수행한다. 

댓글