Дескрипторы классов - это мощный протокол общего назначения. Это механизм, лежащий в основе свойств, методов, статических методов, методов класса и функции super()
. Они используются в самом Python для реализации классов нового стиля. Дескрипторы упрощают базовый C-код и предлагают гибкий набор новых инструментов для повседневных программ Python.
В общем случае, дескриптор - это атрибут объекта с "привязкой поведения", доступ к которому был переопределен методами в протоколе дескриптора. Этими методами являются __get__()
, __set__()
и __delete__()
. Если какой-либо из этих методов определен для объекта, то он называется дескриптором.
Поведение по умолчанию для доступа к атрибуту заключается в получении __get__()
, установке __set__()
или удалении атрибута __delete__()
из словаря объекта. Например, a.x
имеет цепочку поиска, начинающуюся с a.__dict__['x']
, затем type(a).__dict__['x']
и продолжающуюся через базовые классы type(a)
, исключая метаклассы. Если искомое значение является объектом, который определяет один из методов дескриптора, то Python переопределяет поведение по умолчанию и вызывает вместо него метод дескриптора. Это происходит в так называемой цепочке приоритетов, и зависит от того, какие методы дескриптора были определены.
Смотрите также разделы:
- "Обзор дескриптора класса для начинающих" на примерах, от простого к сложному.
- "Создание валидатора, как практического примера дескриптора.
descr.__get__(self, obj, type=None) -> value descr.__set__(self, obj, value) -> None descr.__delete__(self, obj) -> None
Вот и все, что нужно сделать. Определите любой из этих методов и объект будет считаться дескриптором и может переопределить поведение по умолчанию при поиске в качестве атрибута.
Если объект определяет __set__()
или __delete__()
, то он считается дескриптором данных. Дескрипторы, которые определяют только __get__()
, называются дескрипторами без данных, обычно они используются для методов, но возможны и другие применения.
Дескрипторы данных и дескрипторы без данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра.
Чтобы создать дескриптор данных только для чтения, необходимо определить методы как __get__()
, так и __set__()
, при этом метод __set__()
должен вызывать исключение AttributeError
. Определение метода __set__()
с raise
, достаточно, чтобы сделать его дескриптором данных.
Дескриптор можно вызвать напрямую с помощью desc.__get__(obj)
или desc.__get__(None, cls)
. Но чаще всего дескриптор вызывается автоматически при доступе к атрибуту.
Выражение obj.x
ищет атрибут x
в цепочке пространств имен для obj
. Если поиск находит дескриптор за пределами экземпляра __dict__
, то вызывается его метод __get__()
в соответствии с правилами приоритета, перечисленными ниже.
Детали вызова зависят от того, является ли obj
объектом, классом или экземпляром super()
.
Поиск в экземпляре класса просматривает цепочку пространств имен, и присваивает дескрипторам данных наивысший приоритет, за которыми следуют переменные экземпляра, затем дескрипторы, не относящиеся к данным, затем переменные класса и, наконец, __getattr__()
, если он предоставлен.
Если для экземпляра a.x
найден дескриптор, то он вызывается с помощью: desc.__get__(a, type(a))
.
Для экземпляра, логика поиска при доступе через точку, как a.x
, находится в object.__getattribute__()
. Вот чистый эквивалент на Python:
def object_getattribute(obj, name): "Эмуляция PyObject_GenericGetAttr() в Objects/object.c" null = object() objtype = type(obj) cls_var = getattr(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) if hasattr(obj, '__dict__') and name in vars(obj): # переменная экземпляра return vars(obj)[name] if descr_get is not null: # дескриптор, не содержащий данных return descr_get(cls_var, obj, objtype) if cls_var is not null: # переменная класса return cls_var raise AttributeError(name)
Интересно, что поиск атрибутов не вызывает object.__getattribute__()
напрямую. Вместо этого и обращение через точку (a.x
), и функция getattr()
выполняют поиск атрибутов с помощью вспомогательной функции:
def getattr_hook(obj, name): "эмуляция slot_tp_getattr_hook() в Objects/typeobject.c" try: return obj.__getattribute__(name) except AttributeError: if not hasattr(type(obj), '__getattr__'): raise # __getattr__ return type(obj).__getattr__(obj, name)
Итак, если __getattr__()
существует, то он вызывается всегда, когда __getattribute__()
вызывает исключение AttributeError
(либо напрямую, либо в одном из вызовов дескриптора).
Кроме того, если пользователь вызывает object.__getattribute__()
напрямую, то хук __getattr__()
полностью игнорируется.
Для класса, логика поиска при доступе через точку, такого как A.x
, находится в type.__getattribute__()
. Шаги аналогичны шагам для object.__getattribute__()
, но поиск в словаре экземпляра заменен поиском в порядке разрешения методов класса.
Если дескриптор найден, то он вызывается с помощью desc.__get__(None, A)
.
super()
.При использовании super()
логика поиска, при доступе через точку, находится в методе __getattribute__()
, для объекта, возвращаемого функцией super()
.
При вызове super(A, obj).m
, будет выполнятся поиск в obj.__class__.__mro__
для базового класса B
сразу после A
, а затем возвращает B.__dict__['m'].__get__(obj, A)
. Если это не дескриптор, то m
возвращается без изменений.
Механизм дескрипторов встроен в методы __getattribute__()
для объектов, типов и super()
.
Важные моменты, которые следует помнить:
__getattribute__()
,super()
, __getattribute__()
предотвращает автоматические вызовы дескриптора, т.к. вся логика дескриптора находится в этом методе,object.__getattribute__()
и type.__getattribute__()
выполняют разные вызовы метода __get__()
. Первый включает экземпляр и может включать класс. Второй помещает None
для экземпляра и всегда включает классИногда желательно, чтобы дескриптор знал, какое имя переменной класса ему было присвоено. Когда создается новый класс, то метакласс типа сканирует словарь нового класса. Если какая-либо из записей является дескриптором и если они определяют __set_name__()
, то этот метод вызывается с двумя аргументами. Аргумент owner
- это класс, в котором используется дескриптор, а аргумент name
- переменная класса, которой был присвоен дескриптор.
Так как логика обновления имеет type.__new__()
, то уведомления появляются только во время создания класса. Если дескрипторы будут добавлены в класс впоследствии, то метод __set_name__()
необходимо будет вызвать вручную.
Следующий код представляет собой упрощенную схему, которая показывает, как дескрипторы данных можно использовать для реализации объектно-реляционного сопоставления.
Основная идея заключается в том, что данные хранятся во внешней базе данных. Экземпляры 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' # Первичный ключ key = 'title' 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'
Протокол дескриптора прост и предлагает захватывающие возможности. Некоторые варианты использования настолько распространены, что уже включены во встроенные инструменты. Свойства класса, связанные методы, статические методы, методы класса, все они построены на протоколе дескриптора.
property()
как дескриптор данных.Вызов функции 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()
с точки зрения протокола дескриптора:
class Property: "Это эмуляция PyProperty_Type() в 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'unreadable attribute {self._name}') return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError(f"can't set attribute {self._name}") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError(f"can't delete attribute {self._name}") 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
. Последующие усовершенствования программы требуют, чтобы ячейка пересчитывалась при каждом доступе, но программист не хочет влиять на существующий клиентский код, который обращается к атрибуту напрямую. Решение состоит в том, чтобы обернуть доступ к значению атрибута в свойство дескриптора данных:
class Cell: ... @property def value(self): "Пересчет ячейки перед возвращением значения" self.recalc() return self._value
Объектно-ориентированные функции (методы) в Python основаны на простых функциях. Функции и методы легко объединяются, используя дескрипторы, не относящиеся к данным.
Функции, хранящиеся в словарях классов, при вызове превращаются в методы. Методы отличаются от обычных функций только тем, что к другим аргументам добавляется экземпляр объекта. По соглашению, экземпляр называется self
, но может называться переменной с любым другим именем.
Методы можно создавать вручную с помощью types.MethodType
, что примерно эквивалентно:
class MethodType: "Эмуляция PyMethod_Type в 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): "Эмуляция func_descr_get() в Objects/funcobject.c" if obj is None: return self return MethodType(self, obj)
Запуск следующего класса в интерпретаторе показывает, как на практике работает дескриптор функции:
class D: def f(self, x): return x
Функция имеет атрибут __qualname__
для поддержки самоанализа:
>>> 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 0x1012e1f98>
Если вы когда-нибудь задумывались, откуда берется self
в обычных методах или откуда берется cls
в методах класса, то вот оно!
Дескрипторы, не относящиеся к данным, обеспечивают простой механизм для вариаций обычных шаблонов связывания функций в методы.
Напомним, что у функций есть метод __get__()
, так что их можно преобразовать в метод при обращении к ним как к атрибутам. Дескриптор без данных преобразует вызов obj.f(*args)
в f(obj, *args)
. Вызов cls.f(*args)
становится f(*args)
.
Эта таблица суммирует привязку и два ее наиболее полезных варианта:
Преобразование | Вызов из объекта | Вызов из класса |
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(cls, *args) |
staticmethod()
как дескриптор без данных.Статические методы возвращают базовую функцию без изменений. Вызов 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
Используя протокол, который не содержит дескрипторов данных, версия функции staticmethod()
на Python будет выглядеть следующим образом
class StaticMethod: "Эмуляция PyStaticMethod_Type() в Objects/funcobject.c" def __init__(self, f): self.f = f def __get__(self, obj, objtype=None): return self.f def __call__(self, *args, **kwds): return self.f(*args, **kwds)
classmethod()
как дескриптор без данных.В отличие от статических методов, методы класса добавляют ссылку на класс к списку аргументов перед вызовом функции. Этот формат одинаков для того, является ли вызывающий объект объектом или классом:
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): "Эмуляция dict_fromkeys() в 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}
Используя протокол дескриптора без данных, версия функции classmethod()
на чистом Python будет выглядеть так:
class ClassMethod: "Эмуляция PyClassMethod_Type() в Objects/funcobject.c" def __init__(self, f): self.f = f def __get__(self, obj, cls=None): if cls is None: cls = type(obj) if hasattr(type(self.f), '__get__'): return self.f.__get__(cls, cls) return MethodType(self.f, cls)
Путь к коду для hasattr(type(self.f), '__get__')
был добавлен в Python 3.9 и позволяет classmethod()
поддерживать связанные декораторы. Например, метод класса и свойство могут быть объединены в цепочку:
class G: @classmethod @property def __doc__(cls): return f'A doc for {cls.__name__!r}' >>> G.__doc__ # "A doc for 'G'"