Главная страница

Математический анализ. 3е издание


Скачать 4.86 Mb.
Название3е издание
АнкорМатематический анализ
Дата04.02.2022
Размер4.86 Mb.
Формат файлаpdf
Имя файлаpython_01.pdf
ТипДокументы
#351981
страница75 из 98
1   ...   71   72   73   74   75   76   77   78   ...   98
614
Глава 24. Подробнее о программировании классов def __iter__(self):
return SkipIterator(self.wrapped) # Каждый раз новый итератор
if __name__ == '__main__':
alpha = 'abcdef'
skipper = SkipObject(alpha) # Создать объектконтейнер
I = iter(skipper) # Создать итератор для него
print I.next(), I.next(), I.next() # Обойти элементы 0, 2, 4
for x in skipper: # for вызывает __iter__ автоматически
for y in skipper: # Вложенные циклы for также вызывают __iter__
print x + y, # Каждый итератор помнит свое состояние, смещение
Этот пример работает подобно вложенным циклам со строками – каж
дый активный цикл запоминает свое положение в строке, потому что каждый из них получает независимый объект итератора, который хранит свою собственную информацию о состоянии:
% python skipper.py
a c e aa ac ae ca cc ce ea ec ee
Наш более ранний пример класса Squares, напротив, поддерживал все
го одну активную итерацию, нужно было во вложенных циклах вызы
вать Squares снова, чтобы получить новый объект. Здесь у нас имеется единственный объект SkipObject, который создает множество объектов итераторов.
Как и прежде, подобных результатов можно было бы достичь с исполь
зованием встроенных инструментов, например с помощью операции получения среза с третьим граничным значением, чтобы организовать пропуск элементов:
>>> S = 'abcdef'
>>> for x in S[::2]:
... for y in S[::2]: # Новые объекты в каждой итерации
... print x + y,
aa ac ae ca cc ce ea ec ee
Однако это далеко не то же самое по двум причинам. Вопервых, каж
дое выражение извлечения среза физически сохраняет весь список с результатами в памяти, тогда как итераторы воспроизводят по одному значению за раз, что позволяет существенно экономить память в случае большого объема результатов. Вовторых, операции извлечения среза создают новые объекты, поэтому в действительности итерации не про
текают одновременно в одном и том же объекте. Чтобы оказаться бли
же к реализации на основе классов, нам необходимо было бы создать единственный объект для обхода, заранее выполнив операцию извле
чения среза:
>>> S = 'abcdef'
>>> S = S[::2]

Перегрузка операторов
615
>>> S
'ace'
>>> for x in S:
... for y in S: # Тот же самый объект, новые итераторы
... print x + y,
aa ac ae ca cc ce ea ec ee
Эта реализация больше похожа на наше решение, выполненное с помо
щью классов, но здесь попрежнему список с результатами целиком хранится в памяти (на сегодняшний день не существует генераторов,
способных формировать срезы), и эта реализация эквивалентна только для данного конкретного случая пропуска каждого второго элемента.
Итераторы могут выполнять любые действия, которые можно реали
зовать в классах, поэтому они обладают более широкими возможно
стями, чем предполагается в данном примере. Независимо от того,
требуется ли такая широта возможностей в наших приложениях, ите
раторы, определяемые пользователем, представляют собой мощный инструмент – они позволяют создавать произвольные объекты, кото
рые выглядят и ведут себя подобно другим последовательностям и ите
рируемым объектам, с которыми мы встречались в этой книге. Мы могли бы использовать этот механизм, например, для создания объек
та базы данных, чтобы одновременно выполнять несколько итераций в одном и том же наборе данных, извлеченном в результате запроса к базе данных.
_
_getattr_
_ и _
_setattr_
_ перехватывают
обращения к атрибутам
Метод __getattr__ выполняет операцию получения ссылки на атрибут.
Если говорить более определенно, он вызывается с именем атрибута в виде строки всякий раз, когда обнаруживается попытка получить ссылку на неопределенный (несуществующий) атрибут. Этот метод не вызывается, если интерпретатор может обнаружить атрибут посредст
вом выполнения процедуры поиска в дереве наследования. Вследствие этого метод __getattr__ удобно использовать для универсальной обра
ботки запросов к атрибутам. Например:
>>> class empty:
... def __getattr__(self, attrname):
... if attrname == "age":
... return 40
... else:
... raise AttributeError, attrname
>>> X = empty()
>>> X.age
40
>>> X.name

616
Глава 24. Подробнее о программировании классов
...текст сообщения об ошибке опущен...
AttributeError: name
В этом примере класс и его экземпляр X не имеют своих собственных атрибутов, поэтому при обращении к атрибуту X.age вызывается метод
__getattr__
– в аргументе self передается экземпляр (X), а в аргументе attrname
– строка с именем неопределенного атрибута ("age"). Класс вы
глядит так, как если бы он действительно имел атрибут age, возвращая результат обращения к имени X.age (40).В результате получается атри
бут, вычисляемый динамически.
Для атрибутов, обработка которых классом не предусматривается,
возбуждается встроенное исключение AttributeError, чтобы сообщить интерпретатору, что это действительно неопределенные имена, – по
пытка обращения к имени X.name приводит к появлению ошибки. Вы еще раз встретитесь с методом __getattr__, когда мы будем рассматри
вать делегирование и свойства в действии в следующих двух главах,
а об исключениях я подробно буду рассказывать в седьмой части книги.
Родственный ему метод перегрузки __setattr__ перехватывает все по
пытки присваивания значений атрибутам. Если этот метод определен,
выражение self.attr = value будет преобразовано в вызов метода self.__setattr_('attr', value)
. Работать с этим методом немного слож
нее, потому что любая попытка выполнить присваивание любому атри
буту аргумента self приводит к повторному вызову метода __setattr__,
вызывая бесконечный цикл рекурсивных вызовов (и, в конечном ито
ге, исключение переполнения стека!). Если вам потребуется использо
вать этот метод, все присваивания в нем придется выполнять посредст
вом словаря атрибутов, как описывается в следующем разделе. Ис
пользуйте self.__dict__['name'] = x, а не self.name = x:
>>> class accesscontrol:
... def __setattr__(self, attr, value):
... if attr == 'age':
... self.__dict__[attr] = value
... else:
... raise AttributeError, attr + ' not allowed'
>>> X = accesscontrol()
>>> X.age = 40 # Вызовет метод __setattr__
>>> X.age
40
>>> X.name = 'mel'
...текст сообщения об ошибке опущен...
AttributeError: name not allowed
Эти два метода перегрузки операций доступа к атрибутам позволяют контролировать или специализировать доступ к атрибутам в ваших объектах. Они могут играть весьма специфические роли, часть из ко
торых мы рассмотрим далее в этой книге.

Перегрузка операторов
617
Имитация частных атрибутов экземпляра
Следующий фрагмент является обобщением предыдущего примера и позволяет каждому подклассу иметь свой перечень частных имен ат
рибутов, которым нельзя присваивать значения:
class PrivateExc(Exception): pass # Подробнее об исключениях позднее
class Privacy:
def __setattr__(self, attrname, value): # Вызывает self.attrname = value
if attrname in self.privates:
raise PrivateExc(attrname, self)
else:
self.__dict__[attrname] = value # Self.attrname = value
# вызовет зацикливание!
class Test1(Privacy):
privates = ['age']
class Test2(Privacy):
privates = ['name', 'pay']
def __init__(self):
self.__dict__['name'] = 'Tom'
x = Test1()
y = Test2()
x.name = 'Bob'
y.name = 'Sue' # <== ошибка
y.age = 30
x.age = 40 # <== ошибка
Фактически это лишь первая прикидочная реализация частных атри
бутов в языке Python (то есть запрет на изменение атрибутов вне клас
са). Несмотря на то что язык Python не поддерживает возможность объявления частных атрибутов, такие приемы, как этот, могут их ими
тировать. Однако это лишь половинчатое решение – чтобы сделать его более эффективным, его необходимо дополнить возможностью изме
нять значения частных атрибутов из подклассов и использовать метод
__getattr__
и классобертку (иногда называется проксиклассом), что
бы контролировать получение значений частных атрибутов.
Полную реализацию я оставлю как упражнение для самостоятельного решения, потому что хотя таким способом можно имитировать сокры
тие атрибутов, это практически никогда не используется. Программи
сты, использующие язык Python, способны писать крупные объектно
ориентированные платформы без частных объявлений, но существую
щие интересные решения по управлению доступом далеко выходят за рамки нашего обсуждения.
Перехват операций обращения к атрибутам и присваивания им значе
ний – вообще очень полезный прием. Он обеспечивает возможность де+
легирования –
способ, позволяющий обертывать встроенные объекты объектамиконтроллерами, добавлять новое поведение и делегировать

618
Глава 24. Подробнее о программировании классов выполнение операций обернутым объектам (подробнее о делегирова
нии и классахобертках рассказывается в следующей главе).
_
_repr_
_ и _
_str_
_ возвращают строковое представление
В следующем примере реализованы конструктор __init__ и метод пере
грузки __add__, которые мы уже видели, но также в нем реализован ме
тод __repr__, который возвращает строковое представление экземпля
ров. Здесь этот метод используется для преобразования объекта self.da
ta в строку. Если метод __repr__ (или родственный ему метод __str__) оп
ределен, он автоматически будет вызываться при попытках вывести экземпляр класса или преобразовать его в строку. Эти методы позволя
ют определить более удобочитаемый формат вывода ваших объектов:
>>> class adder:
... def __init__(self, value=0):
... self.data = value # Инициализировать атрибут data
... def __add__(self, other):
... self.data += other # Прибавить другое значение
>>> class addrepr(adder): # Наследует __init__, __add__
... def __repr__(self): # Добавляет строковое представление
... return 'addrepr(%s)' % self.data # Преобразует в строку
... # программного кода
>>> x = addrepr(2) # Вызывает __init__
>>> x + 1 # Вызывает __add__
>>> x # Вызывает __repr__
addrepr(3)
>>> print x # Вызывает __repr__
addrepr(3)
>>> str(x), repr(x) # Вызывает __repr__
('addrepr(3)', 'addrepr(3)')
Почему имеется два метода вывода? Дело в том, что сначала выполня
ется попытка использовать метод __str__, чтобы вывести объект в удоб
ном для пользователя виде, как это делают инструкция print и встроен
ная функция str. Метод __repr__ должен возвращать строку, которая могла бы использоваться как программный код для воссоздания объек
та, – он используется при автоматическом выводе результатов выраже
ний в интерактивной оболочке и в функции repr. Если метод __str__ от
сутствует, интерпретатор использует метод __repr__ (но не наоборот):
>>> class addstr(adder):
... def __str__(self): # есть __str__, но нет метода __repr__
... return '[Value: %s]' % self.data # Преобразовать в красивую строку
>>> x = addstr(3)
>>> x + 1
>>> x # По умолчанию вызывается repr
<__main__.addstr instance at 0x00B35EF0>
>>> print x # Вызывает __str__

Перегрузка операторов
619
[Value: 4]
>>> str(x), repr(x)
('[Value: 4]', '<__main__.addstr instance at 0x00B35EF0>')
Вследствие этого, если вам необходимо обеспечить единое отображение во всех контекстах, лучше использовать метод __repr__. Однако, опреде
лив оба метода, вы обеспечите поддержку вывода в различных контек
стах. Например, перед конечным пользователем объект будет отобра
жаться с помощью метода __str__, а перед программистом будет выво
диться информация более низкого уровня с помощью метода __repr__:
>>> class addboth(adder):
... def __str__(self):
... return '[Value: %s]' % self.data # Удобочитаемая строка
... def __repr__(self):
... return 'addboth(%s)' % self.data # Строка программного кода
>>> x = addboth(4)
>>> x + 1
>>> x # Вызывает __repr__
addboth(5)
>>> print x # Вызывает __str__
[Value: 5]
>>> str(x), repr(x)
('[Value: 5]', 'addboth(5)')
Похоже, что метод __str__ (и его низкоуровневый родственник __repr__)
является вторым по частоте использования после __init__ среди методов перегрузки операторов в сценариях на языке Python – всякий раз, когда вам приходится видеть адаптированное отображение при выводе объек
та, это значит, что скорее всего был использован один из этих методов.
_
_radd_
_ обрабатывает правостороннее сложение
C технической точки зрения метод __add__, который использовался в примерах выше, не поддерживает использование объектов экземп
ляров справа от оператора +. Чтобы реализовать поддержку таких вы
ражений и тем самым обеспечить допустимость перестановки операн
дов, необходимо создать метод __radd__. Интерпретатор вызывает ме
тод __radd__, только когда экземпляр вашего класса появляется справа от оператора +, а объект слева не является экземпляром вашего класса.
Во всех других случаях, когда объект появляется слева, вызывается метод __add__:
>>> class Commuter:
... def __init__(self, val):
... self.val = val
... def __add__(self, other):
... print 'add', self.val, other
... def __radd__(self, other):
... print 'radd', self.val, other

620
Глава 24. Подробнее о программировании классов
>>> x = Commuter(88)
>>> y = Commuter(99)
>>> x + 1 # __add__: экземпляр + не_экземпляр
add 88 1
>>> 1 + y # __radd__: не_экземпляр + экземпляр
radd 99 1
>>> x + y # __add__: экземпляр + не_экземпляр
add 88 <__main__.Commuter instance at 0x0086C3D8>
Обратите внимание на изменение порядка следования операндов в вы
зове метода __radd__: аргумент self в действительности находится спра
ва от оператора +, а аргумент other – слева. Любой двухместный опера
тор имеет похожий правосторонний метод перегрузки (например,
__mul__
и __rmul__). Обычно правосторонний метод, такой как __radd__,
просто изменяет порядок следования операндов и повторно выполняет операцию сложения, чтобы вызвать метод __add__, в котором находит
ся основная реализация операции. Кроме того, следует заметить, что здесь x и y – это экземпляры одного и того же класса – когда в выраже
нии участвуют экземпляры разных классов, интерпретатор предпочи
тает вызывать метод экземпляра, расположенного слева.
Правосторонние методы – это достаточно сложная тема, и на практике они используются очень редко – к ним требуется обращаться только в том случае, когда необходимо обеспечить для оператора возмож
ность перестановки операндов, и если вообще необходима реализация поддержки этого оператора. Например, эти методы могут использо
ваться в классе Vector, но в таких классах, как Employee или Button, ско
рее всего, они не нужны.
_
_call_
_ обрабатывает вызовы
Метод __call__ вызывается при вызове вашего экземпляра. Это не повто
ряющееся определение – если метод __call__ присутствует, интерпрета
тор будет вызывать его, когда экземпляр вызывается как функция. Это позволяет экземплярам классов имитировать поведение функций:
>>> class Prod:
... def __init__(self, value):
... self.value = value
... def __call__(self, other):
... return self.value * other
>>> x = Prod(2)
>>> x(3)
6
>>> x(4)
8
В этом примере реализация метода __call__ может показаться ненуж
ной. То же самое поведение можно реализовать с помощью простого метода:

Перегрузка операторов
621
>>> 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__ может оказаться удобнее при взаимодействии с прикладными интерфейсами, где ожидается функция, – это позволя
ет создавать объекты, совместимые с ожидающими получить функцию интерфейсами, которые к тому же способны сохранять информацию о своем состоянии между вызовами. Фактически этот метод занимает третье место среди наиболее часто используемых методов перегрузки операторов – после конструктора __init__ и методов форматирования
__str__
и __repr__.
Функциональные интерфейсы и программный
код обратного вызова
Инструментальный набор для создания графического интерфейса
Tkinter, с которым мы познакомимся далее в этой книге, позволяет ре
гистрировать функции как обработчики событий (они же – функции обратного вызова); когда возникают какиелибо события, Tkinter вы
зывает зарегистрированные объекты. Если вам необходимо реализо
вать обработчик событий, способный сохранять свое состояние между вызовами, вы можете зарегистрировать либо связанный метод класса,
либо экземпляр класса, который с помощью метода __call__ обеспечи
вает совместимость с функциональным интерфейсом. В программном коде этого раздела оба варианта – x.comp из второго примера и экземп
ляр x из первого – могут передаваться в виде объектов функций.
В следующей главе я более подробно расскажу о связанных методах,
а пока разберем гипотетический пример использования метода __call__
для построения графического интерфейса. Следующий класс опреде
ляет объект, поддерживающий функциональный интерфейс, и, кроме того, имеет информацию о состоянии, сохраняя цвет, в который долж
на окрашиваться нажатая кнопка:
class Callback:
def __init__(self, color): # Функция + информация о состоянии
self.color = color def __call__(self): # Поддерживает вызовы без аргументов
print 'turn', self.color
Теперь мы можем зарегистрировать экземпляры этого класса в контек
сте графического интерфейса как обработчики событий для кнопок,

1   ...   71   72   73   74   75   76   77   78   ...   98


написать администратору сайта