Описание Руководства по эксплуатации¶
- Автор:
Раймонд Хеттингер
- Контакт:
<python в rcn dot com>
Descriptors позволяет объектам настраивать поиск, хранение и удаление атрибутов.
Это руководство состоит из четырех основных разделов:
В «руководстве» дается общий обзор, постепенно переходя от простых примеров к добавлению одной функции за раз. Если вы новичок в дескрипторах, начните с этой статьи.
Во втором разделе приведен полный практический пример описания. Если вы уже знакомы с основами, начните с этого.
В третьем разделе представлено более техническое руководство, в котором подробно описывается механизм работы дескрипторов. Большинству людей не нужен такой уровень детализации.
В последнем разделе приведены чистые эквиваленты Python для встроенных дескрипторов, написанных на C. Прочтите это, если вам интересно узнать о том, как функции превращаются в связанные методы, или о реализации таких распространенных инструментов, как
classmethod()
,staticmethod()
,property()
, и __slots__.
Грунтовка¶
В этом руководстве мы начнем с самого простого из возможных примеров, а затем будем добавлять новые возможности одну за другой.
Простой пример: дескриптор, возвращающий константу¶
Класс Ten
- это дескриптор, метод которого __get__()
всегда возвращает константу 10
:
class Ten:
def __get__(self, obj, objtype=None):
return 10
Чтобы использовать дескриптор, он должен быть сохранен как переменная класса в другом классе:
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
Интерактивный сеанс показывает разницу между обычным поиском атрибутов и поиском дескрипторов:
>>> a = A() # Make an instance of class A
>>> a.x # Normal attribute lookup
5
>>> a.y # Descriptor lookup
10
При поиске атрибута a.x
оператор dot находит 'x': 5
в словаре классов. При поиске a.y
оператор dot находит экземпляр дескриптора, распознанный его методом __get__
. Вызов этого метода возвращает 10
.
Обратите внимание, что значение 10
не сохраняется ни в словаре класса, ни в словаре экземпляра. Вместо этого значение 10
вычисляется по запросу.
В этом примере показано, как работает простой дескриптор, но он не очень полезен. Для извлечения констант лучше использовать обычный поиск по атрибутам.
В следующем разделе мы создадим нечто более полезное - динамический поиск.
Динамический поиск¶
Интересные дескрипторы обычно выполняют вычисления вместо того, чтобы возвращать константы:
import os
class DirectorySize:
def __get__(self, obj, objtype=None):
return len(os.listdir(obj.dirname))
class Directory:
size = DirectorySize() # Descriptor instance
def __init__(self, dirname):
self.dirname = dirname # Regular instance attribute
Интерактивный сеанс показывает, что поиск является динамическим — каждый раз он вычисляет разные, обновленные ответы:
>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size # The songs directory has twenty files
20
>>> g.size # The games directory has three files
3
>>> os.remove('games/chess') # Delete a game
>>> g.size # File count is automatically updated
2
Помимо демонстрации того, как дескрипторы могут выполнять вычисления, этот пример также раскрывает назначение параметров __get__()
. Параметр self - это size, экземпляр DirectorySize. Параметром obj является либо g, либо s, экземпляр Directory. Именно параметр obj позволяет методу __get__()
узнать целевой каталог. Параметром objtype является класс Directory.
Управляемые атрибуты¶
Популярным применением дескрипторов является управление доступом к данным экземпляра. Дескриптор присваивается общедоступному атрибуту в словаре классов, в то время как фактические данные хранятся как частный атрибут в словаре экземпляров. Методы дескрипторов __get__()
и __set__()
запускаются при обращении к атрибуту public.
В следующем примере age является общедоступным атрибутом, а _age - частным атрибутом. При обращении к общедоступному атрибуту дескриптор регистрирует поиск или обновление:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAgeAccess:
def __get__(self, obj, objtype=None):
value = obj._age
logging.info('Accessing %r giving %r', 'age', value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', 'age', value)
obj._age = value
class Person:
age = LoggedAgeAccess() # Descriptor instance
def __init__(self, name, age):
self.name = name # Regular instance attribute
self.age = age # Calls __set__()
def birthday(self):
self.age += 1 # Calls both __get__() and __set__()
Интерактивный сеанс показывает, что весь доступ к управляемому атрибуту возраст регистрируется, но обычный атрибут имя не регистрируется:
>>> mary = Person('Mary M', 30) # The initial age update is logged
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40
>>> vars(mary) # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}
>>> mary.age # Access the data and log the lookup
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday() # Updates are logged as well
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31
>>> dave.name # Regular attribute lookup isn't logged
'David D'
>>> dave.age # Only the managed attribute is logged
INFO:root:Accessing 'age' giving 40
40
Одна из основных проблем, связанных с этим примером, заключается в том, что личное имя _age встроено в класс LoggedAgeAccess. Это означает, что у каждого экземпляра может быть только один зарегистрированный атрибут и что его имя не может быть изменено. В следующем примере мы исправим эту проблему.
Индивидуальные имена¶
Когда класс использует дескрипторы, он может информировать каждый дескриптор о том, какое имя переменной было использовано.
В этом примере класс Person
имеет два экземпляра дескриптора: name и age. Когда класс Person
определен, он выполняет обратный вызов __set_name__()
в LoggedAccess, чтобы можно было записать имена полей, присваивая каждому дескриптору свои собственные public_name и private_name:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAccess:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
logging.info('Accessing %r giving %r', self.public_name, value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', self.public_name, value)
setattr(obj, self.private_name, value)
class Person:
name = LoggedAccess() # First descriptor instance
age = LoggedAccess() # Second descriptor instance
def __init__(self, name, age):
self.name = name # Calls the first descriptor
self.age = age # Calls the second descriptor
def birthday(self):
self.age += 1
Интерактивный сеанс показывает, что класс Person
вызвал __set_name__()
, чтобы записать имена полей. Здесь мы вызываем vars()
для поиска дескриптора, не запуская его:
>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}
Новый класс теперь регистрирует доступ как к имени, так и к возрасту:
>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> kate = Person('Catherine C', 20)
INFO:root:Updating 'name' to 'Catherine C'
INFO:root:Updating 'age' to 20
Два экземпляра Person содержат только личные имена:
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}
Заключительные мысли¶
descriptor - это то, что мы называем любым объектом, который определяет __get__()
, __set__()
, или __delete__()
.
Необязательно, дескрипторы могут иметь метод __set_name__()
. Это используется только в тех случаях, когда дескриптору необходимо знать либо класс, в котором он был создан, либо имя переменной класса, которой он был присвоен. (Этот метод, если он присутствует, вызывается, даже если класс не является дескриптором.)
Дескрипторы вызываются оператором dot во время поиска атрибута. Если к дескриптору осуществляется косвенный доступ с помощью vars(some_class)[descriptor_name]
, экземпляр дескриптора возвращается без его вызова.
Дескрипторы работают только тогда, когда используются в качестве переменных класса. Если их поместить в экземпляры, они не будут иметь никакого эффекта.
Основная цель использования дескрипторов - обеспечить перехват, позволяющий объектам, хранящимся в переменных класса, контролировать то, что происходит во время поиска атрибутов.
Традиционно вызывающий класс управляет тем, что происходит во время поиска. Дескрипторы инвертируют эту взаимосвязь и позволяют запрашиваемым данным принимать участие в решении этого вопроса.
Дескрипторы используются во всем языке. Именно так функции превращаются в связанные методы. Такие распространенные инструменты, как classmethod()
, staticmethod()
, property()
, и functools.cached_property()
, реализованы в виде дескрипторов.
Полный практический пример¶
В этом примере мы создаем практичный и мощный инструмент для обнаружения заведомо труднодоступных ошибок, связанных с повреждением данных.
Класс валидатора¶
Валидатор - это дескриптор для управляемого доступа к атрибутам. Перед сохранением любых данных он проверяет, соответствует ли новое значение различным ограничениям по типу и диапазону. Если эти ограничения не соблюдены, он генерирует исключение, чтобы предотвратить повреждение данных в источнике.
Этот Validator
класс является одновременно abstract base class и дескриптором управляемого атрибута:
from abc import ABC, abstractmethod
class Validator(ABC):
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
Пользовательские валидаторы должны наследоваться от Validator
и должны предоставлять метод validate()
для проверки различных ограничений по мере необходимости.
Пользовательские валидаторы¶
Вот три практические утилиты для проверки данных:
OneOf
проверяет, является ли значение одним из ограниченного набора параметров.Number
подтверждает, что значение равноint
илиfloat
. При необходимости, оно проверяет, находится ли значение между заданным минимумом и максимумом.String
проверяет, что значение равноstr
. При необходимости, оно проверяет заданную минимальную или максимальную длину. Также может быть проверено значение, определенное пользователем predicate.
class OneOf(Validator):
def __init__(self, *options):
self.options = set(options)
def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
class Number(Validator):
def __init__(self, minvalue=None, maxvalue=None):
self.minvalue = minvalue
self.maxvalue = maxvalue
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
if self.minvalue is not None and value < self.minvalue:
raise ValueError(
f'Expected {value!r} to be at least {self.minvalue!r}'
)
if self.maxvalue is not None and value > self.maxvalue:
raise ValueError(
f'Expected {value!r} to be no more than {self.maxvalue!r}'
)
class String(Validator):
def __init__(self, minsize=None, maxsize=None, predicate=None):
self.minsize = minsize
self.maxsize = maxsize
self.predicate = predicate
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f'Expected {value!r} to be an str')
if self.minsize is not None and len(value) < self.minsize:
raise ValueError(
f'Expected {value!r} to be no smaller than {self.minsize!r}'
)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError(
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
)
if self.predicate is not None and not self.predicate(value):
raise ValueError(
f'Expected {self.predicate} to be true for {value!r}'
)
Практическое применение¶
Вот как можно использовать средства проверки данных в реальном классе:
class Component:
name = String(minsize=3, maxsize=10, predicate=str.isupper)
kind = OneOf('wood', 'metal', 'plastic')
quantity = Number(minvalue=0)
def __init__(self, name, kind, quantity):
self.name = name
self.kind = kind
self.quantity = quantity
Дескрипторы предотвращают создание недопустимых экземпляров:
>>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
...
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
>>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled
Traceback (most recent call last):
...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
>>> Component('WIDGET', 'metal', -5) # Blocked: -5 is negative
Traceback (most recent call last):
...
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number
Traceback (most recent call last):
...
TypeError: Expected 'V' to be an int or float
>>> c = Component('WIDGET', 'metal', 5) # Allowed: The inputs are valid
Техническое руководство¶
Далее следует более техническое руководство по механике и подробному описанию того, как работают дескрипторы.
Абстрактный¶
Определяет дескрипторы, обобщает протокол и показывает, как вызываются дескрипторы. Приводится пример, показывающий, как работают объектно-реляционные отображения.
Изучение дескрипторов не только открывает доступ к более широкому набору инструментов, но и позволяет глубже понять, как работает Python.
Определение и введение¶
В общем случае, дескриптор - это значение атрибута, которое имеет один из методов в протоколе дескриптора. Этими методами являются __get__()
, __set__()
, и __delete__()
. Если какой-либо из этих методов определен для атрибута, то он называется descriptor.
По умолчанию для доступа к атрибуту требуется получить, установить или удалить атрибут из словаря объекта. Например, a.x
содержит цепочку поиска, начинающуюся с a.__dict__['x']
, затем type(a).__dict__['x']
и продолжающуюся в порядке разрешения метода type(a)
. Если искомое значение является объектом, определяющим один из методов дескриптора, то Python может переопределить поведение по умолчанию и вместо этого вызвать метод дескриптора. Где это происходит в цепочке приоритетов, зависит от того, какие методы дескриптора были определены.
Дескрипторы - это мощный протокол общего назначения. Они являются механизмом, лежащим в основе свойств, методов, статических методов, методов классов и super()
. Они используются во всем Python. Дескрипторы упрощают базовый код на C и предлагают гибкий набор новых инструментов для повседневных программ на Python.
Протокол дескриптора¶
descr.__get__(self, obj, type=None)
descr.__set__(self, obj, value)
descr.__delete__(self, obj)
Вот и все, что нужно сделать. Определите любой из этих методов, и объект будет считаться дескриптором и сможет переопределять поведение по умолчанию при поиске в качестве атрибута.
Если объект определяет __set__()
или __delete__()
, он считается дескриптором данных. Дескрипторы, которые определяют только __get__()
, называются дескрипторами, не относящимися к данным (они часто используются для методов, но возможны и другие применения).
Дескрипторы данных и дескрипторы, не связанные с данными, различаются тем, как вычисляются переопределения для записей в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора, не содержащего данных, запись в словаре имеет приоритет.
Чтобы создать дескриптор данных, доступный только для чтения, определите как __get__()
, так и __set__()
с помощью __set__()
, вызывающего AttributeError
при вызове. Определения метода __set__()
с помощью исключения, вызывающего заполнитель, достаточно, чтобы сделать его дескриптором данных.
Обзор вызова дескриптора¶
Дескриптор может быть вызван непосредственно с помощью desc.__get__(obj)
или desc.__get__(None, cls)
.
Но чаще всего дескриптор вызывается автоматически из доступа к атрибуту.
Выражение obj.x
выполняет поиск атрибута x
в цепочке пространств имен для obj
. Если поиск находит дескриптор за пределами экземпляра __dict__
, вызывается его метод __get__()
в соответствии с правилами приоритета, перечисленными ниже.
Детали вызова зависят от того, является ли obj
объектом, классом или экземпляром super.
Вызов из экземпляра¶
Поиск экземпляра сканирует цепочку пространств имен, придавая наивысший приоритет дескрипторам данных, за которыми следуют переменные экземпляра, затем дескрипторы, не относящиеся к данным, затем переменные класса и, наконец, __getattr__()
, если это указано.
Если найден дескриптор для a.x
, то он вызывается с помощью: desc.__get__(a, type(a))
.
Логика точечного поиска заключается в object.__getattribute__()
. Вот чистый эквивалент Python:
def find_name_in_mro(cls, name, default):
"Emulate _PyType_Lookup() in Objects/typeobject.c"
for base in cls.__mro__:
if name in vars(base):
return vars(base)[name]
return default
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = find_name_in_mro(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
Обратите внимание, что в коде __getattribute__()
нет __getattr__()
. Поэтому прямой вызов __getattribute__()
или с помощью super().__getattribute__
полностью обойдет __getattr__()
.
Вместо этого именно оператор dot и функция getattr()
отвечают за вызов __getattr__()
всякий раз, когда __getattribute__()
вызывает AttributeError
. Их логика инкапсулирована во вспомогательную функцию:
def getattr_hook(obj, name):
"Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
try:
return obj.__getattribute__(name)
except AttributeError:
if not hasattr(type(obj), '__getattr__'):
raise
return type(obj).__getattr__(obj, name) # __getattr__
Вызов из класса¶
Логика для точечного поиска, такого как A.x
, приведена в type.__getattribute__()
. Шаги аналогичны шагам для object.__getattribute__()
, но поиск по словарю экземпляра заменяется поиском по method resolution order класса.
Если дескриптор найден, он вызывается с помощью desc.__get__(None, A)
.
Полную реализацию C можно найти в : c:func:!type_getattro и _PyType_Lookup()
в Objects/typeobject.c.
Вызов от супер¶
Логика точечного поиска super заключается в методе __getattribute__()
для объекта, возвращаемого super()
.
Точечный поиск, такой как super(A, obj).m
, выполняет поиск в obj.__class__.__mro__
для базового класса B
, непосредственно следующего за A
, а затем возвращает B.__dict__['m'].__get__(obj, A)
. Если это не дескриптор, то m
возвращается без изменений.
Полную реализацию C можно найти в super_getattro()
в Objects/typeobject.c. Чистый эквивалент Python можно найти в Guido’s Tutorial.
Краткое описание логики вызова¶
Механизм создания дескрипторов встроен в методы __getattribute__()
для object
, type
, и super()
.
Важными моментами, которые следует помнить, являются:
Дескрипторы вызываются методом
__getattribute__()
.Классы наследуют этот механизм от
object
,type
, илиsuper()
.Переопределение
__getattribute__()
предотвращает автоматические вызовы дескриптора, поскольку вся логика дескриптора содержится в этом методе.object.__getattribute__()
иtype.__getattribute__()
выполняют разные вызовы для__get__()
. Первый включает экземпляр и может включать класс. Второй вводитNone
для экземпляра и всегда включает класс.Дескрипторы данных всегда переопределяют словари экземпляров.
Дескрипторы, не относящиеся к данным, могут быть переопределены словарями экземпляров.
Автоматическое уведомление об имени¶
Иногда желательно, чтобы дескриптор знал, имя переменной какого класса ему было присвоено. Когда создается новый класс, метакласс type
просматривает словарь нового класса. Если какая-либо из записей является дескриптором и если она определяет __set_name__()
, этот метод вызывается с двумя аргументами. owner - это класс, в котором используется дескриптор, а name - это переменная класса, которой был присвоен дескриптор.
Подробные сведения о реализации приведены в type_new()
и set_names()
в Objects/typeobject.c.
Поскольку логика обновления находится в type.__new__()
, уведомления появляются только во время создания класса. Если дескрипторы будут добавлены в класс позже, __set_name__()
нужно будет вызывать вручную.
Пример ORM¶
Следующий код представляет собой упрощенный каркас, показывающий, как можно использовать дескрипторы данных для реализации object relational mapping.
Основная идея заключается в том, что данные хранятся во внешней базе данных. Экземпляры Python содержат только ключи к таблицам базы данных. Дескрипторы отвечают за поиск и обновление:
class Field:
def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]
def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()
Мы можем использовать класс Field
для определения models, который описывает схему для каждой таблицы в базе данных:
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()
def __init__(self, key):
self.key = key
class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()
def __init__(self, key):
self.key = key
Чтобы использовать модели, сначала подключитесь к базе данных:
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
Интерактивный сеанс показывает, как данные извлекаются из базы данных и как они могут быть обновлены:
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'
>>> Song('Country Roads').artist
'John Denver'
>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'
Чистые эквиваленты Python¶
Протокол descriptor прост и предлагает захватывающие возможности. Некоторые варианты использования настолько распространены, что были включены во встроенные инструменты. Свойства, связанные методы, статические методы, методы класса и слоты - все это основано на протоколе дескриптора.
Свойства¶
Вызов property()
- это краткий способ создания дескриптора данных, который запускает вызов функции при обращении к атрибуту. Его сигнатура такова:
property(fget=None, fset=None, fdel=None, doc=None) -> property
В документации показано типичное использование для определения управляемого атрибута x
:
class C:
def getx(self): return self.__x
def setx(self, value): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "I'm the 'x' property.")
Чтобы увидеть, как property()
реализован в терминах протокола дескриптора, вот чистый эквивалент Python:
class Property:
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
self._name = ''
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"property '{self._name}' has no getter")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(f"property '{self._name}' has no setter")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"property '{self._name}' has no deleter")
self.fdel(obj)
def getter(self, fget):
prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def setter(self, fset):
prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def deleter(self, fdel):
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
prop._name = self._name
return prop
Встроенная функция property()
помогает всякий раз, когда пользовательский интерфейс предоставляет доступ к атрибуту, а затем для последующих изменений требуется вмешательство метода.
Например, класс электронной таблицы может предоставлять доступ к значению ячейки через Cell('b10').value
. Последующие усовершенствования программы требуют, чтобы ячейка пересчитывалась при каждом доступе; однако программист не хочет влиять на существующий клиентский код, получающий прямой доступ к атрибуту. Решение состоит в том, чтобы обернуть доступ к атрибуту value в дескриптор данных свойства:
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
В этом примере будет работать либо встроенный property()
, либо наш эквивалент Property()
.
Функции и методы¶
Объектно-ориентированные возможности Python основаны на функциональной среде. При использовании дескрипторов, не связанных с данными, они легко объединяются.
Функции, хранящиеся в словарях классов, при вызове преобразуются в методы. Методы отличаются от обычных функций только тем, что экземпляр object добавляется перед другими аргументами. По соглашению, экземпляр называется self, но может называться this или любым другим именем переменной.
Методы могут быть созданы вручную с помощью types.MethodType
, что примерно эквивалентно:
class MethodType:
"Emulate PyMethod_Type in Objects/classobject.c"
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
Чтобы поддерживать автоматическое создание методов, функции включают метод __get__()
для привязки методов при доступе к атрибутам. Это означает, что функции не являются дескрипторами данных, которые возвращают привязанные методы при точечном поиске из экземпляра. Вот как это работает:
class Function:
...
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return MethodType(self, obj)
Запуск следующего класса в интерпретаторе показывает, как дескриптор функции работает на практике:
class D:
def f(self, x):
return x
Функция имеет атрибут qualified name для поддержки самоанализа:
>>> D.f.__qualname__
'D.f'
Доступ к функции через словарь классов не вызывает __get__()
. Вместо этого он просто возвращает базовый объект функции:
>>> D.__dict__['f']
<function D.f at 0x00C45070>
Точечный доступ из класса вызывает __get__()
, который просто возвращает базовую функцию без изменений:
>>> D.f
<function D.f at 0x00C45070>
Интересное поведение наблюдается при точечном доступе из экземпляра. Точечный поиск вызывает __get__()
, который возвращает связанный объект метода:
>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
Внутренне связанный метод хранит базовую функцию и связанный экземпляр:
>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x00B18C90>
Если вы когда-нибудь задумывались, откуда берется self в обычных методах или откуда берется cls в методах класса, то это оно!
Виды методов¶
Дескрипторы, не связанные с данными, предоставляют простой механизм для изменения обычных шаблонов привязки функций к методам.
Напомним, что функции имеют __get__()
метод, так что при обращении к ним в качестве атрибутов они могут быть преобразованы в метод. Дескриптор, не содержащий данных, преобразует вызов obj.f(*args)
в f(obj, *args)
. Вызов cls.f(*args)
становится f(*args)
.
В этой таблице кратко описан способ привязки и два его наиболее полезных варианта:
Преобразование
Вызывается из объекта
Вызванный из класса
функция
f(obj, *args)
f(*аргументы)
статический метод
f(*аргументы)
f(*аргументы)
метод класса
f(тип(obj), *args)
f(cls, *args)
Статические методы¶
Статические методы возвращают базовую функцию без изменений. Вызов c.f
или C.f
эквивалентен прямому поиску в object.__getattribute__(c, "f")
или object.__getattribute__(C, "f")
. В результате функция становится одинаково доступной как из объекта, так и из класса.
Хорошими кандидатами на роль статических методов являются методы, которые не ссылаются на переменную self
.
Например, статистический пакет может содержать класс-контейнер для экспериментальных данных. Этот класс предоставляет обычные методы вычисления среднего значения, медианы и других описательных статистических данных, которые зависят от данных. Однако могут быть полезные функции, которые концептуально связаны, но не зависят от данных. Например, erf(x)
- это удобная процедура преобразования, которая используется в статистической работе, но напрямую не зависит от конкретного набора данных. Она может быть вызвана либо из объекта, либо из класса: s.erf(1.5) --> .9332
или Sample.erf(1.5) --> .9332
.
Поскольку статические методы возвращают базовую функцию без изменений, примеры вызовов не вызывают интереса:
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
Используя протокол без использования дескриптора данных, чистая версия Python для staticmethod()
выглядела бы следующим образом:
import functools
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, objtype=None):
return self.f
def __call__(self, *args, **kwds):
return self.f(*args, **kwds)
Вызов functools.update_wrapper()
добавляет атрибут __wrapped__
, который ссылается на базовую функцию. Также он содержит атрибуты, необходимые для того, чтобы оболочка выглядела как обернутая функция: __name__
, __qualname__
, __doc__
, и __annotations__
.
Методы класса¶
В отличие от статических методов, методы класса добавляют ссылку на класс к списку аргументов перед вызовом функции. Этот формат одинаков для того, является ли вызывающий объект объектом или классом:
class F:
@classmethod
def f(cls, x):
return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)
Это полезно, когда методу требуется только ссылка на класс, и он не полагается на данные, хранящиеся в конкретном экземпляре. Одно из применений методов класса - создание альтернативных конструкторов классов. Например, метод класса dict.fromkeys()
создает новый словарь из списка ключей. Чистым эквивалентом Python является:
class Dict(dict):
@classmethod
def fromkeys(cls, iterable, value=None):
"Emulate dict_fromkeys() in Objects/dictobject.c"
d = cls()
for key in iterable:
d[key] = value
return d
Теперь новый словарь уникальных ключей может быть создан следующим образом:
>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}
Используя протокол без использования дескриптора данных, чистая версия Python для classmethod()
выглядела бы следующим образом:
import functools
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
# This code path was added in Python 3.9
# and was deprecated in Python 3.11.
return self.f.__get__(cls, cls)
return MethodType(self.f, cls)
Путь к коду для hasattr(type(self.f), '__get__')
был добавлен в Python 3.9 и позволяет classmethod()
поддерживать связанные декораторы. Например, метод класса и свойство могут быть объединены в цепочку. В Python 3.11 эта функциональность была признана устаревшей.
class G:
@classmethod
@property
def __doc__(cls):
return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"
Вызов functools.update_wrapper()
в ClassMethod
добавляет атрибут __wrapped__
, который ссылается на базовую функцию. Также он содержит атрибуты, необходимые для того, чтобы оболочка выглядела как обернутая функция: __name__
, __qualname__
, __doc__
, и __annotations__
.
Объекты-члены и __слотов__¶
Когда класс определяет __slots__
, он заменяет словари экземпляров массивом значений интервалов фиксированной длины. С точки зрения пользователя, это имеет несколько последствий:
1. Provides immediate detection of bugs due to misspelled attribute
assignments. Only attribute names specified in __slots__
are allowed:
class Vehicle:
__slots__ = ('id_number', 'make', 'model')
>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last):
...
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
2. Helps create immutable objects where descriptors manage access to private
attributes stored in __slots__
:
class Immutable:
__slots__ = ('_dept', '_name') # Replace the instance dictionary
def __init__(self, dept, name):
self._dept = dept # Store to private attribute
self._name = name # Store to private attribute
@property # Read-only descriptor
def dept(self):
return self._dept
@property
def name(self): # Read-only descriptor
return self._name
>>> mark = Immutable('Botany', 'Mark Watney')
>>> mark.dept
'Botany'
>>> mark.dept = 'Space Pirate'
Traceback (most recent call last):
...
AttributeError: property 'dept' of 'Immutable' object has no setter
>>> mark.location = 'Mars'
Traceback (most recent call last):
...
AttributeError: 'Immutable' object has no attribute 'location'
3. Saves memory. On a 64-bit Linux build, an instance with two attributes
takes 48 bytes with __slots__
and 152 bytes without. This flyweight
design pattern likely only
matters when a large number of instances are going to be created.
4. Improves speed. Reading instance variables is 35% faster with
__slots__
(as measured with Python 3.10 on an Apple M1 processor).
5. Blocks tools like functools.cached_property()
which require an
instance dictionary to function correctly:
from functools import cached_property
class CP:
__slots__ = () # Eliminates the instance dict
@cached_property # Requires an instance dict
def pi(self):
return 4 * sum((-1.0)**n / (2.0*n + 1.0)
for n in reversed(range(100_000)))
>>> CP().pi
Traceback (most recent call last):
...
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
Невозможно создать точную версию __slots__
на чистом Python, поскольку для этого требуется прямой доступ к структурам C и контроль над распределением памяти объекта. Тем не менее, мы можем построить в основном точную симуляцию, в которой фактическая структура C для слотов эмулируется частным списком _slotvalues
. Операции чтения и записи в эту частную структуру управляются дескрипторами элементов:
null = object()
class Member:
def __init__(self, name, clsname, offset):
'Emulate PyMemberDef in Include/structmember.h'
# Also see descr_new() in Objects/descrobject.c
self.name = name
self.clsname = clsname
self.offset = offset
def __get__(self, obj, objtype=None):
'Emulate member_get() in Objects/descrobject.c'
# Also see PyMember_GetOne() in Python/structmember.c
if obj is None:
return self
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
return value
def __set__(self, obj, value):
'Emulate member_set() in Objects/descrobject.c'
obj._slotvalues[self.offset] = value
def __delete__(self, obj):
'Emulate member_delete() in Objects/descrobject.c'
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
obj._slotvalues[self.offset] = null
def __repr__(self):
'Emulate member_repr() in Objects/descrobject.c'
return f'<Member {self.name!r} of {self.clsname!r}>'
Метод type.__new__()
заботится о добавлении объектов-членов к переменным класса:
class Type(type):
'Simulate how the type metaclass adds member objects for slots'
def __new__(mcls, clsname, bases, mapping, **kwargs):
'Emulate type_new() in Objects/typeobject.c'
# type_new() calls PyTypeReady() which calls add_methods()
slot_names = mapping.get('slot_names', [])
for offset, name in enumerate(slot_names):
mapping[name] = Member(name, clsname, offset)
return type.__new__(mcls, clsname, bases, mapping, **kwargs)
Метод object.__new__()
заботится о создании экземпляров, у которых есть слоты вместо словаря экземпляров. Вот грубое моделирование на чистом Python:
class Object:
'Simulate how object.__new__() allocates memory for __slots__'
def __new__(cls, *args, **kwargs):
'Emulate object_new() in Objects/typeobject.c'
inst = super().__new__(cls)
if hasattr(cls, 'slot_names'):
empty_slots = [null] * len(cls.slot_names)
object.__setattr__(inst, '_slotvalues', empty_slots)
return inst
def __setattr__(self, name, value):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{cls.__name__!r} object has no attribute {name!r}'
)
super().__setattr__(name, value)
def __delattr__(self, name):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{cls.__name__!r} object has no attribute {name!r}'
)
super().__delattr__(name)
Чтобы использовать симуляцию в реальном классе, просто наследуйте от Object
и установите для metaclass значение Type
:
class H(Object, metaclass=Type):
'Instance variables stored in slots'
slot_names = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
На данный момент в метакласс загружены объекты-члены для x и y:
>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
'__doc__': 'Instance variables stored in slots',
'slot_names': ['x', 'y'],
'__init__': <function H.__init__ at 0x7fb5d302f9d0>,
'x': <Member 'x' of 'H'>,
'y': <Member 'y' of 'H'>}
Когда создаются экземпляры, у них есть список slot_values
, в котором хранятся атрибуты:
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
Неправильно написанные или неназначенные атрибуты вызовут исключение:
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'