Книга: Марк Лутц - Изучаем Python, 5-е изд., Т. 2
Назад: Детали реализации классов
Дальше: Проектирование с использованием классов

Перегрузка операций

 

В этой главе продолжается доскональное исследование механики классов с переключением внимания на перегрузку операций. Мы кратко затрагивали тему перегрузки операций в предшествующих главах, а здесь предложим дополнительные детали и рассмотрим несколько широко применяемых методов перегрузки. Хотя мы не будем демонстрировать каждый из многочисленных доступных методов перегрузки операций, те методы, которые реализованы в главе, представляют собой достаточно большую репрезентативную выборку, чтобы исчерпывающе раскрыть возможности данной характеристики классов Python.
Основы
Вообще говоря, “перегрузка операций” просто означает перехват встроенных операций в методах класса — Python автоматически вызывает ваши методы, когда экземпляры класса обнаруживаются во встроенных операциях, и возвращаемое значение вашего метода становится результатом соответствующей операции. Ниже приведен обзор ключевых идей, лежащих в основе перегрузки.
• Перегрузка операций позволяет классам перехватывать нормальные операции Python.
• Классы могут перегружать все операции выражений Python.
• Классы также могут перегружать встроенные операции, такие как вывод, вызовы функций, доступ к атрибутам и т.д.
• Перегрузка делает экземпляры классов более похожими на встроенные типы.
• Перегрузка реализуется за счет предоставления особым образом именованных методов в классе.
Другими словами, когда в классе предоставляются особым образом именованные методы, тогда Python автоматически вызывает их в случае появления экземпляров данного класса в ассоциированных с ними выражениях. Ваш класс снабжает создаваемые из него объекты экземпляров поведением соответствующей операции.
Как вам уже известно, методы перегрузки операций не являются обязательными и обычно не имеют стандартных версий (кроме нескольких, которые ряд классов получают от object). Если вы не пишете код какого-то метода или не наследуете его, то это только означает, что ваш класс не поддерживает операцию, связанную с методом. Однако в случае использования такие методы позволяют классам эмулировать интерфейсы встроенных объектов и потому выглядеть более согласованными.
Конструкторы и выражения:_init_и_sub
В качестве обзора рассмотрим следующий простой пример: класс Number из файла number .ру предоставляет метод для перехвата создания экземпляра (_init_), а
также метод для отлавливания выражений вычитания (_sub_). Специальные методы подобного рода являются привязками, которые дают возможность соединяться со встроенными операциями:
# Файл number .ру
class Number:
# Для Number (start)
# Для экземпляр - other
# Результатом будет новый экземпляр
# Извлечение класса из модуля
# Number._init_(X, 5)
# Number._sub_(X, 2)
# Y является новым экземпляром Number
def _init_(self, start):
self.data = start
def _sub_(self, other):
return Number(self.data - other)
>>> from number import Number
»> X = Number (5)
»> Y = X - 2
»> Y.data
3
Ранее мы выяснили, что реализованный в коде метод конструктора_init_является наиболее употребительным методом перегрузки операций в Python; он присутствует в большинстве классов и применяется для инициализации вновь созданного объекта экземпляра с использованием любых аргументов, указываемых после имени класса. Метод_sub_исполняет роль бинарной операции аналогично методу
_add_из введения главы 27, перехватывая выражения вычитания и возвращая в
качестве своего результата новый экземпляр класса (попутно выполняя_init_).
Формально создание экземпляра сначала запускает метод_new_, который создает и возвращает новый объект экземпляра, передаваемый затем в метод_init_для инициализации. Тем не менее, поскольку метод _new_имеет встроенную реализацию и переопределяется лишь в
крайне ограниченных ситуациях, почти все классы Python инициализируются за счет определения метода_init_. Один сценарий применения
метода_new_будет показан при изучении метаклассов в главе 40; хотя
и редко, но временами он также используется для настройки создания экземпляров неизменяемых типов.
Вы уже достаточно хорошо знаете метод_init_и методы базовых бинарных
операций вроде_sub_, так что мы не будем здесь снова подробно исследовать их
употребление. В главе мы раскроем ряд других инструментов, доступных в данной области, и предложим пример кода, который применяет их в распространенных сценариях использования.
Распространенные методы перегрузки операций
Почти все, что вы можете делать со встроенными объектами, такими как целые числа и списки, имеет соответствующие особым образом именованные методы для перегрузки в классах. Наиболее распространенные из них перечислены в табл. 30.1. В действительности многие методы перегрузки доступны в виде нескольких версий (например, _add_,_radd_и_iadd_для сложения), что является одной из причин настолько большого их количества. Полный список методов с особыми именами ищите в других книгах или в справочнике по языку Python.
Таблица 30.1. Наиболее распространенные методы перегрузки операций
Для чего вызывается
Метод
Что реализует
_init_
_del_
add
or
repr_,_str_
_call_
_getattr_
_setattr_
_delattr_
_getattribute_ _getitem_
_setitem_
_delitem_
_len_
bool
_lt_,
_le_,
_ge_
ne
_radd_
_iadd_
_iter_
next
contains
Конструктор
Деструктор Операция +
Операция | (побитовое “ИЛИ”)
Вывод, преобразования
Вызовы функций
Извлечение атрибута
Присваивание атрибута
Удаление атрибута
Извлечение атрибута
Индексирование, нарезание, итерация
Присваивание по индексу и срезу
Удаление по индексу и срезу Длина
Булевские проверки Сравнения
Правосторонние операции Дополненные на месте операции Итерационные контексты
Проверка членства
Создание объекта:
X = Class(args)
Уничтожение объекта х
х + Y, х += Y, если отсутствует _iadd_
х | Y, х | = Y, если отсутствует _ior_
print(X), repr(X), str(X)
X(*args, **kargs)
X.undefined
X.any = value
del X.any
X. any
X[key], X [ i: j ], циклы for и другие итерационные конструкции, если отсутствует_iter_
X[key] = value,
X[i : j] = iterable
del X[key], del X[i : j]
len (X), проверки истинности, если отсутствует_bool_
bool (X), проверки истинности (в Python 2.Х называется
_nonzero_)
X < Y, X > Y,
X <= Y, X >= Y, X == Y,
х ! = Y (либо иначе_сшр_
только в Python 2.Х)
Other + X
х += Y (либо иначе_add_)
l=iter(X), next (I); циклы for, in, если отсутствует _contains_, все включения, map (F,X), остальные (_next_в Python 2.X называется next)
item in X (любой итерируемый объект)
Окончание табл. 30.1
Метод
Что реализует
Для чего вызывается
index
Целочисленное значение
hex (X), bin (X), oct(X), 0 [X], 0[X: ] (заменяет oct , hex из Python 2.X)
enter , exit
Диспетчер контекста (глава 34)
with obj as var:
get , set , delete
Атрибуты дескриптора (глава 38)
X.attr, X.attr = value, del X.attr
new
Создание (глава 40)
Создание объекта, передinit
Все методы перегрузки операций имеют имена, начинающиеся и заканчивающиеся двумя символами подчеркивания, чтобы отличать их от других имен, которые вы определяете в своих классах. Отображения специальных имен методов на выражения или операции предопределено языком Python и полностью документировано в стандартном руководстве по языку и других справочных ресурсах. Скажем, имя_add_
всегда отображается на выражения + определением языка Python независимо от того, что фактически делает код метода_add_.
Хотя выражения запускают методы операций, остерегайтесь предположения о том, что существует преимущество в скорости, если исключить посредника и вызвать метод операции напрямую. На самом деле вызов метода операции напрямую может оказаться в два раза медленнее, вероятно из-за накладных расходов, связанных с вызовом функции, которых Python избегает или оптимизирует во встроенных операциях.
Ниже сравнивается скорость выполнения 1еп и_1еп_с применением
запускающего модуля Windows и методики измерения времени из главы 21 первого тома в Python 3.7 и 2.7: в обоих случаях вызов_1еп_напрямую занимает вдвое больше времени:
c:\code> ру -3 -m time it -n 1000 -г 5
L. 1еп ()и
-s "L = list(range(100))" "x 1000 loops, best of 5: 0.134 usee per loop
c:\code> py -3 -m timeit -n 1000 -r 5
-s "L = list (range (100) ) " "x = len(L)M 1000 loops, best of 5: 0.063 usee per loop
c:\code> py -2 -m timeit -n 1000 -r 5
= L. 1еп ()"
-s "L = list(range(100))" "x 1000 loops, best of 5: 0.117 usee per loop
c:\code> py -2 -m timeit -n 1000 -r 5
-s "L = list (range (100) ) " "x = len (L) "
1000 loops, best of 5: 0.0596 usee per loop
Это не настолько неестественно, как может казаться — в одном известном научном учреждении мне действительно пришлось столкнуться с рекомендациями использовать более медленную альтернативу, чтобы достичь большей скорости!
Если методы перегрузки операций не определяются, тогда они могут быть унаследованы от суперклассов в точности как любые другие методы. Кроме того, все методы перегрузки операций необязательны — если вы не пишете код или не наследуете такой метод, то связанная с ним операция просто не поддерживается вашим классом, и попытка ее применения приводит к генерированию исключения. Некоторые встроенные операции наподобие вывода имеют стандартные реализации (наследуемые от подразумеваемого класса object в Python З.Х), но большая часть встроенных операций терпят неудачу для классов, если соответствующий метод перегрузки операции отсутствует.
Большинство методов перегрузки операций используются только в развитых программах, которые требуют, чтобы объекты вели себя аналогично встроенным объектам, хотя уже рассмотренный конструктор_init_присутствует в большей части
классов. Давайте исследуем ряд дополнительных методов из табл. 30.1 на примерах.
Индексирование и нарезание:
_getitem и_setitem
Наш первый набор методов позволяет классам имитировать некоторые линии поведения последовательностей и отображений. Если метод_getitem_определен в
классе (или унаследован им), тогда он автоматически вызывается для операций индексирования экземпляров. В случае появления экземпляра X в выражении индексирования вроде X [ i] интерпретатор Python вызывает метод_getitem_, унаследованный
экземпляром, с передачей X в первом аргументе и индекса, указанного в квадратных скобках, во втором.
Например, следующий класс возвращает квадрат значения индекса — возможно нетипично, но иллюстративно для механизма в целом:
>>> class Indexer:
def_getitem_(self, index) :
return index **2
>>> X = Indexer()
>>> X[2] # Для X[i] вызывается X._getitem_(i)
4
>>> for i in range (5) :
print(X[i], end=' ') # Каждый раз выполняется_getitem_(X, i)
0 1 4 9 16
Перехват срезов
Интересно отметить, что в дополнение к индексированию метод_getitem_
также вызывается для выражений срезов — всегда в Python З.Х и условно в Python 2.Х, если вы не предоставили более специфических методов нарезания. Говоря формально, встроенные типы обрабатывают нарезание тем же способом. Скажем, ниже демонстрируется нарезание для встроенного списка с применением верхней и нижней границ и страйда (см. главу 7):
»> L = [5, 6, 7, 8, 9]
>>> Ь[2:4] # Нарезание с использованием синтаксиса: 2. . (4-1)
[1, 8]
»> L [1: ]
[6, 7, 8, 9]
»> L [: -1]
[5, 6, 7, 8]
»> L[: :2]
[5, 7, 9]
Однако на самом деле границы нарезания упаковываются в объект среза и передаются реализации индексирования списка. В действительности вы всегда можете передавать объект среза вручную — синтаксис среза по большей части является синтаксическим сахаром для индексирования объекта среза:
»> L[slice (2, 4)] # Нарезание с помощью объектов срезов
[7, 8]
»> L[slice(1, None)]
[6, 7, 8, 9]
»> L[ slice (None, -1)]
[5, 6, 7, 8]
>» L[slice(None, None, 2)]
[5, 7, 9]
В классах с методом_getitem_это важно — в Python З.Х данный метод будет вызываться для базового индексирования (с индексом) и нарезания (с объектом среза). Наш предыдущий класс не обработает нарезание, потому что его логика предполагает передачу целочисленных индексов, но следующему классу такая обработка удастся. При вызове для индексирования аргументом является целое число, как и ранее:
>» class Indexer:
data = [5, 6, 1, 8, 9]
def getitem (self, index) : # Вызывается для индексирования или нарезания print('getitem:', index)
return self .data [index] # Выполняется индексирование или нарезание »> X = Indexer ()
>>> Х[0] # Индексирование отправляет_getitem_ целое число
getitem: О
5
>>> Х[1]
getitem: 1
6
»> Х[-1]
getitem: -1 9
Тем не менее, в случае вызова для нарезания метод принимает объект среза, который просто передается индексатору внедренного списка в новом выражении индексирования:
>>> Х[2:4] # Нарезание отправляет_getitem_ объект среза
getitem: slice(2, 4, None)
[7, 8]
»> Х[1: ]
getitem: slice(1, None, None)
[6, 7, 8, 9]
»> X[:-l]
getitem: slice(None, -1, None)
[5, 6, 7, 8]
»> X[: : 2]
getitem: slice(None, None, 2)
[5, 7, 9]
Там, где необходимо, метод_getitem_может проверять тип своего аргумента
и извлекать границы объекта среза — объекты срезов имеют атрибуты start, stop и step, любой из которых можно опустить, указав None:
>» class Indexer:
def_getitem_(self, index) :
if isinstance(index, int): # Проверка режима использования print('indexing1, index) else:
print('slicing', index.start, index.stop, index.step)
>>> X = Indexer()
»> X[99]
indexing 99 »> X[1: 99 :2] slicing 1 99 2 »> X [ 1: ]
slicing 1 None None
Когда применяется метод присваивания по индексу_set item_, он похожим образом перехватывает присваивания по индексу и срезу. Во втором случае в Python З.Х (и обычно в Python 2.Х) он принимает объект среза, который может передаваться в другом присваивании по индексу либо использоваться напрямую таким же способом:
class IndexSetter:
def _setitem_(self, index, value): # Перехватывает присваивание
# по индексу или срезу
self. data [index] = value # Присваивает по индексу или срезу
На самом деле метод_getitem_может автоматически вызываться даже в большем количестве контекстов, нежели индексация и нарезание — как вы вскоре увидите, он представляет собой также запасной вариант для итерации. Однако первым делом давайте взглянем на разновидность этих операций в Python 2.Х и выясним потенциальную путаницу в данной категории.
Нарезание и индексирование в Python 2.Х
В Python 2.Х классы могут также определять методы_getslice_и_setslice_
для перехвата извлечений и присваиваний по срезу специфическим образом. Если указанные методы переопределены, тогда им передаются границы выражения среза и для
срезов с двумя пределами они предпочтительнее методов_getitem_и_setitem_.
Во всех остальных случаях такой контекст работает так же, как в Python З.Х; скажем, объект среза по-прежнему создается и передается_getitem_, если метод_getslice_
не найден или применяется расширенная форма среза с тремя пределами:
C:\code> c:\python27\python >>> class Slicer:
def_getitem_(self, index) : print index
def_getslice_(self, i, j) : print i, j
def_setslice_(self, i, j,seq): print i, j,seq
>>> Slicer() [1] # Выполняется_getitem_ с int, как и в Python З.Х
1
»> Slicer() [1:9] # Выполняется_getslice_, если присутствует, иначе_getitem_
1 9
»> Slicer () [1:9:2] # Выполняется_getitem_ с slice (), как и в Python З.Х!
slice(l, 9, 2)
В Python З.Х такие специфичные к срезам методы были удалены, поэтому даже в
Python 2.Х вы обычно должны взамен использовать_getitem_и_setitem_и
допускать передачу в аргументах индексов и объектов срезов — ради прямой совместимости и во избежание необходимости обрабатывать срезы с двумя и тремя пределами по-разному. В большинстве классов все работает без какого-либо специального кода, потому что методам индексирования можно вручную передавать объект среза в квадратных скобках другого выражения индекса, как было показано в примере из предыдущего раздела. Еще один пример перехвата срезов приведен в разделе “Членство: _contains_,_iter_и_getitem_” далее в главе.
Но метод_index_в Python З.Х не имеет отношения к индексированию!
В качестве связанного замечания: при перехвате индексирования в Python З.Х не применяйте (вероятно, неудачно названный) метод_index_— он возвращает целочисленное значение для экземпляра и используется встроенными функциями, которые выполняют преобразование в строки цифр (оглядываясь назад, лучше бы его назвали, например,_as index_):
»> class С:
def_index_(self) :
return 255
»> X = C()
>>> hex(X) # Целочисленное значение 'Oxff'
>>> bin(X)
'Obllllllll'
>» oct(X)
'0o377'
Хотя данный метод не перехватывает индексирование экземпляров подобно
_getitem_, он также применяется в контекстах, которые требуют целого числа —
включая индексацию:
»> ('С' * 256) [255]
'С'
>>> (’С1 * 256) [X] # Как индекс (не X[i])
'С'
»> ('С' * 256) [X: ] # Как индекс (HeX[i:])
'Cf
В Python 2.Х метод_index_работает точно так же, но не вызывается для встроенных функций hex и oct; для перехвата их вызовов в Python 2.Х взамен используйте методы_hex_и_oct_.
Итерация по индексам:_getitem
Существует привязка, которая не всегда очевидна для новичков, но оказывается удивительно полезной. При отсутствии более специфических методов итерации, которые будут представлены в следующем разделе, оператор for работает путем многократного индексирования последовательности от нуля до более высоких индексов, пока не обнаружится исключение выхода за границы IndexError. По этой причине _getitem_также оказывается одним из способов перегрузки итерации в Python — если он определен, тогда циклы for на каждом проходе вызывают метод_getitem
класса с последовательно увеличивающимися смещениями.
Мы имеем дело с ситуацией “реализовав одну возможность, получаем еще одну бесплатно” — любой встроенный или определяемый пользователем объект, который реагирует на индексацию, также реагирует на итерацию в цикле for:
»> class Stepper Index:
def getitem (self, i) : return self.data[i]
# X - объект Stepperlndex
# Для индексирования вызывается_getitem
# Для циклов for вызывается_getitem_
# Цикл for индексирует элементы 0. .N
»> X = StepperIndex ()
>>> X.data = ’’Spam"
»>
»> X[l]
' P1
>» for item in X:
print(i tern, end=1 ')
Spam
На самом деле это случай “реализовав одну возможность, получаем бесплатно целый набор”. Любой класс, поддерживающий циклы for, автоматически поддерживает все итерационные контексты в Python, многие из которых рассматривались в главах первого тома (итерационные контексты были представлены в главе 14 первого тома). Например, проверка членства in, списковые включения, встроенная функция тар, присваивания списков и кортежей, а также конструкторы типов будут автоматически вызывать метод_getitem_, если он определен:
»> 'р' in X # Для всех вызывается_getitem_
True
>>> [с for с in X] # Списковое включение
[’S’, 'р', 'а', 'т']
>» list (map (str. upper, X)) # Вызов map (в Python З.Х используйте list()) ['S', 'Р', 'А', 'М']
»> (а, Ь, с, d) = X # Присваивание последовательности
>» а, с, d ('S', 'а', 'т')
>» list(X) , tuple (X) , ' ' . join(X) # И так далее. . .
(['S', 'р', 'а', 1т'], ('S', 'р1, 'а', 'т'), 'Spam')
»> X
<_main_.Stepperlndex object at 0x000000000297B630>
На практике такую методику можно применять для создания объектов, которые предоставляют интерфейс последовательностей, и для добавления логики, относящейся к операциям встроенных типов последовательностей; мы возвратимся к этой идее при расширении встроенных типов в главе 32.
Итерируемые объекты:_iter_и_next_
Несмотря на то что описанный в предыдущем разделе подход с методом
_getitem_работает, в действительности он является просто запасным вариантом
для итерации. В настоящее время все итерационные контексты языка Python перед _getitem_будут сначала пытаться вызвать метод_iter_. То есть для многократного индексирования объекта они выбирают протокол итерации, который обсуждался в главе 14 первого тома; попытка индексирования предпринимается, только если объект не поддерживает протокол итерации. Вообще говоря, вы также должны отдавать предпочтение методу_iter_— он лучше поддерживает общепринятые итерационные контексты, чем способен_getitem_.
Формально итерационные контексты работают путем передачи итерируемого объекта встроенной функции iter для вызова метода_iter_, который должен возвратить итерируемый объект. Когда этот метод_next_объекта итератора предоставлен, Python будет многократно вызывать его для выпуска элементов до тех пор, пока не сгенерируется исключение Stoplteration. В качестве удобства для выполнения итерации вручную доступна также встроенная функция next — вызов next (I) представляет собой то же самое, что и I._next_(). Сущность протокола итерации иллюстрировалась на рис. 14.1 в главе 14 первого тома.
Такой интерфейс итерируемых объектов имеет более высокий приоритет и опробуется первым. В случае если метод_iter_подобного рода не найден, тогда Python
прибегает к схеме с_getitem_и как прежде многократно индексирует по смещениям, пока не возникнет исключение IndexError.
Примечание, касающееся нестыковки версий. Как упоминалось в главе 14
первого тома, только что описанный метод I._next__() итератора
в Python 2.Х называется I. next (), а встроенная функция next (I) присутствует для совместимости — она вызывает I. next () в Python 2.Х и
I._next_() в Python З.Х. Во всех остальных отношениях итераторы в
Python 2.Х работают аналогично.
Итерируемые объекты, определяемые пользователем
В схеме с методом_iter_классы реализуют определяемые пользователем итерируемые объекты, просто внедряя протокол итерации, представленный в главе 14 и детально исследованный в главе 20 первого тома. Например, в файле squares.ру с показанным ниже содержимым используется класс для создания определяемого пользователем итерируемого объекта, который генерирует квадраты по запросу, а не выдает их все сразу (согласно предыдущей врезке “На заметку!” в Python 2.Х необходимо применять next вместо_next_и print с финальной запятой):
# Файл squares.ру
class Squares:
def _init_(self, start, stop): # Сохранить состояние при создании
self.value = start - 1 self.stop = stop
def _iter_(self) : # Получить объект итератора при вызове iter
return self
def _next_(self) : # Возвратить квадрат на каждой итерации
if self, value == self, stop: # Также вызывается встроенной функцией next raise Stoplteration self.value += 1 return self.value ** 2
После импортирования его экземпляры могут появляться в итерационных контекстах в точности как встроенные объекты:
% python
»> from squares import Squares
»> for i in Squares (1, 5) : # for вызывает встроенную функцию iter,
# которая вызывает_iter_
print(i, end=’ ') # Каждая итерация вызывает_next_
1 4 9 16 25
Здесь объект итератора, возвращаемый_iter_, представляет собой просто экземпляр self, потому что метод_next_является частью самого класса Squares.
В более сложных сценариях объект итератора может быть определен как отдельный класс и объект с собственной информацией о состоянии для поддержки множества активных итераций по тем же самым данным (вскоре мы рассмотрим пример). Об окончании итерации сообщается с помощью оператора raise языка Python — введенного в главе 29 и полностью раскрываемого в следующей части книги, но который всего лишь генерирует исключение, как если бы это сделал сам интерпретатор Python. Итерация вручную работает с определяемыми пользователем итерируемыми объектами так же, как и со встроенными типами:
>>> X = Squares (1, 5) # Итерация вручную: то, что делают циклы
»> I = iter(X) # iter вызывает_iter_
»> next(I) # next вызывает_next_ (в Python З.Х)
1
»> next (I)
4
. . .остальные результаты не показаны. ..
»> next(I)
25
>>> next (I) # Исключение можно перехватить в операторе try
Stoplteration
Эквивалентная реализация такого итерируемого объекта посредством
_getitem_может оказаться менее естественной, поскольку цикл for проходил бы
тогда через все смещения с нуля и выше; передаваемые смещения были бы лишь косвенно связанными с диапазоном выпускаемых значений (0. .N пришлось бы отображать на start. .stop). Из-за того, что объекты_iter_предохраняют явно управляемое поведение между вызовами next, они могут быть более универсальными, чем _getitem_.
С другой стороны, итерируемые объекты, основанные на_iter_, временами
могут быть более сложными и менее функциональными по сравнению с такими объектами, основанными на_getitem_. Они на самом деле предназначены для итерации, а не для произвольного индексирования — в них вообще не перегружается выражение индексирования, хотя их элементы можно собрать в последовательность вроде списка и сделать доступными другие операции:
>>> X = Squares (1, 5)
»> Х[1]
TypeError: 'Squares' object does not support indexing Ошибка типа: объект Squares не поддерживает индексирование »> list(X)[1]
4
Единственный просмотр или множество просмотров
Схема с_iter_также реализована для всех остальных итерационных контекстов, которые мы видели в действии с методом_getitem_— проверка членства,
конструкторы типов, присваивание последовательностей и т.д. Тем не менее, в отличие от предыдущего примера с_getitem_мы также должны знать, что метод _iter_класса может быть предназначен только для единственного обхода, а не для
множества. Классы явным образом выбирают поведение просмотра в своем коде.
Скажем, поскольку текущий метод_iter_класса Squares всегда возвращает
self с только одной копией состояния итерации, он обеспечивает одноразовую итерацию; после итерации экземпляр данного класса становится пустым. Повторный
вызов_iter_на том же самом экземпляре снова возвращает self независимо от
состояния, в котором он был оставлен. Как правило, для каждой новой итерации потребуется создавать новый итерируемый объект:
»> X = Squares (1, 5) # Создать итерируемый объект с состоянием
»> [п for n in X] # Израсходует элементы: _iter_ возвращает self
[1, 4, 9, 16, 25]
»> [n for n in X] # Теперь он пуст: _iter_ возвращает тот же self
П
>>> [n for n in Squares (1, 5)] # Создать новый итерируемый объект [1, 4, 9, 16, 25]
>» list (Squares (1, 3)) # Новый объект для каждого нового вызова _iter_
[1, 4, 9]
Для более прямой поддержки множества итераций мы могли бы также переписать код примера с использованием добавочного класса или другой методики, что мы вскоре и сделаем. Однако в том виде, как есть, за счет создания нового экземпляра для каждой итерации мы получаем свежую копию состояния итерации:
>>> 36 in Squares (1, 10) # Другие итерационные контексты
True
>>> а, Ь, с = Squares (1, 3) # Для каждого объекта вызывается_iter_
# и затем_next_
>» а, Ь, с
(1, 4, 9)
»> 1 :'. join (map (str, Squares (1, 5)))
'1:4:9:16:25'
Подобно встроенным функциям с единственным просмотром, таким как шар, преобразование в список тоже поддерживает множество просмотров, но ценой дополнительного расхода времени и пространства, которые могут быть, а могут и не быть важными в отдельно взятой программе:
>>> X = Squares(1, 5)
»> tuple (X) , tuple (X) # Второй вызов tuple () израсходует элементы в итераторе ((1, 4, 9, 16, 25), ())
>>> Х= list (Squares (1, 5))
>» tuple (X), tuple (X)
((1, 4, 9, 16, 25), (1, 4, 9, 16, 25))
После небольшого сравнения и противопоставления мы улучшим реализацию, чтобы более прямо поддерживать множество просмотров.
Классы или генераторы
Обратите внимание, что предыдущий пример, вероятно, мог бы стать проще, если бы в нем применялись генераторные функции или выражения — инструменты, представленные в главе 20 первого тома, которые автоматически выпускают итерируемые объекты и сохраняют состояние локальных переменных между итерациями:
»> def gsquares (start, stop) :
for i in range (start, stop + 1) : yield i ** 2
>>> for i in gsquares (1, 5) : print(i, end=’ ’)
1 4 9 16 25
>>> for i in (x ** 2 for x in range (1, 6)) : print(i, end=1 ')
1 4 9 16 25
В отличие от классов генераторные функции и выражения неявно сохраняют свое состояние и создают методы, требуемые для соответствия протоколу итерации — с очевидными преимуществами в плане лаконичности кода в случае более простых примеров вроде показанных. С другой стороны, более явные атрибуты и методы класса, дополнительная структура, иерархии наследования и поддержка множества линий поведения могут лучше подходить в сложных сценариях использования.
Разумеется, в этом искусственном примере фактически можно было бы отказаться от обеих методик и просто применить цикл for, встроенную функцию шар или списковое включение, чтобы построить сразу весь список. Тем не менее, если не учитывать данные о производительности, то самый лучший и быстрый способ решения задачи в Python часто также является самым простым:
>>> [х ** 2 for х in range (1, б)]
[1, 4, 9, 16, 25]
Однако классы могут быть лучше при моделировании более сложных итераций, особенно когда они способны извлечь выгоду из средства классов вообще. Например, итерируемый объект, который выпускает элементы из сложного результата запроса к базе данных или веб-службе, может быть в состоянии полнее задействовать в своих интересах преимущество классов. В следующем разделе исследуется еще один сценарий использования для классов в итерируемых объектах, определяемых пользователем.
Множество итераторов в одном объекте
Ранее упоминалось о том, что объект итератора (с методом_next_), производимый итерируемым объектом, может быть определен как отдельный класс с собственной информацией о состоянии, чтобы более прямо поддерживать множество активных итераций по тем же самым данным. Посмотрим, что происходит, когда мы проходим по встроенному типу, подобному строке:
»> S = 'асе'
>>> for х in S:
for у in S:
print (x + y, end=' ’)
aa ас ae ca cc се ea ec ее
Здесь внешний цикл захватывает итератор из строки, вызывая iter, и каждый вложенный цикл делает то же самое, чтобы получить независимый итератор. Поскольку каждый активный итератор имеет собственную информацию о состоянии, каждый цикл может поддерживать свою позицию в строке независимо от любых других активных циклов. Кроме того, мы не обязаны каждый раз создавать новую строку или преобразовывать в список; одиночный строковый объект сам по себе поддерживает множество просмотров.
Связанные примеры приводились в главах 14 и 20 первого тома. Скажем, генераторные функции и выражения, а также встроенные функции наподобие тар и zip, оказались объектами одиночного итератора, соответственно поддерживающими единственный активный просмотр. В противоположность им встроенная функция range и другие встроенные типы вроде списков поддерживают множество активных итераторов с независимыми позициями.
При написании кода определяемых пользователем итерируемых объектов мы самостоятельно решаем, будут они поддерживать единственную активную итерацию или
же много итераций. Чтобы достичь эффекта множества итераторов, методу_iter_
просто необходимо определить для итератора новый объект с состоянием взамен возвращения self в ответ на каждый запрос итератора.
Например, в приведенном далее коде класса SkipObject определяется итерируемый объект, который пропускает каждый второй элемент при выполнении итераций. Поскольку его объект итератора создается заново из дополнительного класса для каждой итерации, он поддерживает множество активных циклов напрямую (код находится в файле skipper.ру):
#!python3
# Файл skipper .ру
class SkipObject:
def _init_(self, wrapped) : # Сохранить объект для использования
self.wrapped = wrapped
def _iter_(self) :
return Skiplterator (self .wrapped) # Каждый раз новый итератор
class Skiplterator:
def _init_(self, wrapped):
self .wrapped = wrapped # Информация о состоянии итератора
self.offset - О
def _next_(self) :
if self.offset >= len(self.wrapped): # Прекратить итерацию raise Stoplteration else:
item = self.wrapped[self.offset] # Иначе возвратить и пропустить self.offset += 2
return item
if
name
alpha = ' abcdef'
skipper = SkipObject(alpha) # Создать объект контейнера
I = iter (skipper) # Создать на нем итератор
print(next (I), next(I), next (I)) # Посетить смещения 0, 2, 4
for x in skipper: # for автоматически вызывает_iter_
for у in skipper: # Вложенные for снова каждый раз
# вызывают_iter_
print (х + у, end=' ') # Каждый итератор имеет собственное
# состояние, смещение
Краткое замечание о переносимости: в том виде, как есть, это код Python З.Х. Чтобы сделать его совместимым с Python 2.Х, импортируйте функцию print из Python
З.Х и либо применяйте next вместо_next_только в Python 2.Х, либо определите
псевдоним в области видимости класса для двойного использования в Python 2.Х/З.Х (как сделано в файле skipper 2х.ру):
#!python
from_future_ import print_function # Совместимость с Python 2.X/3.X
class Skiplterator: def _next_(self):
next = _next__# Совместимость с Python 2.X/3.X
При запуске подходящей версии в одной из двух линеек Python пример работает подобно вложенным циклам со встроенными строками. Каждый активный цикл имеет собственную позицию в строке, потому что каждый получает независимый объект итератора, который записывает собственную информацию о состоянии:
% python skipper.ру
асе
аа ас ае са сс се еа ес ее
По контрасту с этим наш ранний пример Squares поддерживает только одну активную итерацию, если только мы не обращаемся к Squares снова во вложенных циклах для получения новых объектов. Здесь существует только один итерируемый объект SkipObject с множеством объектов итераторов, созданных из него.
Классы или срезы
Как и ранее, мы могли бы достичь похожих результатов с помощью встроенных инструментов — скажем, нарезания с третьей границей для пропуска элементов:
>>> s = 'abcdef'
»> for х in S [: : 2] :
for у in S[: :2] : # Новые объекты на каждой итерации
print(х + у, end=’ ')
аа ас ае са сс се еа ес ее
Тем не менее, прием не совсем такой же по двум причинам. Во-первых, каждое выражение среза будет физически хранить весь результирующий список в памяти; с другой стороны, итерируемые объекты выпускают по одному значению за раз, что может сберечь солидное пространство в случае крупных результирующих списков.
Во-вторых, срезы производят новые объекты, поэтому в действительности мы не выполняли итерацию по тому же самому объекту в нескольких местах. Чтобы быть ближе к классу, нам пришлось бы создать единственный объект для прохода за счет заблаговременного нарезания:
>>> S = 'abcdef’
»> S = S [: : 2]
>>> S ' асе'
>» for х in S:
for у in S: # Тот же самый объект, новые итераторы
print(х + у, end=' ')
аа ас ае са сс се еа ес ее
Прием больше подобен решению на основе классов, но он по-прежнему хранит весь результат среза в памяти (в настоящее время не существует генераторной формы встроенного нарезания) и является единственным эквивалентом конкретного случая с пропуском каждого второго элемента.
Так как итерируемые объекты, определяемые пользователем, способны делать все то же, что и класс, они гораздо более универсальны, чем может вытекать из данного примера. Хотя такая универсальность требуется не во всех приложениях, определяемые пользователем итерируемые объекты представляют собой мощный инструмент — они позволяют заставить произвольные объекты выглядеть и вести себя подобно другим последовательностям и итерируемым объектам, с которыми мы сталкивались в книге. Скажем, мы могли бы применять такую методику с объектом базы данных для поддержки итерации по крупным выборкам из базы данных с множеством курсоров в одном и том же результате запроса.
Альтернативная реализация:_iter_плюс yield
Л теперь обратимся к чему-то совершенно неявному, но потенциально полезному. В ряде приложений имеется возможность свести к минимуму требования к коду итерируемых объектов, определяемых пользователем, за счет комбинирования исследованного здесь метода_iter_и оператора генераторных функций yield, который обсуждался в главе 20. Поскольку генераторные функции автоматически сохраняют состояние локальных переменных и создают обязательные методы итераторов, они хорошо подходят для этой роли и дополняют предохранение состояния и другие полезные вещи, получаемые от классов.
Вспомните, что любая функция, которая содержит оператор yield, превращает^ ся в генераторную функцию. При вызове она возвращает новый генераторный объект с автоматическим предохранением локальной области видимости и позиции в коде,
автоматически созданным методом_iter_, просто возвращающим сам объект, и
автоматически созданным методом_next_(next в Python 2.Х), запускающим функцию или возобновляющим ее выполнение с места, которое она оставила в последний раз:
>» def gen(x) :
for i in range (x) : yield i ** 2
>>> G = gen(5) # Создание генераторного объекта с методами_iter_ и_next_
>» G._iter_() G # Оба метода существуют в том же самом объекте
True
>>> I = iter(G) # Выполняется_iter_: генератор возвращает сам себя
»> next(I) , next(I) # Выполняется_next_ (next в Python 2.Х)
(0, 1)
»> list (gen (5)) # Итерационные контексты автоматически выполняют iter и next
[0, 1, 4, 9, 16]
Все работает, даже если генераторная функция с оператором yield оказывается методом по имени_iter__: всякий раз, когда такой метод вызывается инструментом итерационного контекста, он будет возвращать новый генераторный объект с
необходимым методом_n^xt_. В качестве дополнительного бонуса генераторные
функции, реализованные как методы в классах, имеют доступ к сохраненному состоянию в атрибутах экземпляров и в переменных локальной области видимости.
Например, следующий класс эквивалентен первоначальному определяемому пользователем итерируемому объекту Squares, код которого был написан ранее в файле squares .ру:
# Файл squares_yield,py
class Squares: # Генератор на основе_iter_ + yield
def _init_(self, start, stop): # Метод_next_ является
# автоматическим/подразумеваемым
self.start = start self.stop = stop
def _iter_(self) :
for value in range(self.start, self.stop + 1) : yield value ** 2
Здесь нет никакой необходимости создавать псевдоним next для_next_ради
совместимости с Python 2.Х, потому что данный метод теперь является автоматическим и подразумеваемым посредством yield. Как и ранее, циклы for и другие итерационные инструменты автоматически проходят по экземплярам класса Squares:
% python
»> from squares_yield import Squares
>>> for i in Squares (1, 5) : print (i, end=' ')
1 4 9 16 25
И по обыкновению давайте посмотрим, как все это в действительности работает в итерационных контекстах. Прогон экземпляра класса Squares через iter позволяет получить результат вызова_iter_обычным образом, но в нашем случае результатом будет генераторный объект с автоматически созданным методом_next_того
же самого рода, который мы всегда получаем при вызове генераторной функции, содержащей yield. Единственная разница здесь в том, что генераторная функция автоматически вызывается для iter. Обращение к интерфейсу next результирующего объекта производит результаты по запросу:
>>> S = Squares (1, 5) # Выполняет метод_init_; класс сохраняет
# состояние экземпляра
>>> S
<squares_yield.Squares object at 0х000000000294вб30>
>>> I = iter(S) # Выполняет метод_iter_; возвращает генератор
»> I
<generator object _iter_ at 0x00000000029A8CF0>
>>> next(I)
1
>>> next(I) # Выполняет метод_next_ генератора
4
... и так далее. . .
»> next(I) # Генератор содержит состояние экземпляров
# и локальной области видимости
Stoplteration
Также может помочь и тот факт, что мы могли бы назначить генераторному методу имя, отличающееся от_iter_, и вызывать его вручную для выполнения итерации — скажем, Squares (1,5) . gen (). Использование имени_iter_, вызываемого
автоматически итерационными инструментами, просто позволяет пропустить ручной шаг извлечения атрибута и вызова:
class Squares: # Эквивалент с именем, отличающимся
# от_iter_ (squares_manual .ру)
def _init_(...):
def gen(self):
for value in range(self.start, self.stop + 1) : yield value ** 2
% python
>>> from squares_manual import Squares
>>> for i in Squares(1, 5).gen(): print(i, end=' ')
... те же самые результаты. . .
»> S = Squares (1, 5)
>>> I = iter (S.gen ()) # Вызвать генератор вручную для итерируемого
# объекта/итератора
»> next(I)
... те же самые результаты. . .
Реализация генератора как_iter_устраняет посредника в коде, хотя обе схемы
в итоге создают новый генераторный объект для каждой итерации:
• при наличии_iter_итерация запускает метод_iter_, который возвращает новый генераторный объект с методом_next_;
• при отсутствии_iter_в коде производится вызов для создания генераторного объекта, метод_iter_которого возвращает сам объект.
Дополнительные сведения об операторе yield и генераторах ищите в главе 20 первого тома и сравните последнюю реализацию с более явной версией_next_из показанного ранее файла squares.ру. Вы заметите, что новая версия squares yield.py на 4 строки короче (7 вместо 11). В некотором смысле такая схема сокращает требования к коду классов почти так же, как функции замыканий из главы 17 первого тома, но в данном случае делает это с помощью комбинации методик функционального и объектно-ориентированного программирования, а не альтернативы в форме классов. Например, генераторный метод по-прежнему задействует атрибуты self.
Ряду наблюдателей решение может показаться содержащим слишком много уровней магии — оно опирается как на протокол итерации, так и на создание объектов генераторов, которые оба крайне неявные (в противоположность давнишним темам Python: см. import this). Помимо упомянутых мнений важно понимать также и не использующую yield разновидность итерируемых объектов на основе классов, поскольку она явная, универсальная и временами более широкая в смысле границ.
Однако методика с_iter_/yield может доказать свою эффективность в случаях, где она применима. Она также обеспечивает значительное преимущество, как объясняется в следующем разделе.
Множество итераторов с помощью yield
Кроме лаконичного кода итерируемый объект в виде определяемого пользователем класса из предыдущего раздела, основанный на комбинации_iter_/yield,
предлагает важный дополнительный бонус — он также автоматически поддерживает множество активных итераторов. Это естественным образом следует из того факта, что каждое обращение к_iter_является вызовом генераторной функции, которая возвращает новый генераторный объект с собственной копией локальной области видимости для предохранения состояния:
% python
>>> from squares_yield import Squares # Использование реализации Squares
# с_iter_/yield
»> S = Squares (1, 5)
>» I = iter(S)
>>> next(I) ; next(I)
1
4
>>> J = iter (S) # Благодаря yield мы автоматически имеем множество итераторов »> next(J)
1
>>> next(I) # I не зависит от J: собственное локальное состояние
9
Хотя генераторные функции являются итерируемыми объектами с единственным
просмотром, неявные вызовы_iter_в итерационных контекстах обеспечивают
поддержку новыми генераторами новых независимых просмотров:
»> S = Squares (1, 3)
>>> for i in S: # Каждый for вызывает_iter_
for j in S:
print('%s:%s' % (i, j) , end=' ')
1:1 1:4 1:9 4:1 4:4 4:9 9:1 9:4 9:9
Реализация той же самой функциональности без yield требует добавочного класса, который будет явно и вручную сохранять состояние итератора, используя методики из предыдущего раздела (и код разрастается до 15 строк: на 8 больше, чем версия
с yield):
# Файл squares_nonyield.py class Squares:
def _init_(self, start, stop) : # Генератор, не основанный на yield
self.start = start # Множество просмотров: дополнительный объект
self. stop = stop
def _iter_(self) :
return Squareslter(self.start, self.stop)
class Squareslter:
def _init_(self, start, stop):
self.value = start - 1 self.stop = stop
def _next_(self):
if self.value == self.stop: raise Stoplteration self.value += 1 return self.value ** 2
Итоговый код работает аналогично версии на основе yield с множеством просмотров, но его объем больше и он более явный:
% python
>>> from squares_nonyield import Squares >>> for i in Squares (1, 5) : print (i, end=' ')
1 4 9 16 25 »>
»> S = Squares (1, 5)
»> I = iter(S)
>>> next(I); next(I)
1
4
>>> J = iter(S) # Множество итераторов без yield
»> next(J)
1
»> next (I)
9
»> S = Squares (1, 3)
>>> for i in S: # Каждый for вызывает_iter_
for j in S:
print('%s:%s' % (i, j) , end=' ')
1:1 1:4 1: 9 4 : 1 4 : 4 4 : 9 9: 1 9: 4 9:9
Наконец, подход на основе генераторов мог бы похожим образом устранить необходимость в добавочном классе итератора из предыдущего примера с пропуском элементов (skipper .ру) благодаря его автоматическим методам и предохранению состояния локальных переменных (и получить 9 строк по сравнению с 16 в оригинале):
# Файл skipper_yield.py
class SkipObject: # Еще один генератор на основе_iter_ + yield
def _init_(self, wrapped): # Область видимости экземпляра
# сохраняется нормально
self .wrapped = wrapped # Состояние локальной области видимости
# сохраняется автоматически
def _iter_(self):
offset = О
while offset < len(self.wrapped): item = self.wrapped[offset] offset += 2 yield item
Результирующий код работает аналогично версии с множеством просмотров, не основанной на yield, но его объем меньше и он менее явный:
% python
»> from skipper__yield import SkipObject >» skipper = SkipObject('abcdef')
»> I = iter (skipper)
»> next (I) ; next (I) ; next (I)
'a'
' с'
' e 1
>>> for x in skipper: # Каждый for вызывает_iter_:
# новый автоматический генератор
for у in skipper:
print (x + y, end=' ')
аа ас ае са сс се еа ес ее
Конечно, все это искусственные примеры, которые можно было бы заменить более простыми инструментами вроде включений, и их код может как масштабироваться на реалистичные задачи, так и нет. Изучите предложенные альтернативы и сравните их. Как часто бывает в программировании, наилучший инструмент для работы других с высокой вероятностью окажется наилучшим инструментом и для вашей работы!
Членство:
_contains_,_iter_и_getitem
На самом деле история об итерациях даже обширнее, чем было показано до сих пор. Перегрузка операций часто разделяется на уровни: классы могут предоставлять специфические методы или более универсальные альтернативы, применяемые в качестве запасных вариантов. Ниже приведены примеры.
• Сравнения в Python 2.Х используют специфические методы, такие как_It_
для “меньше, чем ”, если он присутствует, либо иначе универсальный метод
_стр_. Как будет обсуждаться позже в главе, в Python З.Х применяются только
специфические методы, но не_стр_.
• Булевские проверки похожим образом сначала пробуют вызывать специфический метод_bool_(чтобы получить явный результат True/False) и при его
отсутствии переходят на более универсальный метод_1еп_(ненулевая длина
означает True). Как будет показано далее в главе, Python 2.Х работает точно так же, но использует имя_nonzero_вместо_bool_.
В области итераций классы могут реализовывать операцию проверки членства in
в виде итерации с применением методов_iter_или_getitem_. Тем не менее,
для поддержки более специфической операции проверки членства в классах может быть предусмотрен метод_contains_— когда он присутствует, ему отдается предпочтение перед методом_iter_, который предпочтительнее_getitem_. Метод
_contains_должен определять членство для отображений как применение к ключам (и может использовать быстрый просмотр), а для последовательностей как поиск.
Рассмотрим следующий класс, файл которого был снабжен возможностью для двойного применения в Python 2.Х/З.Х с использованием ранее описанных методик. Он реализует все три метода, а также тестирует проверку членства и разнообразные итерационные контексты, применяемые к экземпляру. При вызове его методы выводят трассировочные сообщения:
• Файл contains.ру
from_future_ import print_function # Совместимость с Python 2.Х/З.Х
class Iters:
def _init_(self, value):
self.data = value
def _getitem_(self, i) : # Запасной вариант для итерации
print (' get [ %s] : ' % i, end=,f) # Также для индексирования, нарезания return self.data[i]
def _iter_(self) :
# Предпочтительнее для итерации
# Допускает только один активный итератор
print('iter=> ', end='' self.ix = 0 return self
def _next_(self):
print('next:1, end='')
if self.ix == len(self.data) : raise Stoplteration item = self.data[self.ix] self.ix += 1 return item
def _contains__(self, x) :
# Предпочтительнее для операции m
print('contains : f, end=lf) return x in self.data next = _next_
# Совместимость с Python 2.Х/З.Х
# Создать экземпляр
# Членство
# Циклы for
if _name_ == '_main_1 :
X = Iters ( [1, 2, 3, 4, 5]) print (3 in X) for i in X:
print(i, end=' I ' )
print ()
print ( [i ** 2 for i in X]) # Другие итерационные контексты
print ( list(map(bin, X)) )
I = iter(X) # Итерация вручную
# (то, что делают другие контексты)
while True: try:
print(next(I), end=' 0 ') except Stoplteration:
break
В текущем виде класс из файла contains.ру имеет метод_iter_, который
поддерживает множество просмотров, но в любой момент времени активным может быть только один просмотр (например, вложенные циклы работать не будут), потому что каждая итерация пытается переустановить курсор просмотра в начало. Теперь, когда вам известно об операторе yield в методах итерации, вы должны быть в состоянии сказать, что показанный ниже код является эквивалентом, но разрешает множество активных просмотров. Сами судите, стоит ли его менее явная природа поддержки вложенных просмотров и сокращения объема на 6 строк (код находится в файле contains_yield.py):
class Iters:
def _init_(self, value):
self.data = value
def _getitem_(self, i):
# Запасной вариант для итерации
# Также для индексирования, нарезания
# Предпочтительнее для итерации
# Разрешает множество активных итераторов
#_next_ для создания псевдонима
# next отсутствует
print('get[%s]:' % i, end='') return self.data[i]
def _iter_(self) :
print('iter=> next:', end=11) for x in self.data:
yield x
print(1 next:', end='')
# Предпочтительнее для операции in
def _contains_(self, x) :
print('contains : ', end='') return x in self.data
Запуск любой из двух версий файла в Python З.Х и 2.Х приводит к получению
представленного далее вывода. Специфический метод_contains_перехватывает
операцию проверки членства, универсальный метод_iter_перехватывает итерационные контексты, такие как вызываемый многократно метод_next_(будь они
записаны явно или подразумеваются оператором yield), а метод_getitem_никогда не вызывается: contains: True
iter=>next:l I next:2 | next:3 | next:4 | next:5 | next:
iter=> next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next: ['Obi', 'OblO', 'Obll', 'OblOO',
'OblOl']
iter=> next:l 0 next:2 0 next:3 0 next:4 0 next:5 0 next:
Однако посмотрите, что произойдет в выводе кода, если мы закомментируем метод _contains_— теперь операция проверки членства направляется универсальному методу_iter_:
iter=> next:next:next:True
iter=> next:l I next:2 | next:3 I next:4 | next:5 I next: iter=> next:next:next:next:next:next: [1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next:['Obi', 'OblO', 'Obll', 'OblOO', 'OblOl1] iter=> next:l 0 next:2 0 next:3 0 next:4 0 next:5 0 next:
Наконец, вот вывод в ситуации, когда закомментирован код методов_contains_
и_iter_— для операции проверки членства и других итерационных контекстов
вызывается запасной вариант индексирования_getitem_с последовательно растущими индексами, пока он не сгенерирует исключение IndexError:
get[0]:get[l]:get[2]:True
get[0]:l I get[l]:2 | get[2]:3 I get[3]:4 I get[4]:5 I get[5]:
get[0]:get[l]:get[2]:get[3]:get[4]:get[5]:[1, 4, 9, 16, 25]
get[0] :get[1]:get[2]:get[3]:get[4]:get [5]: [1 Obi', 'OblO1, 'Obll', 'OblOOVOblOl']
get[0]:1 0 get[l]:2 0 get[2]:3 0 get[3]:4 0 get[4]:5 0 get[5]:
Как видно, метод_getitem_даже более универсален: помимо итераций он также перехватывает явное индексирование и нарезание. Выражения срезов запускают
_getitem_с объектом среза, содержащим границы, как для встроенных типов, так
и для определяемых пользователем классов, поэтому нарезание доступно в нашем классе автоматически:
>>> from contains import Iters >>> X = Iters(1 spam1)
# Индексирование
#_getitem_(0)
# Синтаксис нарезания
# Объект среза
>>> X[0]
get[0]:'s '
»> ' spam' [1: ]
' pamf
>>> 'spam'[slice(1, None)]
'pam'
>>>X[1:] #_getitem_(slicef..))
get[slice (1, None, None)]:’pam'
»> X[ :-l]
get[slice(None, -1, None)]:'spa'
>>> list(X) # И также итерация!
iter=> next:next:next:next:next:['s ' , 'p', 'a', 'm']
Но в более реалистичных сценариях использования итерации, не ориентированных на последовательности, метод_iter_может оказаться легче для реализации,
т.к. он не обязан уметь обращаться с целочисленным индексом, а_contains_делает возможной оптимизацию поиска членства в качестве особого случая.
Доступ к атрибутам:
_getattr_и_setattr_
Классы в Python также способны перехватывать базовый доступ к атрибутам (известный как уточнение), когда это необходимо или полезно. В частности, для объекта, созданного из класса, в коде может быть реализована операция точки объект, атрибут, участвующая в контекстах ссылки, присваивания и удаления. Ограниченный пример в рамках такой категории демонстрировался в главе 28, но здесь мы расширим данную тему.
Ссылка на атрибуты
Метод_getattr_перехватывает ссылки на атрибуты. Он вызывается с именем
атрибута в виде строки всякий раз, когда вы пытаетесь уточнить экземпляр с помощью неопределенного (несуществующего) имени атрибута. Метод_getattr_^вызывается, если Python может найти атрибут с применением процедуры поиска в дереве наследования.
Из-за своего поведения метод_getattr_удобен в качестве привязки, обеспечивающей реагирование на запросы атрибутов в обобщенной манере. Он обычно используется для делегирования вызовов внедренным (или “вложенным”) объектам из промежуточного объекта контроллера — подобно тому, как было представлено во
введении в делегирование ъ главе 28. Метод_getattr_также может применяться для
адаптации классов к интерфейсу или последующего добавления средапв доступа к атрибутам — логики в методе, которая проверяет достоверность или вычисляет значение атрибута после того, как он уже используется через простую точечную запись.
Базовый механизм, лежащий в основе достижения указанных целей, прямолинеен. В приведенном ниже классе перехватываются ссылки на атрибуты, динамически вычисляется значение для одного атрибута и генерируется ошибка для остальных неподдерживаемых атрибутов с помощью оператора raise, описанного ранее в главе при рассмотрении итераторов (и полностью раскрываемого в части VII):
>>> class Empty:
def_getattr_(self, attmame) : # Вызывается для неопределенного
# атрибута в self
if attrname == 1 age1: return 40 else:
raise AttributeError (attrname)
»> X = Empty ()
»> X.age
40
>>> X.name
...текст сообщения об ошибке не показан. . .
AttributeError: паше
Ошибка атрибута: name
Здесь класс Empty и его экземпляр X не имеют собственных реальных атрибутов, поэтому доступ к X.age направляется методу_getattr_; self присваивается экземпляр (X), a attrname — строка с именем неопределенного атрибута (' аде *). Класс делает аде похожим на реальный атрибут, возвращая действительное значение в качестве результата выражения уточнения X. аде (40). В сущности, аде становится дина-мически вычисляемым атрибутом — его значение формируется выполняющимся кодом, а не извлечением какого-то объекта.
Для атрибутов, которые класс не знает, как обрабатывать, метод_getattr_генерирует встроенное исключение AttributeError, сообщая Python о том, что они являются по-настоящему неопределенными именами; обращение к X.name инициирует ошибку. Вы увидите метод_getattr_в действии снова, когда мы займемся
делегированием и свойствами в последующих двух главах; а пока давайте перейдем к исследованию связанных инструментов.
Присваивание и удаление атрибутов
В рамках той же области метод_setattr_перехватывает ^присваивания значений атрибутам. Если данный метод определен или унаследован, тогда выражение
self. атрибут = значение становится self._setattr_(1 атрибут', значение).
Подобно_getattr_это позволяет классу перехватывать изменения атрибутов и
выполнять желаемые проверки достоверности или преобразования.
Тем не менее, использовать метод_setattr_ несколько сложнее, т.к. присваивание значения любому атрибуту в self внутри_setattr_снова вызывает
_setattr_, что потенциально может стать причиной бесконечного цикла рекурсии
(и довольно быстро привести к исключению, связанному с переполнением стека!). На самом деле сказанное применимо ко всем присваиваниям атрибутов в self где угодно в классе — все они направляются методу_setattr_, даже присваивания, находящиеся в других методах, и присваивания именам, отличным от тех, которые могут запускать метод_setattr_в первую очередь. Не забывайте, что метод_setattr_
перехватывает все присваивания значений атрибутам.
Если вы хотите использовать метод_setattr_, то можете избежать циклов,
реализуя присваивания значений атрибутам экземпляра в виде присваиваний ключам словаря атрибутов. То есть применяйте self._diet_[ ’name' ] = х, но не
self .паше = х; из-за того, что вы не выполняете присваивание самому_diet_,
цикл не возникает:
>» class Accesscontrol:
def_setattr_(self, attr, value) :
if attr = 'age' :
self._diet_[attr] = value + 10 # He self .имя = значение
# или setattr
else:
raise AttributeError (attr + ' not allowed’) # не разрешено
»> X = Access control ()
>» X.age = 40 # Вызывается_setattr_
»> X.age
50
>» X.name = 'Bob'
...текст не показан...
AttributeError: name not allowed
Ошибка атрибута: name не разрешено
Если вы замените присваивание_diet_любым из следующих присваиваний,
тогда возникнет бесконечный цикл рекурсии и затем исключение — точечная запись и ее эквивалент в виде встроенной функции setattr (аналог getattr, отвечающий за присваивание) потерпят неудачу в случае присваивания аде за пределами класса:
self.age = value +10 # Зацикливание
setattr(self, attr, value + 10) # Зацикливание (attr является 'age')
Присваивание значения другому имени внутри класса тоже инициирует рекурсивный вызов метода_setattr_, хотя в данном классе результатом будет менее драматичное ручное исключение AttributeError:
self.other =99 # Рекурсивный вызов, но без зацикливания: терпит неудачу
Избежать рекурсивного зацикливания в классе, где используется_setattr_,
возможно также путем направления любых присваиваний значений атрибутам расположенному выше суперклассу посредством вызова, а не присваивания значений ключам в_diet_:
self._diet_[attr] = value + 10 # Нормально: зацикливания нет
object._setattr_(self, attr, value + 10) # Нормально: зацикливания нет
# (только классы нового стиля)
Однако поскольку форма object требует применения классов нового стиля в Python 2.Х, мы отложим рассмотрение деталей до главы 38, где исследуется управление атрибутами в целом.
Третий метод управления атрибутами,_delattr_, принимает строку с именем
атрибута и вызывается для всех удалений атрибутов (т.е. del объект. атрибут). Как
и_setattr_, он должен избегать рекурсивного зацикливания, направляя удаления
атрибутов с использованием класса через_diet_или суперкласс.
В главе 32 мы узнаем, что атрибуты, реализованные с помощью средств классов нового стиля, таких как слоты и свойства, физически не хранятся в словаре пространства имен_diet_экземпляра (и слоты могут
даже препятствовать его существованию!). По указанной причине код, где желательно поддерживать такие атрибуты, для присваивания им
значений должен применять__setattr_с показанной здесь схемой
object._setattr_, а не индексирование self._diet_, которое
используется только когда известно, что участвующие классы хранят все свои данные в самом экземпляре. В главе 38 мы также увидим, что метод нового стиля_getattribute_имеет похожие требования. Такое изменение обязательно в Python З.Х, но также применяется к Python 2.Х, если используются классы нового стиля.
Другие инструменты управления атрибутами
Рассмотренные выше методы для перегрузки операций доступа к атрибутам позволяют управлять или специализировать доступ к атрибутам в объектах. Как правило, они исполняют узкоспециализированные роли, часть которых мы исследуем позже
в книге. За еще одним примером метода_getattr_в работе обращайтесь в файл
person-composite .ру из главы 28. Кроме того, имейте в виду, что в Python существуют и другие способы управления доступом к атрибутам.
• Метод_getattribute_перехватывает операции извлечения всех атрибутов,
не только тех, которые не определены, но при его применении вы должны соблюдать большую осторожность, чем с_getattr_, чтобы избегать зацикливания.
• Встроенная функция property позволяет ассоциировать методы с операциями извлечения и установки для специфического атрибута класса.
• Дескрипторы предоставляют протокол для ассоциирования методов_get_и
_set_класса с операциями доступа к специфическому атрибуту класса.
• Атрибуты слотов объявляются в классах, но создают неявное хранилище в каждом экземпляре.
Поскольку перечисленные инструменты относительно сложны и интересны далеко не каждому программисту на Python, мы отложим рассмотрение свойств до главы 32, а всех методик управления атрибутами — до главы 38.
Эмуляция защиты атрибутов экземпляра: часть 1
В следующем коде (privateO .ру) демонстрируется другой сценарий использования таких инструментов. Он обобщает предыдущий пример, чтобы позволить каждому подклассу иметь собственный список закрытых имен, которым экземпляры не могут присваивать значения (и применяет определяемый пользователем класс исключения, что обсуждается в части VII):
class PrivateExc(Exception): pass # Исключения подробно
# рассматриваются в части VII
class Privacy:
def _setattr_(self, attrname, value): # Вызывается для
# self.attrname = value
if attrname in self.privates:
raise PrivateExc(attrname, self) # Сгенерировать определяемое
# пользователем исключение
else:
self._diet_[attrname] = value # Избежать зацикливания,
# используя ключ словаря
class Testl(Privacy): privates = [1 age']
class Test2(Privacy):
privates = ['name', 'pay']
def _init_(self) :
self._diet_['name'] = 'Tom'
# Чтобы сделать лучше,
# обратитесь в главу 39!
if _name_ == '_main_* :
x = Testl () у = Test2 ()
# Работает
# Терпит неудачу
# Работает
# Терпит неудачу
x.name = 'Bob'
#y. name = ' Sue print(x.name)
y.age = 30 #x.age = 40 print(y.age)
Фактически это первое пробное решение для реализации защиты атрибутов в Python — запрет вносить изменения в имена атрибутов за пределами класса. Хотя Python не поддерживает закрытые объявления как таковые, методики вроде показанной здесь способны эмулировать большую часть их предназначения.
Тем не менее, решение получилось неполное и даже неуклюжее. Чтобы сделать его более эффективным, мы должны дополнить классы возможностью устанавливать свои закрытые атрибуты более естественным путем, не проходя каждый раз
через_dict__, как обязан поступать конструктор во избежание запуска метода
_setattr_и генерации исключения. Лучший и более совершенный подход может
требовать класса-оболочки (“посредника”) для препятствования операциям доступа к закрытым атрибутам, выполняемым только за пределами класса, а также метода _getattr_для проверки операций извлечения атрибутов.
Мы отложим полное решение по защите атрибутов до главы 39, где будем использовать декораторы классов для более общего перехвата и проверки атрибутов. Однако, несмотря на то, что защиту атрибутов можно эмулировать подобным образом, на практике так почти никогда не поступают. Программисты на Python в состоянии
разрабатывать крупные объектно-ориентированные фреймворки и приложения без закрытых объявлений — интересные сведения о контроле доступа в целом, которые выходят за рамки преследуемых здесь целей.
Тем не менее, перехват ссылок и присваиваний атрибутов в общем случае является полезным приемом; он поддерживает делегирование — методику проектирования, которая позволяет управляющим объектам становиться оболочками для внедренных объектов, добавлять новые линии поведения и направлять другие операции на выполнение внедренным объектам. Из-за того, что делегирование и внедренные объекты задействуют темы, связанные с проектированием, мы возвратимся к ним в следующей главе.
Строковое представление:_герг_и_str_
Рассматриваемые в настоящем разделе методы имеют дело с форматами отображения — тема, которую мы уже исследовали в предшествующих главах, но подытожим и
формализуем здесь. В приведенном ниже коде используются конструктор_init_и
метод перегрузки_add_, с которыми мы встречались ранее (+ представляет собой
операцию на месте, просто чтобы показать, что она может тут присутствовать; согласно главе 27 именованный метод возможно предпочтительнее). Как нам известно, стандартное отображение объектов экземпляров для класса вроде этого не приносит особой пользы и не может считаться эстетически привлекательным:
>» class adder:
def_init_(self, value=0) :
self.data = value # Инициализировать данные
def_add (self, other) :
self.data += other # Добавить other на месте (плохая форма?)
»> х = adder () # Стандартные отображения
»> print (х)
<_main_.adder object at 0x00000000029736D8>
»> x
<_main_.adder object at 0x00000000029736D8>
Но реализация или наследование методов строкового представления позволяет настраивать отображение — как показано в следующем примере, где в подклассе определяется метод_герг_, который возвращает строковое представление для своих
экземпляров:
# Унаследовать_init_, _add_
# Добавить строковое представление
self .data # Преобразовать в строку как в коде
# Выполняется_init_
# Выполняется_add_ (x.add() лучше?)
# Выполняется_герг_
# Выполняется_герг_
# Выполняется_герг_ для обоих
»> class addrepr (adder) :
def_repr_(self) :
return 1addrepr(%s)’ %
»> x = addrepr (2)
>» x + 1
»> x
addrepr(3)
»> print (x)
addrepr(3)
»> str(x) , repr(x)
('addrepr(3)', 'addrepr(3)')
В случае определения метода_repr_(или близкородственного ему_str_)
он автоматически вызывается, когда экземпляры класса выводятся или преобразуются в строки. Упомянутые методы позволяют определять улучшенный формат отображения для объектов по сравнению со стандартным отображением экземпляров. В
методе_герг_с помощью базового форматирования строк управляемый объект
self. data преобразуется в более дружественную к человеку строку, предназначенную для отображения.
Для чего используются два метода отображения?
Все увиденное до сих пор по большому счету было обзором. Но наряду с тем, что эти методы обычно просты в применении, их роли и поведение имеют тонкие последствия как для проектирования, так и для написания кода. В частности, Python предлагает два метода отображения, призванные поддерживать отличающееся отображение для разной аудитории.
• Метод_str_сначала опробуется для операции print и встроенной функции
str (внутренний эквивалент которой запускает операция print). В общем случае он должен возвратить отображение, дружественное к пользователю.
• Метод_герг_используется во всех остальных контекстах: для эхо-вывода
в интерактивной подсказке, функции герг и вложенных появлений, а также print и str, если метод_str_отсутствует. В общем случае он должен возвратить строку как в коде, которую можно было бы применять для воссоздания объекта, или детальное отображение для разработчиков.
То есть_герг_используется везде, исключая print и str, когда метод_str_
определен. Это означает, что вы можете реализовать метод_герг_для определения единственного формата отображения, применяемого повсюду, и метод_str_
либо для поддержки единственно print и str, либо чтобы предоставить для них альтернативное отображение.
Как отмечалось в главе 28, универсальные инструменты могут также отдавать предпочтение использованию_str_и оставлять другим классам возможность добавления альтернативного отображения_герг_для применения в остальных контекстах
до тех пор, пока инструменту достаточно отображений print и str. И наоборот, универсальный инструмент, который реализует_герг_, по-прежнему оставляет клиентам возможность добавления альтернативного отображения с помощью_str_для
print и str. Другими словами, если реализуется один из двух методов, то другой будет доступным для дополнительного отображения. В случаях, где выбор неочевиден, метод_str_обычно предпочтительнее использовать для более крупных отображений, дружественных к пользователю, а_герг_— для низкоуровневых или отображений как в коде и включающих все ролей.
Давайте напишем код, более конкретно иллюстрирующий отличия между двумя методами. В предыдущем примере из этого раздела было показано, как_герг_применяется в качестве запасного варианта во многих контекстах. Однако хотя вывод прибегает к_герг_, если метод_str_не определен, противоположное неверно — другие контексты, такие как эхо-вывод в интерактивной подсказке, используют только_герг_и вообще не пытаются обращаться к_str_:
>>> class addstr(adder):
def_str_(self) : #_str_, но не_repr_
return ' [Value: %s] ' % self .data # Преобразовать в симпатичную строку
»> х = addstr(3)
»> х + 1
>>> х # По умолчанию_repr_
<_main_.addstr object at 0x00000000029738D0>
»> print(х) # Выполняется_str_
[Value: 4]
>>> str(x) , repr(x)
('[Value: 4]’, '<_main_.addstr object at 0x00000000029738D0>')
По этой причине метод_repr_может оказаться лучше, если вы хотите иметь
единственное отображение для всех контекстов. Тем не менее, за счет определения обоих методов вы можете поддерживать в разных контекстах отличающиеся отображения — например, отображение посредством_str_для конечного пользователя и
низкоуровневое отображение с помощью_герг_для применения программистами
во время разработки. В действительности_str_просто переопределяет_герг_
для контекстов отображения, более дружественных к пользователю:
>>> class addboth(adder):
def_str_(self) :
return '[Value: %s] ' % self.data # Строка, дружественная
# к пользователю
def_repr_(self) :
return 1 addboth (%s) ' % self .data # Строка как в коде
»> х = addboth(4)
»> х + 1
>>> х # Выполняется repr
addboth(5)
>>> print(x) # Выполняется_str_
[Value: 5]
>>> str(x), repr(x)
('[Value: 5]', 'addboth(5)')
Замечания по использованию отображения
Несмотря на простоту использования в целом, я должен привести три замечания относительно этих методов. Во-первых, имейте в виду, что_str_и_герг_обязаны возвращать строки; другие результирующие типы не преобразуются и вызывают ошибки, так что при необходимости обеспечьте их обработку инструментом преобразования в строку (скажем, str или %).
Во-вторых, в зависимости от логики преобразования в строки дружественное к пользователю отображение_str_может применяться, только когда объекты находятся на верхнем уровне операции print; объекты, вложенные внутрь более крупных
объектов, могут по-прежнему выводиться посредством_герг_либо их стандартных
методов. Оба аспекта иллюстрируются в следующем взаимодействии:
»> class Printer:
def_init_(self, val) :
self .val = val
def_str_(self) : # Используется для самого экземпляра
return str(self.val) # Преобразовать в строковый результат
»> objs = [Printer(2), Printer(3)]
>» for x in objs: print(x) #_str_ выполняется при выводе экземпляра
# Но не в ситуации, когда экземпляр находится в списке!
2
3
»> print (objs)
[<_main_.Printer object at 0x000000000297AB38>, <_main_.Printer obj...
etc...>]
>» objs
[<_main_.Printer object at 0x000000000297AB38>, <_main_.Printer obj...
etc...>]
Чтобы обеспечить использование специального отображения во всех контекстах
независимо от контейнера, необходимо реализовывать метод_герг_, а не_str_;
первый из них выполняется во всех случаях, если второй неприменим, включая вложенные появления:
>>> class Printer:
def_init_(self, val) :
self.val = val def_repr_(self) : #_repr_ используется print,
# если отсутствует_str_
return str (self .val) #_repr_ используется при эхо-выводе
# в интерактивной подсказке
# или при вложенном появлении »> objs = [Printer(2), Printer (3)]
>>> for x in objs: print(x) #_str_ отсутствует: выполняется_repr_
2
3
>>> print(objs) # Выполняется_repr_, не_str_
[2, 3]
>>> objs
[2, 3]
В-третьих, и вероятно это самый тонкий момент, в редких контекстах методы отображения также потенциально могут приводить к бесконечным циклам рекурсии — поскольку отображение некоторых объектов предусматривает отображение других объектов, не исключено, что какое-то отображение может запустить отображение выводимого объекта, образуя цикл. Ситуация достаточно редкая и малоизвестная, чтобы не затрагивать ее здесь, но потенциальная возможность зацикливания_герг_обсуждается во врезке “На заметку!” после кода класса Listlnherited в следующей главе.
На практике метод__str__и более охватывающий родственный ему метод
_герг_, похоже, являются вторыми по частоте использования методами перегрузки
операций в Python после_init_. В любое время, когда вы в состоянии выводить объект и видеть его специальное отображение, вероятно, задействован один из этих двух инструментов. В главах 28 и 31 можно найти дополнительные примеры применения данных инструментов и связанные с ними проектные компромиссы, а в главе 35 описана их роль в классах исключений, где метод_str_более востребован, чем_герг_.
Использование с правой стороны и на месте:
_г add_и_iadd_
Следующая группа методов перегрузки расширяет функциональность методов бинарных операций, таких как_add_и_sub_(вызываемых для + и -), которые мы
уже видели. Как упоминалось ранее, одна из причин существования настолько большого количества методов перегрузки операций связана с тем, что они встречаются во многих разновидностях — для каждого бинарного выражения мы можем реализовать варианты с левой стороны, с правой стороны и на месте. Хотя также применяются стандартные реализации, когда не написан код для всех трех вариантов, именно роли ваших объектов диктуют, код скольких вариантов потребуется предоставить.
Правостороннее сложение
Например, реализованные до настоящего времени методы_add_формально не
поддерживают использование объектов экземпляров с правой стороны операции +:
>>> class Adder:
def_init_(self, value=0) :
self .data = value
def_add_(self, other) :
return self.data + other
>>> x = Adder (5)
>» x + 2
7
>» 2 + x
TypeError: unsupported operand type(s) for + : 'int' and 'Adder'
Ошибка типа: неподдерживаемый тип (типы) операнда для +: int и Adder
Чтобы реализовать более универсальные выражения и тем самым поддерживать коммутативные операции, необходимо также написать код метода_г add_.
Интерпретатор Python вызывает_г add_, только когда объектом с правой стороны
операции + является экземпляр вашего класса, но объект с левой стороны не относится к экземплярам вашего класса. Во всех остальных случаях для объекта с левой стороны взамен вызывается метод_add_(представленные в этом разделе пять классов, Commuterl — Commuter5, вместе с кодом самотестирования находятся в файле commuter .ру):
class Commuterl:
def _init_(self, val):
self.val = val
def _add_(self, other):
print('add', self.val, other) return self.val + other
def _radd_(self, other):
print(’radd', self.val, other) return other + self.val
»> from commuter import Commuterl >>> x * Commuterl(88)
>» у = Commuterl (99)
>» x + 1 #_add_; экземпляр + не экземпляр
add 88 1 89
>>> 1 + у #_radd_: не экземпляр + экземпляр
radd 99 1 100
>>> х + у #_add_; экземпляр + экземпляр, запускает_radd_
add 88 ccommuter.Commuterl object at 0x00000000029B39E8>
radd 99 88
187
Обратите внимание на реверсирование порядка в_radd_: на самом деле self
находится с правой стороны операции + , a other — с левой стороны. Также учтите, что х и у здесь являются экземплярами того же самого класса; когда в выражении смешиваются экземпляры разных классов, Python отдает предпочтение классу экземпляра
с левой стороны. При сложении двух экземпляров Python выполняет метод_add_,
который в свою очередь запускает_radd_, упрощая левый операнд.
Повторное использование_add в_radd
Для подлинно коммутативных операций, которые не требуют учета позиции,
иногда вполне достаточно повторно использовать_add_для_radd_: вызвать
_add_напрямую; поменять местами операнды и снова выполнить сложение, чтобы
запустить_add_косвенно; либо просто назначить_radd_в качестве псевдонима
для_add_на верхнем уровне оператора class (т.е. в области видимости класса).
Показанные далее альтернативные версии реализуют три упомянутые схемы и возвращают те же результаты, что и первоначальная версия — хотя последняя экономит один
вызов или направление и потому может оказаться быстрее (во всех версиях_radd_
вызывается, когда self находится с правой стороны операции +):
class Commuter2:
def _init_(self, val):
self.val = val
def _add_(self, other) :
print(’add', self.val, other) return self.val + other
def _radd_(self, other):
return self._add_(other) # Явно вызвать_add_
class Commuter3:
def _init_(self, val):
self.val = val
def _add_(self, other):
print('add', self.val, other) return self.val + other
def _radd_(self, other):
return self + other # Поменять местами и снова сложить
class Coimuter4 :
def _init_(self, val):
self.val = val
def _add_(self, other):
print(’add1, self.val, other) return self.val + other _radd_ = _add__# Псевдоним: исключить посредника
Во всех версиях правосторонние появления экземпляров приводят к запуску
единственного разделяемого метода_add_с передачей для self правого операнда,
чтобы трактовать его точно так же, как левостороннее появление. Для лучшего понимания реализации запустите все версии самостоятельно; возвращаемые ими значения будут такими же, как у исходной версии.
Распространение типа класса
В более реалистичных классах, где может возникать необходимость в распространении типа класса на результаты, ситуация становится сложнее: чтобы выяснить, безопасно ли выполнять преобразование, и таким образом избежать вложенности, иногда требуется проверка типа. Скажем, в следующем коде без проверки is instance мы могли бы в итоге получить экземпляр Commuter5, атрибутом val которого оказался бы еще один экземпляр Commuter5, когда выполняется сложение двух экземпляров и_add_запускает_radd_:
class Commuter5: # Распространение типа класса на результаты
def _init_(self, val) :
self.val = val
def _add_(self, other) :
if isinstance(other, Commuter5):
# Проверка типа во избежание
# вложенности объектов
other = other.val
return Commuter5 (self.val + other) # Иначе результатом операции +
# является еще один Commuter5
def _radd_(self, other):
return Commuter5(other + self.val)
def _str_(self) :
return 1<Commuter5 : %s>' % self.val
»> from commuter import Commuter5 >>> x = Commuters(88)
>» у = Commuters (99)
# Результат - еще один экземпляр Commuter5
>>> print(x + 10)
<Commuter5: 98>
>>> print (10 + y)
<Commuter5: 109>
# Не вложенный: не вызывает рекурсивно_radd
»> z = x + у >>> print(z)
<Commuter5: 187>
»> print(z + 10)
<Commuter5: 197>
>>> print(z + z)
<Commuter5: 374>
>>> print(z + z + 1)
<Commuter5: 375>
Необходимость проверки типа с помощью isinstance здесь очень тонкая — закомментируйте проверку, запустите и отследите, чтобы понять, почему она обязательна. Вы заметите, что в последней части предыдущего теста получаются отличающиеся и вложенные объекты, которые по-прежнему корректно выполняют математические действия, но инициируют бессмысленные рекурсивные вызовы для упрощения своих значений, а добавочные вызовы конструктора формируют результаты:
»> z = х + у # Проверка с помощью isinstance закомментирована
>» print(z)
<Commuter5: <Commuter5: 187»
>>> print(z + 10)
<Commuter5: <Commuter5: 197>>
>>> print(z + z)
<Commuter5: <Commuter5: <Commuter5: <Commuter5: 374»>>
>» print(z + z + 1)
<Commuter5: <Commuter5: <Commuter5: <Commuter5: 375>>>>
Для тестирования остаток содержимого файла commuter .ру должен выглядеть и выполняться так, как показано ниже — классы вполне естественно могут появляться в кортежах:
#!python
from _future_ import print_function # Совместимость с Python 2.X/3.X
. . .здесь определены классы. . .
if _name_ == '_main_1 :
for klass in (Commuterl, Commuter2, Commuter3, Commuter4, Commuter5): print('- ' * 60) x = klass(88)
у = klass(99) print (x + 1) print(1 + у) print (x -(- y)
с:\code> commuter.py
add 88 1 89
radd 99 1 100
add 88 <_main_.Commuterl object at 0x000000000297F2B0>
radd 99 88 187
... и так далее. . .
Вариантов реализации слишком много, чтобы их можно было все здесь исследовать, а потому для лучшего понимания поэкспериментируйте с предложенными классами самостоятельно; например, назначение псевдонима_radd_методу_add_
в Commuter5 не предотвратит вложенность объектов без проверки посредством isinstance. Обсуждение других вариантов в данной области ищите в руководствах по Python; скажем, классы могут также возвращать для неподдерживаемых операндов специальный объект Not Implemented, чтобы влиять на выбор методов (это трактуется, как будто бы метод не был определен).
Сложение на месте
Чтобы реализовать дополненное сложение на месте (+=), понадобится написать код либо_iadd_, либо_add_. Второй метод используется, если отсутствует первый. На самом деле по указанной причине классы Commuter из предыдущего раздела уже поддерживают операцию +=: интерпретатор Python выполняет_add_и присваивает результат. Однако метод_iadd_позволяет более эффективно реализовывать
изменения на месте, где это применимо:
>>> class Number:
def_init_(self, val) :
self.val = val
def_iadd_(self, other) : # Явный метод_iadd_; x += у
self .val += other # Обычно возвращает self
return self
>>> x = Number (5)
>» x += 1 »> x += 1 »> x.val
7
Для изменяемых объектов метод_iadd_часто можно специализировать для выполнения более быстрых изменений на месте:
>>> у = Number([l]) # Изменение на месте быстрее, чем +
»> у += [2]
»> у += [3]
»> у.val
[1, 2, 3]
Нормальный метод_add_выполняется в качестве запасного варианта, но может быть не в состоянии оптимизировать случаи на месте:
>>> class Number:
def_init_(self, val) :
self.val = val
def_add (self, other) : # Запасной вариант_add_: x = (x + y)
return Number (self.val + other) # Распространяет тип класса
»> x = Number(5)
»> x += 1
>>> x += 1 # И += здесь выполняет конкатенацию
»> х.val
7
Несмотря на то что внимание в примере было сосредоточено на операции +, имейте в виду, что каждая бинарная операция имеет похожие методы перегрузки для случаев с правой стороны и на месте, которые работают аналогично (например,_mul_,
_rmul_и_imul_). Тем не менее, правосторонние методы являются сложной
темой и менее часто используются на практике; вы реализуете их лишь тогда, когда операции должны быть коммутативными, и только если вообще нужна поддержка таких операций. Скажем, класс Vector (вектор) может применять эти инструменты, но класс Employee (сотрудник) или Button (кнопка) — вероятно нет.
Выражения вызовов:_call_
Перейдем к нашему следующему методу перегрузки: метод_call_вызывается
при вызове вашего экземпляра. Нет, это вовсе не циклическое определение — если
метод_call_определен, то Python выполняет его для выражений вызова функций,
применяемых к вашему экземпляру, с передачей ему любых позиционных или ключевых аргументов, которые были отправлены. Это позволяет экземплярам соответствовать API-интерфейсу на основе функций:
>>> class Callee:
def_call_(self, *pargs, **kargs) : # Перехватывает вызовы экземпляра
print('Called: 1 , pargs, kargs) # Принимает произвольные аргументы
»> С = Callee ()
»> C(l, 2, 3) # С - вызываемый объект
Called: (1, 2, 3) {}
»> С(1, 2, 3, х=4, у=5)
Called: (1, 2, 3) {'у': 5, 'х' : 4}
Говоря более формально, метод_call_поддерживает все режимы передачи
аргументов, которые были исследованы в главе 18 первого тома — все, что было передано экземпляру, передается методу_call_вместе обычным подразумеваемым
аргументом экземпляра. Скажем, определения метода:
class С:
def_call_(self, a, b, с=5, d=6) : . . . # Нормальные аргументы и аргументы
# со стандартными значениями
class С:
def _call_(self, *pargs, **kargs): ... # Сбор произвольных аргументов
class С:
def_call_(self, *pargs, d=6, **kargs) : # Аргумент с передачей только
# по ключевым словам Python З.Х
соответствуют следующим вызовам экземпляра:
х = СО
X (1, 2) # Стандартные значения не указаны
Х(1, 2, 3, 4) # Позиционные аргументы
Х(а=1, b=2, d=4) # Ключевые аргументы
X (* [ 1, 2], **dict(c=3, d=4)) # Распаковка произвольных аргументов
X (1, *(2,), с=3, **dict (d=4) ) # Смешанные режимы
Чтобы освежить в памяти тему аргументов функций, обратитесь в главу 18 первого тома. Совокупный эффект в том, что классы и экземпляры с методом_call_поддерживают точно такие же синтаксис и семантику аргументов, как нормальные функции и методы.
Перехват выражений вызовов вроде показанных выше позволяет экземплярам класса эмулировать внешний вид и поведение сущностей вроде функций, а также предохраняет информацию о состоянии для использования во время вызовов. При исследовании областей видимости в главе 17 первого тома приводился пример, подобный представленному далее, но теперь вы должны достаточно знать о перегрузке операций, чтобы лучше понимать данную схему:
>>> class Prod:
def_init_(self, value) : # Принимает всего один аргумент
self .value = value
def_call_(self, other) :
return self.value * other
>>> x = Prod(2) # "Запоминает" 2 в состоянии
>» x(3) #3 (переданное значение) * 2 (состояние)
6
»> х (4)
8
Реализация_call_в этом примере на первый взгляд может показаться несколько неоправданной. Простой метод способен обеспечить похожий результат:
>>> class Prod:
def_init_(self, value) :
self.value = value def comp(self, other):
return self.value * other
>>> x = Prod(3)
>>> x.comp(3)
9
>>> x.comp(4)
12
Однако метод_call_может стать более полезным при взаимодействии с API-
интерфейсами (т.е. библиотеками), ожидающими функций — он позволяет реализовывать объекты, которые соответствуют ожидаемому интерфейсу вызова функций, но также предохраняют информацию о состоянии и другие активы классов, такие как наследование. Фактически_call_можно считать третьим по частоте использования методом перегрузки операций после конструктора_init_и альтернативных
форматов отображения_str_и_герг_.
Функциональные интерфейсы и код, основанный на обратных вызовах
В качестве примера отметим, что комплект инструментов для построения графических пользовательских инструментов tkinter (Tkinter в Python 2.Х) позволяет регистрировать функции как обработчики событий (так называемые обратные вызовы) — когда возникают события, tkinter вызывает зарегистрированные объекты. Если вы хотите, чтобы обработчик событий сохранял состояние между событиями, тогда можете зарегистрировать либо связанный метод класса, либо экземпляр, который с помощью _call_обеспечивает соответствие ожидаемому интерфейсу.
Скажем, в коде предыдущего раздела х. сошр из второго примера и х из первого могут таким способом передавать объекты, подобные функциям. Функции замыканий с состоянием из объемлющих областей видимости, рассматриваемые в главе 17 первого тома, способны достигать похожего эффекта, но не обеспечивают столько поддержки для множества операций или настройки.
В следующей главе связанные методы обсуждаются более подробно, а пока ниже
представлен гипотетический пример метода_call_применительно к области
графических пользовательских интерфейсов. Приведенный далее класс определяет объект, поддерживающий интерфейс вызова функций, но также запоминающий цвет, который должна получить кнопка при щелчке на ней в будущем:
class Callback:
def _init_(self, color) : # Функция + информация о состоянии
self.color = color
def _call_(self) : # Поддерживает вызовы без аргументов
print(1 turn’, self.color)
В контексте графического пользовательского интерфейса мы можем зарегистрировать экземпляры этого класса в качестве обработчиков событий для кнопок, хотя графический пользовательский интерфейс рассчитывает на возможность вызова обработчиков событий как простых функций без аргументов:
# Обработчики
cbl = Callback (' blue ') # Запомнить blue
cb2 = Callback('green') # Запомнить green
Bl = Button(command=cbl) # Зарегистрировать обработчики
B2 = Button(command=cb2)
Когда позже на кнопке совершается щелчок, объект экземпляра вызывается как простая функция без аргументов подобно показанным ниже вызовам. Тем не менее, поскольку этот объект сохраняет состояние в виде атрибутов экземпляра, он запоминает, что делать, становясь объектом функции с состоянием:
# События
cbl () # Выводит turn blue
cb2 () # Выводит turn green
В действительности многие считают такие классы лучшим способом предохранения информации о состоянии в языке Python (во всяком случае, согласно общепринятым принципам стиля Python). С помощью ООП запоминание состояния делается явным посредством присваивания значений атрибутам. Данный способ отличается от других методик предохранения состояния (например, глобальные переменные, ссылки из объемлющих областей видимости и изменяемые аргументы со стандартными значениями), которые полагаются на более ограниченное или менее явное поведение. Кроме того, добавленная структура и настройка в классах выходит за рамки одного лишь предохранения состояния.
С другой стороны, в базовых ролях предохранения состояния полезны также инструменты, подобные функциям замыканий, и оператор nonlocal из Python З.Х делает объемлющие области видимости жизнеспособной альтернативой во многих программах. Мы возвратимся к обсуждению таких компромиссов, когда приступим к реализации реальных декораторов в главе 39, но ниже показан эквивалент в форме замыкания:
def callback (color) : # Объемлющая область видимости или атрибуты
def oncall () :
print('turn1, color) return oncall
cb3 - callback ('yellow' ) # Обработчик, подлежащий регистрации
сЬЗ () # При возникновении события выводит turn yellow
Прежде чем двигаться дальше, рассмотрим два других способа, которыми программисты на Python иногда привязывают информацию к функции обратного вызова подобного рода. Один вариант предусматривает использование аргументов со стандартными значениями в функциях lambda:
cb4 = (lambda color='red': 'turn ' + color) # Стандартные значения тоже
# предохраняют состояние
print(сЬ4())
Второй вариант предполагает применение связанных методов класса, которые мы кратко представим здесь. Объект связанного метода — это разновидность объекта, запоминающего экземпляр self и функцию, на которую он ссылается. Такой объект впоследствии может быть вызван как простая функция без указания экземпляра:
class Callback:
def _init_(self, color) : # Класс с информацией о состоянии
self.color = color def changeColor (self) : # Нормальный именованный метод
print('turn1, self.color)
cbl = Callback('blue') cb2 = Callback('yellow')
Bl = Button(command=cbl.changeColor) # Связанный метод: ссылка, не вызывается В2 = Button(command=cb2.changeColor) # Запоминает пару функция + экземпляр self
В данном случае, когда позже на кнопке производится щелчок для имитации его выполнения в графическом пользовательском интерфейсе, вместо самого экземпляра вызывается его метод changeColor, который обработает информацию о состоянии объекта:
cbl = Callback('blue')
obj = cbl.changeColor # Зарегистрированный обработчик событий obj () # При возникновении события выводит turn blue
Обратите внимание, что функция lambda здесь не требуется, т.к. ссылка на связанный метод сама по себе уже откладывает вызов на потом. Такая методика проще, но вероятно менее универсальна, чем перегрузка вызовов с помощью_call_.
Связанные методы более подробно обсуждаются в следующей главе.
Вы увидите еще один пример с методом_call_в главе 32, где мы будем его
использовать для реализации того, что известно как декоратор функции — вызываемый объект, часто применяемый для добавления уровня логики поверх внедренной функции. Поскольку метод_call_позволяет присоединять к вызываемому объекту
информацию о состоянии, он является естественной методикой реализации для функции, которая должна помнить о вызове еще одной функции, когда она вызывается
сама. За дополнительными примерами работы с методом_call_обращайтесь к
вариантам сохранения состояния в главе 17 первого тома, а также к более развитым декораторам и метаклассам в главах 39 и 40.
Сравнения:_It_,_gt_и другие
Следующая группа методов перегрузки поддерживает сравнения. Как было указано в табл. 30.1, классы могут определять методы для перехвата всех шести операций сравнения: <, >, <=, >=, == и ! =. В целом эти методы использовать легко, но необходимо иметь в виду описанные далее ограничения.
• В отличие от обсуждаемой ранее пары_add_/_radd_правосторонних вариантов методов сравнения не существует. Когда сравнение поддерживает только один операнд, взамен применяются зеркальные методы (скажем,_It_и
_gt_зеркальны относительно друг друга).
• Явных взаимоотношений между операциями сравнения не существует. Например, истинность операции == вовсе не подразумевает, что! = даст ложь, поэтому для корректного поведения обеих операций должны быть определены оба метода_eq_и_пе_.
• В Python 2.Х метод_стр_используется для всех сравнений, если не определены более специфические методы сравнений; он возвращает число, которое меньше, равно или больше нуля, соответствующее результату сравнения его двух аргументов (self и второй операнд). Для вычисления своего результата метод
_стр_часто применяет встроенную функцию стр (х, у). В Python З.Х метод
_стр_и встроенная функция стр были удалены: используйте вместо них более специфические методы.
Из-за нехватки места мы не можем провести глубокие исследования методов сравнений, но в качестве краткого введения взгляните на следующий класс и тестовый код:
class С:
data = ' spam'
def _gt_(self, other) : # Версия для Python З.Х и 2.X
return self.data > other
def _It_(self, other):
return self.data < other
X = C()
print (X > 'ham') # True (выполняется_gt_)
print (X < 'ham') # False (выполняется_It_)
При запуске под управлением Python З.Х или 2.Х операции print в конце отображают ожидаемые результаты, отмеченные в комментариях, поскольку методы класса перехватывают и реализуют выражения сравнений. Дополнительные детали ищите в руководствах по Python и в других справочных ресурсах; например,_It_применяется для сортировок в Python3.X, а что касается бинарных операций выражений, то эти методы могут также возвращать объект Not Implemented для неподдерживаемых аргументов.
Метод сир в Python 2.Х
В Python 2.Х метод_стр_используется как запасной вариант, если не определены более специфические методы: его целочисленный результат применяется для оценки выполняемой операции. Например, следующий код дает в Python 2.Х тот же самый результат, что и код из предыдущего раздела, но терпит неудачу в Python З.Х, т.к. метод_стр_больше не используется:
class С:
data = 'spam' # Только в Python 2.Х
def _сшр_(self, other) : # В Python З.Х метод_стр не используется
return emp(self.data, other) # Функция стр в Python З.Х не определена
X = С()
print (X > 'ham') # True (выполняется_emp )
print (X < 'ham’) # False (выполняется_emp )
Обратите внимание, что этот код терпит неудачу в Python З.Х из-за того, что метод _стр_перестал быть специальным, а не потому, что встроенной функции стр больше не существует. Если мы изменим код предыдущего класса, как показано ниже, чтобы попытаться смоделировать вызов стр, то код по-прежнему будет работать в Python
2.Х, но давать отказ в Python З.Х:
class С:
data = ' spam'
def _emp_(self, other):
return (self.data > other) - (self.data < other)
У вас может возникнуть вопрос: для чего рассматривать метод сравнения, который больше не поддерживается в Python З.Х? Хотя легче полностью забыть историю, настоящая книга задумывалась для читателей, применяющих обе линейки, Python 2.Х и
Python З.Х. Поскольку метод_стр может встречаться в коде Python 2.Х, который
читателям придется использовать или сопровождать, он заслуживает рассмотрения
в книге. Кроме того, удаление метода_стр_было большей неожиданностью, чем
изъятие описанного ранее метода_getslice_, так что он мог применяться дольше. Однако если вы используете Python З.Х или заинтересованы в будущем запуске своего кода под управлением Python З.Х, тогда больше не применяйте_стр_: взамен отдавайте предпочтение более специфическим методам сравнений.
Булевские проверки:_bool_и_1еп_
Следующий набор методов действительно полезен. Как уже известно, каждый объект в Python по своему существу является истинным или ложным. При написании кода классов вы можете определять, что это означает для объектов, реализуя методы, которые дают значения True или False экземпляров по запросу. Имена таких методов отличаются в линейках Python; здесь мы начнем с Python З.Х, после чего представим эквивалент в Python 2.Х.
Ранее кратко упоминалось, что в булевских контекстах Python сначала пробует получить прямое булевское значение с помощью метода_bool_; если данный метод
отсутствует, то Python посредством_1еп_пытается вывести значение истинности
из длины объекта. Первый метод для получения булевского значения обычно использует состояние объекта и другую информацию. Вот как он применяется в Python З.Х:
>>> class Truth:
def_bool_(self) : return True
»> X = Truth ()
»> if X: print ('yes! 1)
yes!
>>> class Truth:
def_bool_(self) : return False
>>> X = Truth ()
»> bool (X)
False
Если метод_bool_отсутствует, тогда Python прибегает к методу_len_, т.к.
непустой объект считается истинным (т.е. ненулевая длина означает, что объект истинный, а нулевая — что он ложный):
>>> class Truth:
def_len_(self) : return 0
»> X = Truth ()
»> if not X: print ('no! ')
no!
Если присутствуют оба метода, то Python отдает предпочтение_bool_перед
_len_, потому что он более специфичен:
>>> class Truth:
def_bool_(self) : return True # Python З.Х пробует сначала_bool_
def_len_(self) : return 0 # Python 2.X пробует сначала_len_
»> X = Truth ()
»> if X: print ('yes!')
yes!
Если ни один из двух методов истинности не определен, тогда объект бессодержательно считается истинным (хотя любые потенциальные последствия для склонных к метафизике читателей полностью случайны):
>» class Truth: pass
»> X * Truth ()
»> bool (X)
True
Во всяком случае, так выглядит Truth в Python З.Х. В Python 2.Х приведенные примеры не генерируют исключения, но некоторые из производимых ими результатов могут показаться несколько странными (и стать причиной жизненного кризиса или даже двух), если только вы не прочитаете следующий раздел.
Булевские методы в Python 2.Х
Увы, ситуация не настолько драматична как было объявлено — во всем коде из предыдущего раздела пользователи Python 2.Х просто используют_nonzero_вместо
_bool_. Метод_nonzero_из Python 2.Х в Python З.Х переименован на_bool_,
но в остальном булевские проверки работают точно так же; в качестве запасного варианта в обеих линейках Python З.Х и Python 2.Х применяется метод_len_.
Есть одна тонкость: если вы не используете имя из Python 2.Х, то первая проверка
в предыдущем разделе будет работать точно так же, но лишь потому, что_bool_не
распознается как особое имя метода в Python 2.Х, а объекты по умолчанию считаются истинными! Чтобы увидеть такое отличие между версиями вживую, необходимо возвратить False:
С:\code> с:\python37\python >>> class С:
def_bool_(self) :
print (' in bool') return False
»> X = C()
»> bool (X)
in bool False
»> if X: print(99)
in bool
В Python З.Х код работает, как было заявлено. Тем не менее, в Python 2.Х метод _bool_игнорируется и объект всегда по умолчанию расценивается как истинный:
С: \code> с: \python27\python >>> class С:
def_bool_(self) :
print (' in bool') return False
»> X = C()
»> bool (X)
True
>>> if X: print (99)
99
Краткая история такова: в Python 2.Х применяйте_nonzero_для булевских значений или возвращайте 0 из запасного метода_len_, чтобы указывать на ложное
значение:
С:\code> с:\python27\python >>> class С:
def_nonzero_(self) :
print (' in nonzero')
return False # Возвращает целое число (или True/False, то же что 1/0)
»> X = С()
»> bool (X)
in nonzero False
>>> if X: print (99)
in nonzero
Но имейте в виду, что метод_nonzero_работает только в Python 2.Х. Если использовать _nonzero_в Python З.Х, то он молчаливо игнорируется, а объект по
умолчанию классифицируется как истинный — в точности то, что происходит в случае применения в Python 2.Х метода_bool_из Python З.Х!
И теперь, когда нас удалось перейти в область философии, давайте займемся последним контекстом перегрузки: кончиной объектов.
Уничтожение объектов: del
Пришло время завершать главу — и научиться делать то же самое с нашими объектами классов. Мы видели, как конструктор_init_вызывается всякий раз, когда
генерируется экземпляр (и отметили, что сначала для создания объекта выполняется _new_). Его противоположность, метод деструктора_del_, запускается автоматически при возвращении пространства памяти, занимаемого экземпляром (т.е. во время “сборки мусора”):
»> class Life:
def _init_(self, name=1 unknown *) :
print ('Hello ' + name) self.name = name def live(self):
print(self.name)
def del_(self) :
print('Goodbye ' + self.name)
»> brian = Life ('Brian')
Hello Brian
»> brian.live()
Brian
»> brian * 'loretta'
Goodbye Brian
Когда объекту brian присваивается строка, ссылка на экземпляр Life утрачивается и потому запускается его метод деструктора. Это работает и может быть полезным для реализации действий по очистке, таких как закрытие подключения к серверу. Однако деструкторы не настолько часто используется в Python и в ряде других языков ООП по причинам, которые описаны в следующем разделе.
Замечания относительно использования деструкторов
Метод деструктора работает так, как документировано, но с ним связано несколько известных предостережений и откровенно темных уголков, из-за которых он редко встречается в коде на Python.
• Необходимость. С одной стороны, деструкторы не настолько полезны в Python, как в ряде других языков ООП. Поскольку Python автоматически возвращает все пространство памяти, занимаемое экземпляром, когда экземпляр уничтожается, деструкторы не требуются для управления памятью. В текущей реализации CPython также не нужно закрывать в деструкторах файловые объекты, удерживаемые экземпляром, потому что при уничтожении экземпляра они автоматически закрываются. Тем не менее, в главе 9 первого тома упоминалось, что иногда по-прежнему лучше в любом случае запускать методы закрытия файлов, т.к. поведение автоматического закрытия может варьироваться в альтернативных реализациях Python (скажем, в Jython).
• Предсказуемость. С другой стороны, не всегда легко спрогнозировать, когда экземпляр будет уничтожен. В ряде случаев внутри системных таблиц могут присутствовать долго существующие ссылки на ваши объекты, которые препятствуют выполнению деструкторов, когда программа ожидает их запуска. Вдобавок Python вовсе не гарантирует, что методы деструкторов будут вызваны для объектов, которые все еще существуют при завершении работы интерпретатора.
• Исключения. На самом деле метод_del_может оказаться сложным в применении даже по более тонким причинам. Например, возникающие в нем исключения просто выводят предупреждающее сообщение в sys. stderr (стандартный поток ошибок), а не генерируют событие исключения, из-за непредсказуемого контекста, в котором метод_del_выполняется сборщиком мусора — не всегда возможно знать, куда такое исключение должно быть доставлено.
• Циклы. Кроме того, циклические (круговые) ссылки между объектами могут препятствовать запуску сборки мусора, когда она ожидается. По умолчанию включенный необязательный обнаружитель циклов способен со временем автоматически собирать объекты подобного рода, но только если они не имеют методов
_del_. Поскольку это относительно малоизвестно, здесь мы проигнорируем
дальнейшие детали; за дополнительной информацией обращайтесь к описанию
метода_del_и модуля сборщика мусора дс в стандартных руководствах по
Python.
Из-за таких недостатков часто лучше реализовывать действия завершения в явно вызываемом методе (скажем, shutdown). Как будет описано в следующей части книги, оператор try/ finally также поддерживает действия завершения подобно оператору with для объектов, которые поддерживают модель диспетчеров контекстов.
Резюме
Здесь было приведено столько примеров перегрузки, сколько позволил объем главы. Большинство оставшихся методов перегрузки операций работают подобно исследованным в этой главе, и все они являются просто привязками для перехвата операций над встроенными типами. Например, некоторые методы имеют уникальные списки аргументов или возвращаемые значения, но общая схема использования остается такой же. Позже в книге мы увидим несколько других методов перегрузки в действии:
• в главе 34 применяются методы_enter_и_exit_в диспетчерах контекстов операторов with;
• в главе 38 используются методы извлечения/установки_get_и_set_дескрипторов классов;
• в главе 40 применяется метод создания объектов_new_в контексте метаклассов.
Вдобавок некоторые изученные здесь методы, такие как_call_и_str_, будут задействованы в примерах позже в книге. Однако за более полным описанием обращайтесь к другим источникам документации — дополнительные детали о других методах перегрузки ищите в стандартном руководстве или в различных справочниках по языку Python.
В следующей главе мы оставим область механики классов, чтобы исследовать общепринятые паттерны проектирования — распространенные способы использования и объединения классов, направленные на оптимизацию многократного применения кода. Затем мы рассмотрим несколько продвинутых тем и перейдем к исключениям — последней основной теме книги. Тем не менее, прежде чем двигаться дальше, ответьте на контрольные вопросы главы, закрепив знания приведенных в главе концепций.
Проверьте свои знания: контрольные вопросы
1. Какие два метода перегрузки операций можно использовать для поддержки итерации в классах?
2. Какие два метода перегрузки операций обрабатывают вывод, и в каких контекстах?
3. Как можно перехватывать операции нарезания в классе?
4. Как можно перехватывать сложение на месте в классе?
5. Когда должна предоставляться перегрузка операций?
Проверьте свои знания: ответы
1. Классы могут поддерживать итерацию путем определения (или наследования)
метода_getitem_или_iter_. Во всех итерационных контекстах Python
сначала пытается применить метод_iter_, возвращающий объект, который
поддерживает протокол итерации с помощью метода_next_: если поиск в
иерархии наследования не привел к нахождению метода_iter_, тогда Python
прибегает к методу индексирования_getitem_, многократно вызывая его с
последовательно увеличивающимися индексами. В случае использования оператора yield метод_next_может быть создан автоматически.
2. Методы_str_и_герг_реализуют отображения объектов при выводе.
Первый вызывается встроенными функциями print и str; второй вызывается print и str, если отсутствует_str_, и всегда вызывается встроенной функцией герг, при эхо-выводе в интерактивной подсказке и для вложенных появлений. То есть метод_герг_применяется везде, исключая print и str, когда
определен метод_str_. Метод_str_обычно используется для отображений, дружественных к пользователю, а_герг_предоставляет для объекта дополнительные детали или форму как в коде.
3. Нарезание перехватывается методом индексирования_getitem_: он вызывается с объектом среза, а не с простым целочисленным индексом, и при необходимости объекты срезов можно передавать или ожидать. В Python 2.Х может
также применяться метод_getslice_(исчезнувший в Python З.Х) для срезов
с двумя пределами.
4. Сложение на месте сначала пытается использовать метод_iadd_и затем
_add_с присваиванием. Та же схема применяется для всех бинарных операций. Для правостороннего сложения также доступен метод_radd_.
5. Когда класс естественным образом согласуется с интерфейсами встроенного типа или должен их эмулировать. Например, коллекции могут имитировать интерфейсы последовательностей или отображений, а вызываемые объекты могут быть реализованы для использования с API-интерфейсом, который ожидает функцию. Однако в целом вы не должны реализовывать операции выражений, если они естественно и логически не подходят для ваших объектов — взамен применяйте нормально именованные методы.
ГЛАВА 31
Назад: Детали реализации классов
Дальше: Проектирование с использованием классов