Расширенные возможности классов
В этой главе описание ООП на Python завершается представлением нескольких более сложных тем, связанных с классами: мы рассмотрим создание подклассов из встроенных типов, изменения и расширения классов “нового стиля”, статические методы и методы классов, слоты и свойства, декораторы функций и классов, MRO и встроенную функцию super, а также многое другое.
Как будет показано, модель ООП на Python в своей основе относительно проста и некоторые из обсуждаемых в главе средств настолько продвинуты и необязательны, что вы нечасто будете сталкиваться с ними в своей карьере прикладного программирования на Python. Однако в интересах полноты — и ввиду того, что никогда нельзя предугадать, какое “продвинутое” средство неожиданно обнаружится в используемом вами коде — мы закончим обсуждение классов кратким обзором сложных инструментов такого рода для ООП.
Поскольку глава является последней в данной части, в ее конце приводится раздел, посвященный связанным с классами затруднениям, а также подборка подходящих упражнений. Я призываю вас проработать предложенные упражнения, чтобы закрепить понимание идей, раскрываемых в главе. Я также предлагаю в качестве дополнения к материалам книги выполнить либо изучить более крупные объектно-ориентированные проекты на Python. Подобно многому в вычислительной обработке преимущества ООП становятся более очевидными при практическом применении.
Замечания по содержанию. В настоящей главе собраны более сложные темы, касающиеся классов, но некоторые из них слишком обширные, чтобы их можно было полностью раскрыть в одной главе. Такие темы, как свойства, дескрипторы, декораторы и метаклассы, упоминаются здесь лишь кратко, а более подробно рассматриваются в финальной части книги после исключений. Обязательно просмотрите более полные примеры и расширенное описание ряда тем, которые подпадают под категорию исследуемых в главе.
Вы также заметите, что эта глава самая объемная в книге — я предполагаю наличие у читателей достаточной смелости, чтобы засучив рукава, приступить к ее изучению. Если вас пока не интересуют расширенные темы по ООП, тогда можете перейти сразу к упражнениям в конце главы и вернуться к ее чтению в будущем, когда вы столкнетесь с такими инструментами в коде, с которым придется работать.
Расширение встроенных типов
Помимо реализации новых видов объектов классы иногда используются для расширения функциональности встроенных типов Python с целью поддержки более экзотических структур данных. Например, чтобы добавить к спискам методы вставки и удаления из очереди, вы можете реализовать классы, которые внедряют списковый объект и экспортируют методы вставки и удаления, обрабатывающие список особым образом, с помощью методики делегирования, описанной в главе 31. Начиная с версии Python 2.2, вы также можете применять наследование для специализации встроенных типов. В следующих двух разделах обе методики показаны в действии.
Расширение типов путем внедрения
Помните ли вы функции для работы с множествами, которые были написаны в главах 16 и 18 первого тома? Далее вы увидите, как они выглядят после возрождения в виде класса Python. В приведенном ниже примере (файл setwrapper .ру) реализован новый объектный тип множества за счет переноса ряда функций для работы с множествами в методы и добавления перегрузки базовых операций. По большей части этот класс является всего лишь оболочкой для списка Python с дополнительными операциями над множествами. Но будучи классом, он также поддерживает многочисленные экземпляры и настройку путем наследования в подклассах. В отличие от наших ранних функций использование классов дает возможность создавать набор самодостаточных объектов с предварительно установленными данными и поведением, а не передавать функциям списки вручную:
class Set:
# Конструктор
# Управляет списком
# other - любая последовательность
# self — подчиненный объект
# Выбрать общие элементы
# Возвратить новый объект Set
# other - любая последовательность
# Колировать список
# Добавить элементы в other
def _init_(self, value = []) :
self.data = [] self.concat(value)
def intersect(self, other): res = []
for x in self.data: if x in other: res.append(x) return Set(res)
def union(self, other): res = self.data[:] for x in other:
if not x in res: res.append(x) return Set(res)
def concat(self, value): 4
value: список, Set., Удаляет дубликаты
for x in value: #
if not x in self.data: self.data.append(x)
def _len_(self): return len(self.data)
len(self), если self истинно
# self[i], self[i:j]
# self & other
# self I other
# print (self) ,. . .
# for x in self,. . .
def _getitem_(self, key): return self.data[key]
def_and_(self, other): return self.intersect(other)
def _or_(self, other): return self.union(other)
def_repr_(self): return 'Set:' + repr(self.data)
def iter (self): return iter(self.data)
Для использования класса Set мы создаем экземпляры, вызываем методы и запускаем определенные операции обычным образом:
from setwrapper import Set x = Set( [1, 3, 5, 7])
print (x. union (Set ( [1, 4, 7]))) # Выводится Set:[1, 3, 5, 1, 4]
print (x | Set([l, A, 6])) # Выводится Set:[ 1, 3, 5, 1, 4, 6]
Перегрузка операций, таких как индексирование и итерация, также часто позволяет экземплярам нашего класса Set выдавать себя за реальные списки. Поскольку в упражнении, предложенном в конце главы, вы будете взаимодействовать и расширять этот класс, дальнейшее обсуждение кода откладывается до приложения Г.
Расширение типов путем создания подклассов
Начиная с версии Python 2.2, для всех встроенных типов в языке можно создавать подклассы напрямую. Функции преобразования типов вроде list, str, diet и tuple стали именами встроенных типов — несмотря на прозрачность для вашего сценария, вызов преобразования типа (скажем, list (1 spam1)) теперь на самом деле представляет собой обращение к конструктору объектов типа.
Такое изменение предоставляет возможность настройки или расширения поведения встроенных типов посредством определяемых пользователем операторов class: для их настройки просто создавайте подклассы с новыми именами типов. Экземпляры подклассов ваших типов обычно могут применяться везде, где допускается появление исходных встроенных типов. Например, предположим, что вы испытываете трудности с привыканием к тому факту, что смещения в списках Python начинаются с 0, а не 1. Не переживайте — вы всегда можете создать собственный подкласс, который настраивает эту основную линию поведения списков, как демонстрируется в файле typesub-class.ру:
# Создание подкласса встроенного типа/класса списка
# Отображает 1..N на 0..N-1; обращается к встроенной версии. class MyList(list):
def_getitem_(self, offset):
print(' (indexing %s at %s) ' % (self, offset))
return list._getitem_(self, offset - 1)
if _name_ == '_main_' :
print(list('abc'))
x = MyList('abc') # Метод_init_, унаследованный от списка
print (x) § Метод_repr_, унаследованный от списка
print(х[1]) # MyList._getitem_
print(x [3]) # Настраивает метод из суперкласса списка
х.append('spam'); print(х) # Атрибуты из суперкласса списка x.reverseO; print (х)
Подкласс MyList расширяет только метод индексирования_getitem_встроенного списка, чтобы отображать индексы 1-N на требующиеся 0-N-1. Он всего лишь декрементирует переданный индекс и вызывает версию индексирования из суперкласса, но этого достаточно для выполнения трюка:
% python typesubclass.ру
[' а ' / 'Ь\ 'с' ]
['а', 'Ь\ ’с']
(indexing ['а', 'b1, 'с'] at 1) а
(indexing ['a', 'b', 'c'] at 3)
с
[' a', ' b', ' с', 'spam']
['spam', ' с', 'b', 'a']
Полученный вывод также включает трассировочный текст, который класс отображает при индексировании. Конечно, остался другой вопрос, является ли вообще хорошей идеей подобное изменение индексирования — пользователи класса MyList могут быть крайне озадачены довольно радикальным отступлением от поведения последовательностей в Python! Тем не менее, возможность такого рода настройки встроенных типов может рассматриваться как ценное качество.
Например, эта кодовая схема порождает альтернативный способ реализации множества — как подкласса встроенного спискового типа, а не автономного класса, который управляет внедренным объектом списка, как было показано в предыдущем разделе. Как было указано в главе 5 первого тома, в наши дни Python поступает с мощным встроенным объектом множества наряду с синтаксисом литералов и включений для создания новых множеств. Однако его самостоятельная реализация по-прежнему остается великолепным способом изучить создание подклассов для типов в целом.
Следующий класс из файла setsubclass .ру настраивает списки, добавляя методы и операции для обработки множеств. Поскольку остальные линии поведения наследуются из встроенного суперкласса list, класс Set становится более короткой и простой альтернативой предыдущему варианту — все, что здесь не определено, направляется прямо list:
from _future_ import print_function # Совместимость с Python 2.X
class Set(list) :
def _init_(self, value = []) : # Конструктор
list._init_(self) # Настраивает список
self. concat (value) # Копирует изменяемые стандартные значения
def intersect(self, other): # other - любая последовательность
res = [] # self — подчиненный объект for x in self:
if x in other: # Выбрать общие элементы res.append(x)
return Set (res) # Возвратить новый объект Set
def union(self, other): # other - любая последовательность
res = Set (self) # Копировать текущий и другой список
res.concat(other) return res
def concat(self, value) : # value: список, Set и т.д.
for x in value: # Удаляет дубликаты
if not x in self: self.append(x)
def _and_(self, other): return self.intersect(other)
def_or_(self, other): return self.union(other)
def_repr_(self): return 'Set:' + list._repr_(self)
if
x = Set ( [1,3,5,7])
у = Set([2,1,4,5, 6] )
print(x, y, len(x))
print(x.intersect (у), у.union(x))
print(x & y, x | y)
x.reverseO; print (x)
Ниже приведен вывод кода самотестирования, находящегося в конце файла. Из-за того, что создание подклассов для основных типов — кое в чем сложное средство с ограниченной целевой аудиторией, дальнейшие детали здесь опущены, но я предлагаю вам отследить получение результатов в коде, чтобы изучить его поведение (одинаковое в Python З.Х и 2.Х):
% python setsubclass.ру
Set:[1, 3, 5, 7] Set:[2, 1, 4, 5, б] 4
Set:[1, 5] Set: [2, 1, 4, 5, 6, 3, 7]
Set:[1, 5] Set: [1, 3, 5, 7, 2, 4, 6]
Set: [7, 5, 3, 1]
В Python существуют более эффективные способы реализации множеств с помощью словарей. Они заменяют вложенные линейные просмотры в показанных ранее реализациях множеств более прямыми операциями индексирования в словарях (хеширование) и потому выполняются гораздо быстрее. Подробности ищите в книге Programming Python (http: //www. oreilly. com/catalog/9780596158101). Если вас интересуют множества, тогда снова взгляните на объектный тип set, который был исследован в главе 5 первого тома; он предоставляет обширный набор операций для работы с множествами в виде встроенных инструментов. С реализациями множеств забавно экспериментировать, но в современном Python они больше не являются строго обязательными.
За еще одним примером создания подклассов для типов обращайтесь к реализации типа bool в Python 2.3 и последующих версиях. Как упоминалось ранее в книге, тип bool реализован как подкласс int с двумя экземплярами (True и False), которые ведут себя подобно целым числам 1 и 0, но наследуют специальные методы строкового представления, отображающие их имена.
Модель классов "нового стиля"
В выпуске Python 2.2 была введена новая разновидность классов, известная как классы нового стиля; когда с ними сравнивают классы, которые следуют первоначальной и традиционной модели, их называют классическими классами. В Python З.Х два вида классов были объединены, но для пользователей и кода на Python 2.Х они остаются раздельными.
• В Python З.Х все классы являются тем, что прежде называлось “новым стилем”, унаследованы они явно от object или нет. Указывать суперкласс object не обязательно, и он подразумевается.
• В Python 2.Х классы должны явно наследоваться от object (или другого встроенного типа), чтобы считаться “новым стилем” и получить все линии поведения нового стиля. Без такого наследования классы будут “классическими”.
Поскольку в Python З.Х все классы автоматически становятся классами нового стиля, в этой линейке возможности классов нового стиля считаются просто нормальными функциональными средствами классов. Но я решил оставить их описания здесь раздельными из уважения к пользователям кода на Python 2.Х — классы в таком коде получают возможности и поведение нового стиля только в случае наследования от object.
Другими словами, когда пользователи Python З.Х видят в книге темы, касающиеся “нового стиля”, они должны принимать их за описания существующих характеристик своих классов. Для читателей, использующих Python 2.Х, они будут набором необязательных изменений и расширений, которые могут быть включены или нет, если только эксплуатируемый код уже их не задействовал.
В Python 2.Х идентифицирующее синтаксическое отличие для классов нового стиля связано с тем, что они унаследованы либо от встроенного типа, такого как list, либо от особого встроенного класса object. Встроенное имя object предназначено для того, чтобы служить суперклассом для классов нового стиля, если никакой другой встроенный тип не подходит:
class newstyle(object) : # Явное наследование для класса нового
# стиля в Python 2.Х
. . .нормальный код класса. . . # Не требуется в Python З.Х:
# все происходит автоматически
Любой класс, производный от obj ect или любого другого встроенного типа, автоматически трактуется как класс нового стиля. То есть при условии нахождения встроенного типа где-то в дереве суперклассов класс Python 2.Х получает поведение и расширения классов нового стиля. Классы, не являющиеся производными от встроенных типов вроде object, считаются классическими.
Что нового в новом стиле?
Как мы увидим, классы нового стиля обладают глубокими отличиями, которые оказывают широкое влияние на программы, особенно когда код задействует добавленные в них расширенные возможности. На самом деле, по крайней мере, с точки зрения их поддержки ООП, эти изменения на ряде уровней превращают Python в совершенно особый язык. Они обязательны в линейке Python З.Х, необязательны в линейке Python 2.Х, но только если игнорируются каждым программистом, и в данной области заимствуют намного большее из других языков (и часто обладают сравнимой сложностью).
Классы нового стиля частично являются результатом попытки объединить понятие класса с понятием типа во времена существования версии Python 2.2, хотя для многих они оставались незамеченными, пока не стали необходимым знанием в Python З.Х. Вам нужно самостоятельно оценить успех такого объединения, но как мы выясним, в модели все еще присутствуют различия — теперь между классом и метаклассом — и один из побочных эффектов заключается в том, что нормальные классы оказываются более мощными, но также и значительно более сложными. Скажем, формализованный в главе 40 алгоритм наследования нового стиля усложнился минимум в два раза.
Тем не менее, некоторые программисты, имеющие дело с прямолинейным прикладным кодом, могут заметить только легкое отклонение от традиционных “классических” классов. Ведь нам же удалось добраться до этого места в книге и попутно реализовать значимые примеры, главным образом просто упоминая о данном изменении. Кроме того, модель классических классов, по-прежнему доступная в Python 2.Х, работает в точности так, как работала на протяжении более двух десятилетий.
Однако поскольку классы нового стиля модифицируют основные линии поведения классов, их пришлось вводить в Python 2.Х в виде отдельного инструмента, чтобы избежать влияния на любой существующий код, который полагается на предыдущую модель. Например, тонкие отличия, такие как поиск в иерархии наследования с ромбовидными схемами и взаимодействие встроенных операций и методов управляемых атрибутов наподобие_getattr_, могут приводить к отказу работы некоторого существующего кода, если оставить его без изменений. Применение необязательных расширений в новой модели вроде слотов может дать аналогичный эффект.
Разделение между моделями классов было устранено в линейке Python З.Х, предписывающей использование классов нового стиля, но оно все еще присутствует для читателей, которые применяют Python 2.Х или эксплуатируют в производственной среде большой объем кода на Python 2.Х. Поскольку классы нового стиля в Python 2.Х были необязательным расширением, написанный для данной линейки код мог использовать любую из двух моделей классов.
В следующих двух разделах верхнего уровня приведен обзор отличий классов нового стиля и новых инструментов, которые они предлагают. Рассматриваемые в них темы представляют потенциальные изменения некоторым читателям, работающим с Python 2.Х, но просто дополнительные расширенные возможности классов для многих читателей, применяющих Python З.Х. Если вы относитесь ко второй группе, тогда найдете здесь полное описание, несмотря на то, что его часть дается в контексте изменений. Вы вполне можете воспринимать изменения как функциональные средства, но только в случае, если вам никогда не придется иметь дело с любыми из миллионов строк существующего кода на Python 2.Х.
Изменения в классах нового стиля
Классы нового стиля отличаются от классических классов в нескольких аспектах, часть которых являются тонкими, но способными повлиять на существующий код на Python 2.Х и распространенные стили написания кода. Ниже представлены наиболее заметные отличия как предварительный обзор.
Извлечение атрибутов для встроенных операций: экземпляр пропускается
Обобщенные методы перехвата извлечения атрибутов__getattr__и
_getattribute_по-прежнему выполняются для атрибутов, к которым производится доступ по явному имени, но больше не запускаются для атрибутов, неявно извлекаемых встроенными операциями. Они не вызываются для имен
методов перегрузки операций_X_только во встроенных контекстах — поиск
таких имен начинается в классах, не в экземплярах. Это нарушает работу или усложняет объекты, которые служат в качестве промежуточных для интерфейса другого объекта, если внутренние объекты реализуют перегрузку операций. Методы подобного рода должны быть переопределены из-за отличающегося координирования встроенных операций в классах нового стиля.
Классы и типы объединены: проверка типа
Классы теперь являются типами, а типы — классами. На самом деле по существу они представляют собой синонимы, хотя метаклассы, которые теперь относятся к категории типов, все еще кое в чем отличаются от нормальных классов. Встроенная функция type (I) возвращает класс, из которого создан экземпляр, а не обобщенный тип экземпляра, и обычно дает такой же разультат, как
I._class_. Более того, классы являются экземплярами класса type, и можно реализовывать подклассы type для настройки создания классов с помощью метаклассов, записываемых посредством операторов class. Это может повлиять на код, который проверяет типы или по-другому полагается на предыдущую модель классов.
Автоматический корневой класс object: стандартные методы
Все классы нового стиля (отсюда и типы) наследуются от object, который содержит небольшой набор стандартных методов перегрузки операций (скажем, _герг_). В Python З.Х класс object автоматически добавляется выше определяемых пользователем корневых (т.е. самых верхних) классов в дереве и не нуждается в явном указании в качестве суперкласса. Это может повлиять на код, который допускает отсутствие стандартных методов и корневых классов.
Порядок поиска в иерархии наследования: MRO и ромбы
Ромбовидные схемы множественного наследования имеют слегка отличающийся порядок поиска — грубо говоря, в ромбах поиск производится раньше и более в стиле сначала в ширину, чем сначала в глубину. Такой порядок поиска атрибутов, известный как MRO, можно отследить с помощью нового атрибута
_mro_, доступного в классах нового стиля. Новый порядок поиска в основном
применяется только к деревьям классов с ромбами, хотя сам подразумеваемый корень object новой модели образует ромб во всех деревьях множественного наследования. Код, который полагается на предыдущий порядок, не будет работать таким же образом.
Алгоритм наследования: глава 40
Алгоритм, используемый при наследовании в классах нового стиля, существенно сложнее, чем модель “сначала в глубину” классических классов, и включает особые случаи для дескрипторов, метаклассов и встроенных операций. Мы не в состоянии формально описать этот алгоритм до главы 40, где будут более подробно рассматриваться метаклассы и дескрипторы, но отметим, что он может оказать воздействие на код, в котором не ожидаются связанные с ним добавочные ухищрения.
Новые расширенные инструменты: влияние на код
Классы нового стиля обладают новыми механизмами, в том числе слотами, свойствами, дескрипторами, встроенной функцией super и методом
_getattribute_. Большинство из них ориентированы на решение весьма
специфических задач построения инструментов. Тем не менее, их применение также способно повлиять или нарушить работу существующего кода; например, слоты иногда вообще препятствуют созданию словарей пространств имен экземпляров, а обобщенные обработчики атрибутов могут требовать написания другого кода.
Мы исследуем расширения, упомянутые в последнем пункте, в отдельном разделе далее в главе и, как отмечалось выше, отложим раскрытие алгоритма, используемого при наследовании, до главы 40. Однако поскольку другие элементы в приведенном списке потенциально способны нарушить функционирование традиционного кода на Python, давайте пристальнее взглянем на каждый из них по очереди.
, .„ Замечание по содержанию. Имейте в виду, что изменения, введенные в классы нового стиля, применимы к обеим линейкам Python З.Х и 2.Х, хотя в Python 2.Х | они являются лишь возможным вариантом. Как в главе, так и в книге в целом
^ функциональные особенности помечаются как изменения линейки Python З.Х, чтобы противопоставить их с традиционным кодом на Python 2.Х.
Тем не менее, некоторые из них формально появились в классах нового стиля — они обязательные в Python З.Х, но могут встречаться также в коде на Python 2.Х Здесь мы часто указываем на такое различие, но его не следует воспринимать категорично. Усложняя различие, одни изменения, касающиеся классов в Python З.Х, объясняются появлением классов нового
стиля (скажем, пропуск_getattr_для методов операций), тогда как
другие — нет (например, замена несвязанных методов функциями). Кроме того, многие программисты на Python 2.Х придерживаются использования классических классов, игнорируя то, что они считают функциональной особенностью Python З.Х. Однако классы нового стиля не новы и применяются в обеих линейках Python — раз уж они появляются в коде на Python
2.Х, то обязательны для изучения также пользователями Python 2.Х.
Процедура извлечения атрибутов для встроенных операций пропускает экземпляры
Мы представляли это изменение, введенное в классах нового стиля, во врезках в главах 28 и 31 по причине его влияния на предшествующие примеры и темы. В классах нового стиля (и потому во всех классах в Python З.Х) обобщенные методы перехвата атрибутов экземпляров_getattr_и_getattribute_больше не вызываются
встроенными операциями для имен методов перегрузки операций_X_— поиск таких имен начинается с классов, а не экземпляров. Тем не менее, доступ к атрибутам по явным именам обрабатывается упомянутыми методами, несмотря на наличие у них
имен_X_. Следовательно, такое изменение касается главным образом поведения
встроенных операций.
Говоря более формально, если в классе определен метод перегрузки операции
индексирования_getitem_и X представляет собой экземпляр данного класса,
тогда выражение индексирования вида X [ I ] приблизительно эквивалентно вызову
X._getitem_(I) для классических классов, но type (X) ._getitem_(X, I) для
классов нового стиля. Второе выражение начинает свой поиск в классе, поэтому пропускает шаг поиска_getattr_в экземпляре для неопределенного имени.
В действительности поиск метода для встроенных операций вроде X [ I ] использует нормальную процедуру наследования, начинающуюся с уровня класса, и инспектирует только словари пространств имен всех классов, от которых наследуется X . Такое различие может иметь значение в модели метаклассов, которую мы кратко рассмотрим позже в настоящей главе и более подробно в главе 40 — в рамках данной модели классы способны вести себя по-разному. Однако процедура поиска для встроенных операций пропускает экземпляр.
Почему изменился поиск?
Вы можете найти формальные обоснования данного изменения где-то в другом месте; в этой книге нечасто приводятся обстоятельства, объясняющие причину введения того или иного изменения, которое нарушает работу многих существующих программ. Но оно представляется как путь оптимизации и решение, казалось бы, неясной проблемы схемы вызовов. Первое логическое обоснование подкрепляется частотой применения встроенных операций. Скажем, если каждая операция + требует выполнения дополнительных шагов для экземпляра, то она может уменьшить быстродействие программ — особенно с учетом множества расширений на уровне атрибутов в модели нового стиля.
Второе логическое обоснование менее ясно и описано в руководствах по Python; выражаясь кратко, оно отражает сложную проблему, привнесенную моделью мета-классов. Так как классы теперь являются экземплярами метаклассов и поскольку в метаклассах могут определяться методы встроенных операций для обработки классов, генерируемых метаклассами, то вызов метода, запускаемый для класса, обязан пропустить сам класс и произвести поиск на один уровень выше, чтобы подобрать метод, который обработает этот класс, а не выбрать собственную версию метода, принадлежащую классу. Собственная версия привела бы к вызову несвязанного метода, потому что собственный метод класса обрабатывает экземпляры более низкого уровня. Это всего лишь обычная модель несвязанных методов, которая обсуждалась в предыдущей главе, но она потенциально усугубляется тем фактом, что классы способны получать от метаклассов также и поведение типов.
В результате, поскольку классы сами по себе являются и типами, и экземплярами, при поиске методов встроенных операций все экземпляры пропускаются. Такой прием применяется к нормальным экземплярам предположительно ради единообразия и согласованности, но для невстроенных имен, а также прямых и явных обращений к встроенным именам по-прежнему осуществляется проверка экземпляра. Несмотря на то что вероятно это последствие, обусловленное введением модели классов нового стиля, для ряда программистов оно может выглядеть как решение, принятое в пользу менее естественного и более неясного принципа, нежели широко применяемый ранее. Его роль в качестве пути оптимизации кажется в большей степени оправданной, но также не без оговорок.
В частности, изменение поиска оказывает потенциально обширное влияние на классы, основанные на делегировании, которые часто называют классами-посредниками, когда внедренные объекты реализуют перегрузку операций. В классах нового стиля такой класс-посредник обычно должен переопределять любые имена подобного рода для перехвата и делегирования, либо вручную, либо посредством инструментов. Конечным результатом становится либо значительное усложнение, либо полный отказ от целой категории программ. Мы исследовали делегирование в главах 28 и 31; оно является распространенной схемой, используемой для дополнения или адаптации интерфейса другого класса — чтобы добавить проверку достоверности, трассировку, измерение времени и многие другие разновидности логики. Хотя в типичном коде на Python классы-посредники могут быть скорее исключением, чем правилом, многие программы полагаются на них.
Последствия для перехвата атрибутов
Выполнив показанное ниже взаимодействие под управлением Python 2.Х, чтобы посмотреть, чем отличаются классы нового стиля, мы обнаружим, что индексирование и вывод в традиционных классах направляются методу_getattr_, но в классах
нового стиля вывод использует стандартный метод:
>>> class С:
data = ' spam'
def_getattr_(self, name) : # Классический класс в Python 2.X:
# перехватывает встроенные операции
print(name)
return getattr (self .data, name)
»> х = со »> Х[0]
_getitem_
' s'
>>> print(X) # Классический класс не наследует стандартный метод
_str_
spam
>>> class С(object): # Класс нового стиля в Python 2.Х и З.Х
. ..оставшаяся часть класса не изменялась...
»> X = С() # Встроенные операции не направляются getattr
»> Х[0]
TypeError: 'С' object does not support indexing Ошибка типа: объект С не поддерживает индексирование >» print (X)
<_main_.С object at 0х02205780>
Несмотря на очевидную рационализацию в плане методов метакласса для класса и оптимизации встроенных операций, данное расхождение не касается нормальных экземпляров, имеющих метод_getattr_, и применяется только к встроенным операциям — не к нормально именованным методам или явным вызовам встроенных методов по имени:
>>> class С: pass # Классический класс Python 2.Х
»> X = С()
»> X.normal = lambda: 99 >>> X.normal ()
99
>>> X._add = lambda (у) : 88 + у
»> X._add_(1)
89
»> X + 1
89
»> class С(object): pass # Класс нового стиля Python 2.X/3.X »> X = C()
>>> X.normal = lambda: 99
»> X.normal () # Нормальные методы по-прежнему поступают из экземпляра
99
>>> X._add = lambda (у) : 88 + у
>>> X._add (1) # То же самое для явных встроенных имен
89
»> X + 1
TypeError: unsupported operand type(s) for +: 'C' and 'int'
Ошибка типа: неподдерживаемые типы операндов для +: С и int
Такое поведение наследуется методом перехвата атрибутов_getattr_:
>» class С (object) :
def_getattr_(self, name) : print (name)
»> X = C()
»> X.normal # Нормальные имена по-прежнему направляются getattr
normal
>>> X._add # Прямые вызовы по имени тоже, но выражения - нет!
_add_
»> X + 1
TypeError: unsupported operand type(s) for +: ' C' and 'int'
Ошибка типа: неподдерживаемые типы операндов для +: Си int
Требования к коду классов-посредников
В более реалистичном сценарии делегирования это значит, что встроенные операции вроде выражений больше не работают таким же образом, как эквивалентные им традиционные прямые вызовы. И наоборот, прямые обращения к именам встроенных методов по-прежнему работают, но эквивалентные выражения — нет, потому что вызовы через тип терпят неудачу для имен не на уровне класса и выше. Другими словами, это различие возникает только во встроенных операциях, явные извлечения выполняются корректно:
>» class С (object) : data = ' spam'
def_getattr_(self, name) :
print ('getattr: ' + name) return getattr (self .data, name)
»> X = C()
>>> X. getitem_(1) # Традиционное отображение работает,
# но отображение нового стиля -нет
getattr: _getitem_
' Р1
»> Х[1]
TypeError: 'С' object does not support indexing Ошибка типа: объект С не поддерживает индексирование »> type(X) . getitem (X, 1)
AttributeError: type object 'С1 has no attribute '_getitem_'
Ошибка атрибута: объект типа С не имеет атрибута _getitem_
>>> X._add (1 eggs1) # То же самое для +: экземпляр пропускается
# только для выражения
getattr: _add_
'spameggs'
»> X + 'eggs'
TypeError: unsupported operand type(s) for +: 'C' and 'str'
Ошибка типа: неподдерживаемые типы операндов для +: С и str »> type(X)._add (X, ’eggs')
AttributeError: type object ' С ' has no attribute '_add_'
Ошибка атрибута: объект типа С не имеет атрибута _add_
Подытожим: при написании кода посредника объекта, к интерфейсу которого частично могут обращаться встроенные операции, классы нового стиля требуют метода _getattr_для нормальных имен, а также переопределений методов для всех
имен, к которым имеют доступ встроенные операции — кодируются они вручную, получаются из суперклассов или генерируются инструментами. Когда переопределения включаются подобным образом, вызовы через экземпляры и через типы эквивалентны встроенным операциям, хотя переопределенные имена больше не направляются обобщенному обработчику неопределенных имен_getattr_даже для явных обращений к именам:
»> class С(object): # Новый стиль: Python З.Х и 2.Х
data = 'spam'
def getattr_(self, name) : § Перехватывать нормальные имена
print ('getattr: ’ + name) return getattr (self .data, name) def getitem (self, i) : # Переопределить встроенные операции
print ('getitem: ' ♦ str(i))
return self.data[i]
def_add_(self, other) :
# Выполнить выражение или getattr add ')(other)
print('add: ' + other) return getattr(self.data,
»> X = С ()
>>> X.upper
getattr: upper
<built-in method upper of str object at 0x0233D670>
>>> X.upper() getattr: upper 'SPAM'
»> X[l]
# Встроенная операция (неявная)
getitem: 1 'p'
»> X._getitem_(1)
# Традиционный эквивалент (явный)
getitem: 1 'P'
»> type(X)._getitem_(X, 1)
# Эквивалент нового стиля
getitem: 1 1P'
>>> X + 'eggs' # To же самое для + и остальных
add: eggs 'spameggs'
»> X._add_(’eggs')
add: eggs 'spameggs'
>>> type(X) ._add_(X, 'eggs')
add: eggs ’spameggs'
Дополнительные сведения
Мы возвратимся к обсуждению данного изменения в главе 40, посвященной метаклассам, а также на примере в контекстах управления атрибутами в главе 38 и декораторах защиты доступа в главе 39. В последней из упомянутых глав мы вдобавок исследуем кодовые структуры для обобщенного снабжения посредников обязательными методами операций — вовсе не невозможная задача, которую может понадобиться решить только раз, если сделать это хорошо. Дополнительные сведения о разновидностях кода, затрагиваемых такой проблемой, ищите в более поздних главах и в предшествующих примерах из глав 28 и 31.
Поскольку проблема будет подробно рассматриваться позже в книге, здесь приводится только краткое описание. Ниже приведено несколько рекомендаций.
• Рецепты по инструментам. Ознакомьтесь с рецептом на Python, доступным по ссылке http: //code. activestate.com/recipes/252151 и описывающим инструмент, который автоматически представляет специальные имена методов как обобщенные диспетчеры вызовов в классе-посреднике, созданном с помощью рассматриваемых далее в главе методик метаклассов. Тем не менее, данный инструмент по-прежнему должен предложить вам передать имена методов операций, которые внутренний объект может реализовать (он обязан, т.к. компоненты интерфейса внутреннего объекта могут быть унаследованы от произвольных источников).
• Другие подходы. Поиск в веб-сети в наши дни выявит множество дополнительных инструментов, которые аналогично заполняют классы-посредники методами для перегрузки; это широко распространенная задача! Кроме того, в главе 39 будет показано, каким образом писать код прямолинейных и универсальных суперклассов, которые предоставят требующиеся методы или атрибуты как подмешиваемые, без метаклассов, избыточной генерации кода или подобных сложных методик.
Разумеется, с течением времени история может эволюционировать, но она была проблемой на протяжении многих лет. На сегодняшний день классические классы-посредники для объектов, перегружающих любые операции, фактически не могут работать как классы нового стиля. И в Python 2.Х, и в Python З.Х такие классы требуют написания кода или генерации оболочек для всех неявно вызываемых методов операций, которые может поддерживать внутренний объект. Решение не идеально для программ подобного рода (некоторые посредники могут требовать десятков методов оболочки; потенциально свыше 50!), но оно отражает или, по крайней мере, является артефактом целей проектирования у разработчиков классов нового стиля.
Обязательно ознакомьтесь с описанием метаклассов в главе 40, чтобы уви-I деть еще одну демонстрацию проблемы и ее логическое обоснование. Там
I мы также покажем, что такое поведение встроенных операций квалифици
руется как особый случай в наследовании нового стиля. Хорошее понимание этого требует большего объема сведений о метаклассах, чем способна предложить текущая глава; заслуживающий сожаления побочный продукт метаклассов в целом связан с тем, что они стали предварительным условием для более широкого употребления, нежели могли предвидеть их создатели.
Изменения модели типов
Перейдем к следующему изменению, привнесенному новым стилем: в зависимости от вашей оценки различие между типом и классом в классах нового стиля либо значительно сократилось, либо полностью исчезло, как описано ниже.
Классы являются типами
Объект type генерирует классы как свои экземпляры, а классы генерируют экземпляры самих себя. Оба считаются типами, потому что они генерируют экземпляры. На самом деле не существует реальной разницы между встроенными типами, такими как списки и строки, и определяемыми пользователем типами, реализованными в виде классов. Вот почему мы можем создавать подклассы встроенных типов, как было показано ранее в главе — подкласс встроенного типа наподобие list квалифицируется как класс нового стиля и становится новым типом, определяемым пользователем.
Типы являются классами
Новые типы, генерирующие классы, могут быть реализованы на Python как метаклассы, рассматриваемые позже в главе — подклассы определяемого пользователем типа, которые записываются посредством нормальных операторов class и управляют созданием классов, являющихся их экземплярами. Как будет показано, метаклассы являются одновременно классами и типами, хотя они отличаются вполне достаточно, чтобы поддерживать разумный аргумент в пользу того, что предшествующее разветвление “тип/класс” превратилось в “метакласс/класс”, возможно ценой добавочной сложности в нормальных классах.
Помимо появления возможности создавать подклассы встроенных типов и реализовывать метаклассы один из самых практических контекстов, где такое объединение “тип/класс” становится наиболее очевидным, касается явной проверки типов. Для классических классов Python 2.Х типом экземпляра класса является обобщенный “экземпляр” (instance), но типы встроенных объектов более специфичны:
С:\code> с:\python27\python
>» class С: pass # Классические классы в Python 2.Х
>>> I = С{) # Экземпляры создаются из классов »> type(I), I._class_
(<type ’instance'>, cclass _main_.C at 0x02399768>)
>» type(C) # Но классы не являются тем же, что и типы
<type 'classobj'>
>>> С._class_
AttributeError: class С has no attribute 1_class_'
Ошибка атрибута: класс С не имеет атрибута _class
»> type([l, 2, 3]) , [1, 2, 3] ._class_
(ctype ’list'>, ctype 'list’>)
>» type (list), list._class_
(<type 'type'>, <type 'type*>)
Однако для классов нового стиля в Python 2.Х типом экземпляра класса будет класс, из которого он создавался, поскольку классы представляют собой просто определяемые пользователем типы — типом экземпляра является его класс, а тип класса, определяемого пользователем, такой же, как тип встроенного объекта. Теперь классы тоже имеют атрибут_class_, потому что они считаются экземплярами type:
С:\code> с:\python27\python
>>> class С (object) : pass # Классы нового стиля в Python 2.Х
>>> I = С() # Типом экземпляра будет класс, из которого он создавался >>> type(I), I._class_
(<class '_main_.C>, <class '_main_.C’>)
>>> type(C) , C._class_ # Классы являются определяемыми пользователем типами
(<tvpe 'type’>, <type 'type'>)
To же самое справедливо для всех классов в Python З.Х, т.к. все классы автоматически относятся к новому стилю, даже если для них явно не указаны суперклассы. Фактически различие между встроенными типами и типами определяемых пользователем классов в Python З.Х, похоже, вообще исчезло:
С:\code> с:\python37\python >>> class С: pass
>>> I = С() # Все классы в Python З.Х являются классами нового стиля
»> type(I), I._class_ # Типом экземпляра будет класс, из которого он создавался
(<class '_main_.С’>, cclass '_main_.С’>)
>» type (С) , С._class__# Класс - это тип, а тип - это класс
(cclass 'type'>, cclass 'type’>)
>» type ([ 1, 2, 3]), [1, 2, 3] ._class_
(cclass 'list^, cclass ’list'>)
»> type (list) , list._class_ # Классы и встроенные типы работают одинаково
(cclass 'type'>, cclass * type’>)
Как видите, в Python З.Х классы — это типы, но типы — также и классы. Формально каждый класс генерируется метаклассом, т.е. классом, который обычно является либо самим type, либо подклассом type, настроенным для расширения или управления сгенерированными классами. Кроме влияния на код, который делает проверку типов, это оказывается важной привязкой для разработчиков инструментов. Мы обсудим метаклассы более подробно позже в настоящей главе и еще подробнее в главе 40.
Последствия для проверки типов
Помимо предоставления настройки встроенных типов и привязками метаклассов объединение классов и типов в модели классов нового стиля способно оказывать влияние на код, в котором выполняется проверка типов. Например, в Python З.Х типы экземпляров классов сравниваются напрямую и содержательно, плюс тем же самым способом, что и объекты встроенных типов. Это следует из того факта, что теперь классы являются типами, а типом экземпляра будет класс экземпляра:
С:\code> с:\python37\python >>> class С: pass >>> class D: pass
»> с, d = C(), D()
>>> type (с) == type (d) # Python З.Х: сравниваются классы экземпляров False
>>> type(с), type(d)
(<class '_main_.C’>, cclass '_main_.D'>)
>>> c._class_, d._class_
(cclass '_main_.C'>, cclass '_main_.D'>)
»> cl, c2 = C() , C()
>>> type(cl) == type(c2)
True
Тем не менее, с классическими классами в Python 2.Х сравнение типов экземпляров практически бесполезно, потому что все экземпляры имеют тот же самый тип instance. Чтобы действительно сравнивать типы, потребуется сравнивать атрибуты
_class_экземпляров (если вас заботит переносимость, то такой прием работает и
в Python З.Х, но там он необязателен):
C:\code> c:\python27\python >>> class С: pass »> class D: pass
»> с, d = C() , D()
>>> type (с) = type(d) # Python 2.X: все экземпляры имеют тот же самый тип! True
>>> с._class_== d._class_ # При необходимости классы можно
# сравнивать явно
False
>>> type (с), type(d)
(ctype 'instance'>, ctype ' instance'>)
>>> c._class_, d._class_
(cclass _main_.C at 0x024585A0>, cclass _main_.D at 0x024588D0>)
И как вы уже к данному моменту должны ожидать, классы нового стиля в Python 2.Х работают в этом отношении точно так же, как все классы в Python З.Х — при сравнении типов экземпляров автоматически сравниваются классы экземпляров:
C:\code> с:\python27\python »> class С (object) : pass >>> class D(object): pass
>» сf d = C() , D()
>>> type(c) == type(d) # Классы нового стиля в Python 2.Х: так же,
# как все классы в Python З.Х
False
>» type(с), type(d)
(<class '_main_.C>, <class 1_main_,D'>)
>» c._class_, d._class_
(<class '_main_.C'>, <class '_main_.D’>)
Конечно, как я уже неоднократно отмечал, проверка типов обычно считается неправильным действием в программах на Python (мы пишем код для интерфейсов объектов, а не типов объектов). Более универсальная встроенная функция isinstance вероятнее всего будет тем, что вы захотите использовать в тех редких ситуациях, когда должны запрашиваться типы экземпляров классов. Однако знание модели типов Python может помочь пролить свет на модель классов в целом.
Все классы являются производными от object
Еще одно последствие изменения типов в модели классов нового стиля состоит в том, что поскольку все классы являются производными (унаследованными) от класса object, либо неявно, либо явно, и по причине того, что теперь все типы — это классы, каждый объект оказывается производным от встроенного класса object, будь то напрямую или через суперкласс.
Взгляните на следующее взаимодействие в Python З.Х:
>>> class С: pass # Для классов нового стиля
»> X = С()
>» type(X) , type (С) # Тип - это экземпляр класса, из которого он создавался
(cclass '_main_.С'>, cclass 'type’>)
Как и ранее, типом экземпляра класса будет класс, из которого он был создан, а типом класса — класс type, потому что классы и типы объединены. Тем не менее, также верно и то, что экземпляр и класс унаследованы от встроенного класса и типа obj ect, т.е. неявного или явного суперкласса каждого класса:
>>> isinstance(X, object)
True
>>> isinstance(С, object) # Классы всегда унаследованы от object True
Предшествующие вызовы isinstance в наши дни возвращают одинаковые результаты для классических классов и классов нового стиля в Python 2.Х, хотя результаты type из Python 2.Х отличаются. Как вскоре будет показано, более важно то, что тип object не добавляется и потому отсутствует в кортеже_bases_классических классов Python 2.Х, следовательно, obj ect не может считаться подлинным суперклассом.
То же самое отношение остается справедливым для встроенных типов вроде списков и строк, т.к. в модели нового стиля типы являются классами — встроенные типы теперь стали классами и их экземпляры тоже унаследованы от object:
>>> type(’spam'), type(str)
(cclass 'str'>, cclass 'type'>)
>>> isinstance(1 spam' , object) # To же самое для встроенных типов (классов) True
>» isinstance (str, object)
True
В действительности сам type унаследован от object, a object от type, хотя они представляют собой разные объекты — циклическое отношение, которое завершает объектную модель и происходит из того факта, что типы являются классами, генерирующими классы:
>>> type (type) # Все классы - это типы и наоборот
<class 'type'>
>» type (object)
cclass ’type 1>
»> isinstance (type, object) # Все классы являются производными от object,
# даже type
True
>>> isinstance (object, type) # Типы создают классы и type - это класс True
>>> type is object
False
Последствия для стандартных методов
Приведенные выше сведения могут показаться непонятными, но с этой моделью связано несколько практических последствий. Прежде всего, временами мы должны знать о стандартных методах, которые поступают из явного или неявного корневого класса obj ect в классах нового стиля:
с: \code> ру -2 >>> dir(object)
['_class_'_delattr_'_doc_'_format_1_getattribute_
'_hash_1_init_'_new_’, '_reduce_'_reduce_ex_
'_repr_', '_setattr_'_sizeof_'_str_'_subclasshook_' ]
>>> class C: pass
>>> C._bases__# Классические классы не унаследованы от object
О
»> X = С()
>>> X._герг_
AttributeError: С instance has no attribute 1_repr_'
AttributeError: экземпляр С не имеет атрибута _repr_
>>> class С (object) : pass # Классы нового стиля наследуют стандартные
# методы object
»> С._bases_
(ctype 'object'>,)
»> х = со
»> X._repr_
cmethod-wrapper '_repr_' of С object at 0x00000000020B5978>
с: \code> py -3
>>> class C: pass # Это означает, что в Python З.Х все классы
# получают стандартные методы
>» С._bases_
(cclass 'object’>,)
>» С() ._repr_
cmethod-wrapper '_repr_1 of С object at 0x0000000002955630>
Модель классов нового стиля также учитывает меньше особых случаев, чем бывшее в модели классических классов различие между типом и классом, и позволяет нам писать код, который безопасно предполагает наличие и пользуется суперклассом object (скажем, принимая его в качестве “привязки” в рассматриваемых далее ролях встроенной функции super и передавая ему вызовы методов для запуска стандартной линии поведения). Мы продемонстрируем примеры с участием super позже в книге, а пока давайте займемся исследованием последнего значительного изменения в модели классов нового стиля.
Изменение ромбовидного наследования
Финальное изменение в модели классов нового стиля также является одним из самых заметных: слегка отличающийся порядок поиска для так называемых деревьев множественного наследования с ромбовидными схемами. Наличие в таких деревьях более чем одного суперкласса приводит к тому же самому расположенному выше суперклассу (их название происходит от ромбовидной формы дерева, если вы его нарисуете — квадрат, опирающийся на один из его углов).
Ромбовидная схема — довольно сложная концепция проектирования, которая встречается только в деревьях множественного наследования и обычно редко применяется в практике программирования на Python, поэтому мы не будем раскрывать данную тему особенно глубоко. Однако отличающиеся порядки поиска были кратко представлены при рассмотрении множественного наследования в предыдущей главе.
Для классических классов (стандарт в Python 2.Х): DFLR
Путь поиска при наследовании проходит строго сначала в глубину и затем слева направо — Python поднимается все время к верху, придерживаясь левой стороны дерева, и только потом останавливается и начинает просмотр дальше вправо. Такой порядок поиска известен как DFLR (Depth-First, Left-to-Right — сначала в глубину, слева направо).
Для классов нового стиля (необязательные в Python 2.Х и автоматические в Python З.Х): MRO
Путь поиска при наследовании в ромбовидных схемах выполняется больше в манере сначала в ширину — Python сначала ищет в любых суперклассах справа от только что просмотренного и только потом поднимается к общему суперклассу вверху. Другими словами, поиск проходит по уровням, прежде чем двигаться вверх. Такой порядок поиска называется MRO нового стиля (Method Resolution Order — порядок распознавания методов), а часто ради краткости — просто MRO, когда используется для противопоставления с порядком DFLR. Несмотря на свое название, он применяется для всех атрибутов в Python, а не только для методов.
Алгоритм MRO нового стиля немного сложнее, чем было представлено выше (и позже мы опишем его более формально), но именно столько нужно знать многим программистам. Тем не менее, важно отметить, что он обладает не только важными преимуществами для кода с классами нового стиля, но и потенциалом нарушения работы существующего кода с классическими классами.
Например, алгоритм MRO нового стиля дает возможность нижним суперклассам перегружать атрибуты верхних суперклассов, не обращая внимания на разновидности деревьев множественного наследования, в которых они смешаны. Кроме того правило поиска нового стиля позволяет избежать посещения того же самого суперкласса более одного раза, когда он доступен из множества подклассов. Вероятно алгоритм MRO лучше DFLR, но он применяется к небольшому подмножеству пользовательского кода на Python; однако, как мы увидим, модель классов нового стиля сама по себе делает ромбы гораздо более распространенными, a MRO более важным.
В то же самое время новый алгоритм MRO будет определять местонахождение атрибутов по-другому, создавая потенциальную несовместимость для классических классов Python 2.Х. Давайте перейдем к исследованию какого-нибудь кода, чтобы посмотреть, как отличия проявляются на практике.
Последствия для деревьев ромбовидного наследования
Для иллюстрации отличий поиска по алгоритму MRO нового стиля рассмотрим упрощенный пример множественного наследования с ромбовидной схемой для классических классов. Здесь суперклассы В и С класса D ведут к тому же самому общему предку А:
>>> class A: attr = 1 # Классический класс (Python 2.Х)
»> class В (А) : pass # В и С ведут к А
»> class С (А) : attr = 2
>>> class D (В, С) : pass # Проверяет А перед С
»> х = D()
>» x.attr # Ищет в х, D, В, А
1
Атрибут x.attr обнаруживается в суперклассе А, потому что в классических классах поиск при наследовании поднимается настолько высоко, насколько может, прежде чем остановиться и начать движение вправо. Полный порядок поиска DFLR посетит х, D, В, А, С и затем А. Для указанного атрибута поиск прекращается, как только attr встречается в А, выше В.
Тем не менее, в классах нового стиля, производных от встроенного типа вроде object (и во всех классах Python З.Х), порядок поиска отличается: Python просматривает С справа от В до проверки А выше В. Полный порядок поиска MRO посетит х, D, В, С и затем А. Для атрибута х. attr поиск прекращается, когда attr встречается в С:
>>> class A (object) : attr = 1 # Классы нового стиля
# (в Python З.Х указывать object не требуется)
»> class В (А) : pass »> class С (А) : attr = 2
>>> class D(B, С) : pass # Проверяет С перед А
»> х = D()
»> x.attr # Ищет в к, D, В, С
2
Изменение в процедуре поиска при наследовании основано на допущении о том, что если вы подмешиваете класс С ниже в дереве, то вероятно намереваетесь захватить его атрибуты, отдавая им предпочтение перед атрибутами из А. Также допускалось, что класс С всегда должен переопределять атрибуты А во всех контекстах. Скорее всего, так и будет, если класс С используется автономно, но может не произойти, когда он подмешивается в ромбовидную схему с классическими классами — при написании кода класса об этом можно даже не подозревать.
Однако поскольку наиболее вероятно, что программист имел в виду как раз переопределение классом С атрибутов А в подобной ситуации, классы нового стиля посещают класс С первым. Иначе в ромбовидном контексте класс С мог бы стать по существу бесполезным для любых имен в А — настройка А оказалась бы невозможной, и применялись бы только имена, уникальные для С.
Явное устранение конфликтов
Разумеется, проблема с допущениями в том, что они что-то предполагают! Если такое расхождение порядка поиска кажется слишком тонким, чтобы помнить о нем, или вам необходим более полный контроль над процессом поиска, то вы всегда можете принудительно выбрать атрибут откуда угодно в дереве, выполнив присваивание или по-другому указав желаемый атрибут в месте, где классы смешиваются. Скажем, ниже демонстрируется выбор порядка нового стиля в классическом классе за счет явного выбора:
>>> class A: attr = 1 # Классический класс
»> class В (А) : pass >» class С (А) : attr = 2
>» class D(B, С) : attr = С.attr # <== Выбор С, справа »> х = D()
>>> x.attr # Работает подобно классам нового стиля
# (всем классам в Python З.Х)
2
Здесь дерево классических классов эмулирует порядок поиска, принятый в классах нового стиля, для специфического атрибута: присваивание значения атрибуту в D выбирает версию в С, нарушая тем самым нормальный путь поиска при наследовании (D. attr будет самым нижним в дереве). Классы нового стиля могут аналогичным образом эмулировать классические классы за счет выбора более высокой версии целевого атрибута в месте, где они смешиваются:
>>> class A (object) : attr = 1 # Классы нового стиля
»> class В (А) : pass >>> class С (А) : attr = 2
>>> class D (В, С) : attr = В. attr # <== Выбор A. attr, выше »> х = D()
>» x.attr # Работает подобно классическим классам
# (стандарт в Python 2.Х)
1
Если вы готовы всегда устранять конфликты подобным образом, то можете почти полностью проигнорировать различие в порядке поиска и не полагаться на допущения о том, что вы имели в виду при написании кода своих классов.
Естественно, выбираемые таким способом атрибуты также могут быть функциями методов — методы являются нормальными поддерживающими присваивание атрибутами, которые предназначены для ссылки на вызываемые объекты функций:
»> class А:
def meth(s) : print ('A.meth1)
»> class С (A) :
def meth(s): print(fC.meth')
>>> class В(A): pass
>>> class D (В, C) : pass # Использовать стандартный порядок поиска »> х = D() # Будет варьироваться в зависимости от типа класса
»> х.meth() # Стандартный классический порядок в Python 2.Х
A.meth
>>> class D(В, С) : meth = С.meth # <== Выбирает метод из С: новый стиль
# (и Python З.Х)
»> х = D()
»> x.meth()
С.meth
>>> class D(B, C) : meth = B.meth # <== Выбирает метод из В: классический »> х = D()
»> х.meth О A.meth
Мы выбираем методы, явно присваивая значения именам ниже в дереве. Мы могли бы также просто явно обращаться к желаемому классу; на практике показанная схема может оказаться более распространенной, особенно для таких вещей, как конструкторы:
class D(В, С):
def meth(self) : # Переопределить нижний
С. meth (self) # <== Выбирает метод из С по вызову
Такой выбор по присваиванию или обращению в точках смешивания может эффективно изолировать ваш код от различия в вариантах классов. Конечно, прием применим только к атрибутам, поддерживаемым подобным образом, но явное устранение конфликтов гарантирует, что ваш код не будет варьироваться от версии к версии Python, во всяком случае, с точки зрения выбора при конфликте атрибутов. Другими словами, это способно служить методикой переносимости для классов, которые может потребоваться запускать в рамках моделей классов нового стиля и классических классов.
, . _ ^ явное луцще неявного - для распознавания методов тоже. Даже без расхождения | между классическими классами и классами нового стиля показанная здесь
i методика явного распознавания методов в целом может пригодиться во
* многих сценариях наследования. Например, если вам нужна часть суперкласса слева и часть суперкласса справа, тогда вам может понадобиться сообщить Python, какие одинаково именованные атрибуты выбирать, за счет использования в подклассах явных присваиваний или обращений. Мы еще возвратимся к этой идее при рассмотрении затруднений в конце главы.
Также обратите внимание, что наследование с ромбовидными схемами в ряде случаев может оказаться более проблематичным, чем вытекает из обсуждения выше (скажем, что если В и С оба требуют конструкторов, которые вызывают конструктор в А?). Поскольку при реальной разработке на Python такие контексты встречаются редко, мы рассмотрим эту тему после исследования встроенной функции super ближе к концу главы. Кроме предоставления обобщенного доступа суперклассам в деревьях одиночного наследования встроенная функция super поддерживает кооперативный режим для устранения конфликтов в деревьях множественного наследования путем упорядочения вызовов методов в соответствии с MRO — при условии, что такой порядок имеет смысл в данном контексте!
Пределы влияния изменения в порядке поиска
Подводя итоги, по умолчанию поиск в ромбовидной схеме для классических классов и классов нового стиля выполняется по-разному, и это изменение не является обратно совместимым. Тем не менее, имейте в виду, что данное изменение влияет главным образом на случаи множественного наследования с ромбовидными схемами; поиск при наследовании с классами нового стиля работает одинаково для большинства других структур деревьев наследования. Вдобавок не исключено, что вся проблема может иметь скорее теоретическое, нежели практическое значение. Поскольку поиск нового стиля не был достаточно важным для решения до версии Python 2.2 и не стал стандартом вплоть до версии Python 3.0, то он вряд ли повлияет на большинство кода на Python.
После сказанного я также должен отметить, что даже если вы не будете применять ромбовидные схемы в собственных классах, из-за наличия подразумеваемого суперкласса object выше любого корневого класса в Python З.Х на сегодняшний день каждый случай множественного наследования демонстрирует ромбовидную схему. То есть в классах нового стиля object автоматически исполняет ту же роль, которую исполнял класс А в рассмотренном ранее примере. Следовательно, правило поиска MRO нового стиля не только модифицирует логическую семантику, но также представляет собой важную оптимизацию производительности — оно позволяет избежать посещения и поиска в отдельно взятом классе более одного раза, даже в автоматически добавляемом классе object.
Не менее важен и тот факт, что подразумеваемый суперкласс object в модели классов нового стиля предоставляет стандартные методы для разнообразных встроенных
операций, включая методы форматов отображения_str_и_герг_. Запустите
dir (object) , чтобы получить перечень доступных методов. Без правила поиска MRO нового стиля в сценариях с множественным наследованием стандартные методы в object всегда замещали бы переопределения в пользовательских классах, если только переопределения не располагались бы в крайнем слева суперклассе. Другими словами, сама модель классов нового стиля делает использование порядка поиска нового стиля более важным!
Чтобы ознакомиться с наглядным примером работы с подразумеваемым суперклассом object в Python З.Х и другими примерами создаваемых им ромбовидных схем, просмотрите вывод класса ListTree из файла lister .ру, представленного в предыдущей главе, а также код примера обхода деревьев classtree .ру в главе 29 — равно как и материал следующего раздела.