-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmro.py
More file actions
213 lines (179 loc) · 9.74 KB
/
mro.py
File metadata and controls
213 lines (179 loc) · 9.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Copyright (c) 2015 Artur Eganyan
#
# This work is provided "AS IS", WITHOUT ANY WARRANTY, express or implied.
#-------------------------------------------------------------------------------
# Кратко:
# - MRO - Method Resolution Order - порядок поиска атрибутов
# - атрибут экземпляра ищется в экземпляре и в иерархии классов
# - атрибут класса ищется в иерархии классов и метаклассов
# - в python 2.x иерархия классов просматривается сначала в глубину, потом в ширину
# - в python 3.x иерархия классов просматривается по алгориму C3
# - C3 ставит любой класс до его родителей, и учитывает порядок наследования
# - некоторые иерархии не могут быть обработаны по C3 - тогда генерируется ошибка
# MRO - Method Resolution Order - это порядок поиска методов (а в python -
# поиска любых атрибутов, не только методов).
#
# Для old-style классов атрибут ищется "сначала в глубину, потом в ширину".
# Порядок такой: экземпляр, затем его первый родительский класс, родители
# этого класса, второй родительский класс, родители этого класса, и т.д.
# A, B, C, D - old-style классы
class A:
x = 1
def f(self):
print "A.f"
class B(A):
pass
class C(A):
x = 2
def f(self):
print "C.f"
class D(B, C):
pass
d = D()
# В этом примере "ромбовидная" иерархия (diamond-shaped inheritance):
#
# A
# / \
# B C
# \ /
# D
#
# Поиск атрибута "d.f" будет идти так: d, D, B, A, C, A. Дойдя до A первый
# раз, поиск обнаружит функцию "f" и остановится, поэтому тут будет
# вызвана A.f, а не C.f.
d.f() # A.f
print d.x # 1, т.е. A.x
# Для new-style классов поиск атрибута делается алгоритмом C3.
#
# Если говорить упрощенно, поиск делается по следующим критериям:
# 1. Любой класс проверяется раньше своих родителей.
# 2. Если родителей несколько, они проверяются в том порядке, в котором от
# них наследуют.
# 3. Если для класса C родитель A проверяется раньше родителя B, то это
# будет верно и для любого потомка C.
#
# В данной иерархии это означает такой порядок: d, D, B, C, A.
# A, B, C, D - new-style классы
class A(object):
x = 1
def f(self):
print "A.f"
class B(A):
pass
class C(A):
x = 2
def f(self):
print "C.f"
class D(B, C):
pass
d = D()
d.f() # C.f
print d.x # 2, т.е. C.x
# Такой поиск возможен не всегда. Например, если теперь добавить класс
# H(C, B), и попробовать создать класс G(D, H), возникнет конфликт.
class H(C, B):
pass
try:
class G(D, H):
pass
# TypeError: Error when calling the metaclass bases
# Cannot create a consistent method resolution order (MRO) for bases B, C
except TypeError as e:
print e
# Дело в том, что у D и H родители стоят в разном порядке: B, C и C, B. Поэтому
# нельзя расположить классы от G до A так, чтобы выполнить три условия,
# перечисленные выше: после G, D, H, должен идти либо B (тогда нарушен порядок
# наследования для H), либо C (нарушен порядок для D). Здесь надо вручную
# привести классы к одному порядку наследования.
# При обращении к атрибуту класса ("класс.атрибут"), в MRO участвует также
# иерархия метаклассов - она идет после родительских классов. Но при обращении
# к атрибуту экземпляра метаклассы не проверяются.
class M(type):
x = 1
y = 2
class A(object):
__metaclass__ = M
x = 3
print A.x # 3, это A.x
print A.y # 2, это M.y
a = A()
#print a.y # AttributeError: 'A' object has no attribute 'y'
# Порядок поиска можно узнать через атрибут "класс.__mro__". Порядок
# определяется при создании класса, и его можно переопределить в метаклассе.
# Для этого метаклассу надо добавить метод mro(класс), который возвращает
# последовательность классов.
# В этом метаклассе mro() возвращает кортеж (A, object), т.е. поиск атрибутов
# будет делаться только в A и object.
class M(type):
def mro(cls):
print u"M.mro() вызван, чтобы определить MRO для", cls
print u"Результат будет сохранен в __mro__"
return (A, object)
class A(object):
def f(self): print "A.f"
class B(A):
__metaclass__ = M
def f(self): print "B.f"
b = B()
b.f() # A.f
print B.__mro__ # (<class '__main__.A'>, <type 'object'>)
# Замечание: Метод метакласс.mro() вызывается при создании класса, и его
# результат сохраняется в атрибуте __mro__. Интересно, что этот атрибут
# находится в type, является дескриптором, и как-то хранит MRO для каждого
# метакласса, наследующего от type.
#
# Замечание: Судя по тестам, mro() должен вернуть последовательность,
# содержащую object, иначе будет ошибка при создании экземпляров класса:
# "TypeError: cannot create 'класс' instances". Также, все элементы
# последовательности должны быть классами, иначе будет ошибка
# на этапе создания класса: TypeError: Error when calling the metaclass
# bases mro() returned a non-class (...).
# Теория:
# Порядок, в котором идут классы при поиске атрибута, называется линеаризацией.
# Построение линеаризации алгоритмом C3 выглядит так:
#
# L(C) = [C] + merge(L(B1), ..., L(BN), [B1, ..., BN])
#
# где C - класс, наследующий от B1, ..., BN, L - функция построения
# линеаризации, "+" означает добавление в список, а merge - функция объединения
# линеаризаций. merge принимает списки классов и работает следующим образом:
# 1. Находит первый из списков, у которого заголовок не встречается
# в хвосте каждого из списков.
# 2. Удаляет найденный заголовок из всех списков и добавляет его к результату.
# Если заголовок не найден и есть непустой список, то линеаризация
# невозможна, генерируется ошибка.
# 3. Если остались непустые списки, выполняет 1. Иначе возвращает результат.
# "Заголовок" - это первый элемент списка, "хвост" - все остальные. Список
# родительских классов, переданный последним параметром в merge, нужен, чтобы
# выполнить критерий 2 (написан выше).
#
# Если записать списки вертикально, без "[]" и без "+", это будет:
#
# Z BN
# Y Z Y .
# X Y X B2
# L(C) = merge(B1, B2, ..., BN, B1)
# C
#
# Тогда проще увидеть, как работает merge - она имеет линеаризации каждого из
# родительских классов B1, ..., BN, рассматривает только первые элементы,
# выбирает тот, который не встречается в хвостах, и добавляет его в итоговую
# линеаризацию:
#
#
# Z Z Y BN
# Y Y X .
# L(C) = merge(X, B2, ..., BN, B2)
# B1
# C
#
#
# Z Y BN
# Y Z X .
# L(C) = merge(X, Y, ..., BN, B3)
# B2
# B1
# C
#