Python 2.3 메서드 결정 순서¶
참고
이 문서는 공식 문서의 부록으로 제공되는 역사적 문서입니다. 여기서 논의된 메서드 결정 순서는 Python 2.3에서 도입 되었으며, 현재까지 포함하여 Python 3을 포함한 이후 버전에서도 여전히 사용되고 있습니다.
Michele Simionato <https://www.phyast.pitt.edu/~micheles/> 작성.
- 개요:
이 문서는 Python 2.3에서 사용되는 C3 메서드 결정 순서를 이해하고자 하는 Python 프로그래머를 대상으로 합니다. 초보자를 위한 것은 아니지만, 많은 예제 풀이가 포함되어 있어 교육적입니다. 동일한 범위를 가진 다른 공개 문서가 없는 것으로 알고 있으므로 유용할 것입니다.
면책 조항:
본 문서는 Python 2.3 라이선스에 따라 Python Software Foundation에 기부합니다. 이러한 상황에서 통상적으로 그렇듯, 본문이 정확할 것이라고 경고하나 어떠한 보증도 제공하지 않습니다. 본인의 책임하에 사용하십시오.
감사의 말:
지지를 보내준 모든 Python 메일링 리스트 사용자들. 다양한 오류를 지적하고 로컬 우선순위 정렬 부분을 추가하도록 한 Paul Foley. reStructuredText 형식을 돕는 데 기여한 David Goodger. 편집을 도와준 David Mertz. 그리고 이 문서를 공식 Python 2.3 홈페이지에 열정적으로 추가해 준 Guido van Rossum에게 감사를 표합니다.
시작¶
Felix qui potuit rerum cognoscere causas – Virgil
Everything started with a post by Samuele Pedroni to the Python development mailing list [1]. In his post, Samuele showed that the Python 2.2 method resolution order is not monotonic and he proposed to replace it with the C3 method resolution order. Guido agreed with his arguments and therefore now Python 2.3 uses C3. The C3 method itself has nothing to do with Python, since it was invented by people working on Dylan and it is described in a paper intended for lispers [2]. The present paper gives a (hopefully) readable discussion of the C3 algorithm for Pythonistas who want to understand the reasons for the change.
먼저 말씀드리자면, 제가 설명할 내용은 Python 2.2에서 도입된 새 스타일 클래스 에만 적용됩니다. 클래식 클래스 는 기존의 메서드 결정 순서(깊이 우선 후 왼쪽에서 오른쪽으로)를 유지합니다. 따라서 클래식 클래스의 경우 기존 코드가 깨질 염려가 없으며, 원칙적으로 Python 2.2 새 스타일 클래스의 코드가 깨질 가능성이 있더라도 실제로는 C3 결정 순서와 Python 2.2 메서드 결정 순서가 일치하지 않는 경우가 매우 드물어 실제 코드 장애는 예상되지 않습니다. 따라서:
겁내지 마세요!
또한, 다중 상속을 광범위하게 사용하거나 복잡한 계층 구조를 가지고 있지 않다면 C3 알고리즘을 이해할 필요가 없으며 이 문서를 건너뛸 수 있습니다. 반대로, 다중 상속이 어떻게 작동하는지 정말로 알고 싶다면 이 문서가 도움이 될 것입니다. 좋은 소식은 내용이 생각만큼 복잡하지 않다는 점입니다.
기본적인 정의부터 시작하겠습니다.
복잡한 다중 상속 계층 구조 내에 있는 클래스 C가 주어졌을 때, 메서드가 재정의되는 순서, 즉 C의 조상들의 순서를 지정하는 것은 결코 간단한 작업이 아닙니다.
자기 자신을 포함하여 가장 가까운 조상부터 가장 먼 조상까지 순서대로 나열한 클래스 C의 조상 목록은 클래스 우선순위 리스트 또는 C의 선형화(linearization) 라고 합니다.
메서드 결정 순서 (MRO)는 선형화를 구축하는 규칙의 집합입니다. 파이썬 문헌에서 “C의 MRO”라는 표현은 클래스 C의 선형화와 동의어로 사용됩니다.
예를 들어 단일 상속 계층의 경우, C가 C1의 서브클래스이고 C1이 C2의 서브클래스라면 C의 선형화는 단순히 [C, C1, C2] 목록이 됩니다. 그러나 다중 상속 계층에서는 로컬 우선순위 정렬 과 단조성 을 준수하는 선형화를 구성하기가 더 어렵기 때문에 선형화 구축이 더 번거롭습니다.
로컬 우선순위 정렬은 나중에 논의하겠지만, 단조성의 정의는 여기서 제시할 수 있습니다. 다음 조건이 충족될 때 MRO는 단조롭습니다: C의 선형화에서 C1이 C2보다 앞에 위치하면, C의 모든 서브클래스의 선형화에서도 C1이 C2보다 앞에 위치한다. 그렇지 않으면 새로운 클래스를 파생시키는 무해한 작업이 메서드의 결정 순서를 변경하여 매우 미묘한 버그를 발생시킬 수 있습니다. 이러한 사례는 나중에 보여줄 것입니다.
모든 클래스가 선형화를 허용하는 것은 아닙니다. 복잡한 계층 구조에서 모든 요구 속성을 만족하는 선형화를 갖는 방식으로 클래스를 파생시키는 것이 불가능한 경우도 있습니다.
이러한 상황에 대한 예를 제시하겠습니다. 다음 계층 구조를 고려해 보십시오.
>>> O = object
>>> class X(O): pass
>>> class Y(O): pass
>>> class A(X,Y): pass
>>> class B(Y,X): pass
이 구조는 다음 상속 그래프로 표현될 수 있으며, 여기서 O는 새 스타일 클래스의 모든 계층의 시작점인 object 클래스를 의미합니다.
----------- | | | O | | / \ | - X Y / | / | / | / |/ A B \ / ?
이 경우, A와 B로부터 새로운 클래스 C를 파생시키는 것이 불가능합니다. 왜냐하면 A에서는 X가 Y보다 앞에 나오지만, B에서는 Y가 X보다 앞서기 때문에 C에서 메서드 결정 순서가 모호해지기 때문입니다.
파이썬 2.3은 이러한 상황에서 예외(TypeError: MRO conflict among bases Y, X)를 발생시켜 미숙한 프로그래머가 모호한 계층 구조를 만드는 것을 방지합니다. 반면 파이썬 2.2는 예외를 발생시키지 않고 임의적인(ad hoc) 순서(이 경우 CABXYO)를 선택합니다.
C3 메서드 결정 순서¶
이어지는 논의에 유용한 몇 가지 간단한 표기법을 소개하겠습니다. 다음 단축 표기법을 사용합니다:
C1 C2 ... CN
클래스 목록 [C1, C2, … , CN]을 나타냅니다.
리스트의 head 는 첫 번째 요소입니다:
head = C1
반면 tail 은 리스트의 나머지 부분입니다:
tail = C2 ... CN.
다음 표기법도 사용합니다:
C + (C1 C2 ... CN) = C C1 C2 ... CN
리스트의 합 [C] + [C1, C2, … ,CN]을 나타냅니다.
이제 Python 2.3에서 MRO가 어떻게 작동하는지 설명하겠습니다.
B1, B2, …, BN과 같은 베이스 클래스로부터 상속받는 다중 상속 계층 구조 내의 클래스 C를 가정해 보겠습니다. 우리는 클래스 C의 선형화(linearization) L[C]를 계산하고자 합니다. 규칙은 다음과 같습니다.
C의 선형화는 C에 부모들의 선형화와 부모들의 목록을 병합(merge)한 결과입니다.
기호 표기법으로 나타내면 다음과 같습니다:
L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
특히, 부모가 없는 object 클래스인 경우 선형화는 당연하게 결정됩니다:
L[object] = object.
하지만 일반적으로는 다음과 같은 절차에 따라 병합을 계산해야 합니다.
첫 번째 리스트의 헤드(head), 즉 L[B1][0]을 가져옵니다. 이 헤드가 다른 모든 리스트의 꼬리(tail) 부분에 포함되어 있지 않다면, 이를 C의 선형화에 추가하고 병합 대상 리스트에서 제거합니다. 그렇지 않다면 다음 리스트의 헤드를 확인하고 그것이 유효한 헤드라면 선택합니다. 이 과정을 모든 클래스가 제거되거나 적절한 헤드를 찾을 수 없을 때까지 반복합니다. 적절한 헤드를 찾을 수 없는 경우, 병합을 구성할 수 없으므로 Python 2.3은 클래스 C 생성을 거부하고 예외를 발생시킵니다.
이 절차는 순서가 유지될 수 있는 경우 병합 연산 시 해당 순서를 보존 하도록 보장합니다. 반대로, 순서를 유지할 수 없는 경우(앞서 논의한 심각한 순서 불일치 사례와 같은 경우)에는 병합을 계산할 수 없습니다.
C가 단 하나의 부모만 가진 경우(단일 상속) 병합 계산은 간단합니다. 이 경우:
L[C(B)] = C + merge(L[B],B) = C + L[B]
하지만 다중 상속의 경우 절차가 더 복잡하므로 몇 가지 예시 없이는 규칙을 이해하기 어려울 수 있습니다. ;-)
예시¶
첫 번째 예시입니다. 다음 계층 구조를 고려해 보십시오.
>>> O = object
>>> class F(O): pass
>>> class E(O): pass
>>> class D(O): pass
>>> class C(D,F): pass
>>> class B(D,E): pass
>>> class A(B,C): pass
이 경우 상속 그래프는 다음과 같이 그려질 수 있습니다.
6 --- Level 3 | O | (more general) / --- \ / | \ | / | \ | / | \ | --- --- --- | Level 2 3 | D | 4| E | | F | 5 | --- --- --- | \ \ _ / | | \ / \ _ | | \ / \ | | --- --- | Level 1 1 | B | | C | 2 | --- --- | \ / | \ / \ / --- Level 0 0 | A | (more specialized) ---
O, D, E, F의 선형화는 당연하게 결정됩니다:
L[O] = O
L[D] = D O
L[E] = E O
L[F] = F O
B의 선형화는 다음과 같이 계산될 수 있습니다:
L[B] = B + merge(DO, EO, DE)
D가 유효한 헤드이므로 이를 선택하고, merge(O,EO,E) 를 계산하는 문제로 축소됩니다. 이제 O는 EO 시퀀스의 꼬리 부분에 포함되어 있으므로 유효한 헤드가 아닙니다. 이 경우 규칙에 따라 다음 시퀀스로 넘어갑니다. 그다음 E가 유효한 헤드임을 확인하고 이를 선택하면, merge(O,O) 를 계산하는 문제로 축소되며 그 결과는 O가 됩니다. 따라서:
L[B] = B D E O
동일한 절차를 사용하여 다음과 같은 결과를 얻습니다:
L[C] = C + merge(DO,FO,DF)
= C + D + merge(O,FO,F)
= C + D + F + merge(O,O)
= C D F O
이제 다음과 같이 계산할 수 있습니다:
L[A] = A + merge(BDEO,CDFO,BC)
= A + B + merge(DEO,CDFO,C)
= A + B + C + merge(DEO,DFO)
= A + B + C + D + merge(EO,FO)
= A + B + C + D + E + merge(O,FO)
= A + B + C + D + E + F + merge(O,O)
= A B C D E F O
이 예시에서 선형화는 상속 수준에 따라 꽤나 적절하게 정렬되어 있습니다. 즉, 더 낮은 수준(더 구체화된 클래스)이 더 높은 우선순위를 가집니다(상속 그래프 참조). 하지만 이것은 일반적인 사례가 아닙니다.
두 번째 예시의 선형화를 계산하는 과정을 독자의 연습 과제로 남겨둡니다.
>>> O = object
>>> class F(O): pass
>>> class E(O): pass
>>> class D(O): pass
>>> class C(D,F): pass
>>> class B(E,D): pass
>>> class A(B,C): pass
이전 예시와 유일한 차이점은 B(D,E)가 B(E,D)로 바뀐 것입니다. 그러나 이러한 작은 수정만으로도 계층 구조의 순서가 완전히 달라집니다.
6 --- Level 3 | O | / --- \ / | \ / | \ / | \ --- --- --- Level 2 2 | E | 4 | D | | F | 5 --- --- --- \ / \ / \ / \ / \ / \ / --- --- Level 1 1 | B | | C | 3 --- --- \ / \ / --- Level 0 0 | A | ---
계층의 두 번째 수준에 있는 클래스 E가 첫 번째 수준에 있는 클래스 C보다 앞에 위치함에 주목하십시오. 즉, E는 더 높은 단계에 있더라도 C보다 더 구체화된 것입니다.
귀찮은 프로그래머라면 Python 2.2에서 직접 MRO를 얻을 수도 있습니다. 이 경우에는 Python 2.3의 선형화 결과와 일치하기 때문입니다. 클래스 A의 mro() 메서드를 호출하는 것만으로 충분합니다.
>>> A.mro()
[<class 'A'>, <class 'B'>, <class 'E'>,
<class 'C'>, <class 'D'>, <class 'F'>,
<class 'object'>]
마지막으로, 첫 번째 섹션에서 논의한 심각한 순서 불일치가 포함된 사례를 살펴보겠습니다. 이 경우 O, X, Y, A, B의 선형화를 계산하는 것은 매우 간단합니다.
L[O] = 0 L[X] = X O L[Y] = Y O L[A] = A X Y O L[B] = B Y X O
하지만 A와 B를 상속하는 클래스 C의 선형화를 계산하는 것은 불가능합니다:
L[C] = C + merge(AXYO, BYXO, AB)
= C + A + merge(XYO, BYXO, B)
= C + A + B + merge(XYO, YXO)
이 시점에서 XYO와 YXO 리스트를 병합할 수 없습니다. 왜냐하면 X는 YXO의 꼬리에 있고, Y는 XYO의 꼬리에 있기 때문입니다. 따라서 유효한 헤스가 없으며 C3 알고리즘은 중단됩니다. Python 2.3은 오류를 발생시키고 클래스 C 생성을 거부합니다.
잘못된 메서드 결정 순서¶
MRO가 지역 우선순위 순서나 단조성과 같은 기본적인 속성을 깨뜨릴 때 이를 나쁜 MRO라고 합니다. 이 섹션에서는 기존 방식(classic)의 클래스에 대한 MRO와 Python 2.2에서 새로운 방식(new style)의 클래스에 대한 MRO가 모두 나쁘다는 것을 보여줄 것입니다.
지역 우선순위 순서부터 시작하는 것이 더 쉽습니다. 다음 예시를 고려해 보십시오.
>>> F=type('Food',(),{'remember2buy':'spam'})
>>> E=type('Eggs',(F,),{'remember2buy':'eggs'})
>>> G=type('GoodFood',(F,E),{}) # under Python 2.3 this is an error!
상속도 포함
O | (buy spam) F | \ | E (buy eggs) | / G (buy eggs or spam ?)
클래스 G가 E보다 앞선 F와 E로부터 상속됨을 알 수 있습니다. 따라서 우리는 속성 G.remember2buy 이 E.remember2buy 가 아닌 F.remember2buy 에 의해 상속될 것으로 기대합니다. 그럼에도 불구하고 Python 2.2는 다음 결과를 내놓습니다.
>>> G.remember2buy
'eggs'
이는 로컬 우선순위 순서가 깨진 것입니다. 즉, G의 부모 리스트인 로컬 우선순위 목록의 순서가 Python 2.2의 G 선형화에서 유지되지 않았기 때문입니다:
L[G,P22]= G E F object # F가 E 뒤에 옴
Python 2.2 선형화에서 F가 E 뒤에 오는 이유가 F가 E의 슈퍼클래스이므로 E보다 덜 구체적이기 때문이라고 주장할 수도 있습니다. 그럼에도 불구하고 로컬 우선순위 순서의 파괴는 상당히 직관적이지 않으며 오류를 유발하기 쉽습니다. 이는 특히 다음과 같은 구식 클래스와 다르기 때문에 더욱 그러합니다.
>>> class F: remember2buy='spam'
>>> class E(F): remember2buy='eggs'
>>> class G(F,E): pass
>>> G.remember2buy
'spam'
이 경우 MRO는 GFEF이며 로컬 우선순위 순서가 유지됩니다.
일반적인 규칙으로, F가 E를 재정의해야 하는지 아니면 그 반대인지 불분명한 이전과 같은 계층 구조는 피해야 합니다. Python 2.3은 클래스 G를 생성할 때 예외를 발생시킴으로써 이러한 모호성을 해결하고 프로그래머가 모호한 계층을 생성하는 것을 막습니다. 그 이유는 C3 알고리즘이 다음 병합 과정에서 실패하기 때문입니다.
merge(FO,EFO,FE)
F가 EFO의 꼬리에 있고 E가 FE의 꼬리에 있어 계산할 수 없습니다.
진정한 해결책은 모호하지 않은 계층을 설계하는 것입니다. 즉, G를 F와 E가 아닌 E와 F(더 구체적인 것을 먼저)로부터 파생시키는 것입니다. 이 경우 MRO는 의심의 여지 없이 GEF이 됩니다.
O | F (spam) / | (eggs) E | \ | G (eggs, no doubt)
Python 2.3은 프로그래머가 좋은 계층(또는 적어도 오류 발생 가능성이 낮은 계층)을 작성하도록 강제합니다.
관련된 내용으로, Python 2.3 알고리즘이 명백한 실수를 감지할 만큼 똑똑하다는 점을 언급하고 싶습니다. 예를 들어 부모 리스트에 클래스가 중복되는 경우:
>>> class A(object): pass
>>> class C(A,A): pass # error
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: duplicate base class A
이 상황에서 Python 2.2(구식과 신식 클래스 모두 포함)는 어떠한 예외도 발생시키지 않습니다.
마지막으로 이 사례를 통해 배운 두 가지 교훈을 말씀드리고자 합니다.
이름에도 불구하고, MRO는 메서드뿐만 아니라 속성의 결정 순서도 결정합니다.
파이썬 개발자들의 기본식은 스팸입니다! (하지만 여러분은 이미 알고 계셨을 겁니다. ;-)
로컬 우선순위 순서 문제를 논의했으므로, 이제 단조성(monotonicity) 문제를 살펴보겠습니다. 제 목표는 구식 클래스의 MRO나 Python 2.2 신식 클래스의 MRO 중 어느 것도 단조적이지 않다는 것을 보여주는 것입니다.
구식 클래스의 MRO가 비단조적임을 증명하는 것은 매우 간단하며, 다이아몬드 도표를 확인하면 충분합니다.
C / \ / \ A B \ / \ / D
일관성이 없는 부분이 쉽게 식별됩니다:
L[B,P21] = B C # B가 C보다 앞섬: B의 메서드 승리
L[D,P21] = D A C B C # B가 C 뒤에 옴: C의 메서드 승리!
반면, Python 2.2 및 2.3 MRO에는 문제가 없으며 둘 다 다음을 제공합니다:
L[D] = D A B C
Guido는 그의 에세이(참조 [3])에서 구식 MRO가 실무에서는 그렇게 나쁘지 않다고 지적합니다. 이는 구식 클래스의 경우 일반적으로 다이아몬드 구조를 피할 수 있기 때문입니다. 그러나 모든 신식 클래스는 object 를 상속받으므로, 다이아몬드 구조는 불가피하며 모든 다중 상속 그래프에서 불일치가 나타납니다.
Python 2.2의 MRO는 단조성 파괴를 어렵게 만들지만 불가능하게 하지는 않습니다. Samuele Pedroni가 제공한 다음 예시는 Python 2.2의 MRO가 비단조적임을 보여줍니다.
>>> class A(object): pass
>>> class B(object): pass
>>> class C(object): pass
>>> class D(object): pass
>>> class E(object): pass
>>> class K1(A,B,C): pass
>>> class K2(D,B,E): pass
>>> class K3(D,A): pass
>>> class Z(K1,K2,K3): pass
다음은 C3 MRO에 따른 선형화 결과입니다(독자는 연습 과제로 이 선형화들을 확인하고 상속 도표를 그려보시기 바랍니다. ;-)
L[A] = A O
L[B] = B O
L[C] = C O
L[D] = D O
L[E] = E O
L[K1]= K1 A B C O
L[K2]= K2 D B E O
L[K3]= K3 D A O
L[Z] = Z K1 K2 K3 D A B C E O
Python 2.2는 A, B, C, D, E, K1, K2, K3에 대해 정확히 동일한 선형화를 제공하지만, Z에 대해서는 다른 선형화를 제공합니다:
L[Z,P22] = Z K1 K3 A K2 D B C E O
이 선형화가 틀렸음*을 알 수 있습니다. 왜냐하면 K3의 선형화에서 A는 D보다 뒤에 오는데, 이 선형화에서는 A가 D보다 앞에 나오기 때문입니다. 다시 말해, K3에서는 D에서 파생된 메서드가 A에서 파생된 메서드를 재정의하지만, 여전히 K3의 하위 클래스인 Z에서는 A에서 파생된 메서드가 D에서 파생된 메서드를 재정의합니다! 이는 단조성의 위반입니다. 더욱이, Z의 Python 2.2 선형화는 로컬 우선순위 순서와도 일치하지 않습니다. 클래스 Z의 로컬 우선순위 목록은 [K1, K2, K3]으로 K2가 K3보다 앞에 오지만, Z의 선형화에서는 K2가 K3을 *뒤따르기 때문입니다. 이러한 문제들 때문에 2.2 규칙는 폐기되고 C3 규칙이 채택되었습니다.
끝¶
이 섹션은 이전의 모든 섹션을 건너뛰고 바로 끝으로 넘어온 성급한 독자를 위한 것입니다. 또한 머리를 쓰고 싶어 하지 않는 게으른 프로그래머를 위한 것이기도 합니다. 마지막으로, 이 섹션은 약간의 자부심을 가진 프로그래머를 위한 것인데, 그렇지 않다면 다중 상속 계층에서의 C3 메서드 결정 순서에 관한 논문을 읽고 있지 않을 것이기 때문입니다 ;-) 이 세 가지 미덕이 합쳐졌을 때(개별적으로가 아니라), 하나의 보상을 받을 자격이 있습니다. 그 보상은 바로 여러분의 머리에 무리를 주지 않고 2.3 MRO를 계산할 수 있게 해주는 짧은 Python 2.2 스크립트입니다. 이 논문에서 다룬 다양한 예제들을 확인하려면 마지막 줄을 수정하기만 하면 됩니다.:
그게 전부입니다.
이상입니다.
즐기세요!