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

Управляемые атрибуты

 

В этой главе расширяется ранее представленная методика перехвата атрибутов и вводится еще одна, после чего они используются в нескольких более крупных примерах. Подобно всему остальному в данной части книги глава позиционируется, как посвященная сложным темам, и предназначена для факультативного чтения, поскольку большинству программистов приложений не нужно беспокоиться об обсуждаемых здесь материалах — они могут извлекать и устанавливать атрибуты объектов, не заботясь о реализации атрибутов.
Однако в особенности для разработчиков инструментов управление доступом к атрибутам может быть важной частью гибких API-интерфейсов. Кроме того, понимание раскрываемой в главе дескрипторной модели может сделать связанные инструменты, такие как слоты и свойства, более ясными и даже стать обязательным для изучения, если они встречаются в коде, с которым вы должны работать.
Для чего используются управляемые атрибуты?
Атрибуты объектов являются центральной частью большинства программ на Python — именно в них мы часто храним информацию о сущностях, обрабатываемых сценариями. Обычно атрибуты представляют собой просто имена для объектов; скажем, атрибут паше лица может быть строкой, извлекаемой и устанавливаемой с помощью базового синтаксиса атрибутов:
person.name # Извлечь значение атрибута
person.name = значение # Изменить значение атрибута
В большинстве случаев атрибуты находятся в самом объекте или наследуются из класса, от которого произведен объект. Такой базовой модели достаточно для большей части программ, которые вам доведется писать на протяжении вашей карьеры как программиста на Python.
Тем не менее, иногда требуется обеспечить высокую гибкость. Предположим, что вы пишете программу для использования атрибута name напрямую, но затем требования меняются — например, вы решаете, что имена должны проверяться посредством логики при установке или каким-то образом видоизменяться при извлечении. Реализовать методы для управления доступом к значению атрибута довольно легко (valid и transform здесь абстрактны):
class Person:
def getName(self): if not valid():
raise TypeError('cannot fetch name') # не удается извлечь имя else:
return self.name.transform ()
def setName(self, value): if not valid(value):
raise TypeError ('cannot change name1) # не удается изменить имя else:
self.name = transform(value)
person = Person ()
person.getName ()
person.setName('value')
Однако это также требует внесения изменений во всех местах целой программы, где применяются имена — вероятно нетривиальная задача. Вдобавок такой подход требует, чтобы программе было известно, каким образом экспортируются значения: как простые имена или как вызываемые методы. Если вы начинали с интерфейса к данным на основе методов, тогда клиенты невосприимчивы к изменениям; если же нет, то ситуация может стать проблематичной.
Подобная проблема способна возникать чаще, чем ожидается. Скажем, значение ячейки в программе для работы с электронными таблицами может начать свое существование как простая дискретная величина, но позже видоизмениться в произвольное вычисление. Так как интерфейс объекта должен быть достаточно гибким, чтобы поддерживать такие изменения в будущем, не нарушая работу существующего кода, переключение на методы в более позднее время далеко от идеала.
Вставка кода для запуска при доступе к атрибутам
Более удачное решение позволило бы запускать код автоматически при доступе к атрибутам, когда есть необходимость. В том и заключается одна из главных ролей управляемых атрибутов — они предоставляют способы добавления логики средства доступа к атрибутам после их создания. В более общем случае они поддерживают произвольные режимы использования атрибутов, которые выходят за рамки простого хранения данных.
В различных местах книги нам встречались инструменты Python, которые позволяют сценариям динамически вычислять значения атрибутов при их извлечении и проверять либо изменять значения атрибутов при их сохранении. В этой главе мы собираемся раскрыть уже представленные инструменты, исследовать другие доступные инструменты и изучить ряд более крупных примеров применения в данной предметной области. В частности, в главе рассматриваются четыре методики доступа.
• Методы_getattr_и_setattr_, предназначенные для направления операций извлечения неопределенных атрибутов и операций присваивания всех атрибутов обобщенным методам обработчиков.
• Метод_getattribute_, предназначенный для направления операций извлечения всех атрибутов обобщенному методу обработчика.
• Встроенная функция property, предназначенная для направления операции доступа к конкретному атрибуту функциям обработчиков получения и установки.
• Дескрипторный протокол, предназначенный для направления операций доступа к конкретному атрибуту экземплярам классов с произвольными методами обработчиков получения и установки и являющийся основой для других инструментов, таких как свойства и слоты.
Инструменты в первом пункте списка доступны во всех версиях Python, а в следующих трех пунктах — в Python З.Х и классах нового стиля в Python 2.Х. Они впервые появились в Python 2.2 вместе со многими другими расширенными инструментами, описанными в главе 32, такими как слоты и super. Инструменты из первого и третьего пунктов кратко упоминались в главах 30 и 32, а инструменты из второго и четвертого пунктов — по большому счету новые темы, которые мы исследуем в настоящей главе.
Вы увидите, что все четыре методики в известной мере разделяют цели, поэтому заданную задачу обычно можно решить с использованием любой из них. Тем не менее, они имеют важные отличия. Например, последние две методики применяются к конкретным атрибутам, тогда как первые две являются достаточно обобщенными, чтобы использоваться основанными на делегировании промежуточными классами, которые обязаны направлять внутренним объектам запросы произвольных атрибутов. Как будет показано далее, все четыре схемы также отличаются с точки зрения сложности и эстетики, о чем вы должны судить самостоятельно, посмотрев на них в действии.
Помимо изучения особенностей четырех методик перехвата атрибутов, перечисленных ваше, в настоящей главе также предоставляется возможность исследования более сложных программ, чем можно было видеть в других местах книги. Скажем, учебный пример CardHolder в конце главы послужит иллюстрацией крупных классов в действии. Некоторые из обрисованных здесь методик мы будем применять также в следующей главе при написании декораторов, поэтому прежде чем двигаться дальше, удостоверьтесь в том, что хотя бы в целом понимаете темы, рассматриваемые в текущей главе.
Свойства
Протокол свойств позволяет направлять операции извлечения, установки и удаления конкретного атрибута предоставляемым нами функциям или методам, что дает возможность вставлять код, подлежащий автоматическому выполнению при доступе к атрибуту, перехватывать операции удаления атрибута и при желании обеспечивать документацией по атрибутам.
Свойства создаются с помощью встроенной функции property и присваиваются атрибутам класса в точности как функции методов. Соответственно, подобно любым другим атрибутам класса они наследуются подклассами и экземплярами. Их функции перехвата доступа снабжаются аргументом экземпляра self, который дает доступ к информации состояния и атрибутам класса, доступным в переданном экземпляре.
Свойство управляет одним конкретным атрибутом. Хотя свойство не может обобщенно перехватывать все операции доступа к атрибуту, оно позволяет контролировать операции извлечения и присваивания и свободно делать вычисляемым атрибут, представляющий собой хранилище простых данных; работа существующего кода при этом не нарушается. Вы увидите, что свойства тесно связаны с дескрипторами; по существу они считаются их ограниченной формой.
Основы
Свойство создается присваиванием результата вызова встроенной функции атрибуту класса:
атрибут = property(fget, fset, fdel, doc)
Ни один из аргументов встроенной функции property не является обязательным, и все они получают стандартное значение None, если не передается иное. Для первых трех аргументов None означает, что соответствующая операция не поддерживается, и попытка ее выполнить приведет к генерации исключения AttributeError.
Когда мы используем аргументы property, то передаем в fget функцию для перехвата операций извлечения атрибута, в fset функцию для перехвата операций присваивания и в fdel функцию для перехвата операций удаления. Формально все три аргумента принимают любой вызываемый объект, в том числе метод класса, имеющий первый аргумент для получения уточняемого экземпляра. При последующем вызове функция fget возвращает вычисленное значение атрибута, fset и fdel ничего не возвращают (на самом деле None) и все три могут генерировать исключения, чтобы отклонять запросы на доступ.
Аргумент doc принимает строку документации для атрибута, если ее наличие желательно; в противном случае свойство копирует строку документации функции fget, которая обычно по умолчанию установлена в None.
Вызов встроенной функции property возвращает объект свойства, который мы присваиваем имени атрибута, подлежащего управлению в области видимости класса, где он будет наследоваться каждым экземпляром.
Первый пример
Чтобы показать, как все выглядит в коде, в следующем классе применяется свойство для отслеживания доступа к атрибуту по имени name; действительные хранящиеся данные называются _name, так что они не конфликтуют со свойством:
class Person: # Добавить (object) в Python 2.Х
def _init_(self, name):
self._name = name def getName(self):
print('fetch...') # извлечение...
return self._name def setName(self, value):
print(’change...') # изменение...
self._name = value def delName(self) :
print('remove...') # удаление...
del self._name
name = property(getName, setName, delName, "name property docs")
# документация по свойству name bob = Person ('Bob Smith') # Экземпляр bob имеет управляемый атрибут
print(bob.name) # Запускается getName
bob.name = 'Robert Smith' # Запускается setName
print(bob.name)
del bob.name # Запускается delName
print('-'*20)
sue = Person ('Sue Jones') # Экземпляр sue тоже наследует свойство
print(sue.name)
print(Person.name._doc ) # Или help(Person.name)
Свойства доступны в Python 2.Х и З.Х, но для корректной работы с операциями присваивания в Python 2.Х требуют наследования нового стиля от object — чтобы выполнить код в Python 2.Х, добавьте object в качестве суперкласса. Указывать суперкласс object можно и в Python З.Х, но он подразумевается и не требуется, а потому временами в книге опущен ради снижения перегруженности.
Это конкретное свойство мало что делает (оно просто перехватывает операции доступа и отслеживает атрибут), но предназначено для демонстрации самого протокола. После запуска кода два экземпляра наследуют свойство, как поступили бы с любым другим атрибутом, присоединенным к их классу. Однако операции доступа к атрибуту перехватываются:
c:\code> ру -3 prop-person.ру
fetch...
Bob Smith change... fetch...
Robert Smith remove...
fetch...
Sue Jones
name property docs
Как и все атрибуты класса, свойства наследуются экземплярами и подклассами на более низких уровнях. Например, если мы изменим код примера:
class Super:
...первоначальный код класса Person...
name = property(getName, setName, delName, 'name property docs')
class Person(Super):
pass # Свойства наследуются (подобно всем атрибутам класса)
bob = Person('Bob Smith')
. . . остальной код остался прежним. . .
то вывод останется тем же самым — подкласс Person наследует свойство паше от Super, а экземпляр bob получает его из Person. С точки зрения наследования свойства работают аналогично нормальным методам; поскольку они имеют доступ к аргументу экземпляра self, то могут обращаться к информации состояния и методам независимо от того, насколько глубоко находится подкласс, как в дальнейшем иллюстрируется в следующем разделе.
Вычисляемые атрибуты
В примере из предыдущего раздела было реализовано просто отслеживание доступа к атрибуту. Тем не менее, обычно свойства способны делать гораздо большее — скажем, динамически вычислять значение атрибута при извлечении, как показано ниже:
class PropSquare:
def _init_(self, start):
self.value = start def getX(self) : # При извлечении атрибута
return self.value ** 2 def setX(self, value) : # При присваивании атрибута
self.value = value
X = property (getX, setX) # Удаления и документации не предусмотрено
# Два экземпляра класса со свойством
# Каждый имеет отличающуюся информацию состояния
P = PropSquare (3)
Q = PropSquare (32)
print(P.X) P.X = 4
# 3 ** 2
# 4 ** 2
# 32 ** 2 (1024)
print (P.X) print(Q.X)
В классе PropSquare определен атрибут X, доступ к которому осуществляется так, как если бы он был статическими данными, но на самом деле при извлечении атрибута X запускается код для вычисления его значения. Эффект во многом похож на неявный вызов метода. Во время выполнения кода значение сохраняется в экземпляре как информация состояния, но каждый раз, когда мы извлекаем его через управляемый атрибут, значение автоматически возводится в квадрат:
c:\code> ру -3 ргор-сотриted.ру
9
16
1024
Обратите внимание, что мы создали два разных экземпляра — поскольку методы свойства автоматически принимают аргумент self, они имеют доступ к информации состояния, хранящейся в экземплярах. В рассматриваемом случае это означает, что при извлечении вычисляется квадрат собственных данных переданного в self экземпляра.
Реализация свойств с помощью декораторов
Хотя мы откладываем исследование дополнительных деталей до следующей главы, основы декораторов функций были представлены ранее в главе 32. Вспомните, что синтаксис декораторов функций:
@decorator
def func(args): ...
автоматически транслируется интерпретатором Python в показанный ниже эквивалент для повторной привязки имени функции к результату, который возвращает вызываемый объект decorator:
def func(args): ...
func = decorator(func)
Из-за такого отображения оказывается, что встроенная функция property может использоваться в качестве декоратора для определения функции, которая будет запускаться автоматически при извлечении атрибута:
class Person:
@property
def name (self) : . . . # Повторная привязка: name = property (name)
Во время выполнения декорированный метод автоматически передается в первом аргументе встроенной функции property. На самом деле декораторы функций являются всего лишь альтернативным синтаксисом для создания свойства и повторной привязки имени атрибута вручную, но в такой роли его можно считать более явным подходом:
class Person:
def name(self): ... name = property(name)
Декораторы для установки и удаления
Начиная с версий Python 2.6 и 3.0, объекты свойств также имеют методы getter, setter и deleter, которые назначают соответствующие методы доступа к свойству и возвращают копию самого свойства. Мы можем применять их для указания компонентов свойств, также декорируя нормальные методы, хотя компонент getter обычно заполняется автоматически самим процессом создания свойства:
class Person:
def _init_(self, name):
self._name = name
0property def name(self):
# пате = property (пате)
# документация по свойству name
# извлечение...
"name property docs" print('fetch...') return self._name
@name.setter def name(self, value): print('change. . . ' ) self._name = value
# name = name.setter (name)
# изменение. . .
0name.deleter def name(self) :
# name = name.deleter (name)
# удаление...
# Экземпляр bob имеет управляемый атрибут
# Запускается метод getter свойства name (name 1)
# Запускается метод setter свойства name (name 2)
# Запускается метод deleter свойства name (name 3)
# Экземпляр sue тоже наследует свойство
# Или help(Person.name)
print('remove...') del self._name
bob = Person(’Bob Smith') print(bob.name) bob.name = 'Robert Smith' print(bob.name) del bob.name
print('-'*20) sue = Person('Sue Jones') print(sue.name) print(Person.name._doc_)
Приведенный код фактически эквивалентен первому примеру в настоящем разделе — декорирование в данном случае представляет собой лишь альтернативный способ реализации свойств. Запуск кода дает те же самые результаты:
c:\code> ру -3 prop-person-deco.py
fetch...
Bob Smith change... fetch...
Robert Smith remove...
fetch...
Sue Jones
name property docs
По сравнению с ручным присваиванием результатов property использование декораторов для реализации свойств требует только трех дополнительных строк кода — разница, кажущаяся пренебрежимо малой. Однако, как часто бывает с альтернативными инструментами, выбор между двумя методиками по большей части субъективен.
Дескрипторы
Дескрипторы предлагают альтернативный способ перехвата доступа к атрибутам; они тесно связаны со свойствами, которые обсуждались в предыдущем разделе. В действительности свойство является разновидностью дескриптора — говоря формально, встроенная функция property представляет собой всего лишь упрощенный способ создания особого типа дескриптора, который запускает функции методов при доступе к атрибуту. По сути, дескрипторы — это внутренний механизм реализации для разнообразных инструментов, относящихся к классам, в том числе свойств и слотов.
С функциональной точки зрения дескрипторный протокол позволяет направлять операции извлечения, установки и удаления конкретного атрибута методам объекта экземпляра отдельного класса, которые мы предоставляем. В итоге мы получаем возможность вставлять код, подлежащий автоматическому запуску во время операций извлечения и присваивания, перехватывать операции удаления атрибутов и при желании снабжать документацией по атрибутам.
Дескрипторы создаются в виде независимых классов и присваиваются атрибутам классов в точности как функции методов. Подобно любым другим атрибутам классов они наследуются подклассами и экземплярами. Их методы перехвата доступа снабжаются как self для экземпляра самого дескриптора, так и экземпляром клиентского класса, чей атрибут ссылается на объект дескриптора. По указанной причине они могут предохранять собственную информацию состояния, а также информацию состояния экземпляра клиентского класса. Например, дескриптор может вызывать методы, доступные в клиентском классе, плюс определенные в нем методы, специфичные для дескриптора.
Как и свойство, дескриптор управляет одним конкретным атрибутом. Хотя дескриптор не способен перехватывать все операции доступа к атрибуту обобщенным образом, он обеспечивает контроль над операциями извлечения и присваивания и позволяет свободно изменять имя атрибута с простого хранилища данных на вычисляемый атрибут, не нарушая работу существующего кода. Свойства на самом деле являются просто удобным способом создания особого вида дескриптора и, как мы увидим, они могут быть непосредственно реализованы как дескрипторы.
В отличие от свойств дескрипторы охватывают более широкие границы и предоставляют более универсальный инструмент. Скажем, из-за того, что дескрипторы реализуются как нормальные классы, они имеют собственное состояние, способны принимать участие в иерархиях наследования дескрипторов, могут применять композицию для агрегирования объектов и предлагают естественную структуру для написания кода внутренних методов и строк документации по атрибутам.
Основы
Как уже упоминалось, дескрипторы реализуются как отдельные классы и предоставляют особым образом именованные методы доступа для операций доступа к атрибутам, которые они перехватывают. Методы извлечения, установки и удаления автоматически запускаются, когда в отношении атрибута, которому присвоен экземпляр класса дескриптора, выполняется соответствующая операция доступа:
class Descriptor:
"docstring goes here" # Строка документации
def _get_(self, instance, owner): ... # Возвращает значение атрибута
def _set_(self, instance, value): ... # Ничего не возвращает (None)
def _delete_(self, instance): ... # Ничего не возвращает (None)
Классы с любым таким методом считаются дескрипторами, а их методы будут специальными, когда один из их экземпляров присваивается атрибуту другого класса — при доступе к атрибуту методы автоматически вызываются. Если любой из методов отсутствует, то обычно это значит, что соответствующий вид доступа не поддерживается. Тем не
менее, в отличие от свойств пропуск_set_позволяет присваивать значение имени
атрибута дескриптора и, следовательно, переопределять его в экземпляре, тем самым скрывая дескриптор — чтобы сделать атрибут допускающим только чтение, потребуется определить_set_для перехвата операций присваивания и генерации исключения.
Дескрипторы с методами_set_также имеют последствия в особых случаях в отношении наследования, рассмотрение которых мы в основном отложим до главы 40, где будут раскрыты метаклассы и дана полная спецификация наследования. Вкратце
дескриптор с методом_set_формально известен как дескриптор данных и получает
преимущество перед другими именами, которые ищутся согласно нормальным правилам наследования. Например, унаследованный дескриптор для имени_class_
переопределяет то же самое имя в словаре пространств имен экземпляра. Это также помогает гарантировать, что дескрипторы данных, реализуемые вами в собственных классах, имеют преимущество перед остальными.
Аргументы методов дескриптора
Прежде чем мы напишем какой-то реалистичный код, давайте кратко ознакомимся с основами. Всем трем методам дескриптора, обрисованным в предыдущем разделе, передаются экземпляр класса дескриптора (self) и экземпляр клиентского класса, к которому присоединен экземпляр дескриптора (instance).
Метод доступа_get_ вдобавок принимает аргумент, указывающий класс, к
которому присоединен экземпляр дескриптора. Его аргумент instance представляет собой либо экземпляр, через который осуществлялся доступ к атрибуту (для instance, attr), либо None, когда доступ к атрибуту производился напрямую через класс владельца (для class, attr). Первый вариант, как правило, вычисляет значение для операции доступа через экземпляр, а второй обычно возвращает self, если поддерживается доступ к объекту дескриптора.
Скажем, в приведенном ниже сеансе Python З.Х при извлечении X.attr интерпретатор Python автоматически запускает метод_get_экземпляра класса Descriptor,
который был присвоен атрибуту класса Subject.attr. В Python 2.Х используйте эквивалент оператора print и унаследуйте оба класса от object, т.к. дескрипторы являются инструментом классов нового стиля; в Python З.Х такое наследование подразумевается и может быть опущено, хотя его наличие вреда не причинит:
»> class Descriptor: # Добавить (object) в Python 2.Х
def_get_(self, instance, owner) :
print(self, instance, owner, sep=1\n')
»> class Subject: # Добавить (object) в Python 2.X
attr = Descriptor () # Экземпляр Descriptor - атрибут класса
»> X = Subject ()
>>> X.attr
<_main_.Descriptor object at 0x0281E690>
<_main_.Subject object at 0x028289B0>
cclass '_main_.Subject'>
>>> Subject.attr
<_main_.Descriptor object at 0x0281E690>
None
<class '_main_.Subject'>
Обратите внимание на аргументы, автоматически переданные методу_get_при
первом извлечении атрибута — когда извлекается X.attr, то словно происходит следующая трансляция (хотя Subject.attr здесь не вызывает_get_еще раз):
X.attr -> Descriptor._get_(Subject.attr, X, Subject)
Дескриптору известно, что к нему обращаются напрямую, если его аргументом экземпляра оказывается None.
Дескрипторы атрибутов только для чтения
Как упоминалось ранее, в отличие от свойств простого отсутствия метода_set_
в дескрипторе недостаточно для того, чтобы сделать атрибут допускающим только чтение, т.к. имени дескриптора может быть присвоено значение в экземпляре. В следующем сеансе присваивание атрибуту X. а сохраняет а в объекте экземпляра X, тем самым скрывая дескриптор, который хранится в классе С:
»> class D:
def get_(*args): print('get')
»> class C:
a = D() # Атрибут a - это экземпляр дескриптора
»> X = C()
>» Х.а # Запускается метод_get_ унаследованного дескриптора
get
»> С.а
get
>>> Х.а = 99 # Сохраняется в X, скрывая С. а!
»> Х.а 99
>» list(X. diet_.keys ())
['а']
»> Y = С()
»> У.а # Y по-прежнему наследует дескриптор
get
»> С.а
get
Таким способом в Python работают все операции присваивания атрибутам экземпляров, что позволяет классам избирательно переопределять стандартные установки уровня классов в их экземплярах. Чтобы сделать основанный на дескрипторе атрибут допускающим только чтение, перехватывайте операцию присваивания в классе дескриптора и генерируйте исключение для предотвращения присваивания атрибуту. Когда выполняется присваивание атрибуту, являющемуся дескриптором, интерпретатор Python фактически обходит нормальное поведение присваивания на уровне экземпляров и направляет операцию объекту дескриптора:
>>> class D:
def_get_(*args) : print ('get')
def_set_(*args) : raise AttributeError('cannot set')
# He может быть установлен
»> class С: a = D()
»> X = C()
»> Х.а # Направляется С. a._get_
get
»> X.a = 99 # Направляется С. a._set_
AttributeError: cannot set
Ошибка атрибута: не может быть установлен
Также будьте осторожны, чтобы не спутать метод_delete_дескриптора с универсальным методом_del_. Первый вызывается при попытках
удалить имя управляемого атрибута из экземпляра класса владельца; второй представляет собой универсальный метод деструктора экземпляра, запускаемый в то время, когда экземпляр любого класса подвергается сборке мусора. Метод_delete_более тесно связан с обобщенным методом
удаления атрибутов_delattr_, который мы встретим позже в главе.
Методы перегрузки операций подробно обсуждались в главе 30.
Первый пример
Чтобы посмотреть, как все объединяется в более реалистичном примере, мы начнем с того же первого примера, который был написан для свойств. Ниже определяется дескриптор, который перехватывает доступ к атрибуту по имени паше в своих клиентах. Его методы применяют аргумент instance для доступа к информации состояния в передаваемом экземпляре, где фактически хранится строка имени. Подобно свойствам дескрипторы работают надлежащим образом только с классами нового стиля, поэтому если вы используете Python 2.Х , то обязательно унаследуйте от object оба класса — недостаточно унаследовать только дескриптор или только его клиент:
# Добавить (object) в Python 2.Х
class Name:
"name descriptor docs"
def _get_(self, instance,
print('fetch...') return instance._name
def_set_(self, instance, value)
print('change...') instance._name = value
def _delete_(self, instance):
print('remove...') del instance._name
class Person:
def _init_(self, name):
self._name = name name = Name ()
bob = Person('Bob Smith') print(bob.name) bob.name = 'Robert Smith' print(bob.name) del bob.name
# документация по свойству name
owner):
# извлечение.,
# изменение,,
# удаление.,
# Добавить (object) в Python 2.Х
# Присвоить дескриптор атрибуту
# Экземпляр bob имеет управляемый атрибут
# Запускается Name._get_
# Запускается Name._set_
# Запускается Name._delete_
print('-'*20) sue = Person('Sue Jones print(sue.name) print(Name._doc_)
# Экземпляр sue тоже наследует дескриптор
)
# Или help (Name)
Обратите внимание в коде на то, что мы присваиваем экземпляр класса дескриптора атрибуту класса в клиентском классе; благодаря этому он наследуется всеми экземплярами класса в точности как методы класса. Вообще говоря, мы обязаны присваивать дескриптор атрибуту класса — дескриптор не будет работать, если взамен присвоить
его атрибуту экземпляра self. При запуске методу_get_дескриптора передаются
три объекта для определения его контекста:
• self — экземпляр класса Name;
• instance — экземпляр класса Person;
• owner — класс Person.
Во время выполнения этого кода методы дескриптора перехватывают операции доступа к атрибуту во многом подобно версии со свойством. В действительности вывод оказывается снова таким же:
c:\code> ру -3 desc-person.py
fetch...
Bob Smith change... fetch...
Robert Smith remove...
fetch...
Sue Jones
name descriptor docs
Также подобно примеру со свойством экземпляр класса дескриптора является атрибутом класса и потому наследуется всеми экземплярами клиентского класса и его подклассов. Скажем, если мы изменим класс Person в примере следующим образом, то вывод сценария останется тем же самым:
class Super:
def _init_(self, name) :
self._name = name name = Name ()
class Person (Super) : # Дескрипторы наследуются
# (т.к. являются атрибутами класса)
pass
Кроме того, когда класс дескриптора бесполезен за рамками клиентского класса, то вполне разумно синтаксически внедрить определение дескриптора в его клиент. Вот как наш пример выглядит в случае применения вложенного класса:
class Person:
def _init_(self, name):
self._name = name
class Name: # Использование вложенного класса
"name descriptor docs"
def _get_(self, instance, owner):
print(’fetch... ') return instance._name
def _set_(self, instance, value):
print('change...1) instance._name = value
def _delete_(self, instance):
print('remove...’) del instance._name name = Name ()
При такой реализации Name становится локальной переменной в области видимости, принадлежащей оператору определения класса Person, и не будет конфликтовать с любыми именами вне класса. Данная версия работает аналогично первоначальной версии — мы просто перенесли определение класса дескриптора внутрь области видимости клиентского класса, — но последнюю строку тестового кода потребуется изменить, чтобы извлекать строку документации из нового местоположения (файл desc-person-nested.py):
print (Person. Name ._doc_) # Отличие: не Name._doc_ вне класса
Вычисляемые атрибуты
Как и в случае использования свойств, наш первый пример дескриптора из предыдущего раздела делал не особо многое — он всего лишь выводил трассировочные сообщения по мере доступа к атрибутам. На практике дескрипторы могут также применяться для вычисления значений атрибутов при каждом их извлечении. Сказанное иллюстрируется ниже в переделанном примере со свойством, где теперь используется дескриптор для автоматического возведения в квадрат значения атрибута каждый раз, когда оно извлекается:
class DescSquare:
def _init_(self, start):
self.value = start
def _get_(self, instance, owner)
return self.value ** 2
def _set_(self, instance, value)
self.value = value
# Каждый дескриптор имеет
# собственное состояние
# При извлечении атрибута
# При присваивании атрибута
# Операция удаления и строка
# документации отсутствуют
class Clientl:
X = DescSquare(3)
class Client2:
X = DescSquare(32)
# Присвоить экземпляр дескриптора атрибуту класса
# Еще один экземпляр в другом клиентском классе § Можно было бы также предусмотреть
# два экземпляра в том же самом классе
cl = Clientl() c2 = Client2()
print(cl.X) cl.X = 4 print(cl.X) print(c2.X)
# 3
* *
# 4 ** 2
# 32 ** 2
(1024)
Вывод, полученный в результате запуска примера, будет таким же, как в первоначальной версии со свойством, но здесь операции доступа к атрибуту перехватываются объектом класса дескриптора:
c:\code> ру -3 desс-computed.ру
9
16
1024
Использование информации состояния в дескрипторах
Изучив реализованные до сих пор два примера с дескрипторами, вы можете заметить, что они получают свою информацию из разных мест — первый (пример с атрибутом паше) работает с данными, хранящимися в экземпляре клиентского класса, а второй (пример с возведением в квадрат значения атрибута) задействует данные, присоединенные к самому объекту дескриптора (self). Фактически дескрипторы могут использовать состояние экземпляра и состояние дескриптора, а также любое их сочетание.
• Состояние дескриптора применяется для управления либо данными, используемыми при внутренней работе дескриптора, либо данными, которые охватывают все экземпляры. Оно может варьироваться в зависимости от места появления атрибута (часто в зависимости от клиентского класса).
• Состояние экземпляра хранит информацию, связанную и возможно созданную клиентским классом. Оно может варьироваться в зависимости от экземпляра клиентского класса (т.е. в зависимости от объекта приложения).
Другими словами, состояние дескриптора представляет собой данные для каждого дескриптора, а состояние экземпляра — данные для каждого экземпляра клиента. Как принято в ООП, вы обязаны тщательно выбирать состояние. Например, обычно вы не должны применять состояние дескриптора для записи имен сотрудников, поскольку каждый экземпляр клиента требует собственного значения — если они хранятся в дескрипторе, то каждый экземпляр клиентского класса будет фактически разделять ту же самую одиночную копию. С другой стороны, как правило, вы не будете использовать состояние экземпляра для записи данных, относящихся к внутренней реализации дескриптора — если они хранятся в каждом экземпляре, тогда окажется множество разных копий.
Методы дескриптора могут применять любую из двух форм состояния, но состояние дескриптора часто делает ненужным использование специальных соглашений по именованию с целью избегания конфликтов имен в экземпляре для данных, которые не специфичны к экземпляру. Скажем, следующий дескриптор присоединяет информацию к собственному экземпляру, поэтому она не конфликтует с информацией в экземпляре клиентского класса, но и разделяет такую информацию между двумя экземплярами клиента:
class DescState: # Использование состояния дескриптора, (object) в Python 2.Х
def _init_(self, value):
self.value = value
def _get_(self, instance, owner) : # При извлечении атрибута
print('DescState get') return self.value * 10
def _set_(self, instance, value): # При присваивании атрибута
print('DescState set') self.value = value
• Клиентский класс
class CalcAttrs:
# Атрибут класса дескриптора
# Атрибут класса
# Атрибут экземпляра
X = DescState(2)
Y = 3
def _init_(self)
self.Z = 4
obj = CalcAttrs()
print(obj.X, obj.Y, obj.Z) # X вычисляется, остальные нет
obj.X = 5 # Присваивание X перехватывается
CalcAttrs.Y = 6 # У повторно присваивается в классе
obj.Z = 7 # Z присваивается в экземпляре
print(obj.X, obj.Y, obj.Z)
obj2 = CalcAttrs () # Но X использует разделяемые данные подобно Yl
print(obj2.X, obj2.Y, obj2.Z)
Информация о внутреннем атрибуте value существует только в дескрипторе, следовательно, в случае применения того же самого имени в экземпляре клиента конфликт не возникает. Обратите внимание, что здесь управляемым является только атрибут дескриптора — операции извлечения и установки X перехватываются, но операции доступа к Y и Z нет (атрибут Y присоединен к клиентскому классу, a Z к экземпляру). Во время выполнения приведенного выше кода атрибут X вычисляется при извлечении, но его значение также остается одинаковым для всех экземпляров клиента из-за использования состояния уровня дескриптора:
c:\code> ру -3 desc-state-desc.py
DescState get 20 3 4
DescState set DescState get 50 6 7
DescState get 50 6 4
Кроме того, для дескриптора вполне реально хранить или применять атрибут, присоединенный к экземпляру клиентского класса, а не к самому себе. Важно отметить, что в отличие от данных, хранящихся в самом дескрипторе, это делает возможными данные, которые способны варьироваться в зависимости от экземпляра клиентского класса. Дескриптор в показанном далее примере предполагает, что экземпляр имеет атрибут X, присоединенный клиентским классом, и использует его для вычисления значения атрибута, который он представляет:
class InstState: # Использование состояния экземпляра, (object) в Python 2.Х
def _get_(self, instance, owner) :
print ('InstState get') # Предполагается, что установлен клиентским классом return instance._Х * 10
def _set_(self, instance, value):
print('InstState set') instance._X = value
# Клиентский класс class CalcAttrs:
X = InstState() # Атрибут класса дескриптора
Y = 3 # Атрибут класса
def _init_(self) :
self._X =2 # Атрибут экземпляра
self.Z =4 # Атрибут экземпляра
obj = CalcAttrs()
print(obj.X, obj.Y, obj.Z) # X вычисляется, остальные нет
obj.X = 5 # Присваивание X перехватывается
CalcAttrs.Y = 6 # Y повторно присваивается в классе
obj.Z = 7 # Z присваивается в экземпляре
print(obj.X, obj.Y, obj.Z)
obj2 = CalcAttrs () # Яо X теперь отличается подобно Z!
print(obj2.X, obj2.Y, obj2.Z)
Как и ранее, X присваивается дескриптор, управляющий доступом. Однако новый дескриптор здесь не содержит информации, а применяет атрибут, который предположительно существует в экземпляре — во избежание конфликтов с именем самого дескриптора данный атрибут назван X. Результаты выполнения этой версии похожи, но значение атрибута дескриптора может варьироваться в зависимости от экземпляра клиента из-за отличающейся политики состояния:
c:\code> ру -3 desc-state-inst.ру
InstState get 20 3 4
InstState set InstState get 50 6 7
InstState get
20 6 4
С состоянием дескриптора и состоянием экземпляра связаны свои роли. На самом деле это и есть то общее преимущество, которым дескрипторы обладают по сравнению со свойствами — поскольку они имеют собственное состояние, то могут легко сохранять данные внутренне, не добавляя их к пространству имен в объекте экземпляра клиента. В качестве сводки в следующем дескрипторе используются оба источника состояния — в self .data хранится информация для каждого атрибута, тогда как instance .data может изменяться от экземпляра к экземпляру клиента:
>» class DescBoth:
def_init_(self, data) :
self .data = data
def get_(self, instance, owner):
return ?%s, %s' % (self.data, instance.data)
def_set_(self, instance, value) :
ins tance. data = value
>>> class Client:
def_init_(self, data) :
self .data = data managed = DescBoth (' spam')
>» I = Client (1 eggs')
# Показать оба источника данных
# Изменить данные экземпляра
>» I.managed
'spam, eggs'
>» I.managed = 'SPAM'
>>> I.managed
'spam, SPAM'
Мы еще вернемся к последствиям выбора в более крупном учебном примере позже в главе. Прежде чем двигаться дальше, вспомните из обсуждения слотов в главе 32, что с помощью инструментов, подобных dir и getattr, мы можем получать доступ к “виртуальным” атрибутам вроде свойств и дескрипторов, хотя они не существуют в словаре пространств имен экземпляра. То, должны ли вы обращаться к ним таким способом, вероятно, зависит от программы — свойства и дескрипторы могут выполнять произвольные вычисления и быть менее очевидными “данными” экземпляра, чем слоты:
»> I. diet_
{'data': 'SPAM'}
»> [х for х in dir (I) if not x. startswith (1_')]
[1 data', ’managed']
»> getattr (I, 'data')
'SPAM'
»> getattr (I, 'managed')
'spam, SPAM'
>>> for attr in (x for x in dir (I) if not x. startswith (_')) :
print('%s => %s' % (attr, getattr(I, attr)))
data => SPAM managed => spam, SPAM
Более обобщенные инструменты_getattr_и_getattribute_, которые мы
встретим позже, не рассчитаны на поддержку такой функциональности — из-за того, что они не имеют атрибутов уровня класса, имена их “виртуальных” атрибутов не появляются в результатах dir1. Взамен они также не ограничиваются специфичными именами атрибутов, реализованных в виде свойств или дескрипторов: как объясняет^ ся в следующем разделе, данные инструменты разделяют даже больше, чем это поведение.
Связь между свойствами и дескрипторами
Как упоминалось ранее, свойства и дескрипторы тесно связаны — встроенная функция property является всего лишь удобным способом создания дескриптора. Теперь, когда вам известно, как работают оба средства, вы должны быть в состоянии видеть возможность эмуляции встроенной функции property с помощью класса дескриптора:
class Property:
def _init_(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget self.fset = fset
self, fdel = fdel # Сохранить несвязанный метод
self._doc_ = doc # или другие вызываемые объекты
def _get_(self, instance, instancetype=None):
if instance is None: return self if self.fget is None:
raise AttributeError ("can't get attribute") # нельзя извлечь атрибут return self.fget(instance) # Передать instance экземпляру self
# в методах доступа к свойствам
def _set_(self, instance, value):
if self.fset is None:
raise AttributeError ("can't set attribute") # нельзя установить атрибут self.fset(instance, value)
def _delete_(self, instance):
if self.fdel is None:
raise AttributeError ("can't delete attribute") # нельзя удалить атрибут self.fdel(instance) class Person:
def getName(self): print('getName...')
def setName(self, value): print('setName...' )
name = Property(getName, setName) # Использовать подобно property () x = Person() x.name
x. name = ' Bob' del x.name
Класс Property перехватывает операции доступа к атрибуту с помощью дескрип-торного протокола и направляет запросы функциям или методам, которые были переданы и сохранены в состоянии дескриптора, когда класс создавался. Скажем, операции извлечения атрибута направляются из класса Person методу_get_класса
Property и обратно методу getName класса Person. Благодаря дескрипторам все “просто работает”:
c:\code> ру -3 prop-desc-equiv.py
getName...
setName...
AttributeError: can't delete attribute
Ошибка атрибута: нельзя удалить атрибут
Тем не менее, обратите внимание, что эквивалентный класс дескриптора Property обрабатывает только базовое применение свойств; чтобы использовать декорашорный синтаксис @ также для спецификации операций установки и удаления, нам пришлось бы расширить класс Property методами setter и deleter, которые сохраняли бы декорированную функцию доступа и возвращали бы объект свойства (self должно быть достаточно). Так как встроенная функция property уже делает это, мы опустим здесь формальную реализацию такого расширения.
Дескрипторы, слоты и многое другое
Вероятно, теперь вы можете, по крайней мере, частично представить себе, как дескрипторы применяются для реализации расширения слотов в Python: словари атрибутов экземпляра аннулируются за счет создания дескрипторов уровня класса, которые перехватывают доступ к именам слотов и отображают такие имена на последовательное пространство хранения в экземпляре. Однако в отличие от вызова property большая часть магии, связанной со слотами, организуется во время создания класса автоматически и неявно, когда в классе присутствует атрибут_slots_.
Дополнительные сведения о слотах ищите в главе 32 (там также изложены причины, почему их не рекомендуется использовать кроме как в патологических сценариях). Дескрипторы применяются и для других инструментов, связанных с классами, но мы не будем здесь приводить дальнейшие внутренние подробности; нужная информация доступна в руководствах и исходном коде Python.
IjjZ | В главе 39 мы будем использовать дескрипторы для реализации декорашо-
I ров функций, которые применяются как к функциям, так и к методам. Вы увидите, что поскольку дескрипторы получают экземпляры дескрипторов » и клиентских классов, они хороши в этой роли, хотя вложенные функции обычно обеспечивают концептуально гораздо более простое решение.
В той же главе 39 мы еще задействуем дескрипторы как один из способов для перехвата извлечения методов встроенных операций.
Также обязательно ознакомьтесь с описанием приоритета дескрипторов данных в упоминавшейся ранее полной модели наследования: с помощью
_set_дескрипторы переопределяют другие имена и потому являются
довольно связывающими — они не могут скрываться именами в словарях экземпляров.
До сих пор мы изучали свойства и дескрипторы — инструменты для управления конкретными атрибутами. Методы перегрузки операций _getattr__и
_getattribute_предоставляют очередные способы перехвата извлечения атрибутов для экземпляров классов. Подобно свойствам и дескрипторам они позволяют вставлять код, подлежащий автоматическому запуску при доступе к атрибутам. Как вы увидите, указанные два метода могут также использоваться более универсально. Из-за того, что методы_getattr_и_getattribute_ перехватывают доступ к произвольным именам, они применяются в более широких ролях вроде делегирования, но могут также влечь за собой дополнительные вызовы в ряде контекстом и являются чересчур динамическими, чтобы регистрироваться в результатах dir.
Перехват извлечения атрибутов имеет две разновидности, реализуемые посредством двух разных методов.
• Метод_getattr_запускается для неопределенных атрибутов — поскольку он
выполняется только для атрибутов, которые не хранятся в экземпляре или не наследуются от одного из его классов, используется он прямолинейно.
• Метод_getattribute_запускается для каждого атрибута — поскольку он
включает все, вы должны соблюдать осторожность при его применении, чтобы избежать рекурсивных циклов из-за передачи суперклассу операций доступа к атрибутам.
Первый метод встречался в главе 30; он доступен во всех версиях Python. Второй метод доступен для классов нового стиля в Python 2.Х и для всех классов (неявно нового стиля) в Python З.Х. Эти два метода являются представителями набора методов
перехвата атрибутов, куда также входят_setattr_и_delattr_. Тем не менее,
так как данные методы исполняют похожие роли, мы будем трактовать их здесь в целом как одну тему.
В отличие от свойств и дескрипторов указанные методы относятся к универсальному протоколу перегрузки операций — множества особым образом именованных методов в классе, которые наследуются подклассами и автоматически запускаются, когда экземпляры используются в подразумеваемой встроенной операции. Подобно всем нормальным методам класса каждый из них при вызове принимает аргумент self, дающий доступ к любой необходимой информации состояния экземпляра, а также к другим методам класса, в котором они появляются.
Методы _getattr_и_getattribute_являются более обобщенными, чем
свойства и дескрипторы — они могут применяться для перехвата операции извлечения любого атрибута экземпляра (или даже всех), а не только одного конкретного имени. В итоге эти два метода хорошо подходят в универсальных кодовых схемах, основанных на делегировании — они могут использоваться для реализации объектов оболочек (известных как промежуточные объекты), которые управляют всем доступом к внутреннему объекту. Наоборот, мы должны определять по одному свойству или дескриптору для каждого атрибута, который хотим перехватывать. Как вскоре будет показано, в классах нового стиля такая роль несколько неполноценна для встроенных операций, но по-прежнему применима ко всем именованным методам в интерфейсе внутреннего объекта.
Наконец, эти два метода более узко ориентированы, чем ранее рассмотренные альтернативы: они перехватывают только операции извлечения, но не присваивания. Чтобы перехватывать также изменения атрибутов посредством присваивания, нам потребуется реализовать метод_setattr_— метод перегрузки операций, запускаемый для присваивания каждого атрибута, который должен позаботиться об избегании рекурсивных циклов за счет прогона операций присваивания атрибутов через словарь пространств имен экземпляра или метод суперкласса. Хотя и менее часто, мы
также можем реализовать метод перегрузки_delattr_(который обязан избегать
зацикливания аналогичным образом) для перехвата операций удаления атрибутов. Напротив, свойства и дескрипторы перехватывают операции извлечения, установки и удаления по определению.
Большинство методов перегрузки операций были представлены ранее в книге; здесь мы расширим их использование и выясним их роли в более крупных контекстах.
Основы
Методы_getattr_и_setattr_представлялись в главах 30 и 32, а метод
_getattribute_упоминался в главе 32. Говоря кратко, если класс определяет или
наследует следующие методы, то они будут автоматически запускаться, когда экземпляр задействован в контексте, описанном справа в комментарии:
def_getattr_(self, name) : # При извлечении неопределенных атрибутов [obj.name]
def _getattribute_(self, name) : # При извлечении всех атрибутов [obj . name]
def_setattr_(self, name, value) : # При присваивании всех атрибутов [obj. name=value]
def_delattr_(self, name) : # При удалении всех атрибутов [del obj.name]
Во всех методах self представляет собой объект экземпляра, на котором они вызываются, name — строковое имя атрибута, подвергающегося доступу, и value — объект, присваиваемый атрибуту. Два метода извлечения обычно возвращают значение атрибута, другие два ничего не возвращают (None). Все они могут генерировать исключения, сигнализируя о запрете доступа.
Например, для перехвата извлечения каждого атрибута мы можем применять любой из первых двух описанных ранее методов, а для перехвата присваивания каждого атрибута — третий метод. Приведенный далее класс использует_getattr_и переносим между Python 2.Х и З.Х без необходимости в наследовании нового стиля от object в Python 2.Х:
class Catcher:
def _getattr_(self, name):
print(’Get: %s' % name)
def _setattr_(self, name, value):
print('Set: %s %s' I (name, value))
X = Catcher()
X.job # Выводится Get: job
X.pay # Выводится Get: pay
X.pay =99 # Выводится Set: pay 99
Применение_getattribute_в этом конкретном случае работает точно так же,
но требует (только) в Python 2.Х наследования от object и обладает едва заметным потенциалом зацикливания, которым мы займемся в следующем разделе:
class Catcher(object): # В Python 2.Х требуется (object)
def _getattribute_(self, name): # Работает здесь так же, как getattr
print ('Get: Is' % name) i Яо в целом подвержен зацикливанию
. . . остальной код остался прежним. . .
Такая кодовая структура может использоваться для реализации паттерна проектирования с делегированием, обсуждавшегося в главе 31. Поскольку доступ ко всем атрибутам обобщенным образом направляется нашим методам перехвата, мы можем проверять и передавать их внедренным управляемым объектам. Скажем, в следующем классе (позаимствованном из главы 31) отслеживается извлечение каждого атрибута, сделанное другим объектом, который передается классу оболочки:
class Wrapper:
def _init_(self, object):
self.wrapped = object # Сохранить объект
def _getattr_(self, attrname):
print('Trace: ' + attrname) # Отслеживать извлечение
return getattr(self.wrapped, attrname) # Делегировать извлечение
X = Wrapper([1, 2, 3] )
X.append(4) # Выводится Trace: append
print (X.wrapped) # Выводится [1, 2, 3, 4]
Такого аналога для свойств и дескрипторов не существует, если не считать реализацию методов доступа для каждого возможного атрибута в каждом объекте, который может быть внутренним. С другой стороны, когда такая универсальность не требуется, обобщенные методы доступа могут влечь за собой дополнительные вызовы для операций присваивания в ряде контекстов — компромисс, который описан в главе 30 и упоминается в учебном примере, рассматриваемом в конце главы.
Избегание циклов в методах перехвата атрибутов
В целом обсуждаемые здесь методы применять легко; единственный связанный с ними более сложный аспект — возможность зацикливания (т.е. рекурсии). Из-за того, что метод_getattr_вызывается только для неопределенных атрибутов, в своем коде он может свободно извлекать другие атрибуты. Однако поскольку _getattribute_и_setattr_запускаются для всех атрибутов, при доступе к другим атрибутам в их коде потребуется проявлять осмотрительность, чтобы избежать повторного вызова самих себя и образования рекурсивного цикла.
Например, извлечение еще одного атрибута внутри кода метода_getattribute_
снова запустит_getattribute_и в коде обычно происходит зацикливание до тех
пор, пока не будет исчерпана доступная память:
def _getattribute_(self, name):
x = self.other # ЗАЦИКЛИВАНИЕ!
Формально метод_getattribute_еще более подвержен зацикливанию, чем
можно было предполагать — ссылка на атрибут self, производимая где угодно в классе, который определяет этот метод, запустит_getattribute_и в зависимости от логики класса тоже обладает потенциалом зацикливания. Как правило, такое поведение является желательным — в конце концов, данный метод предназначен для перехвата извлечения каждого атрибута, — но вы должны осознавать, что метод перехватывает операции извлечения всех атрибутов, где бы они не находились в коде. При появлении в коде самого метода_getattribute_они почти всегда приводят к зацикливанию. Чтобы избежать зацикливания, взамен прогоняйте операцию извлечения через расположенный выше суперкласс, пропустив версию текущего уровня — так как класс object всегда выступает в качестве суперкласса нового стиля, он хорошо подходит для такой роли:
def _getattribute_(self, name):
x = object._getattribute_(self, 'other') # Передача расположенному
# выше суперклассу
Для метода_setattr_ситуация аналогична, как было подытожено в главе 30 —
присваивание любому атрибуту внутри данного метода приводит к повторному запуску _setattr_и может создать похожий цикл:
def _setattr_(self, паше, value):
self.other = value # Рекурсия (и возможное ЗАЦИКЛИВАНИЕ!)
Здесь операции присваивания атрибуту self в любом месте класса, определяющего метод_setattr_, запускают этот метод снова, хотя потенциал зацикливания гораздо выше, когда операция присваивания атрибуту self находится в самом методе
_setattr_. Чтобы обойти проблему, вы можете взамен выполнить присваивание
атрибуту как ключу в словаре пространств имен_diet_экземпляра, избежав прямого присваивания:
def _setattr_(self, name, value):
self._diet_['other'] = value # Использование словаря атрибутов
Есть и менее традиционный подход — во избежание зацикливания метод _setattr_может также передавать собственные операции присваивания атрибутам расположенному выше суперклассу, почти как_getattribute_(и согласно
врезке “На заметку!” далее в главе такая схема временами предпочтительнее):
def _setattr_(self, name, value):
object._setattr_(self, 'other', value # Передача расположенному
# выше суперклассу
Тем не менее, для избегания циклов в_getattribute_использовать прием с
_diet_нельзя:
def _getattribute_(self, name) :
x = self._diet_['other'] # Зацикливание!
Извлечение самого атрибута_diet_приводит к повторному запуску метода
_getattribute_, образуя рекурсивный цикл. Странно, но это правда!
На практике метод_delattr_применяется менее часто, но в случае реализации
он вызывается для каждой операции удаления атрибута (в точности как_setattr_
вызывается для каждой операции присваивания атрибуту). Когда используется метод
_delattr_, вы обязаны позаботиться об избегании зацикливания при удалении
атрибутов посредством тех же самых методик: операций через словари пространств имен и обращений к методам суперкласса.
Как отмечалось в главе 30, атрибуты, которые реализованы с помощью средств классов нового стиля, таких как слоты и свойства, физически не хранятся в словаре пространств имен_diet_экземпляра (и слоты могут даже воспрепятствовать его существованию). По указанной причине в коде, где желательно поддерживать атрибуты подобного рода, должен
быть реализован метод_setattr_, чтобы выполнять присваивание по
показанной выше схеме object._setattr_, а не через индексирование self._diet_. Операций с_diet_достаточно для классов, о которых известно, что они хранят данные в экземплярах, как в автономных примерах, приводимых в главе; однако, в универсальных инструментах предпочтение должно отдаваться операциям с object.
Первый пример
Обобщенно управлять атрибутами не настолько сложно, как могло вытекать из предыдущего раздела. Давайте посмотрим, как изложенные идеи воплощаются на практике. Мы снова обращаемся к первому примеру, который применялся для демонстрации свойств и дескрипторов в действии, но на этот раз он реализован с использованием методов перегрузки операций. Из-за крайне обобщенного характера таких методов мы проверяем имена атрибутов, чтобы знать, когда происходит доступ к управляемому атрибуту; остальным атрибутам разрешено проходить нормально:
class Person: # Код переносимый: Python 2.Х или З.Х
def _init_(self, паше) : # При [PersonO ]
self._name = name # Запускается_setattr_/
def _getattr_(self, attr) : # При [obj . неопредел енный_атрибут]
print('get: ' + attr)
if attr == 'name' : # Перехват имени name: не хранится в экземпляре
return self._name # Зацикливания нет: реальный атрибут
else: # Остальные являются ошибками raise AttributeError(attr)
def _setattr_(self, attr, value) : # При [obj .любо й_а три бут = value]
print(’set: 1 + attr) if attr == ' name ' :
attr = '_name' # Установка внутреннего имени
self._diet_[attr] = value # Избегание зацикливания
def _delattr_(self, attr) : # При [del obj .любой_атрибут]
print('del: ' + attr) if attr == ' name ' :
attr = '_name' # Избегание зацикливания,
del self._diet_[attr] # но оно гораздо менее распространено
bob = Person('Bob Smith') # Экземпляр bob имеет управляемый атрибут
print(bob.name) # Запускается_getattr_
bob.name = 'Robert Smith' # Запускается_setattr_
print(bob.name)
del bob.паше # Запускается_delattr_
print('-'*20)
sue = Person ('Sue Jones' ) # Экземпляр sue также наследует свойство print(sue.name)
# print(Person.name._doc_) # Эквивалент здесь отсутствует
Обратите внимание, что присваивание атрибуту в конструкторе_init_тоже запускает метод_setattr_— он перехватывает операции присваивания всем атрибутам, даже внутри самого класса. При выполнении кода получается тот же самый вывод, но теперь он представляет собой результат работы нормального механизма перегрузки операций, поддерживаемого Python, и наших методов перехвата доступа к атрибутам:
c:\code> ру -3 getattr-person.py
set: _name get: name Bob Smith set: name get: name Robert Smith del: name
set: _name get: name Sue Jones
Также отметьте, что в отличие от свойств и дескрипторов здесь отсутствует прямое понятие указания документации для нашего атрибута; управляемые атрибуты существуют внутри кода методов перехвата, а не как отдельные объекты.
Использование_getattribute_
Для получения точно таких же результатов посредством_getattribute_замените метод_getattr_в примере приведенным далее кодом; поскольку он перехватывает извлечение всех атрибутов, в данной версии необходимо избегать зацикливания, передавая новые операции извлечения суперклассу, и в целом нельзя предполагать, что неизвестные имена являются ошибками:
# Замените_getattr_ следующим кодом
def _getattribute_(self, attr) : # При [obj .любой_атрибут]
print('get: ' + attr)
if attr == 'name' : # Перехват всех имен
attr = '_name' # Отображение на внутреннее имя
return object._getattribute_(self, attr) # Избегание зацикливания
Запуск кода после внесения изменений дает похожий вывод, но мы имеем добавочный вызов_getattribute_для операции извлечения в_setattr_(в первый
раз возникшей в_init_):
c:\code> ру -3 getattribute-person.py
set: _name
get: _diet_
get: name Bob Smith set: name
get: _diet_
get: name Robert Smith del: name get: _diet_
set: _name
get: _diet_
get: name Sue Jones
Пример эквивалентен тому, что был реализован для свойств и дескрипторов, но он несколько надуман и в действительности не подчеркивает возможности этих инструментов. Из-за своей обобщенной природы методы_getattr_и_getattribute_
вероятно чаще применяются в коде, основанном на делегировании (как уже упоминалось), где операции доступа к атрибутам проверяются на предмет достоверности и направляются внутреннему объекту. Там, где необходимо управлять всего лишь одним атрибутом, свойства и дескрипторы могут подходить в равной степени хорошо или даже лучше.
Вычисляемые атрибуты
Как и ранее, наш предыдущий пример на самом деле не делал ничего кроме отслеживания операций извлечения атрибутов; вычисление значения атрибута при извлечении требует не намного больше работы. Что касается свойств и дескрипторов, следующий код создает виртуальный атрибут X, при извлечении которого запускается вычисление:
class AttrSquare:
def _init_(self, start):
self.value = start # Запускается_setattr_!
def _getattr_(self, attr): # При операциях извлечения
# неопределенных атрибутов
if attr == ’X':
return self.value **2 # value не является неопределенным
else:
raise AttributeError(attr)
def_setattr_(self, attr, value): # При операциях присваивания всех атрибутов
if attr == 'X' : attr = ’value1 self._diet_[attr] = value
A 7 AttrSquare(3) # 2 экземпляра класса с перегрузкой
В = AttrSquare (32) # Каждый имеет отличающуюся информацию состояния
print(А.X) # 3 ** 2
А. X = 4
print(А.X) # 4 ** 2
print (В.Х) # 32 ** 2 (1024)
Результатом выполнения кода оказывается тот же самый вывод, который мы получали при использовании свойств и дескрипторов, но механика сценария основана на обобщенных методах перехвата атрибутов:
c:\code> ру -3 getattr-сотриted.ру
9
16
1024
Использование_getattribute_
Мы по-прежнему можем достичь того же эффекта с применением_getattribute_
else:
return object._getattribute_(self, attr)
def_setattr_(self, attr, value) : # При операциях присваивания всех атрибутов
if attr == 'X' : attr = 'value' object._setattr_(self, attr, value)
Когда эта версия, находящаяся в файле getattribute-computed.py, запускается, то снова получаются те же самые результаты. Тем не менее, обратите внимание, что в методах класса происходит неявное перенаправление:
• self, value = start внутри конструктора запускает_setattr_;
• self. value внутри_getattribute_снова запускает_getattribute_.
На самом деле метод_getattribute_запускается дважды каждый раз, когда мы
извлекаем атрибут X. В версии с_getattr_подобное не происходит, потому что
атрибут value не является неопределенным. Если вы заботитесь о скорости и хотите избежать двукратного вызова, тогда измените_getattribute_, чтобы для извлечения value также применять суперкласс:
def _getattribute_(self, attr):
if attr == 'X':
return object._getattribute_(self, 'value') ** 2
Конечно, здесь по-прежнему происходит вызов метода суперкласса, но не дополнительный рекурсивный вызов до того, как мы туда доберемся. Добавление к методам вызовов print позволит выяснить, каким образом и когда они запускаются.
Сравнение_getattr_и_getattribute_
Чтобы подытожить отличия между_getattr_и_getattribute_, в следующем примере оба метода используются для реализации трех атрибутов — атрибута класса attrl, атрибута экземпляра attr2 и виртуального управляемого атрибута attr3, вычисляемого при извлечении:
class GetAttr:
attrl = 1 ‘
def _init_(self) :
self.attr2 = 2
def _getattr_(self, attr): # Только при операциях извлечения
# неопределенных атрибутов print ('get: ' + attr) # Не при извлечении атрибута attrl:
# наследуется из класса
if attr == 'attr3' : # Не при извлечении атрибута attr2:
# хранится в экземпляре
return 3 else:
raise AttributeError(attr)
X = GetAttr() print(X.attrl) print(X.attr2) print(X.attr3) print('-'*20)
class GetAttribute(object): # Добавить (object) в Python 2.X
attrl = 1
def _init_(self) :
self.attr2 = 2
def _getattribute_(self, attr) : # При операциях извлечения всех атрибутов
print ('get: ' + attr) # Использование суперкласса во избежание
# за цикливания
if attr == 'attr3': return 3
else:
return object._getattribute_(self, attr)
X = GetAttribute() print(X.attrl) print(X.attr2) print(X.attr3)
Версия с_getattr_перехватывает только доступ к атрибуту attr3, т.к. он не
определен. С другой стороны, версия с_getattribute_перехватывает операции
извлечения всех атрибутов и обязана направлять те, которыми она не управляет, методу извлечения из суперкласса, чтобы избежать появления циклов:
c:\code> ру -3 getattr-v-getattr.ру
1
2
get: attr3 3
get: attrl
1
get: attr2
2
get: attr3
3
Хотя метод_getattribute_способен перехватывать больше операций извлечения атрибутов, чем_getattr_, на практике они часто являются лишь вариациями на тему — если атрибуты не хранятся физически, то оба метода обеспечивают тот же самый эффект.
Сравнение методик управления
Для подведения итогов по отличиям между всеми четырьмя схемами управления атрибутами, рассмотренными в главе, давайте проработаем более полный пример с вычисляемыми атрибутами, в котором применяется каждая методика и который рассчитан на выполнение в Python З.Х или 2.Х. В показанной ниже первой версии с использованием свойств перехватываются и вычисляются атрибуты square и cube. Обратите внимание, что их базовые значения хранятся в именах, начинающихся с символа подчеркивания, чтобы они не конфликтовали с именами самих свойств:
# Два динамически вычисляемых атрибута, реализованные с помощью свойств
class Powers(object): # В Python 2.Х требуется (object)
def _init_(self, square, cube):
self._square = square # _square - базовое значение
self._cube = cube # square - имя свойства
def getSquare(self):
return self._square ** 2 def setSquare(self, value): self._square = value square = property(getSquare, setSquare)
def getCube(self):
return self._cube ** 3 cube = property(getCube)
X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print (X. cube) #4 ** 3 - 64
X.square = 5
print(X.square) #5 ** 2 = 25
Чтобы сделать то же самое посредством дескрипторов, мы определяем атрибуты с помощью полных классов. Обратите внимание, что дескрипторы хранят базовые значения в виде состояния экземпляра, поэтому они должны применять ведущие символы подчеркивания, чтобы не конфликтовать с именами дескрипторов. В финальном примере главы мы увидим, что такого требования по переименованию можно было бы избежать за счет хранения базовых значений как состояния дескрипторов, но это не касается непосредственно данных, которые должны варьироваться в зависимости от экземпляра клиентского класса:
# То же самое, но с помощью дескрипторов (состояние для каждого экземпляра) class DescSquare(object):
def _get_(self, instance, owner):
return instance._square ** 2
def_set_(self, instance, value):
instance._square = value class DescCube(object):
def_get_(self, instance, owner):
return instance._cube ** 3
class Powers (object) : # В Python 2.X требуется (object)
square = DescSquare () cube = DescCube ()
def _init_(self, square, cube):
self._square = square # self.square = square тоже работает, self._cube = cube # т.к. приводит к запуску_set_ дескриптора !
X = Powers(3, 4)
print(X.square) #3 **2=9
print(X.cube) # 4 ** 3 = 64
X.square = 5
print(X.square) #5 ** 2 = 25
Для получения того же результата с помощью метода перехвата извлечения _getattr_мы снова сохраняем базовые значения в именах, предваренных символами подчеркивания, так что управляемые имена при доступе оказываются неопределенными и потому вызывается наш метод. Нам также необходимо реализовать метод
_setattr_для перехвата операций присваивания и позаботиться об устранении
возможности зацикливания:
# То же самое, но с помощью обобщенного перехвата неопределенных
# атрибутов методом_getattr_
class Powers:
def _init_(self, square, cube):
self._square = square self._cube = cube
def _getattr_(self, name) :
if name == ’square':
return self._square ** 2 elif name == 'cube':
return self._cube ** 3 else:
raise TypeError(’unknown attr:' + name)
def _setattr_(self, name, value):
if name == 'square':
# Или использовать object
self._diet_['_square'] = value
else:
self._diet_[name] = value
X = Powers (3, 4) print(X.square) print(X.cube)
# 3 ** 2 = 9
# 4 ** 3 = 64
X.square = 5 print(X.square)
# 5 ** 2 = 25
Последний вариант, в котором используется_getattribute_, похож на предыдущую версию. Однако поскольку теперь мы перехватываем доступ к каждому атрибуту, то должны также направлять операции извлечения базовых значений суперклассу, избегая зацикливания или добавочных вызовов — извлечение self . square напрямую тоже работает, но инициирует второй вызов_getattribute_:
# То же самое, но с помощью обобщенного перехвата неопределенных атрибутов
# методом_getattribute_
class Powers(object) : # В Python 2.X требуется (object)
def _init_(self, square, cube):
self._square = square self._cube = cube
def _getattribute_(self, name):
if name == 'square':
return object._getattribute_(self, '_square') ** 2
el if name == 'cube':
return object._getattribute_(self, '_cube') ** 3
else:
return object._getattribute_(self, name)
def _setattr_(self, name, value):
if name == 'square':
object._setattr_(self, '_square', value) # Либо использовать_diet_
else:
object._setattr_(self, name , value)
X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print (X. cube) # 4 ** 3 = 64
X.square = 5
print(X.square) # 5 ** 2 = 25
Несмотря на отличающиеся формы, которые каждая методика принимает в коде, все четыре при выполнении производят одинаковые результаты:
9
64
25
Дополнительные сведения, касающиеся сравнения таких альтернатив, и другие варианты реализации будут предоставлены при разработке более реалистичного приложения в разделе “Пример: проверка достоверности атрибутов” далее в главе. Тем не менее, сначала нам нужно кратко ознакомиться со связанной с классами нового стиля ловушкой, которая скрыта в двух этих инструментах — обобщенными методами перехвата, описанными в текущем разделе.
Перехват атрибутов для встроенных операций
Если вы читали настоящую книгу последовательно, тогда часть данного раздела будет обзором и уточнением материалов, раскрытых ранее, особенно в главе 32. Для остальных тема представлена в главе в надлежащем контексте.
Во время представления методов_getattr_и_getattribute_я заявил о
том, что они перехватывают операции извлечения соответственно неопределенных и всех атрибутов, идеально подходя для кодовых схем с делегированием. Хотя сказанное справедливо в отношении нормально именованных и явно вызываемых атрибутов, их поведение требует дополнительного прояснения: для атрибутов имен методов, неявно извлекаемых встроенными операциями, такие методы могут вообще не запускаться. Это означает, что вызовы методов перегрузки операций не могут делегироваться внутренним объектам, если только классы оболочек самостоятельно не переопределят данные методы.
Скажем, операции извлечения атрибутов для методов__str__,__add__и
_getitem_, запускаемые неявно соответственно выводом, выражениями + и индексированием, в Python З.Х не направляются методам перехвата атрибутов. В частности:
• в Python З.Х ни getattr_, ни_getattribute_не запускаются для таких атрибутов;
• в классических классах Python 2.Х метод_getattr_запускается для таких атрибутов, если они определены в классе;
• в Python 2.Х метод_getattribute_доступен только для классов нового стиля и работает так, как в Python З.Х.
Другими словами, во всех классах Python З.Х (и классах нового стиля Python 2.Х) не существует прямого способа обобщенного перехвата встроенных операций вроде вывода и сложения. В стандартных классических классах Python 2.Х методы таких операций ищутся во время выполнения в экземплярах подобно всем остальным атрибутам; в классах нового стиля Python З.Х такие методы взамен ищутся в классах. Поскольку в Python З.Х классы нового стиля обязательны, а в Python 2.Х по умолчанию применяются классические классы, это вполне естественно для Python З.Х, но может также произойти в коде нового стиля Python 2.Х. Однако в Python 2.Х вы, по крайней мере, располагаете способом избежать такого изменения, тогда как в Python З.Х — нет.
Согласно главе 32 официальное (хотя и скудно документированное) обоснование для данного изменения, похоже, вращается вокруг метаклассов и оптимизации встроенных операций. С учетом того, что все атрибуты — нормально именованные и другие — при явном доступе по имени по-прежнему направляются обобщенным образом через экземпляр и упомянутые методы, как представляется, это не препятствует делегированию в целом; оно больше похоже на шаг для оптимизации неявного поведения встроенных операций. Тем не менее, в итоге кодовые схемы, основанные на делегировании, в Python З.Х становятся более сложными, т.к. промежуточные классы для объектных интерфейсов не могут обобщенно перехватывать вызовы методов перегрузки операций и направлять их внутренним объектам.
Это неудобство, но не обязательно непреодолимое препятствие — классы оболочек могут обойти ограничение, самостоятельно переопределяя все имеющие отношение к делу методы перегрузки операций, чтобы делегировать вызовы. Дополнительные методы можно добавить вручную, посредством инструментов или путем их определения и наследования от общих суперклассов. Однако в результате объекты-оболочки требуют больше работы, чем обычно, когда методы перегрузки операций являются частью интерфейса внутреннего объекта.
Имейте в виду, что проблема касается только методов__getattr__и
_getattribute_. Поскольку свойства и дескрипторы определяются только для
конкретных атрибутов, в действительности они вообще не применяются к классам, основанным на делегировании — единственное свойство или дескриптор не может использоваться для перехвата произвольных атрибутов. Более того, класс, в котором определены и методы перегрузки операций, и перехват атрибутов, будут работать корректно безотносительно к типу определенного перехвата атрибутов. Здесь мы заботимся только о классах, которые не имеют определенных методов перегрузки операций, но пытаются перехватывать их обобщенным образом.
Рассмотрим следующий пример из файла getattr-bultins .ру, где тестируются различные типы атрибутов и встроенных операций на экземплярах класса, содержащего методы_getattr_и_getattribute_:
class GetAttr:
eggs = 88 # eggs хранится в классе, spam - в экземпляре
def _init_(self) :
self.spam = 77
def _len_(self): # Реализовать здесь len,
# иначе_getattr_ вызывается с_len_
print('_len_: 42')
return 42
def _getattr_(self, attr) : # Предоставить_str_ по запросу,
# иначе фиктивную функцию
print ('getattr: ' + attr)
if attr =- '_str_' :
return lambda *args: '[Getattr str]' else:
return lambda *args: None
class GetAttribute(object): # object требуется в Python 2.X
# и подразумевается в Python З.Х eggs = 88 # В Python 2.Х все автоматически
# isinstance(object)
def _init_(self) : # Но нужно наследовать от object,
# чтобы получить инструменты нового self, spam = 77 # стиля, включая_getattribute_
# и ряд стандартных методов_X_
def _len_(self):
print (1_len_: 42’)
return 42
def _getattribute_(self, attr) :
print(1getattribute: ' + attr)
if attr == 1_str_' :
return lambda *args: ' [GetAttribute str] ' else:
return lambda *args: None for Class in GetAttr, GetAttribute:
print('\n' + Class._name_.ljust(50, '='))
X = Class ()
X.eggs # Атрибут класса X.spam # Атрибут экземпляра X.other # Отсутствующий атрибут len(X) #_len_ определено явно
# Классы нового стиля обязаны поддерживать [], + , прямой вызов: переопределить
X._call_() #_call_? (явно, не наследуется)
print(X._str_()) #_str_? (явно, наследуется от типа)
print(X) #_str_? (неявно через встроенную операцию)
При запуске в том виде, как есть, под управлением Python 2.Х метод_getattr_
будет получать разнообразные запросы на неявное извлечение атрибутов для встроенных операций, потому что при нормальных обстоятельствах интерпретатор Python ищет такие атрибуты в экземплярах. И наоборот,_getattribute_не будет запускаться для любых имен перегрузки операций, вызываемых встроенными операциями, поскольку в модели классов нового стиля такие имена ищутся только в классах:
c:\code> ру -2 getattr-builtins.ру
GetAttr===========================================
getattr: other
_len_: 42
getattr: _getitem_
getattr: _coerce_
getattr: _add_
getattr: _call_
getattr: _call_
getattr: _str_
[Getattr str]
getattr: _str_
[Getattr str]
GetAttribute======================================
getattribute: eggs getattribute: spam getattribute: other
_len_: 4 2
fail [] fail + fail ()
getattribute: _call_
getattribute: _str_
[GetAttribute str]
<_main_.GetAttribute object at 0x02287898>
Обратите внимание на то, как метод_getattr_ перехватывает неявные и
явные извлечения_call_и_str_в Python 2.Х. По контрасту с этим метод
_getattribute_отказывается перехватывать неявные извлечения любого из двух
имен атрибутов для встроенных операций.
На самом деле ситуация с_getattribute_в Python 2.Х будет такой же, как в
Python З.Х, потому что для применения данного метода в Python 2.Х классы должны быть превращены в классы нового стиля за счет их наследования от object. Наследовать от object в Python З.Х необязательно, т.к. там все классы являются классами нового стиля.
Тем не менее, при запуске под управлением Python З.Х результаты для_getattr_
отличаются — ни один из неявно выполняемых методов перегрузки операций не запускает тот или другой метод перехвата атрибутов, когда их атрибуты извлекаются встроенными операциями. При распознавании таких имен интерпретатор Python З.Х (и классы нового стиля в целом) обходят обычный механизм поиска в экземпляре, хотя нормально именованные методы по-прежнему перехватываются, как и ранее: c:\code> ру -3 getattr-builtins.ру GetAttг===========================================
getattr: other
_len_: 42
fail [] fail + fail ()
getattr: _call_
<_main_.GetAttr object at 0x02987CC0>
<_main_.GetAttr object at 0x02987CC0>
GetAttribute======================================
getattribute: eggs
getattribute: spam getattribute: other
_len_: 42
fail [] fail + fail ()
getattribute: _call_
getattribute: _str_
[GetAttribute str]
<_main_.GetAttribute object at 0x02987CF8>
Имея вывод, отыщите соответствующие вызовы print в сценарии, чтобы понять его работу. Ниже отмечено несколько важных моментов.
• Доступ к методу_str_не удалось перехватить дважды методом_getattr_
в Python З.Х: один раз для встроенной операции вывода и один раз для явных извлечений из-за наследования стандартного метода от класса (на самом деле от встроенного класса object, который является автоматическим суперклассом для каждого класса в Python З.Х).
• Доступ к методу_str_не удалось перехватить только раз универсальным обработчиком _getattribute_во время выполнения встроенной операции вывода; явные извлечения обходят унаследованную версию.
• Доступ к методу_call_не удалось перехватить в обеих схемах Python З.Х для
выражений вызова встроенных операций, но он перехватывается обеими схемами при явном извлечении; в отличие от_str_для экземпляров object не
существует унаследованного стандартного метода_call_, чтобы привести к
неудаче_getattr_.
• Доступ к методу_len_перехватывается обоими классами просто потому, что
он является явно определенным методом в самих классах — хотя в Python З.Х его
имя не направляется к_getattr_или_getattribute_, если мы удаляем
методы_len_класса.
• Все остальные встроенные операции не удалось перехватить обеими схемами в Python З.Х.
И снова совокупный эффект заключается в том, что методы перегрузки операций, неявно запускаемые встроенными операциями, никогда не прогоняются через любой из двух методов перехвата атрибутов в Python З.Х: классы нового стиля Python
З.Х ищут такие атрибуты в классах, полностью пропуская шаг поиска в экземплярах. Нормально именованных атрибутов это не касается.
Такая особенность делает классы оболочек, основанные на делегировании, более сложными в реализации с помощью классов нового стиля Python З.Х — если внутренние классы могут содержать методы перегрузки операций, то эти методы придется избыточно переопределять в классе оболочки, чтобы делегировать работу внутреннему объекту. В универсальных инструментах делегирования может потребоваться добавить десятки дополнительных методов.
Разумеется, добавление таких методов можно отчасти автоматизировать посредством инструментов, дополняющих классы новыми методами (здесь способны помочь декораторы классов и метаклассы, рассматриваемые в последующих двух главах). Кроме того, суперкласс может быть в состоянии один раз определить все дополнительные методы для наследования в классах, основанных на делегировании. И все же кодовые схемы делегирования в классах Python З.Х требуют выполнения добавочной работы.
Более реалистичная иллюстрация данного явления вместе с обходным приемом представлена в примере декоратора Private в следующей главе. Там мы исследуем альтернативы реализации методов операций, требуемых промежуточными классами в Python З.Х, включая модели с многократно используемыми подмешиваемыми суперклассами. Также будет показано, что в клиентский класс можно вставить метод
_getattribute_, чтобы предохранить его исходный тип, хотя данный метод по-
прежнему не будет вызываться для методов перегрузки операций; например, вывод
все еще запускает метод_str_, непосредственно определенный в таком классе, а
не прогоняет запрос через_getattribute_.
В качестве реальной демонстрации в следующем разделе возрождается наш учебный пример по классам. Теперь, когда вы понимаете, как работает перехват атрибутов, у меня появилась возможность объяснить один из странных моментов, связанных с ним.
Снова о классах для регистрации и обработки сведений о людях, основанных на делегировании
В обучающем руководстве по ООП в главе 28 был представлен класс Manager, где применялось внедрение объектов и делегирование выполнения методов для настройки его суперкласса вместо наследования. Ниже приведен его код, из которого удалено не относящееся к делу тестирование:
class Person:
def _init_(self, name, job=None, pay=0):
self.name = name self.job = job self.pay = pay def lastName(self):
return self.name.split () [-1] def giveRaise(self, percent):
self.pay = int(self.pay * (1 + percent))
def _repr (self):
return '[Person: %s, %s]' % (self.name, self.pay)
class Manager:
def _init_(self, name, pay):
self.person = Person(name, 'mgr', pay) # Внедрение объекта Person def giveRaise(self, percent, bonus=.10):
self .person. giveRaise (percent + bonus) # Перехват и делегирование
def _getattr_(self, attr) :
return getattr(self.person, attr) # Делегирование всех
# остальных атрибутов
def _repr_(self):
return str(self.person) # Снова требуется перегрузка (в Python З.Х)
if _name_ == '_main_1 :
sue = Person('Sue Jones', job='dev', pay=100000) print(sue.lastName()) sue.giveRaise(.10) print(sue)
tom = Manager(1 Tom Jones', 50000) # Manager._init_
print(tom.lastName()) # Manager._getattr_ -> Person.lastName
tom.giveRaise(.10) # Manager.giveRaise -> Person.giveRaise
print (tom) # Manager._repr_ -> Person._repr_
Комментарии в конце файла показывают, какие методы вызываются для операции в каждой строке. В частности, обратите внимание на то, что вызовы lastName не определены в классе Manager и потому направляются обобщенному методу_getattr_,
а оттуда внутреннему объекту Person. Далее приведен вывод сценария — объект sue получает повышение на 10% от Person, но объект tom — на 20%, т.к. метод giveRaise был настроен в классе Manager:
c:\code> ру -3 getattr-delegate.py
Jones
[Person: Sue Jones, 110000]
Jones
[Person: Tom Jones, 60000]
Однако по контрасту с этим взгляните, что происходит, когда мы выводим объект Manager в конце сценария: вызывается метод_герг_класса оболочки и делегирует выполнение работы методу_герг_внедренного объекта Person. Имея данный
факт в виду, посмотрим, что случится, если мы удалим метод Manager._repr_:
# Удаление метода _str_ в классе Manager
class Manager:
def _init_(self, name, pay):
self.person = Person(name, 'mgr', pay) # Внедрение объекта Person def giveRaise(self, percent, bonus=.10):
self .person. giveRaise (percent + bonus) # Перехват и делегирование
def _getattr_(self, attr):
return getattr(self.person, attr) # Делегирование всех
# остальных атрибутов
Теперь в случае классов нового стиля Python З.Х вывод не прогоняет операции
извлечения своих атрибутов через обобщенный метод перехвата_getattr_для
объектов Manager. Взамен находится и запускается стандартный метод отображения _герг_, унаследованный из неявного суперкласса object для класса (объект
sue по-прежнему выводится корректно, потому что класс Person имеет явный метод _герг_):
c:\code> py -3 getattr-delegate.py
Jones
[Person: Sue Jones, 110000]
Jones
<_main_.Manager object at 0x029E7B70>
Выполнение без метода_repr_запускает метод_getattr_в классических
классах Python 2.Х, т.к. атрибуты перегрузки операций прогоняются через данный метод и такие классы не наследуют стандартный метод_герг_:
c:\code> ру -2 getattr-delegate.py
Jones
[Person: Sue Jones, 110000]
Jones
[Person: Tom Jones, 60000]
Переключение на метод getattribute_здесь не поможет интерпретатору
Python З.Х — подобно_getattr_он не запускается для атрибутов перегрузки операций, подразумеваемых встроенными операциями в Python 2.Х или в Python З.Х:
# Замена_getattr_ методом __getattribute_
class Manager(object): # Использовать (object) в Python 2.X
def _init_(self, name, pay):
self.person = Person(name, 'mgr', pay) # Внедрение объекта Person def giveRaise(self, percent, bonus=.10):
self.person.giveRaise(percent + bonus) # Перехват и делегирование
def _getattribute_(self, attr) :
print ('**', attr)
if attr in ['person', 'giveRaise']:
return object._getattribute_(self, attr) # Извлечение моих атрибутов
else:
return getattr (self .person, attr) # Делегирование всех остальных атрибутов
Независимо от того, какой метод перехвата атрибутов используется в Python З.Х,
мы все равно должны включать в Manager переопределенный метод_герг_(как
было показано ранее), чтобы перехватывать операции вывода и направлять их внедренному объекту Person:
C:\code> ру -3 getattr-delegate.py
Jones
[Person: Sue Jones, 110000]
** lastName ** person Jones
** giveRaise ** person
<_main_.Manager object at 0x028E0590>
Обратите внимание на то, что_getattribute_вызывается для методов дважды — один раз для имени метода и еще раз для извлечения внедренного объекта self .person. Этого можно было бы избежать за счет написания отличающегося кода,
но мы по-прежнему обязаны переопределять метод_герг_для перехвата вывода,
хотя и по-другому (self ♦ person мог бы привести к отказу_getattribute_):
# Другая реализация_getattribute_ с целью минимизации добавочных вызовов
class Manager:
def _init_(self, name, pay):
self.person = Person(name, 'mgr', pay)
def _getattribute_(self, attr) :
print('**’, attr)
person = object._getattribute_(self, 'person')
if attr == 'giveRaise':
return lambda percent: person.giveRaise(percent*.10) else:
return getattr(person, attr)
def _repr_(self) :
person = object._getattribute_(self, 'person')
return str(person)
При выполнении такой альтернативной версии наш объект выводится надлежащим образом, но лишь потому, что мы добавили в класс оболочки явный метод _герг_— данный атрибут не направляется имеющемуся обобщенному методу перехвата атрибутов:
Jones
[Person: Sue Jones, 110000]
** lastName
Jones
** giveRaise
[Person: Tom Jones, 60000]
Вкратце история заключается в том, что основанные на делегировании классы вроде Manager должны переопределять некоторые методы перегрузки операций (подобные _герг_и_str_) для их направления внедренным объектам в Python З.Х,
но не в Python 2.Х, если только не применяются классы нового стиля. Похоже, нам доступны лишь варианты использования_getattr_и Python 2.Х либо избыточного
переопределения методов перегрузки операций для классов оболочек в Python З.Х.
Опять-таки задача не считается невыполнимой; многие классы оболочек могут спрогнозировать требуемый набор методов перегрузки операций, а инструменты и суперклассы способны автоматизировать часть решения задачи — на самом деле мы изучим необходимые кодовые схемы в следующей главе. Кроме того, не все классы используют методы перегрузки операций (в действительности большинство классов приложений обычно не должны их применять). Тем не менее, об этом важно помнить при работе с моделями делегирования в Python З.Х; когда методы перегрузки операций являются частью интерфейса объекта, то классы оболочек обязаны приспособиться к ним переносимым образом за счет локального переопределения.
Пример: проверка достоверности атрибутов
В заключение главы давайте рассмотрим более реалистичный пример, реализующий все четыре схемы управления атрибутами. В примере будет определен объект CardHolder с четырьмя атрибутами, три из которых управляемы. При излечении и сохранении управляемые атрибуты подвергаются проверке достоверности или видоизменению. Для того же самого тестового кода все четыре версии производят одинаковые результаты, но они реализуют свои атрибуты совершенно по-разному. Примеры предназначены в основном для самостоятельного изучения; хотя я не буду подробно останавливаться на их коде, все они используют концепции, уже исследованные в данной главе.
Использование свойств для проверки достоверности
В первой версии для управления тремя атрибутами применяются свойства. Как обычно, вместо управляемых атрибутов мы могли бы использовать простые методы, но свойства помогут, если атрибуты уже были задействованы в существующем коде. Свойства запускают код автоматически при доступе к атрибутам, но сосредоточены на конкретном наборе атрибутов; они не могут применяться для перехвата всех атрибутов обобщенным образом.
Чтобы понять этот код, важно помнить о том, что операции присваивания атрибутов внутри метода конструктора_init_тоже запускают методы установки свойств.
Например, когда в_init_присваивается значение self .name, автоматически вызывается метод setName, который видоизменяет значение и присваивает его атрибуту экземпляра по имени_name, не конфликтующему с именем самого свойства.
Такое переименование (иногда называемое корректировкой имен) является необходимым, поскольку свойства используют общее состояние экземпляра, не имея собственного. Данные хранятся в атрибуте по имени_name, тогда как атрибут по имени
name — всегда свойство, а не данные. В главе 31 мы выяснили, что имена вроде_name
известны как псевдозакрытые атрибуты, которые при сохранении в пространстве имен экземпляра интерпретатор Python изменяет, добавляя имя включающего класса; здесь это помогает отличать атрибуты, специфичные к реализации, от остальных, в том числе от управляющего ими свойства.
В конечном счете, реализованный класс управляет атрибутами name, age и acct, разрешает прямой доступ к атрибуту addr и предоставляет атрибут только для чтения по имени remain, который является полностью виртуальным и вычисляется по запросу. Для сравнения версия на основе свойств занимает 39 строк кода, не считая двух начальных строк, и включает наследование от object, требующееся в Python 2.Х, но необязательное в Python З.Х:
# Файл validate_properties .ру
# В Python 2.X требуется (object)
# Данные класса
age, addr):
# Данные экземпляра
# Тоже запускают методы установки
# свойств!
# Имя_X корректируется, чтобы
# содержать имя класса
# Имя addr не корректируется
# Свойство remain не имеет данных
class CardHolder(object): acctlen = 8 retireage =59.5
def _init_(self, acct, name,
self.acct = acct self, name = name
self.age = age
self, addr = addr
def getName(self):
return self._name
def setName(self, value):
value = value.lower().replace (' ', '_')
self._name = value
name = property(getName, setName)
def getAge(self):
return self._age
def setAge(self, value):
if value < 0 or value > 150:
raise ValueError('invalid age’ # недопустимый возраст
else:
self._age = value
age = property(getAge, setAge)
def getAcct(self):
return self._acct[:-3] + '***'
def setAcct(self, value):
value = value.replace (1 - ' , ’') if len(value) != self.acctlen:
raise TypeError (' invald acct number’) # недопустимый номер счета else:
self._acct = value
acct = property(getAcct, setAcct)
def remainGet (self) : # Могло быть методом, а не атрибутом,
return self.retireage - self.age # если только уже не используется
# как атрибут
remain = property(remainGet)
Тестовый код
Показанный далее код из файла validate tester .ру тестирует наш класс; запускайте этот сценарий, передавая имя модуля класса (без .ру) как единственный аргумент командной строки (большую часть тестового кода можно было бы также добавить в конец каждого файла либо интерактивно импортировать его из модуля после импортирования класса). Для тестирования всех четырех версий в примере мы будем применять тот же самый код. После запуска он создает два экземпляра нашего класса с управляемыми атрибутами, после чего извлекает и изменяет различные атрибуты. Операции с ожидаемым отказом помещены внутрь операторов try, а идентичное поведение в Python 2.Х поддерживается за счет включения функции print из Python З.Х:
# Файл validate_tester.ру
from _future_ import print_function # Python 2.X
def loadclass () :
import sys, importlib
modulename = sys.argv[l] # Имя модуля в командной строке
module = importlib. import_module (modulename) # Импортирование модуля
# по имени в строке print(’ [Using: %s] ' % module.CardHolder) # getattr () здесь не требуется return module.CardHolder
def printholder(who):
print(who.acct, who.name, who.age, who.remain, who.addr, sep=' / ')
if _name_ == '_main_' :
CardHolder = loadclass ()
bob = CardHolder (' 1234-5678 ' , 'Bob Smith', 40, '123 main st')
printholder(bob)
bob.name = 'Bob Q. Smith'
bob.age =50
bob.acct = '23-45-67-89' printholder(bob)
sue = CardHolder ('5678-12-34 ' , 'Sue Jones', 35, '124 main st')
printholder(sue)
try:
sue.age = 200
except:
print ('Bad age for Sue') # Недопустимый возраст для sue
try:
sue.remain = 5 except:
print("Can't set sue.remain") # Невозможно установить sue.remain try:
sue.acct = '1234567' except:
print ('Bad acct for Sue') # Недопустимый номер счета для sue
Ниже приведен вывод кода самотестирования в Python З.Х и 2.Х; он одинаков для всех четырех версий примера кроме имени тестируемого класса. Отследите код, чтобы посмотреть, как вызываются методы класса; номера расчетных счетов отображаются с несколькими скрытыми цифрами, имена преобразуются в стандартный формат, а время, оставшееся до выхода на пенсию, вычисляется при извлечении с использованием вычитания атрибутов класса:
c:\code> ру -3 validate_tester.ру validate_jproperties
[Using: <class 'validate_properties.CardHolder'>]
12345*** / bob_smith / 40 / 19.5 / 123 main st 23456*** / bob_q._smith / 50 / 9.5 / 123 main st 56781*** / sue_jones / 35 / 24.5 / 124 main st Bad age for Sue Can’t set sue.remain Bad acct for Sue
Использование дескрипторов для проверки достоверности
А теперь давайте перепишем наш пример с применением дескрипторов вместо свойств. Как демонстрировалось ранее, дескрипторы очень похожи на свойства в плане функциональности и ролей; на самом деле свойства по существу представляют собой ограниченную форму дескрипторов. Подобно свойствам дескрипторы предназначены для обработки конкретных атрибутов, а не обобщенного доступа к атрибутам. В отличие от свойств дескрипторы могут также иметь собственное состояние и являются более универсальной схемой.
Вариант 1: проверка достоверности с помощью разделяемого состояния экземпляра дескриптора
Для понимания показанного далее кода снова важно помнить о том, что операции
присваивания значений атрибутам внутри метода конструктора_init_запускают
методы_set_дескриптора. Скажем, когда в методе конструктора выполняется присваивание self .name, то автоматически вызывается метод Name._set_(), который
видоизменяет значение и присваивает его атрибуту дескриптора по имени name.
В конце концов, класс реализует те же самые атрибуты, что и предыдущая версия: он управляет атрибутами name, age и acct, разрешает прямой доступ к атрибуту addr и предоставляет атрибут только для чтения по имени remain, который является полностью виртуальным и вычисляется по запросу. Обратите внимание, что мы обязаны перехватывать операции присваивания значений имени remain в его дескрипторе и генерировать исключение; как объяснялось ранее, если не поступать так, тогда операция присваивания этому атрибуту молча создаст атрибут экземпляра, который скроет дескриптор атрибута класса.
Для сравнения версия на основе дескрипторов требует 45 строк кода; я добавил обязательное наследование от object к основным классам дескрипторов ради совместимости с Python 2.Х (его можно опустить в коде, запускаемом только в Python З.Х, но оно ничем не вредит и содействует переносимости):
# Файл validate_descriptorsl .ру: использование разделяемого состояния
# экземпляра дескриптора
# В Python 2.Х требуется (object)
# Данные класса
addr):
# Данные экземпляра
# Тоже запускают методы_set_!
# Имя X не требуется: в дескрипторе
# Имя addr не является управляемым
# remain не имеет данных
# Имена классов: локальные в CardHolder
class CardHolder(object): acctlen = 8 retireage =59.5
def _init_(self, acct, name, age,
self.acct = acct self.name = name self.age = age self, addr = addr
class Name(object):
def _get_(self, instance, owner):
return self.name
def_set_(self, instance, value):
value = value.lower().replace (’ ', '_') self.name = value name = Name ()
class Age(object):
def _get_(self, instance, owner):
return self .age # Использовать данные дескриптора
def _set_(self, instance, value):
if value < 0 or value > 150:
raise ValueError('invalid age') # недопустимый возраст else:
self.age = value age = Age ()
class Acct(object):
def_get_(self, instance, owner):
return self.acct[:-3] + '***'
def _set_(self, instance, value):
value = value.replace (' - ', ' ' )
if len(value) != instance.acctlen: # Использовать данные
# экземпляра класса raise TypeError (' invald acct number' ) # недопустимый номер счета else:
self.acct = value acct = Acct ()
class Remain(object):
def_get_(self, instance, owner):
return instance.retireage - instance.age # Запускается Age._get_
def _set_(self, instance, value):
raise TypeError (' cannot set remain') # Установка не разрешена
remain = Remain()
При запуске с предыдущим тестовым сценарием все примеры в настоящем разделе производят тот же самый вывод, показанный ранее для версии со свойствами, за исключением отличающегося имени класса в первой строке:
C:\code> python validate_tester.py validate_descriptorsl
. . . тот же самый вывод, что и в версии со свойствами, кроме имени класса. . .
Вариант 2: проверка достоверности с помощью состояния для каждого экземпляра клиентского класса
Однако в отличие от предшествующего варианта, основанного на свойствах, в этом случае действительное значение name присоединяется к объекту дескриптора, а не к экземпляру клиентского класса. Хотя мы могли бы хранить значение name либо в состоянии экземпляра, либо в состоянии дескриптора, в последней ситуации устраняется необходимость в корректировке имен за счет добавления символов подчеркивания во избежание конфликтов. В клиентском классе CardHolder атрибут по имени name всегда будет не данными, а объектом дескриптора.
Важно упомянуть о недостатке такой схемы — состояние, хранящееся внутри самого дескриптора, представляет собой данные уровня класса, которые фактически разделяются всеми экземплярами клиентского класса и потому не могут варьироваться между ними. То есть хранение состояния в экземпляре дескриптора вместо экземпляра владеющего (клиентского) класса означает, что состояние будет тем же самым во всех экземплярах владеющего класса. Состояние дескриптора может отличаться только для каждого атрибута.
Чтобы увидеть подход в работе, мы попробуем в предыдущей версии класса CardHolder, основанной на дескрипторах, вывести атрибуты экземпляра bob после создания второго экземпляра, sue. Значения управляемых атрибутов в sue (name, age и acct) переписывают значения одноименных атрибутов в ранее созданном объекте bob, потому что оба объекта разделяют тот же самый одиночный экземпляр дескриптора, присоединенный к классу:
# Файл validate_tester2.ру
from _future_ import print_function # Python 2.X
from validate_tester import loadclass CardHolder = loadclass ()
bob = CardHolder('1234-5678 ', 'Bob Smith', 40, '123 main st') print('bob:1, bob.name, bob.acct, bob.age, bob.addr)
sue = CardHolder ('5678-12-34 ', 'Sue Jones', 35, '124 main st')
print('sue:', sue.name, sue.acct, sue.age, sue.addr) # addr отличается:
# клиентские данные print('bob:', bob.name, bob.acct, bob.age, bob.addr) # name, acct, age
# переписываются?
Результаты подтверждают подозрение — с точки зрения управляемых атрибутов объект bob превратился в sue!
c:\code> ру -3 validate_tester2.ру validate_descriptorsl
[Using: <class 'validate_descriptorsl.CardHolder'>] bob: bob_smith 12345*** 40 123 main st sue: sue_jones 56781*** 35 124 main st bob: sue_jones 56781*** 35 123 main st
Разумеется, существуют допустимые сценарии использования для состояния дескриптора — управление реализацией дескриптора и данными, охватывающими все экземпляры, — и код был реализован в целях иллюстрации методики. Кроме того, в этом месте книги последствия, касающиеся области видимости состояния, для атрибутов класса и экземпляров должны быть более-менее ясны.
Тем не менее, в рассматриваемом конкретном сценарии атрибуты объектов CardHolder вероятно лучше хранить в виде данных для каждого экземпляра, а не данных экземпляра дескриптора, возможно с применением того же соглашения об именовании _X, которое использовалось в версии на основе свойств, чтобы избежать
конфликтов имен в экземпляре — на этот раз более важный фактор, т.к. клиентом является другой класс с собственными атрибутами состояния. Вот необходимые изменения в коде; количество строк осталось прежним (45):
# Файл validate_descriptors2 .ру: использование состояния для каждого экземпляра клиентского класса
class CardHolder(object): # В Python 2.Х требуется (object)
acctlen =8 # Данные класса
retireage =59.5
def_init_(self, acct, name, age, addr):
# Данные экземпляра клиентского класса
# Тоже запускают методы_set_!
# Имя_X требуется: в экземпляре клиента
# Имя addr не является управляемым
# remain является управляемым,
# но не имеет данных
self.acct = acct self.name = name self.age = age self, addr = addr
class Name(object):
def get_(self, instance, owner): # Имена классов: локальные в CardHolder
return instance._name
def __set_(self, instance, value) :
value = value.lower().replace(' ', '_')
instance._name = value
name = Name() # class. name или скорректированное имя атрибута
class Age(object):
def __get_(self, instance, owner):
return instance._age # Использовать данные дескриптора
def _set_(self, instance, value):
if value < 0 or value > 150:
raise ValueError('invalid age') # недопустимый возраст else:
instance._age = value
age = Age () # class. age или скорректированное имя атрибута
class Acct(object):
def get_(self, instance, owner):
return instance._acct[:-3] + '***'
def _set_(self, instance, value) :
value = value.replace (’ - 1, '1)
if len(value) != instance.acctlen: # Использовать данные
# экземпляра класса raise TypeError('invald acct number') # недопустимый номер счета else:
instance._acct = value
acct = Acct() # class. acct или скорректированное имя атрибута
class Remain(object):
def _get_(self, instance, owner):
return instance.retireage - instance.age # Запускается Age._get_
def _set_(self, instance, value):
raise TypeError ('cannot set remain') # Установка не разрешена
remain = Remain()
Данные в управляемых полях name, age и acct вполне ожидаемо поддерживаются для каждого экземпляра (bob остается bob), а остальные тесты проходят, как и ранее:
c:\code> ру -3 validate_tester2.ру validate_descriptors2
[Using: <class 'validate_descriptors2.CardHolder'>] bob: bob_smith 12345*** 40 123 main st sue: sue_jones 56781*** 35 124 main st bob: bob_smith 12345*** 40 123 main st
c:\code> py -3 validate_tester.py validate_descriptors2
. . .тот же самый вывод, что и в версии со свойствами, кроме имени класса. . .
Одно небольшое предостережение: в имеющемся виде данная версия не поддерживает доступ к дескриптору через класс, поскольку в таком случае аргументу экземпляра передается None (также обратите внимание на то, что из-за корректировки имя атрибута _X стало выглядеть как Name_name в сообщении об ошибке, возникающей при
попытке извлечения):
>>> from validate_descriptorsl import CardHolder
»> bob = CardHolder(11234-5678 1 , 'Bob Smith’ , 40, '123 main st')
»> bob.name
'bob_smith'
>>> CardHolder.name
1bob_smith'
»> from validate_descriptors2 import CardHolder
>>> bob = CardHolder ('1234-5678' , 'Bob Smith', 40, '123 main st')
>>> bob.name
'bob_smith'
>>> CardHolder.name
AttributeError: 'NoneType' object has no attribute '_Name_name'
Ошибка атрибута: объект NoneType не имеет атрибута _Name_name
Мы могли бы выявить ситуацию с помощью небольшого объема дополнительного кода, чтобы более явно генерировать ошибку, но вероятно поступать так не имеет смысла. Из-за того, что текущая версия хранит данные в экземпляре клиентского класса, дескрипторы ничего не значат, если они не сопровождаются клиентским экземпляром (во многом подобно нормальному несвязанному методу экземпляра). На самом деле в том и заключается весь смысл изменения в этой версии!
Будучи классами, дескрипторы являются удобным и мощным инструментом, но они преподносят варианты, которые способны оказать глубокое влияние на поведение программы. Как всегда в ООП, тщательно выбирайте политики предохранения состояния.
Использование_getattr_для проверки достоверности
Как мы уже видели, метод_getattr_перехватывает все неопределенные атрибуты, так что он обеспечивает более обобщенное решение, чем применение свойств и дескрипторов. В нашем примере мы просто проверяем имя атрибута, чтобы выяснить, когда извлекается управляемый атрибут; остальные атрибуты физически хранятся в экземпляре и потому никогда не достигнут_getattr_. Несмотря на большую
универсальность подхода по сравнению со свойствами или дескрипторами, имитирование ориентации на конкретные атрибуты других инструментов может потребовать дополнительной работы. Нам необходимо проверять имена во время выполнения, и мы должны реализовать метод_setattr_для перехвата и проверки достоверности операций присваивания значений атрибутам.
Что касается версий со свойствами и дескрипторами рассматриваемого примера, то важно обратить внимание, что операции присваиваний значений атрибутам
внутри метода конструктора_init_тоже запускают метод_setattr_класса.
Скажем, когда в_init_присваивается self .паше, то автоматически вызывается
метод_setattr_, который видоизменяет значение и присваивает его атрибуту
экземпляра по имени паше. Хранение пате в экземпляре гарантирует, что будущие операции доступа не приведут к запуску_getattr__. Напротив, acct хранится как acct, так что последующие операции доступа к acct инициируют вызовы _getattr_.
В конечном итоге очередная версия класса, как и предшествующие две, управляет атрибутами name, age и acct, разрешает прямой доступ к атрибуту addr и предоставляет атрибут только для чтения по имени remain, который является полностью виртуальным и вычисляется по запросу.
Для сравнения эта версия содержит 32 строки кода — на 7 меньше, чем версия на основе свойств и на 13 меньше, чем версия, в которой используются дескрипторы. Конечно, ясность кода важнее его размера, но добавочный код иногда подразумевает дополнительную работу по реализации и сопровождению. Вероятно, здесь более важны роли: обобщенные инструменты, подобные_getattr_, могут лучше подходить
для обобщенного делегирования, в то время как свойства и дескрипторы в большей степени предназначены для управления конкретными атрибутами.
Также обратите внимание, что в коде возникают дополнительные вызовы при установке неуправляемых атрибутов (например, addr), но такие вызовы отсутствуют при извлечении неуправляемых атрибутов, т.к. они определены. Хотя для большинства программ результатом будут, скорее всего, пренебрежимо малые накладные расходы, более узко сфокусированные свойства и дескрипторы приводят к дополнительному вызову только при доступе к управляемым атрибутам и также появляются в результатах dir, когда в них нуждаются обобщенные инструменты.
Вот версия метода_getattr_для проверки достоверности:
# Файл validate_getattr .ру
class CardHolder:
acctlen =8 # Данные класса
retireage =59.5
def _init_(self, acct, name, age, addr):
self .acct = acct # Данные экземпляра
self.name = name # Тоже запускают_setattr_
self.age = age # Имя _acct не корректируется:
# проверяется name
self .addr = addr # Имя addr не является управляемым
# remain не имеет данных
def _getattr_(self, name):
if name == 'acct' : # При извлечении неопределенных атрибутов
return self._acct[:-3] + '***' # name, age, addr определены elif name == 'remain1:
return self.retireage - self.age # He запускает_getattr_
else:
raise AttributeError(name)
def _setattr_(self, name, value) :
if name == 'name' : # При операциях присваивания всех атрибутов
value = value.lower().replace(' '_') # addr хранится напрямую elif name == 'age' : # acct корректируется в _acct
if value < 0 or value > 150:
raise ValueError (' invalid age') # недопустимый возраст
elif name == 'acct': name = '_acct'
value = value.replace('- ', '') if len(value) != self.acctlen: raise TypeError('invald acct number’) # недопустимый номер счета elif name == 'remain':
raise TypeError('cannot set remain')
self._diet_[name] = value # Избегание зацикливания
# (или через object)
При запуске кода с помощью любого из двух тестовых сценариев получается тот же самый вывод (с другим именем класса):
c:\code> ру -3 validate_tester.ру validate_getattr
. . . тот же самый вывод, что и в версии со свойствами, кроме имени класса. . .
c:\code> ру -3 validate_tester2 .ру validate_getattr
. . . тот же самый вывод, что и в версии с дескрипторами состояния экземпляра,
кроме имени класса. . .
Использование_getattribute_ для проверки достоверности
В финальном варианте применяется метод_getattribute_для перехвата
операций извлечения атрибутов и управления ими надлежащим образом. Здесь перехватывается извлечение каждого атрибута, так что мы проверяем имена атрибутов, чтобы обнаруживать управляемые атрибуты и направлять все остальные суперклассу для нормальной обработки операций извлечения. Для перехвата операций
присваивания данная версия использует тот же самый метод_setattr_, что и
предыдущая версия.
Код работает очень похоже на версию__getattr__, поэтому полное описание здесь не повторяется. Однако обратите внимание, что поскольку методу
_getattribute_направляется операция извлечения каждого атрибута, нам нет
нужды корректировать имена для их перехвата (acct хранится как acct). С другой стороны, код обязан позаботиться о направлении операций извлечения неуправляемых атрибутов суперклассу во избежание зацикливания или дополнительных вызовов.
Также имейте в виду, что в этой версии возникают дополнительные вызовы для установки и извлечения неуправляемых атрибутов (скажем, addr); если первостепенным требованием является скорость, тогда текущая альтернатива может оказаться самой медленной в наборе. Для сравнения данная версия содержит 32 строки кода, как и предыдущая версия, и включает необходимое наследование от object для совместимости с Python 2.Х; подобно свойствам и дескрипторам метод_getattribute_
представляет собой инструмент классов нового стиля:
# Файл validate_getattribute.py
class CardHolder(object) : # В Python 2.X требуется (object)
acctlen =8 # Данные класса retireage =59.5
def _init_(self, acct, name, age, addr):
self.acct = acct # Данные экземпляра
# Тоже запускают_setattr_
# Имя _acct не корректируется:
self.name = name self.age = age
self, addr = addr
# проверяется name
# Имя addr не является управляемым
# remain не имеет данных
def _getattribute_(self, name):
superget = object._getattribute
if name == ' acct' :
return superget(self, 'acct')[: elif name == 'remain':
-3] + ' ***'
# Зацикливания нет:
# на один уровень выше
# При извлечении всех атрибутов
return superget(self, 'retireage') - superget(self, 'age')
else:
return superget(self, name) # name, age, addr: хранятся
def _setattr_(self, name, value):
if name == 'name' : # При операциях присваивания всех атрибутов
value = value. lower (). replace (' ', ) # addr хранится напрямую
elif name == 'age' :
if value < 0 or value > 150:
raise ValueError (' invalid age') # недопустимый возраст
elif name == 'acct':
value = value.replace ('-', '') if len(value) != self.acctlen: raise TypeError('invald acct number') # недопустимый номер счета elif name == 'remain':
raise TypeError('cannot set remain') self._diet_[name] = value # Избегание зацикливания, исходные имена
При запуске с обоими тестовыми сценариями в Python 2.Х или З.Х версии getattr* и getattribute* работают точно так же, как версии со свойствами и дескрипторами для каждого экземпляра клиентского класса. Они демонстрируют четыре способа достижения той же самой цели в Python, хотя имеют отличающиеся структуры и вероятно менее избыточны в ряде других ролей. Обязательно изучите и запустите код из настоящего раздела самостоятельно, чтобы получить лучшее представление о методиках реализации управляемых атрибутов.
Резюме
В главе были раскрыты разнообразные приемы управления доступом к атрибутам
в Python, включая методы перегрузки операций_getattr_и_getattribute_,
свойства классов и дескрипторы атрибутов классов. Попутно было проведено сравнение рассматриваемых инструментов и предложено несколько сценариев использования для демонстрации их поведения.
В главе 39 обзор средств для построения инструментов продолжается исследованием декораторов — кода, который автоматически запускается во время создания функций и классов, а не при доступе к атрибутам. Но прежде чем переходить к ее чтению, ответьте на контрольные вопросы главы, чтобы закрепить полученные знания.
Проверьте свои знания: контрольные вопросы
1. Чем между собой отличаются_getattr_и_getattribute_?
2. Чем между собой отличаются свойства и дескрипторы?
3. Как связаны друг с другом свойства и декораторы?
4. Каковы главные функциональные отличия между__getattr__и
_getattribute_, а также между свойствами и дескрипторами?
5. Разве все это сравнение возможностей — не просто разновидность спора?
Проверьте свои знания: ответы
1. Метод_getattr_запускается только для операций извлечения неопределенных
атрибутов (т.е. тех, которые не присутствуют в экземпляре и не наследуются из
любого его класса). По контрасту с ним метод_getattribute_вызывается
для операции извлечения каждого атрибута, определен он или нет. По этой причине код внутри_getattr_может свободно извлекать другие атрибуты, если
они определены, тогда как в методе_getattribute_для извлечения таких
атрибутов должен использоваться специальный код, чтобы избежать зацикливания или дополнительных вызовов (он обязан направлять операции извлечения суперклассу, пропуская себя).
2. Свойства исполняют особую роль, в то время как дескрипторы более универсальны. Свойства определяют функции извлечения, установки и удаления для конкретного атрибута; дескрипторы снабжают класс методами для таких действий, но предоставляют добавочную гибкость с целью поддержки более произвольных действий. На самом деле свойства являются простым способом создания специфического вида дескриптора — такого, который запускает функции при доступе к атрибутам. Реализация тоже отличается: свойство создается с помощью встроенной функции, а дескриптор — посредством класса; таким образом, дескрипторы могут воспользоваться в своих интересах всеми обычными возможностями ООП, касающимися классов, вроде наследования. Кроме того, вдобавок к информации состояния экземпляра дескрипторы имеют собственное локальное состояние, так что временами они способны избегать конфликтов имен в экземпляре.
3. Свойства могут быть реализованы с помощью декораторного синтаксиса. Поскольку встроенная функция property принимает единственный аргумент типа функции, она может использоваться напрямую как декоратор функции для определения свойства с доступом по извлечению. Благодаря поведению декораторов, предусматривающему повторную привязку имен, имя декорированной функции присваивается свойству, чей метод извлечения устанавливается в исходную функцию (паше = property (name)). Атрибуты setter и deleter свойства позволяют дополнительно добавлять методы установки и удаления посредством декораторного синтаксиса — они устанавливают метод доступа в декорированную функцию и возвращают дополненное свойство.
4. Методы_getattr_и_getattribute_являются более обобщенными: они
могут применяться для перехвата произвольно большого количества атрибутов. В противоположность им каждое свойство или дескриптор обеспечивают перехват доступа только для одного конкретного свойства — перехватывать операцию извлечения каждого атрибута с помощью единственного свойства или дескриптора не удастся. С другой стороны, свойства и дескрипторы изначально
обрабатывают операции извлечения и присваивания ддя атрибута:_getattr_
и_getattribute_обрабатывают только операции извлечения; чтобы перехватывать также операции присваивания, потребуется реализовать еще и метод
_setattr_. Реализация тоже отличается:_getattr_и_getattribute_
являются методами перегрузки операций, тогда как свойства и дескрипторы представляют собой объекты, вручную присвоенные атрибутам класса. В отличие от остальных свойства и дескрипторы способны иногда избегать дополнительных вызовов при присваивании неуправляемых имен плюс автоматически отображаются в результатах dir, но пределы их возможностей более узкие — они не могут достичь целей обобщенной координации вызовов. С развитием Python новые средства обычно предлагают альтернативы, но не полностью соответствуют тому, что было раньше.
5. Нет, это не так. Ниже дается вольная интерпретация дискуссии из скетча “Летающий цирк Монти Пайтона”.
Спор - это ряд взаимосвязанных доводов, призванных отстоять свою позицию. Нет, это не так.
Да, это так! Это не просто отрицание.
Послушай, если я спорю с тобой, то должен занять противоположную позицию. Да, но это не значит просто сказать: " Нет, это не так".
Да, это так!
Нет, это не так!
Да, это так!
Нет, это не так. Спор - это интеллектуальный процесс. Отрицание -всего лишь возражение на любой довод, приводимый другим человеком.
(после короткой паузы) Нет, это не так.
Это так.
Вовсе нет.
А теперь послушай...
ГЛАВА 39
Назад: Unicode и байтовые строки
Дальше: Декораторы