> **Источник:** https://python-all.ru/3.3/howto/descriptor.html
>
> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.

---

# [Руководство по дескрипторам](https://python-all.ru/3.3/howto/descriptor.html#id1)

| Автор: | Рэймонд Хеттингер |
| --- | --- |
| Контакт: | \<python at rcn dot com\> |

Содержание

- [Руководство по дескрипторам](https://python-all.ru/3.3/howto/descriptor.html#descriptor-howto-guide)

  - [Аннотация](https://python-all.ru/3.3/howto/descriptor.html#abstract)
  - [Определение и введение](https://python-all.ru/3.3/howto/descriptor.html#definition-and-introduction)
  - [Протокол дескрипторов](https://python-all.ru/3.3/howto/descriptor.html#descriptor-protocol)
  - [Вызов дескрипторов](https://python-all.ru/3.3/howto/descriptor.html#invoking-descriptors)
  - [Пример дескриптора](https://python-all.ru/3.3/howto/descriptor.html#descriptor-example)
  - [Свойства](https://python-all.ru/3.3/howto/descriptor.html#properties)
  - [Функции и методы](https://python-all.ru/3.3/howto/descriptor.html#functions-and-methods)
  - [Статические методы и методы класса](https://python-all.ru/3.3/howto/descriptor.html#static-methods-and-class-methods)

## [Аннотация](https://python-all.ru/3.3/howto/descriptor.html#id2)

Определяет дескрипторы, обобщает протокол и показывает, как вызываются дескрипторы. Рассматривает пользовательский дескриптор и несколько встроенных дескрипторов Python, включая функции, свойства, статические методы и методы класса. Показывает, как каждый из них работает, приводя эквивалент на чистом Python и пример приложения.

Изучение дескрипторов не только даёт доступ к более широкому набору инструментов, но и позволяет глубже понять, как работает Python, и оценить элегантность его устройства.

## [Определение и введение](https://python-all.ru/3.3/howto/descriptor.html#id3)

В общем случае дескриптор – это атрибут объекта с «поведением привязки», доступ к которому переопределён методами протокола дескриптора. Такими методами являются [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__), [`__set__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__set__) и [`__delete__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__delete__). Если для объекта определён хотя бы один из этих методов, говорят, что он является дескриптором.

Поведение по умолчанию при доступе к атрибуту – получить, установить или удалить атрибут из словаря объекта. Например, `a.x` проходит цепочку поиска, начиная с `a.__dict__['x']`, затем `type(a).__dict__['x']` и далее по базовым классам `type(a)`, исключая метаклассы. Если найденное значение – это объект, определяющий один из методов дескриптора, Python может переопределить поведение по умолчанию и вызвать вместо него метод дескриптора. Где именно это происходит в цепочке приоритетов, зависит от того, какие методы дескриптора были определены.

Дескрипторы – это мощный универсальный протокол. Они лежат в основе свойств, методов, статических методов, методов класса и [`super()`](https://python-all.ru/3.3/library/functions.html#super). Они используются в самом Python для реализации классов нового стиля, появившихся в версии 2.2. Дескрипторы упрощают внутренний C-код и предоставляют гибкий набор новых инструментов для повседневных программ на Python.

## [Протокол дескриптора](https://python-all.ru/3.3/howto/descriptor.html#id4)

`descr.__get__(self, obj, type=None) --> значение`

`descr.__set__(self, obj, value) --> None`

`descr.__delete__(self, obj) --> None`

Вот и всё. Определите любой из этих методов – и объект считается дескриптором и может переопределять поведение по умолчанию при поиске в качестве атрибута.

Если объект определяет и [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__), и [`__set__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__set__), он считается дескриптором данных. Дескрипторы, определяющие только [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__), называются дескрипторами не-данных (обычно они используются для методов, но возможны и другие применения).

Дескрипторы данных и не-данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора не-данных, приоритет имеет запись словаря.

Чтобы сделать дескриптор данных доступным только для чтения, определите и [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__), и [`__set__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__set__), причём [`__set__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__set__) должен возбуждать [`AttributeError`](https://python-all.ru/3.3/library/exceptions.html#AttributeError) при вызове. Определения [`__set__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__set__) с заглушкой, возбуждающей исключение, достаточно, чтобы сделать его дескриптором данных.

## [Вызов дескрипторов](https://python-all.ru/3.3/howto/descriptor.html#id5)

Дескриптор можно вызвать напрямую по имени метода. Например, `d.__get__(obj)`.

Но чаще дескриптор вызывается автоматически при доступе к атрибуту. Например, `obj.d` ищет `d` в словаре `obj`. Если `d` определяет метод [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__), то вызывается `d.__get__(obj)` в соответствии с правилами приоритета, приведёнными ниже.

Подробности вызова зависят от того, является ли `obj` объектом или классом.

Для объектов механизм находится в [`object.__getattribute__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattribute__), который преобразует `b.x` в `type(b).__dict__['x'].__get__(b, type(b))`. Реализация работает по цепочке приоритетов, где дескрипторы данных имеют приоритет над переменными экземпляра, переменные экземпляра – над дескрипторами не-данных, а самый низкий приоритет у [`__getattr__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattr__), если он определён. Полную реализацию на C можно найти в [`PyObject_GenericGetAttr()`](https://python-all.ru/3.3/c-api/object.html#PyObject_GenericGetAttr) в файле [Objects/object.c](https://python-all.ru/3.3/howto/descriptor.html).

Для классов механизм находится в `type.__getattribute__()`, который преобразует `B.x` в `B.__dict__['x'].__get__(None, B)`. На чистом Python это выглядит так:

```python
def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
       return v.__get__(None, self)
    return v
```

Важно запомнить следующее:

- дескрипторы вызываются методом [`__getattribute__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattribute__)
- переопределение [`__getattribute__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattribute__) предотвращает автоматический вызов дескрипторов
- [`object.__getattribute__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattribute__) и `type.__getattribute__()` по-разному вызывают [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__).
- дескрипторы данных всегда переопределяют словари экземпляров.
- дескрипторы без данных могут быть переопределены словарями экземпляров.

Объект, возвращаемый `super()`, также имеет собственный метод [`__getattribute__()`](https://python-all.ru/3.3/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.3/reference/datamodel.html#object.__getattribute__).

Подробности реализации находятся в `super_getattro()` в файле [Objects/typeobject.c](https://python-all.ru/3.3/howto/descriptor.html), а эквивалент на чистом Python можно найти в [руководстве Гвидо](https://python-all.ru/3.3/howto/descriptor.html).

Из изложенного видно, что механизм дескрипторов встроен в методы [`__getattribute__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattribute__) для [`object`](https://python-all.ru/3.3/library/functions.html#object), [`type`](https://python-all.ru/3.3/library/functions.html#type) и [`super()`](https://python-all.ru/3.3/library/functions.html#super). Классы наследуют этот механизм, если они производны от [`object`](https://python-all.ru/3.3/library/functions.html#object) или имеют метакласс, предоставляющий аналогичную функциональность. Классы также могут отключить вызов дескрипторов, переопределив [`__getattribute__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattribute__).

## [Пример дескриптора](https://python-all.ru/3.3/howto/descriptor.html#id6)

Следующий код создаёт класс, объекты которого являются дескрипторами данных, выводящими сообщение при каждом get или set. Переопределение [`__getattribute__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__getattribute__) – это альтернативный подход, который мог бы делать это для каждого атрибута. Однако этот дескриптор удобен для мониторинга лишь нескольких выбранных атрибутов:

```python
class RevealAccess(object):
    """Дескриптор данных, который обычным образом устанавливает и возвращает значения, а также выводит сообщение, регистрируя их доступ.
       обычным образом и выводит сообщение, регистрируя их доступ.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print('Retrieving', self.name)
        return self.val

    def __set__(self, obj, val):
        print('Updating', self.name)
        self.val = val

>>> class MyClass(object):
    x = RevealAccess(10, 'var "x"')
    y = 5

>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
```

Протокол прост и открывает захватывающие возможности. Некоторые сценарии использования настолько распространены, что были упакованы в отдельные вызовы функций. Свойства (properties), связанные (bound) и несвязанные (unbound) методы, статические методы и методы класса – все основаны на протоколе дескрипторов.

## [Свойства](https://python-all.ru/3.3/howto/descriptor.html#id7)

Вызов [`property()`](https://python-all.ru/3.3/library/functions.html#property) – это краткий способ создания дескриптора данных, который запускает вызовы функций при доступе к атрибуту. Его сигнатура:

```python
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
```

В документации показан типичный пример использования для определения управляемого атрибута `x`:

```python
class C(object):
    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()`](https://python-all.ru/3.3/library/functions.html#property) реализован на основе протокола дескрипторов, вот эквивалент на чистом Python:

```python
class Property(object):
    "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

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)
```

Встроенная функция [`property()`](https://python-all.ru/3.3/library/functions.html#property) полезна всякий раз, когда пользовательский интерфейс предоставил доступ к атрибуту, а последующие изменения потребовали вмешательства метода.

Например, класс электронной таблицы может предоставлять доступ к значению ячейки через `Cell('b10').value`. Последующие улучшения программы требуют пересчитывать ячейку при каждом доступе; однако программист не хочет затрагивать существующий клиентский код, обращающийся к атрибуту напрямую. Решение – обернуть доступ к атрибуту value в дескриптор данных property:

```python
class Cell(object):
    . . .
    def getvalue(self, obj):
        "Recalculate cell before returning value"
        self.recalc()
        return obj._value
    value = property(getvalue)
```

## [Функции и методы](https://python-all.ru/3.3/howto/descriptor.html#id8)

Объектно-ориентированные возможности Python построены на функциональной основе. С помощью дескрипторов, не являющихся дескрипторами данных, эти две парадигмы объединяются бесшовно.

Словари классов хранят методы как функции. В определении класса методы записываются с помощью [`def`](https://python-all.ru/3.3/reference/compound_stmts.html#def) и [`lambda`](https://python-all.ru/3.3/reference/expressions.html#lambda) – обычных инструментов для создания функций. Единственное отличие от обычных функций в том, что первый аргумент зарезервирован для экземпляра объекта. По соглашению Python ссылка на экземпляр называется *self*, но может называться *this* или любым другим именем переменной.

Для поддержки вызовов методов функции включают метод [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__) для привязки методов при доступе к атрибуту. Это означает, что все функции являются дескрипторами не-данных, которые возвращают связанные или несвязанные методы в зависимости от того, вызываются ли они от объекта или от класса. На чистом Python это работает так:

```python
class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        return types.MethodType(self, obj, objtype)
```

Запуск интерпретатора показывает, как дескриптор функции работает на практике:

```python
>>> class D(object):
     def f(self, x):
          return x

>>> d = D()
>>> D.__dict__['f'] # Внутренне хранится как функция
<function f at 0x00C45070>
>>> D.f             # При получении из класса становится несвязанным методом
<unbound method D.f>
>>> d.f             # При получении из экземпляра становится связанным методом
<bound method D.f of <__main__.D object at 0x00B18C90>>
```

Вывод показывает, что связанные и несвязанные методы – это два разных типа. Хотя их можно было бы так реализовать, реальная реализация на C [`PyMethod_Type`](https://python-all.ru/3.3/c-api/method.html#PyMethod_Type) в файле [Objects/classobject.c](https://python-all.ru/3.3/howto/descriptor.html) представляет собой один объект с двумя разными представлениями в зависимости от того, установлено ли поле `im_self` или равно *NULL* (эквивалент *None* на C).

Аналогично, результат вызова объекта метода зависит от поля `im_self`. Если оно установлено (то есть метод связан), исходная функция (хранящаяся в поле `im_func`) вызывается как обычно, с первым аргументом, равным экземпляру. Если метод не связан, все аргументы передаются исходной функции без изменений. Реальная C-реализация `instancemethod_call()` лишь немного сложнее, так как включает некоторую проверку типов.

## [Статические методы и методы класса](https://python-all.ru/3.3/howto/descriptor.html#id9)

Дескрипторы, не являющиеся дескрипторами данных, предоставляют простой механизм для вариаций обычных шаблонов привязки функций к методам.

Итак, функции имеют метод [`__get__()`](https://python-all.ru/3.3/reference/datamodel.html#object.__get__), чтобы их можно было преобразовать в метод при доступе в качестве атрибута. Дескриптор не-данных преобразует вызов `obj.f(*args)` в `f(obj, *args)`. Вызов `klass.f(*args)` становится вызовом `f(*args)`.

Эта таблица обобщает привязку и два наиболее полезных её варианта:

> | Преобразование | Вызов из объекта | Вызов из класса |
> | --- | --- | --- |
> | функция | f(obj, \*args) | f(\*args) |
> | staticmethod | f(\*args) | f(\*args) |
> | classmethod | f(type(obj), \*args) | f(klass, \*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`.

Поскольку статические методы возвращают базовую функцию без изменений, примеры вызовов неинтересны:

```python
>>> class E(object):
     def f(x):
          print(x)
     f = staticmethod(f)

>>> print(E.f(3))
3
>>> print(E().f(3))
3
```

С использованием протокола дескриптора не данных, чистая Python-версия [`staticmethod()`](https://python-all.ru/3.3/library/functions.html#staticmethod) выглядела бы так:

```python
class StaticMethod(object):
 "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

 def __init__(self, f):
      self.f = f

 def __get__(self, obj, objtype=None):
      return self.f
```

В отличие от статических методов, методы класса перед вызовом функции добавляют ссылку на класс в начало списка аргументов. Этот формат одинаков как для вызова от объекта, так и от класса:

```python
>>> class E(object):
     def f(klass, x):
          return klass.__name__, x
     f = classmethod(f)

>>> print(E.f(3))
('E', 3)
>>> print(E().f(3))
('E', 3)
```

Такое поведение полезно, когда функции нужна только ссылка на класс и ей не важны какие-либо базовые данные. Одно из применений classmethod – создание альтернативных конструкторов класса. В Python 2.3 classmethod [`dict.fromkeys()`](https://python-all.ru/3.3/library/stdtypes.html#dict.fromkeys) создаёт новый словарь из списка ключей. Чистая реализация на Python выглядит так:

```python
class Dict(object):
    . . .
    def fromkeys(klass, iterable, value=None):
        "Emulate dict_fromkeys() in Objects/dictobject.c"
        d = klass()
        for key in iterable:
            d[key] = value
        return d
    fromkeys = classmethod(fromkeys)
```

Теперь новый словарь уникальных ключей можно создать так:

```python
>>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}
```

С использованием протокола дескриптора не данных, чистая Python-версия [`classmethod()`](https://python-all.ru/3.3/library/functions.html#classmethod) выглядела бы так:

```python
class ClassMethod(object):
     "Emulate PyClassMethod_Type() in Objects/funcobject.c"

     def __init__(self, f):
          self.f = f

     def __get__(self, obj, klass=None):
          if klass is None:
               klass = type(obj)
          def newfunc(*args):
               return self.f(klass, *args)
          return newfunc
```
