descriptor.md
1> **Источник:** https://python-all.ru/3.2/howto/descriptor.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# [Руководство по дескрипторам](https://python-all.ru/3.2/howto/descriptor.html#id1)89| Автор: | Рэймонд Хеттингер |10| --- | --- |11| Контакт: | \<python at rcn dot com\> |1213Содержание1415- [Руководство по дескрипторам](https://python-all.ru/3.2/howto/descriptor.html#descriptor-howto-guide)1617 - [Аннотация](https://python-all.ru/3.2/howto/descriptor.html#abstract)18 - [Определение и введение](https://python-all.ru/3.2/howto/descriptor.html#definition-and-introduction)19 - [Протокол дескрипторов](https://python-all.ru/3.2/howto/descriptor.html#descriptor-protocol)20 - [Вызов дескрипторов](https://python-all.ru/3.2/howto/descriptor.html#invoking-descriptors)21 - [Пример дескриптора](https://python-all.ru/3.2/howto/descriptor.html#descriptor-example)22 - [Свойства](https://python-all.ru/3.2/howto/descriptor.html#properties)23 - [Функции и методы](https://python-all.ru/3.2/howto/descriptor.html#functions-and-methods)24 - [Статические методы и методы класса](https://python-all.ru/3.2/howto/descriptor.html#static-methods-and-class-methods)2526## [Аннотация](https://python-all.ru/3.2/howto/descriptor.html#id2)2728Определяет дескрипторы, обобщает протокол и показывает, как вызываются дескрипторы. Рассматривает пользовательский дескриптор и несколько встроенных дескрипторов Python, включая функции, свойства, статические методы и методы класса. Показывает, как каждый из них работает, приводя эквивалент на чистом Python и пример приложения.2930Изучение дескрипторов не только даёт доступ к более широкому набору инструментов, но и позволяет глубже понять, как работает Python, и оценить элегантность его устройства.3132## [Определение и введение](https://python-all.ru/3.2/howto/descriptor.html#id3)3334В общем случае дескриптор – это атрибут объекта с «поведением привязки», доступ к которому переопределён методами протокола дескриптора. Такими методами являются [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__), [`__set__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__set__) и [`__delete__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__delete__). Если для объекта определён хотя бы один из этих методов, говорят, что он является дескриптором.3536Поведение по умолчанию при доступе к атрибуту заключается в получении, установке или удалении атрибута из словаря объекта. Например, `a.x` имеет цепочку поиска, начиная с `a.__dict__['x']`, затем `type(a).__dict__['x']`, и продолжая через базовые классы `type(a)`, исключая метаклассы. Если найденное значение является объектом, определяющим один из методов дескриптора, то Python может переопределить поведение по умолчанию и вместо этого вызвать метод дескриптора. То, где это происходит в цепочке приоритетов, зависит от того, какие методы дескриптора были определены. Обратите внимание, что дескрипторы вызываются только для объектов или классов нового стиля (класс является классом нового стиля, если он наследуется от [`object`](https://python-all.ru/3.2/library/functions.html#object) или [`type`](https://python-all.ru/3.2/library/functions.html#type)).3738Дескрипторы – это мощный универсальный протокол. Они лежат в основе свойств, методов, статических методов, методов класса и [`super()`](https://python-all.ru/3.2/library/functions.html#super). Они используются в самом Python для реализации классов нового стиля, появившихся в версии 2.2. Дескрипторы упрощают внутренний C-код и предоставляют гибкий набор новых инструментов для повседневных программ на Python.3940## [Протокол дескриптора](https://python-all.ru/3.2/howto/descriptor.html#id4)4142`descr.__get__(self, obj, type=None) --> значение`4344`descr.__set__(self, obj, value) --> None`4546`descr.__delete__(self, obj) --> None`4748Вот и всё. Определите любой из этих методов – и объект считается дескриптором и может переопределять поведение по умолчанию при поиске в качестве атрибута.4950Если объект определяет и [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__), и [`__set__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__set__), он считается дескриптором данных. Дескрипторы, определяющие только [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__), называются дескрипторами не-данных (обычно они используются для методов, но возможны и другие применения).5152Дескрипторы данных и не-данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора не-данных, приоритет имеет запись словаря.5354Чтобы сделать дескриптор данных доступным только для чтения, определите и [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__), и [`__set__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__set__), причём [`__set__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__set__) должен возбуждать [`AttributeError`](https://python-all.ru/3.2/library/exceptions.html#AttributeError) при вызове. Определения [`__set__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__set__) с заглушкой, возбуждающей исключение, достаточно, чтобы сделать его дескриптором данных.5556## [Вызов дескрипторов](https://python-all.ru/3.2/howto/descriptor.html#id5)5758Дескриптор можно вызвать напрямую по имени метода. Например, `d.__get__(obj)`.5960Но чаще дескриптор вызывается автоматически при доступе к атрибуту. Например, `obj.d` ищет `d` в словаре `obj`. Если `d` определяет метод [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__), то вызывается `d.__get__(obj)` в соответствии с правилами приоритета, приведёнными ниже.6162Подробности вызова зависят от того, является ли `obj` объектом или классом. В любом случае дескрипторы работают только с объектами и классами нового стиля. Класс является классом нового стиля, если он является подклассом [`object`](https://python-all.ru/3.2/library/functions.html#object).6364Для объектов механизм находится в [`object.__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__), который преобразует `b.x` в `type(b).__dict__['x'].__get__(b, type(b))`. Реализация работает по цепочке приоритетов, где дескрипторы данных имеют приоритет над переменными экземпляра, переменные экземпляра – над дескрипторами не-данных, а самый низкий приоритет у [`__getattr__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattr__), если он определён. Полную реализацию на C можно найти в [`PyObject_GenericGetAttr()`](https://python-all.ru/3.2/c-api/object.html#PyObject_GenericGetAttr) в файле [Objects/object.c](https://python-all.ru/3.2/howto/descriptor.html).6566Для классов механизм находится в `type.__getattribute__()`, который преобразует `B.x` в `B.__dict__['x'].__get__(None, B)`. На чистом Python это выглядит так:6768```python69def __getattribute__(self, key):70 "Emulate type_getattro() in Objects/typeobject.c"71 v = object.__getattribute__(self, key)72 if hasattr(v, '__get__'):73 return v.__get__(None, self)74 return v75```7677Важно запомнить следующее:7879- дескрипторы вызываются методом [`__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__)80- переопределение [`__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__) предотвращает автоматический вызов дескрипторов81- [`__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__) доступен только для классов и объектов нового стиля82- [`object.__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__) и `type.__getattribute__()` по-разному вызывают [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__).83- дескрипторы данных всегда переопределяют словари экземпляров.84- дескрипторы без данных могут быть переопределены словарями экземпляров.8586Объект, возвращаемый `super()`, также имеет собственный метод [`__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__) для вызова дескрипторов. Вызов `super(B, obj).m()` ищет в `obj.__class__.__mro__` базовый класс `A`, следующий непосредственно за `B`, и затем возвращает `A.__dict__['m'].__get__(obj, A)`. Если это не дескриптор, `m` возвращается без изменений. Если его нет в словаре, `m` возвращается к поиску с помощью [`object.__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__).8788Обратите внимание: в Python 2.2 `super(B, obj).m()` вызывал бы [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__), только если `m` был дескриптором данных. В Python 2.3 дескрипторы не данных также вызываются, если только не задействован класс старого стиля. Подробности реализации находятся в `super_getattro()` в [Objects/typeobject.c](https://python-all.ru/3.2/howto/descriptor.html), а чистый Python-эквивалент можно найти в [руководстве Гвидо](https://python-all.ru/3.2/howto/descriptor.html).8990Из изложенного видно, что механизм дескрипторов встроен в методы [`__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__) для [`object`](https://python-all.ru/3.2/library/functions.html#object), [`type`](https://python-all.ru/3.2/library/functions.html#type) и [`super()`](https://python-all.ru/3.2/library/functions.html#super). Классы наследуют этот механизм, если они производны от [`object`](https://python-all.ru/3.2/library/functions.html#object) или имеют метакласс, предоставляющий аналогичную функциональность. Классы также могут отключить вызов дескрипторов, переопределив [`__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__).9192## [Пример дескриптора](https://python-all.ru/3.2/howto/descriptor.html#id6)9394Следующий код создаёт класс, объекты которого являются дескрипторами данных, выводящими сообщение при каждом get или set. Переопределение [`__getattribute__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__getattribute__) – это альтернативный подход, который мог бы делать это для каждого атрибута. Однако этот дескриптор удобен для мониторинга лишь нескольких выбранных атрибутов:9596```python97class RevealAccess(object):98 """Дескриптор данных, который обычным образом устанавливает и возвращает значения, а также выводит сообщение, регистрируя их доступ.99 обычным образом и выводит сообщение, регистрируя их доступ.100 """101102 def __init__(self, initval=None, name='var'):103 self.val = initval104 self.name = name105106 def __get__(self, obj, objtype):107 print('Retrieving', self.name)108 return self.val109110 def __set__(self, obj, val):111 print('Updating', self.name)112 self.val = val113114>>> class MyClass(object):115 x = RevealAccess(10, 'var "x"')116 y = 5117118>>> m = MyClass()119>>> m.x120Retrieving var "x"12110122>>> m.x = 20123Updating var "x"124>>> m.x125Retrieving var "x"12620127>>> m.y1285129```130131Протокол прост и открывает захватывающие возможности. Некоторые сценарии использования настолько распространены, что были упакованы в отдельные вызовы функций. Свойства (properties), связанные (bound) и несвязанные (unbound) методы, статические методы и методы класса – все основаны на протоколе дескрипторов.132133## [Свойства](https://python-all.ru/3.2/howto/descriptor.html#id7)134135Вызов [`property()`](https://python-all.ru/3.2/library/functions.html#property) – это краткий способ создания дескриптора данных, который запускает вызовы функций при доступе к атрибуту. Его сигнатура:136137```python138property(fget=None, fset=None, fdel=None, doc=None) -> property attribute139```140141В документации показан типичный пример использования для определения управляемого атрибута `x`:142143```python144class C(object):145 def getx(self): return self.__x146 def setx(self, value): self.__x = value147 def delx(self): del self.__x148 x = property(getx, setx, delx, "I'm the 'x' property.")149```150151Чтобы увидеть, как [`property()`](https://python-all.ru/3.2/library/functions.html#property) реализован на основе протокола дескрипторов, вот эквивалент на чистом Python:152153```python154class Property(object):155 "Emulate PyProperty_Type() in Objects/descrobject.c"156157 def __init__(self, fget=None, fset=None, fdel=None, doc=None):158 self.fget = fget159 self.fset = fset160 self.fdel = fdel161 self.__doc__ = doc162163 def __get__(self, obj, objtype=None):164 if obj is None:165 return self166 if self.fget is None:167 raise AttributeError("unreadable attribute")168 return self.fget(obj)169170 def __set__(self, obj, value):171 if self.fset is None:172 raise AttributeError("can't set attribute")173 self.fset(obj, value)174175 def __delete__(self, obj):176 if self.fdel is None:177 raise AttributeError("can't delete attribute")178 self.fdel(obj)179```180181Встроенная функция [`property()`](https://python-all.ru/3.2/library/functions.html#property) полезна всякий раз, когда пользовательский интерфейс предоставил доступ к атрибуту, а последующие изменения потребовали вмешательства метода.182183Например, класс электронной таблицы может предоставлять доступ к значению ячейки через `Cell('b10').value`. Последующие улучшения программы требуют пересчитывать ячейку при каждом доступе; однако программист не хочет затрагивать существующий клиентский код, обращающийся к атрибуту напрямую. Решение – обернуть доступ к атрибуту value в дескриптор данных property:184185```python186class Cell(object):187 . . .188 def getvalue(self, obj):189 "Recalculate cell before returning value"190 self.recalc()191 return obj._value192 value = property(getvalue)193```194195## [Функции и методы](https://python-all.ru/3.2/howto/descriptor.html#id8)196197Объектно-ориентированные возможности Python построены на функциональной основе. С помощью дескрипторов, не являющихся дескрипторами данных, эти две парадигмы объединяются бесшовно.198199Словари классов хранят методы как функции. В определении класса методы записываются с помощью [`def`](https://python-all.ru/3.2/reference/compound_stmts.html#def) и [`lambda`](https://python-all.ru/3.2/reference/expressions.html#lambda) – обычных инструментов для создания функций. Единственное отличие от обычных функций в том, что первый аргумент зарезервирован для экземпляра объекта. По соглашению Python ссылка на экземпляр называется *self*, но может называться *this* или любым другим именем переменной.200201Для поддержки вызовов методов функции включают метод [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__) для привязки методов при доступе к атрибуту. Это означает, что все функции являются дескрипторами не-данных, которые возвращают связанные или несвязанные методы в зависимости от того, вызываются ли они от объекта или от класса. На чистом Python это работает так:202203```python204class Function(object):205 . . .206 def __get__(self, obj, objtype=None):207 "Simulate func_descr_get() in Objects/funcobject.c"208 return types.MethodType(self, obj, objtype)209```210211Запуск интерпретатора показывает, как дескриптор функции работает на практике:212213```python214>>> class D(object):215 def f(self, x):216 return x217218>>> d = D()219>>> D.__dict__['f'] # Внутренне хранится как функция220<function f at 0x00C45070>221>>> D.f # При получении из класса становится несвязанным методом222<unbound method D.f>223>>> d.f # При получении из экземпляра становится связанным методом224<bound method D.f of <__main__.D object at 0x00B18C90>>225```226227Вывод показывает, что связанные и несвязанные методы – это два разных типа. Хотя их можно было бы так реализовать, реальная реализация на C [`PyMethod_Type`](https://python-all.ru/3.2/c-api/method.html#PyMethod_Type) в файле [Objects/classobject.c](https://python-all.ru/3.2/howto/descriptor.html) представляет собой один объект с двумя разными представлениями в зависимости от того, установлено ли поле `im_self` или равно *NULL* (эквивалент *None* на C).228229Аналогично, результат вызова объекта метода зависит от поля `im_self`. Если оно установлено (то есть метод связан), исходная функция (хранящаяся в поле `im_func`) вызывается как обычно, с первым аргументом, равным экземпляру. Если метод не связан, все аргументы передаются исходной функции без изменений. Реальная C-реализация `instancemethod_call()` лишь немного сложнее, так как включает некоторую проверку типов.230231## [Статические методы и методы класса](https://python-all.ru/3.2/howto/descriptor.html#id9)232233Дескрипторы, не являющиеся дескрипторами данных, предоставляют простой механизм для вариаций обычных шаблонов привязки функций к методам.234235Итак, функции имеют метод [`__get__()`](https://python-all.ru/3.2/reference/datamodel.html#object.__get__), чтобы их можно было преобразовать в метод при доступе в качестве атрибута. Дескриптор не-данных преобразует вызов `obj.f(*args)` в `f(obj, *args)`. Вызов `klass.f(*args)` становится вызовом `f(*args)`.236237Эта таблица обобщает привязку и два наиболее полезных её варианта:238239> | Преобразование | Вызов из объекта | Вызов из класса |240> | --- | --- | --- |241> | функция | f(obj, \*args) | f(\*args) |242> | staticmethod | f(\*args) | f(\*args) |243> | classmethod | f(type(obj), \*args) | f(klass, \*args) |244245Статические методы возвращают базовую функцию без изменений. Вызов `c.f` или `C.f` эквивалентен прямому поиску в `object.__getattribute__(c, "f")` или `object.__getattribute__(C, "f")`. В результате функция становится одинаково доступной как из объекта, так и из класса.246247Хорошими кандидатами для статических методов являются методы, не ссылающиеся на переменную `self`.248249Например, пакет статистики может содержать класс-контейнер для экспериментальных данных. Класс предоставляет обычные методы для вычисления среднего, среднего арифметического, медианы и других описательных статистик, которые зависят от данных. Однако, могут быть полезные функции, которые концептуально связаны, но не зависят от данных. Например, `erf(x)` – это удобная функция преобразования, которая встречается в статистической работе, но напрямую не зависит от конкретного набора данных. Её можно вызвать как из объекта, так и из класса: `s.erf(1.5) --> .9332` или `Sample.erf(1.5) --> .9332`.250251Поскольку статические методы возвращают базовую функцию без изменений, примеры вызовов неинтересны:252253```python254>>> class E(object):255 def f(x):256 print(x)257 f = staticmethod(f)258259>>> print(E.f(3))2603261>>> print(E().f(3))2623263```264265С использованием протокола дескриптора не данных, чистая Python-версия [`staticmethod()`](https://python-all.ru/3.2/library/functions.html#staticmethod) выглядела бы так:266267```python268class StaticMethod(object):269 "Emulate PyStaticMethod_Type() in Objects/funcobject.c"270271 def __init__(self, f):272 self.f = f273274 def __get__(self, obj, objtype=None):275 return self.f276```277278В отличие от статических методов, методы класса перед вызовом функции добавляют ссылку на класс в начало списка аргументов. Этот формат одинаков как для вызова от объекта, так и от класса:279280```python281>>> class E(object):282 def f(klass, x):283 return klass.__name__, x284 f = classmethod(f)285286>>> print(E.f(3))287('E', 3)288>>> print(E().f(3))289('E', 3)290```291292Такое поведение полезно, когда функции нужна только ссылка на класс и ей не важны какие-либо базовые данные. Одно из применений classmethod – создание альтернативных конструкторов класса. В Python 2.3 classmethod [`dict.fromkeys()`](https://python-all.ru/3.2/library/stdtypes.html#dict.fromkeys) создаёт новый словарь из списка ключей. Чистая реализация на Python выглядит так:293294```python295class Dict:296 . . .297 def fromkeys(klass, iterable, value=None):298 "Emulate dict_fromkeys() in Objects/dictobject.c"299 d = klass()300 for key in iterable:301 d[key] = value302 return d303 fromkeys = classmethod(fromkeys)304```305306Теперь новый словарь уникальных ключей можно создать так:307308```python309>>> Dict.fromkeys('abracadabra')310{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}311```312313С использованием протокола дескриптора не данных, чистая Python-версия [`classmethod()`](https://python-all.ru/3.2/library/functions.html#classmethod) выглядела бы так:314315```python316class ClassMethod(object):317 "Emulate PyClassMethod_Type() in Objects/funcobject.c"318319 def __init__(self, f):320 self.f = f321322 def __get__(self, obj, klass=None):323 if klass is None:324 klass = type(obj)325 def newfunc(*args):326 return self.f(klass, *args)327 return newfunc328```329