ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 클래스 상속과 super()함수의 동작이해
    Python 2022. 1. 12. 15:51

    자바와 비교해보는 파이썬 클래스 상속

    super()에 대해서 삽질한 내용을 다뤘다.

     

    이전 포스팅에서 공부한 내용

    [Python] - [Python] 클래스 기초 문법

     

    클래스 선언

    생성자 __init__메서드(인스턴스 초기화)

    인스턴스 생성 __new__메서드

    인스턴스 메서드

    소멸자 __del__메서드

    클래스 변수

    static 메서드 vs class 메서드

    메서드 오버로딩


    이번 포스팅에서 공부한 내용

    클래스 상속

    상속 관계시 조상 클래스들의 메서드(생성자 메서드)호출

    super()

    method resolution order (MRO)


    클래스 상속

     

    파이썬은 다중 상속을 허용하는 언어이다.

    특정 클래스들을 상속하는 클래스 선언

    class 클래스명(상속받을 클래스1, 상속받을 클래스2,....):
    	pass

    파이썬의 최상위 클래스는 object 이다.

    모든 클래스는 object를 상속받기 때문에 object 클래스의 속성을 사용할 수 있다.

    # In Python 3.x "class Person" is 
    # equivalent to "class Person(object)"

     


    상속 관계시 생성자 호출

     

    이전 포스팅에서 파이썬의 생성자는 __init__형태의 인스턴스 메서드라는 것을 강조했다.

    또한 오버라이딩 관점으로 생각해야 했다.

    다중 상속시 메서드를 호출할 때 어떤 조상 클래스의 메서드가 호출될지 알아야 생성자 또한 쉽게 이해할 수 있다.

     

    자식 클래스에서 __init__()메서드를 정의하지 않으면 조상의 __init__()메서드가 호출된다.

     

    자식 클래스 미구현시

    class Parent:
    
        def __init__(self):
            print("조상 생성자")
            self.parentName = '김부모'
    
    
    class Child(Parent):
        pass
    
    
    child = Child()
    print(child.parentName)

    또는 자식 클래스 생성자 미구현시

    class Parent:
    
        def __init__(self):
            print("조상 생성자")
            self.parentName = '김부모'
    
    
    class Child(Parent):
    
        def test(self): # 빈 클래스를 만들지 않기 위해 메서드를 하나 정의해놓음
            pass
    
    
    child = Child()
    print(child.parentName)

    어떤 글과 영어 사이트에서는 파이썬이 default 생성자를 추가시켜준다고 써있는 글이 있는데 이는 틀린내용인 것 같다.

     

    파이썬이 자동으로 default 생성자를 추가시켜 준것이 아님을 확인해보자.

    class Parent:
    
        def __init__(self,name):
            print("조상 생성자")
            self.parentName = name
    
    
    class Child(Parent):
        pass
    
    
    child = Child('김부모')
    print(child.parentName)

    이전 포스팅에서 파이썬은 오버로딩이 존재하지 않음을 확인하였다. 이름이 같은 메서드는 시그니쳐가 어떻든 마지막에 선언된 것만 존재하는 것으로 취급되었다. 

    Child 인스턴스를 생성하면 __init__()메서드가 호출될텐데 바로 조상인 Parent에 __init__()메서드가 정의되어 있기 때문에 이 메서드가 사용되고 파라메터 또한 적절하기 때문에 문제가 없다.

     

    다음의 경우는 어떨까?

    class Parent:
        pass
    
    
    class Child(Parent):
        pass
    
    
    child = Child('김부모')

    인자를 취하면 안된다고 한다.

    이는 실제로 Child,Parent 클래스 모두 __init__()메서드가 정의되어 있지 않아 

    object클래스의 __init__()메서드가 호출되는데 빈 파라메터의 메서드이기 때문에 인자를 넣어 에러가 발생한 것이다.

     

    지금까지 Parent 클래스의 생성자 내부에서 object의 __init__메서드를 호출하지 않았다. 

    object 클래스의 생성자 메서드 실제로 아무것도 하지 않아도 호출하지 않아도 별 문제가 되지 않는다.

    그러나 호출하지 않으면 문제가 되는 경우가 있는데 밑에서 살펴본다.

     

    객체 지향에서는 자식 클래스에서 생성자 메서드를 오버라이딩 하였으면 조상 클래스의 생성자가 먼저 호출되도록 호출하여야 한다. 자바에서도 마찬가지로 생성자의 처음에 조상의 생성자를 호출하는 것과 마찬가지였다.

    파이썬에서는 조상 생성자 메서드 호출은 강제가 아니다. 아마 다중 상속의 문제 때문에 그런것 같다.

     

    다음의 경우 조상 생성자 메서드가 호출되지 않았다.

    class Parent:
    
        def __init__(self, name):
            print("조상 생성자")
            self.parentName = name
    
    
    class Child(Parent):
    
        def __init__(self, name):
            # 자바 관점으로는 조상 생성자 호출해야 하는데 없어도 파이썬은 에러가 없다.
            print('자식 생성자')
    
    
    child = Child('김부모')

    파이썬은 이렇게 해도 에러가 없다. (파이썬은 다중 상속 때문에 자바의 생성자와는 방식이 다르다.)

     

    이러한 경우 parentName을 사용할 경우 에러가 난다. 

     

    파이썬은  조상 생성자를 어떻게 호출할까?

    먼저 저번 포스팅에서 설명한 unbound 방식으로 호출해보자.

    class Parent:
    
        def __init__(self, name):
            print("조상 생성자")
            self.parentName = name
    
    
    class Child(Parent):
    
        def __init__(self, name):
            Parent.__init__(self, name)  # 조상 생성자 호출
            print('자식 생성자')
    
    
    child = Child('김부모')
    print(child.parentName)

    Parent 클래스의 이름이 바뀌어 버리면 Parent.__init__의 Parent를 수정해줘야 하는 문제가 있다.

     

    bound 방식으로 조상 클래스의 메서드를 호출하려면 super() 함수를 사용할 수 있다.

    super()함수를 이해하기 위해서 파이썬이 조상 메서드 호출하는 MRO와 다중상속을 알아야한다.

     

    다중상속이 되어있는 다음의 코드가 존재

    class Father:
    
        def __init__(self, name):
            print("아빠 생성자")
            self.father_name = name
    
    
    class Mother:
    
        def __init__(self, name):
            print("엄마 생성자")
            self.mother_name = name
    
    
    class Child(Father, Mother):
    
        def __init__(self, father_name, mother_name, child_name):
            print('자식 생성자')
            Father.__init__(self, father_name)
            Mother.__init__(self, mother_name)
            self.child_name = child_name
    
    
    child = Child('김아빠', '김엄마', '김자식')

    클래스 상속도가 매우 간단한 형태이다. 이렇게 명시적으로 unbound 형식으로 조상들의 생성자 메서드를 호출하는 것은 문제가 없다.

     

    다음의 경우 어떤 부모의 생성자가 호출될까?

    class Father:
    
        def __init__(self, name):
            print("아빠 생성자")
            self.father_name = name
    
    
    class Mother:
    
        def __init__(self, name):
            print("엄마 생성자")
            self.mother_name = name
    
    
    class Child(Father, Mother):
        pass
    
    
    child = Child('누구의 __init__이 호출될까??')

    파이썬은 다중상속을 허용한다. 

    이러한 경우 어느 조상 클래스의 속성을 호출할 것인지에 대해 정해진 규칙이 method resolution order(MRO) 이다.

    현재 Child 클래스에서 상속 클래스를 정의할 때 Father를 먼저 선언하여 이에 맞게 MRO가 정해진다.

     

    파이썬2와 파이썬3의 MRO 는 다른 알고리즘을 사용한다.

    https://www.geeksforgeeks.org/method-resolution-order-in-python-inheritance/

     

    이는 클래스.mro()를 통해 확인해 볼 수 있다.

    print(Child.mro())

    앞서 나온 클래스에 속성이 존재하지 않으면 바로 다음 클래스에서 속성을 찾는다. object 클래스까지 가서 찾다가 속성이 없으면 에러가 나게된다.

     

    아래는 조금 복잡한 클래스 상속도에서 M 클래스의 MRO를 상속 순서를 변경하여 MRO를 출력한 코드이다.

    https://www.programiz.com/python-programming/multiple-inheritance

    class X:
        pass
    
    
    class Y:
        pass
    
    
    class Z:
        pass
    
    
    class A(X, Y):
        pass
    
    
    class B(Y, Z):
        pass
    
    
    class M1(B, A, Z):
        pass
    
    
    class M2(A, B, Z):
        pass
    
    
    print('M1(B,A,Z): ', M1.mro())
    print('M2(A,B,Z): ', M2.mro())

    MRO에 대해서는 여기까지...

     

    다중 상속이 다음과 같이 되어있다고 하자.

     

    unbound 형식으로 명시적으로 메서드(생성자 포함)를 호출한다면 지옥이 된다. 설계를 잘못함에 따라 생성자나 메서드가 두번 이상 호출될 수도 있다. 

    이를 해결할 수 있는 것이 super()내장함수를 통한 조상 클래스 속성 호출이다.

     

    이 super 놈때문에 삽질을 많이했다.

    super함수 문법은 파이썬2와 파이썬3가 다르다. 파이썬2와의 하위 호환을 위해 파이썬2방식도 사용할 수 있다.

    https://velog.io/@gwkoo/%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%83%81%EC%86%8D-%EB%B0%8F-super-%ED%95%A8%EC%88%98%EC%9D%98-%EC%97%AD%ED%95%A0

     

    클래스 상속 및 super 함수의 역할

    파이썬 클래스 상속 및 super 함수의 역할에 대해 정리해 보았습니다.

    velog.io

     

    MRO에 대해서 설명했다. super() 함수는조상 클래스의 메서드를 호출할 수 있는 porxy 객체를 리턴해주는데 이 객체를 가지고 객체.메서드 하면 현재 MRO 순서에 해당하는 메서드가 호출된다. 말로 설명하기 어렵다 코드를 통해 확인하자.

     

    파이썬의 최상위 클래스인 object의 __init__메서드가 호출되는 것을 육안으로 확인하기 위해 object 클래스를 모방한 FakeObject 클래스를 선언하고 다음과 같이 다이아몬드 상속 구조를 정의하였다.

     

    아래는 간단한 클래스 상속구조이지만 명시적으로 부모 클래스의 생성자를 호출하는 것을 잘못설계해서 FakeObject의 생성자 메서드가 두번 호출되었다.

    class FakeObject:
    
        def __init__(self):
            print('object 모방 생성자 start')
            print('object 모방 생성자 end')
    
    
    class Father(FakeObject):
    
        def __init__(self):
            print("아빠 생성자 start")
            FakeObject.__init__(self)
            print("아빠 생성자 end")
    
    
    class Mother(FakeObject):
    
        def __init__(self):
            print("엄마 생성자 start")
            FakeObject.__init__(self)
            print("엄마 생성자 end")
    
    class Child(Father, Mother):
    
        def __init__(self):
            print('자식 생성자 start')
            Father.__init__(self)
            Mother.__init__(self)
            print('자식 생성자 end')
    
    
    child = Child()

     

     

    super() 내장함수를 사용하면 MRO대로 생성자가 한번만 호출되게 할 수 있기 때문에 이를 사용하는 것이 정석이지 않을 까 싶다.

    https://docs.python.org/3.9/library/functions.html#super

    super(B, 객체).속성 -> MRO순서상 B의 다음것인 C부터 탐색한다고 써있다.

     

    파이썬3 - super().__init__() 

    파이썬2 - super(해당 클래스,self).__init__() (파이썬3에서도 사용가능)

     

    생성자가 두번 호출될 수도 있던 위의 예와는 다르게 아래처럼 super()를 사용하면 한번만 호출된다.

     

    class FakeObject:
    
        def __init__(self):
            print('object 모방 생성자 start')
            print('object 모방 생성자 end')
    
    
    class Father(FakeObject):
    
        def __init__(self):
            print("아빠 생성자 start")
            FakeObject.__init__(self)
            print("아빠 생성자 end")
    
    
    class Mother(FakeObject):
    
        def __init__(self):
            print("엄마 생성자 start")
            FakeObject.__init__(self)
            print("엄마 생성자 end")
    
    class Child(Father, Mother):
    
        def __init__(self):
            print('자식 생성자 start')
            Father.__init__(self)
            Mother.__init__(self)
            print('자식 생성자 end')
    
    
    child = Child()

     

    https://dev-navill.tistory.com/4

     

    Python - MRO(Method Resolution Order)

    Django REST Framework 문서에서 mixin 부분을 읽다가 예전에 공부 했었던 MRO(Method Resolution Order)에 대해 정리해 두는것이 좋을 것 같아 오랜만에 글을 쓰게 되었습니다. Method Resolrution Order 파이썬..

    dev-navill.tistory.com

     

    이렇게 super()를 사용할때 object클래스 전까지 super()를 통한 메서드호출이 끊기지 않았다면

    특정 자손 클래스의 MRO기준으로 모든 조상 클래스의 메서드가 딱 한번만 그리고 하나라도 빠지는 것없이 호출된다.

     

    다시 한번 공부중이지만 다중상속은 사용할 일이 없을 것 같은 개인적의견이 든다?..

    'Python' 카테고리의 다른 글

    [NumPy] Broadcasting  (0) 2022.01.23
    [Python] list comprehension  (0) 2022.01.20
    [Python] 클래스 기초  (0) 2022.01.11
    [Python] False,True 관련 연산자 동작(+None)  (0) 2022.01.07
    [Python] immutable, mutable 객체  (0) 2022.01.06

    댓글

Designed by Tistory.