본문 바로가기
CS

[Python] 상속: 득과 실

by 왕밤빵도라에몽 2025. 4. 19.
728x90

super() 함수

유지보수가 용이한 객체지향 파이썬 프로그래밍을 위해서는 super() 내장 함수의 일관된 사용이 절대적으로 중요

상속 관계에서 부모 클래스의 메서드를 호출할 때 사용

class LastUpdatedOrderedDict(OrderedDict):
    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        self.move_to_end(key)

오버라이드된 __init__() 메서드를 호출하는 것은 슈퍼클래스가 인스턴스를 초기화하는 데 필요한 일을 할 수 있게 하므로 특히 중요하다.

def __init__(self, a, b):
    super().__init__(a, b)
    ... # 추가 초기화 코드

특히 다중 상속 구조에서 MRO를 따르게 해줌

  • MRO: 클래스의 상속 구조에서 어떤 순서로 메서드를 찾을지 정해놓은 규칙
  • 특히 다중 상속에서는 같은 이름의 메서드가 여러 부모 클래스에 있을 수 있으니까, 어떤 부모의 메서드를 먼저 호출할지가 중요
  • super()부모 호출이 아니라 다음 MRO 항목 호출

python2

super(MyClass, self).method()
  • MyClass`: 현재 클래스 -> MRO를 어디서부터 찾을지 결정
  • self: 현재 인스턴스 -> MRO에서 다음 메서드를 어떻게 찾을지 결정
  • 반드시 명시해야함

python3

super().method()
  • 둘 다 생략 가능
  • 자동으로 현재 클래스와 인스턴스 추론
  • 클래스 내부에서만 동작, 정의된 메서드가 클래스 스코프 안에 있어야 함

내장형 상속의 문제점

내장형 상속: 파이썬 기본 제공 타입(예: list, dict, str, int)을 상속해서 새로운 클래스를 만드는 것

class MyList(list):
    pass

문제점

  • 내부 메서드 간 연결 없음
    • 메서드 오버라이딩(덮어쓰기)을 해도 다른 메서드 내부에서 우리가 오버라이드한 메서드를 호출하지 않음
    • 예) extend()가 내부에서 append()를 호출하지 않음, append()를 오버라이딩해도 extend()가 그걸 안 씀
    • 설계 구조 자체의 문제임
  • 일부 동작이 직접적으로 메서드를 호출하지 않음
    • __getitem__, __setitem__, __iter__ 같은 걸 재정의해도, list나 dict 내부의 루프, 복사, 슬라이스 처리 같은 부분은 재정의한 메서드를 우회해서 동작할 수 있음
    • 파이썬 문법이 실제로는 C에서 직접 처리되므로 메서드가 무시됨
  • 호환성과 유지보수 이슈

대안
collections.UserList, collections.UserDict, collections.UserString 같은 랩핑(wrapping) 기반 클래스 상속

from collections import UserList

class MyList(UserList):
    def append(self, item):
        print(f"Appending {item}")
        super().append(item)
  • 모든 동작이 정의한 메서드를 거쳐서 발생하기 때문에 안정적

다중 상속과 메서드 결정 순서

다중 상속: 한 클래스가 여러 부모 클래스를 동시에 상속받는 구조

다이아몬드 문제: 다중 상속을 구현하는 언어는 슈퍼클래스들이 동일한 이름으로 메서드를 구현할때 발생하는 이름 충돌 문제

class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
  • D는 B와 C를 상속하고, B, C는 각각 A를 상속 → A가 중복 상속됨
    A
   / \
  B   C
   \ /
    D
  • D에서 A의 메서드를 호출하면 → 어느 경로로 A를 호출할지 모호해짐

MRO가 클래스들의 우선순위를 하나의 선형 리스트로 만들어줌

D.__mro__

(<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
  • 파이썬은 이 순서를 따라 메서드를 탐색 -> 중복 호출 없이, 명확한 순서로만 진행

그래서 super()중요!!!
cooperative inheritance (협조적 상속): 다중 상속 구조에서는 모든 클래스가 super()를 사용할 것을 전제로 해야 함

믹스인 클래스

다중 상속에서 다른 클래스와 함께 사용해서 추가 기능을 제공하는 클래스(독립적인 재사용 목적)

  • 독립 실행 불가: 자체적으로 인스턴스를 만들진 않고, 다른 클래스에 기능을 추가하는 용도
  • 기능 단위: 작고 독립적인 기능 하나에 집중
  • 다른 클래스에 끼워넣는 보조 클래스: 주로 메인 클래스와 함께 다중 상속 구조로 사용
  • 상속 순서에 따라 MRO 영향 있음: 믹스인이 super()를 사용하면 전체 MRO 흐름에 참여함
class JsonMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal, JsonMixin):
    pass

dog = Dog("Charlie")
print(dog.to_json())  # {"name": "Charlie"}
  • DogJsonMixin에서 to_json()만 믹스해서 사용

주의 사항

  • 상태(state) 갖지 말기 - 메인 클래스랑 충돌 예방
  • 단일 책임 원칙 지키기
  • Mixin이라고 명시해주기

믹스드인 활용 - 메서드 체이닝

class Step1Mixin:
    def process(self, data):
        print("Step 1")
        data += " -> step1"
        return super().process(data)

class Step2Mixin:
    def process(self, data):
        print("Step 2")
        data += " -> step2"
        return super().process(data)

class FinalStep:
    def process(self, data):
        print("Final step")
        return data + " -> done"

class Chain(Step1Mixin, Step2Mixin, FinalStep):
    pass

result = Chain().process("start")
print(result)
  • super().process()가 MRO에 따라 다음 클래스의 process() 호출
Step 1  
Step 2  
Final step  
start -> step1 -> step2 -> done

상속 처리

  • 상속하기 전에 객체 구성을 먼저 생각해라
  • 상속하는 이유를 명확히 해라
  • ABC로 인터페이스임을 명시해라
  • 코드 재사용하고싶으면 믹스인 써라
  • 사용자에게 집합 클래스 제공해라
  • 상속하도록 설계된 클래스만 상속해라
  • 구상 클래스 상속 피해라

References

  • 『 전문가를 위한 파이썬(2판) 』, 루시아누 하말류, 한빛미디어, Part 1 - Chapter 14

  • 믹스인? 첨들었으나... 재밌군 믹스인으로 메서드 체이닝하는걸로 넘어가니 랭체인이 이런 원리를 기반으로 하나 생각이 들었다 재밌다
  • 하지만 랭체인은 합성(composition) + invoke() 체이닝 방식으로 여기서말하는 믹스인 + MRO랑은 좀 다르다 함
  • 책은 재미없음
728x90