Руководство по дескрипторам
- Автор:
-
Raymond Hettinger
- Контакт:
-
<python at rcn dot com>
Дескрипторы позволяют объектам настраивать поиск, хранение и удаление атрибутов.
Это руководство состоит из четырёх основных разделов:
- В разделе «Основные сведения» представлен общий обзор: от простых примеров мы постепенно переходим к более сложным, добавляя по одной возможности за раз. Если вы впервые знакомитесь с дескрипторами, начните отсюда.
- Во втором разделе приведён полный практический пример дескриптора. Если вы уже знаете основы, начните с него.
- В третьем разделе представлено более техническое руководство с подробным описанием механики работы дескрипторов. Большинству читателей такой уровень подробностей не нужен.
- В последнем разделе приведены эквиваленты на чистом 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 оператор точки находит 'x': 5 в словаре класса. При поиске a.y оператор точки находит экземпляр дескриптора, распознаваемый по его методу __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__() вызываются при обращении к публичному атрибуту.
В следующем примере 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__()
Интерактивный сеанс показывает, что все обращения к управляемому атрибуту age регистрируются, а обращения к обычному атрибуту name — нет:
>>> 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'}
Теперь новый класс регистрирует обращения и к 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}
Заключительные замечания
Дескриптором называется любой объект, определяющий __get__(), __set__() или __delete__().
Дескрипторы могут также иметь метод __set_name__(). Он используется только в тех случаях, когда дескриптору нужно знать, в каком классе он был создан, или имя переменной класса, которой он был присвоен. (Этот метод, если он определён, вызывается, даже если класс не является дескриптором.)
Дескрипторы вызываются оператором точки во время поиска атрибута. Если доступ к дескриптору осуществляется косвенно с помощью vars(some_class)[descriptor_name], экземпляр дескриптора возвращается без его вызова.
Дескрипторы работают только при использовании в качестве переменных класса. Если поместить их в экземпляры, они не будут оказывать никакого эффекта.
Главная причина использовать дескрипторы — предоставить механизм, позволяющий объектам, хранящимся в переменных класса, управлять поведением при поиске атрибутов.
Традиционно поведение при поиске определяет вызывающий класс. Дескрипторы меняют эту взаимосвязь и позволяют искомым данным влиять на процесс.
Дескрипторы используются во всём языке. Именно благодаря им функции превращаются в связанные методы. Такие распространённые инструменты, как classmethod(), staticmethod(), property() и functools.cached_property(), реализованы как дескрипторы.
Полный практический пример
В этом примере мы создадим практичный и мощный инструмент для поиска крайне трудно обнаруживаемых ошибок повреждения данных.
Класс Validator
Валидатор — это дескриптор для управления доступом к атрибутам. Прежде чем сохранить какие-либо данные, он проверяет, соответствует ли новое значение различным ограничениям типа и диапазона. Если эти ограничения не соблюдены, он вызывает исключение, предотвращая повреждение данных в источнике.
Этот класс Validator является одновременно абстрактным базовым классом и дескриптором управляемого атрибута:
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. При необходимости он проверяет минимальную или максимальную длину. Кроме того, он может проверять определённый пользователем предикат.
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__(). Если для атрибута определён любой из этих методов, такой атрибут считается дескриптором.
По умолчанию при обращении к атрибуту он извлекается из словаря объекта, записывается в него или удаляется из него. Например, цепочка поиска для 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__().
Вместо этого за вызов __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__(), но вместо поиска в словаре экземпляра выполняется поиск в порядке разрешения методов класса.
Если найден дескриптор, он вызывается с аргументами desc.__get__(None, A).
Полную реализацию на C можно найти в type_getattro() и _PyType_Lookup() в файле Objects/typeobject.c.
Вызов из super
Логика поиска через точку для 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 приведён в руководстве Гвидо.
Краткое описание логики вызова
Механизм дескрипторов встроен в методы __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
Следующий код представляет собой упрощённую схему использования дескрипторов данных для реализации объектно-реляционного отображения.
Основная идея заключается в том, что данные хранятся во внешней базе данных. Экземпляры 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 для определения моделей, описывающих схему каждой таблицы базы данных:
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
Протокол дескрипторов прост и открывает широкие возможности. Некоторые варианты использования настолько распространены, что для них уже созданы встроенные инструменты. Свойства, связанные методы, статические методы, методы класса и __slots__ основаны на протоколе дескрипторов.
Свойства
Вызов 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!r} of {type(obj).__name__!r} object has no getter'
)
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no setter'
)
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object 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 в дескриптор данных property:
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
В этом примере подойдёт как встроенный property(), так и наш эквивалент Property().
Функции и методы
Объектно-ориентированные возможности Python построены на функциональной основе. Благодаря дескрипторам, не являющимся дескрипторами данных, эти две модели бесшовно объединяются.
Функции, хранящиеся в словарях классов, при вызове превращаются в методы. Методы отличаются от обычных функций только тем, что экземпляр объекта добавляется перед остальными аргументами. По соглашению экземпляр называется 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
У функции есть атрибут квалифицированное имя, предназначенный для интроспекции:
>>> 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(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(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__
Когда класс определяет __slots__, словари экземпляров заменяются массивом фиксированной длины, содержащим значения слотов. С точки зрения пользователя это имеет несколько последствий:
1. Позволяет сразу обнаруживать ошибки из-за опечаток при присваивании атрибутов. Допустимы только имена атрибутов, указанные в __slots__:
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. Помогает создавать неизменяемые объекты, в которых дескрипторы управляют доступом к закрытым атрибутам, хранящимся в __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. Экономит память. В 64-разрядной сборке Linux экземпляр с двумя атрибутами занимает 48 байт при использовании __slots__ и 152 байта без него. Этот шаблон проектирования «Приспособленец» имеет значение, вероятно, только при создании большого количества экземпляров.
4. Повышает скорость. Чтение переменных экземпляра выполняется на 35% быстрее при использовании __slots__ (по результатам измерений на Python 3.10 с процессором Apple M1).
5. Не позволяет использовать такие инструменты, как functools.cached_property(), которым для корректной работы нужен словарь экземпляра:
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 и задать метакласс 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'
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/howto/descriptor.html