descriptor.md
1> **Источник:** https://python-all.ru/3.8/howto/descriptor.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# [Руководство по дескрипторам](https://python-all.ru/3.8/howto/descriptor.html#id1)89**Автор**1011Рэймонд Хеттингер1213**Контакт**1415\<python at rcn dot com\>1617Содержание1819- [Руководство по дескрипторам](https://python-all.ru/3.8/howto/descriptor.html#descriptor-howto-guide)2021 - [Аннотация](https://python-all.ru/3.8/howto/descriptor.html#abstract)22 - [Определение и введение](https://python-all.ru/3.8/howto/descriptor.html#definition-and-introduction)23 - [Протокол дескрипторов](https://python-all.ru/3.8/howto/descriptor.html#descriptor-protocol)24 - [Вызов дескрипторов](https://python-all.ru/3.8/howto/descriptor.html#invoking-descriptors)25 - [Пример дескриптора](https://python-all.ru/3.8/howto/descriptor.html#descriptor-example)26 - [Свойства](https://python-all.ru/3.8/howto/descriptor.html#properties)27 - [Функции и методы](https://python-all.ru/3.8/howto/descriptor.html#functions-and-methods)28 - [Статические методы и методы класса](https://python-all.ru/3.8/howto/descriptor.html#static-methods-and-class-methods)2930## [Аннотация](https://python-all.ru/3.8/howto/descriptor.html#id2)3132Определяет дескрипторы, описывает протокол и показывает, как вызываются дескрипторы. Рассматривается пользовательский дескриптор и несколько встроенных дескрипторов Python, включая функции, свойства, статические методы и методы класса. Показывается, как каждый из них работает, с помощью эквивалента на чистом Python и примера применения.3334Изучение дескрипторов не только даёт доступ к более широкому набору инструментов, но и позволяет глубже понять, как работает Python, и оценить элегантность его устройства.3536## [Определение и введение](https://python-all.ru/3.8/howto/descriptor.html#id3)3738В общем случае дескриптор – это атрибут объекта с «поведением связывания», доступ к которому переопределён методами протокола дескриптора. Такими методами являются [`__get__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__get__), [`__set__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__set__) и [`__delete__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__delete__). Если для объекта определён хотя бы один из этих методов, он считается дескриптором.3940Поведение по умолчанию при доступе к атрибуту – получение, установка или удаление атрибута из словаря объекта. Например, `a.x` имеет цепочку поиска, начинающуюся с `a.__dict__['x']`, затем `type(a).__dict__['x']`, и далее по базовым классам `type(a)`, исключая метаклассы. Если найденное значение является объектом, определяющим один из методов дескриптора, то Python может переопределить поведение по умолчанию и вместо этого вызвать метод дескриптора. Место в цепочке приоритетов зависит от того, какие методы дескриптора были определены.4142Дескрипторы – это мощный универсальный протокол. Они лежат в основе свойств, методов, статических методов, методов класса и [`super()`](https://python-all.ru/3.8/library/functions.html#super). Они используются во всём Python для реализации классов нового стиля, появившихся в версии 2.2. Дескрипторы упрощают внутренний C-код и предоставляют гибкий набор новых инструментов для повседневных программ на Python.4344## [Протокол дескриптора](https://python-all.ru/3.8/howto/descriptor.html#id4)4546`descr.__get__(self, obj, type=None) -> value`4748`descr.__set__(self, obj, value) -> None`4950`descr.__delete__(self, obj) -> None`5152Вот и всё. Определите любой из этих методов – и объект считается дескриптором и может переопределять поведение по умолчанию при поиске в качестве атрибута.5354Если объект определяет [`__set__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__set__) или [`__delete__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__delete__), он считается дескриптором данных. Дескрипторы, определяющие только [`__get__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__get__), называются дескрипторами без данных (обычно они используются для методов, но возможны и другие применения).5556Дескрипторы данных и не-данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора не-данных, приоритет имеет запись словаря.5758Чтобы создать дескриптор данных только для чтения, определите и [`__get__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__get__), и [`__set__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__set__), причём [`__set__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__set__) должен возбуждать [`AttributeError`](https://python-all.ru/3.8/library/exceptions.html#AttributeError) при вызове. Определения метода [`__set__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__set__) с заглушкой, возбуждающей исключение, достаточно, чтобы сделать его дескриптором данных.5960## [Вызов дескрипторов](https://python-all.ru/3.8/howto/descriptor.html#id5)6162Дескриптор можно вызвать напрямую по имени его метода. Например, `d.__get__(obj)`.6364Чаще, однако, дескриптор вызывается автоматически при доступе к атрибуту. Например, `obj.d` ищет `d` в словаре `obj`. Если `d` определяет метод [`__get__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__get__), то `d.__get__(obj)` вызывается в соответствии с правилами приоритета, приведёнными ниже.6566Детали вызова зависят от того, является ли `obj` объектом или классом.6768Для объектов механизм находится в [`object.__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__), который преобразует `b.x` в `type(b).__dict__['x'].__get__(b, type(b))`. Реализация работает через цепочку приоритетов, где дескрипторы данных имеют приоритет над переменными экземпляра, переменные экземпляра – над дескрипторами без данных, а самый низкий приоритет присваивается [`__getattr__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattr__), если он предоставлен. Полную реализацию на C можно найти в [`PyObject_GenericGetAttr()`](https://python-all.ru/3.8/c-api/object.html#c.PyObject_GenericGetAttr) в [Objects/object.c](https://python-all.ru/src/3.8/Objects/object.c).6970Для классов механизм находится в `type.__getattribute__()`, который преобразует `B.x` в `B.__dict__['x'].__get__(None, B)`. На чистом Python это выглядит так:7172```python73def __getattribute__(self, key):74 "Emulate type_getattro() in Objects/typeobject.c"75 v = object.__getattribute__(self, key)76 if hasattr(v, '__get__'):77 return v.__get__(None, self)78 return v79```8081Важно запомнить следующее:8283- дескрипторы вызываются методом [`__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__)84- переопределение [`__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__) предотвращает автоматический вызов дескрипторов85- [`object.__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__) и `type.__getattribute__()` выполняют разные вызовы [`__get__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__get__).86- дескрипторы данных всегда переопределяют словари экземпляров.87- дескрипторы без данных могут быть переопределены словарями экземпляров.8889Объект, возвращаемый `super()`, также имеет пользовательский метод [`__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__) для вызова дескрипторов. Поиск атрибута `super(B, obj).m` ищет в `obj.__class__.__mro__` базовый класс `A`, следующий сразу за `B`, и возвращает `A.__dict__['m'].__get__(obj, B)`. Если это не дескриптор, `m` возвращается без изменений. Если атрибут не найден в словаре, `m` обращается к поиску с помощью [`object.__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__).9091Детали реализации находятся в `super_getattro()` в [Objects/typeobject.c](https://python-all.ru/src/3.8/Objects/typeobject.c). Эквивалент на чистом Python можно найти в [руководстве Гвидо](https://python-all.ru/3.8/howto/descriptor.html).9293Приведённые выше детали показывают, что механизм дескрипторов встроен в методы [`__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__) для [`object`](https://python-all.ru/3.8/library/functions.html#object), [`type`](https://python-all.ru/3.8/library/functions.html#type) и [`super()`](https://python-all.ru/3.8/library/functions.html#super). Классы наследуют этот механизм, если они являются подклассами [`object`](https://python-all.ru/3.8/library/functions.html#object) или имеют метакласс, предоставляющий аналогичную функциональность. Классы также могут отключать вызов дескрипторов, переопределяя [`__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__).9495## [Пример дескриптора](https://python-all.ru/3.8/howto/descriptor.html#id6)9697Следующий код создаёт класс, объекты которого являются дескрипторами данных, выводящими сообщение при каждом получении или установке значения. Переопределение [`__getattribute__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__getattribute__) – это альтернативный подход, который мог бы делать это для каждого атрибута. Однако этот дескриптор удобен для отслеживания лишь нескольких выбранных атрибутов:9899```python100class RevealAccess(object):101 """Дескриптор данных, который обычным образом устанавливает и возвращает значения, а также выводит сообщение, регистрируя их доступ.102 обычным образом и выводит сообщение, регистрируя их доступ.103 """104105 def __init__(self, initval=None, name='var'):106 self.val = initval107 self.name = name108109 def __get__(self, obj, objtype):110 print('Retrieving', self.name)111 return self.val112113 def __set__(self, obj, val):114 print('Updating', self.name)115 self.val = val116117>>> class MyClass(object):118... x = RevealAccess(10, 'var "x"')119... y = 5120...121>>> m = MyClass()122>>> m.x123Retrieving var "x"12410125>>> m.x = 20126Updating var "x"127>>> m.x128Retrieving var "x"12920130>>> m.y1315132```133134Протокол прост и открывает интересные возможности. Некоторые варианты использования настолько распространены, что были оформлены в виде отдельных функций. Свойства, связанные методы, статические методы и методы класса – все они основаны на протоколе дескриптора.135136## [Свойства](https://python-all.ru/3.8/howto/descriptor.html#id7)137138Вызов [`property()`](https://python-all.ru/3.8/library/functions.html#property) – это краткий способ создания дескриптора данных, который вызывает функции при доступе к атрибуту. Его сигнатура:139140```python141property(fget=None, fset=None, fdel=None, doc=None) -> property attribute142```143144В документации показан типичный пример использования для определения управляемого атрибута `x`:145146```python147class C(object):148 def getx(self): return self.__x149 def setx(self, value): self.__x = value150 def delx(self): del self.__x151 x = property(getx, setx, delx, "I'm the 'x' property.")152```153154Чтобы увидеть, как [`property()`](https://python-all.ru/3.8/library/functions.html#property) реализуется через протокол дескрипторов, вот эквивалент на чистом Python:155156```python157class Property(object):158 "Emulate PyProperty_Type() in Objects/descrobject.c"159160 def __init__(self, fget=None, fset=None, fdel=None, doc=None):161 self.fget = fget162 self.fset = fset163 self.fdel = fdel164 if doc is None and fget is not None:165 doc = fget.__doc__166 self.__doc__ = doc167168 def __get__(self, obj, objtype=None):169 if obj is None:170 return self171 if self.fget is None:172 raise AttributeError("unreadable attribute")173 return self.fget(obj)174175 def __set__(self, obj, value):176 if self.fset is None:177 raise AttributeError("can't set attribute")178 self.fset(obj, value)179180 def __delete__(self, obj):181 if self.fdel is None:182 raise AttributeError("can't delete attribute")183 self.fdel(obj)184185 def getter(self, fget):186 return type(self)(fget, self.fset, self.fdel, self.__doc__)187188 def setter(self, fset):189 return type(self)(self.fget, fset, self.fdel, self.__doc__)190191 def deleter(self, fdel):192 return type(self)(self.fget, self.fset, fdel, self.__doc__)193```194195Встроенная функция [`property()`](https://python-all.ru/3.8/library/functions.html#property) помогает, когда пользовательский интерфейс предоставил доступ к атрибуту, а последующие изменения требуют вмешательства метода.196197Например, класс электронной таблицы может предоставлять доступ к значению ячейки через `Cell('b10').value`. Последующие улучшения программы требуют пересчёта ячейки при каждом доступе; однако программист не хочет влиять на существующий клиентский код, обращающийся к атрибуту напрямую. Решение – обернуть доступ к атрибуту значения в дескриптор данных свойства:198199```python200class Cell(object):201 . . .202 def getvalue(self):203 "Recalculate the cell before returning value"204 self.recalc()205 return self._value206 value = property(getvalue)207```208209## [Функции и методы](https://python-all.ru/3.8/howto/descriptor.html#id8)210211Объектно-ориентированные возможности Python построены на функциональной основе. С помощью дескрипторов, не являющихся дескрипторами данных, эти две парадигмы объединяются бесшовно.212213Словари классов хранят методы как функции. В определении класса методы записываются с помощью [`def`](https://python-all.ru/3.8/reference/compound_stmts.html#def) или [`lambda`](https://python-all.ru/3.8/reference/expressions.html#lambda) – обычных средств создания функций. Методы отличаются от обычных функций только тем, что первый аргумент зарезервирован для экземпляра объекта. По соглашению Python ссылка на экземпляр называется *self*, но может называться *this* или любым другим именем переменной.214215Для поддержки вызова методов функции включают метод [`__get__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__get__), связывающий методы при доступе к атрибуту. Это означает, что все функции являются дескрипторами без данных, которые возвращают связанные методы при вызове от объекта. На чистом Python это работает так:216217```python218class Function(object):219 . . .220 def __get__(self, obj, objtype=None):221 "Simulate func_descr_get() in Objects/funcobject.c"222 if obj is None:223 return self224 return types.MethodType(self, obj)225```226227Запуск интерпретатора показывает, как дескриптор функции работает на практике:228229```python230>>> class D(object):231... def f(self, x):232... return x233...234>>> d = D()235236# Access through the class dictionary does not invoke __get__.237# It just returns the underlying function object.238>>> D.__dict__['f']239<function D.f at 0x00C45070>240241# Dotted access from a class calls __get__() which just returns242# the underlying function unchanged.243>>> D.f244<function D.f at 0x00C45070>245246# The function has a __qualname__ attribute to support introspection247>>> D.f.__qualname__248'D.f'249250# Dotted access from an instance calls __get__() which returns the251# function wrapped in a bound method object252>>> d.f253<bound method D.f of <__main__.D object at 0x00B18C90>>254255# Internally, the bound method stores the underlying function and256# the bound instance.257>>> d.f.__func__258<function D.f at 0x1012e5ae8>259>>> d.f.__self__260<__main__.D object at 0x1012e1f98>261```262263## [Статические методы и методы класса](https://python-all.ru/3.8/howto/descriptor.html#id9)264265Дескрипторы, не являющиеся дескрипторами данных, предоставляют простой механизм для вариаций обычных шаблонов привязки функций к методам.266267Повторим: функции имеют метод [`__get__()`](https://python-all.ru/3.8/reference/datamodel.html#object.__get__), чтобы их можно было преобразовать в метод при доступе как атрибуты. Дескриптор, не являющийся дескриптором данных, преобразует вызов `obj.f(*args)` в `f(obj, *args)`. Вызов `klass.f(*args)` становится `f(*args)`.268269Эта таблица обобщает привязку и два наиболее полезных её варианта:270271> | Преобразование | Вызов из объекта | Вызов из класса |272> | --- | --- | --- |273> | функция | f(obj, \*args) | f(\*args) |274> | staticmethod | f(\*args) | f(\*args) |275> | classmethod | f(type(obj), \*args) | f(klass, \*args) |276277Статические методы возвращают базовую функцию без изменений. Вызов `c.f` или `C.f` эквивалентен прямому поиску в `object.__getattribute__(c, "f")` или `object.__getattribute__(C, "f")`. В результате функция становится одинаково доступной как из объекта, так и из класса.278279Хорошими кандидатами для статических методов являются те, которые не ссылаются на переменную `self`.280281Например, в пакете статистики может быть класс-контейнер для экспериментальных данных. Класс предоставляет обычные методы для вычисления среднего, медианы и других описательных статистик, которые зависят от данных. Однако могут быть полезные функции, которые концептуально связаны, но не зависят от данных. Например, `erf(x)` – это удобная процедура преобразования, которая встречается в статистической работе, но не зависит напрямую от конкретного набора данных. Её можно вызвать как от объекта, так и от класса: `s.erf(1.5) --> .9332` или `Sample.erf(1.5) --> .9332`.282283Поскольку статические методы возвращают базовую функцию без изменений, примеры вызовов неинтересны:284285```python286>>> class E(object):287... def f(x):288... print(x)289... f = staticmethod(f)290...291>>> E.f(3)2923293>>> E().f(3)2943295```296297Используя протокол дескриптора без данных, чистая версия [`staticmethod()`](https://python-all.ru/3.8/library/functions.html#staticmethod) на Python выглядела бы так:298299```python300class StaticMethod(object):301 "Emulate PyStaticMethod_Type() in Objects/funcobject.c"302303 def __init__(self, f):304 self.f = f305306 def __get__(self, obj, objtype=None):307 return self.f308```309310В отличие от статических методов, методы класса перед вызовом функции добавляют ссылку на класс в начало списка аргументов. Этот формат одинаков как для вызова от объекта, так и от класса:311312```python313>>> class E(object):314... def f(klass, x):315... return klass.__name__, x316... f = classmethod(f)317...318>>> print(E.f(3))319('E', 3)320>>> print(E().f(3))321('E', 3)322```323324Такое поведение полезно, когда функции нужна только ссылка на класс и не важны данные экземпляра. Одно из применений classmethod – создание альтернативных конструкторов класса. В Python 2.3 classmethod [`dict.fromkeys()`](https://python-all.ru/3.8/library/stdtypes.html#dict.fromkeys) создаёт новый словарь из списка ключей. Чистый эквивалент на Python выглядит так:325326```python327class Dict(object):328 . . .329 def fromkeys(klass, iterable, value=None):330 "Emulate dict_fromkeys() in Objects/dictobject.c"331 d = klass()332 for key in iterable:333 d[key] = value334 return d335 fromkeys = classmethod(fromkeys)336```337338Теперь новый словарь уникальных ключей можно создать так:339340```python341>>> Dict.fromkeys('abracadabra')342{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}343```344345Используя протокол дескриптора без данных, чистая версия [`classmethod()`](https://python-all.ru/3.8/library/functions.html#classmethod) на Python выглядела бы так:346347```python348class ClassMethod(object):349 "Emulate PyClassMethod_Type() in Objects/funcobject.c"350351 def __init__(self, f):352 self.f = f353354 def __get__(self, obj, klass=None):355 if klass is None:356 klass = type(obj)357 def newfunc(*args):358 return self.f(klass, *args)359 return newfunc360```361