Source: Python descriptors

← Rendered

markdown source:

---
description: Дескрипторы в python
tags: python-standart-library python
title: Python descriptors
---
В python существует три варианта доступа к атрибуту:

1. Получить значение атрибута, var = obj.a
2. Изменить значение, obj.a = 'new value'
3. Удалить атрибут, del obj.a

Python позволяет перехватить доступ к атрибуту и переопределить связанное с этим доступом поведение. Это реализуется через механизм протокола дескрипторов.

Чаще всего это используется для проверки данных, передаваемых в атрибут.

Дескриптор - это любой объект, который определяет методы `__get__()`, `__set__()` или `__delete__()`.

Исплементация дескрипторов описана [тут](https://docs.python.org/3/reference/datamodel.html#implementing-descriptors)

Пример реализации:

```python
>>> class NonNegative:

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

...     def __get__(self, instance, owner):
...         return instance.__dict__[self.name]

...     def __set__(self, instance, value):
...         if value < 0:
...             raise ValueError('Cannot be negative.')
...         instance.__dict__[self.name] = value

>>> class Order:

...     price = NonNegative('price')
...     quantity = NonNegative('quantity')

...     def __init__(self, name, price, quantity):
...         self._name = name
...         self.price = price
...         self.quantity = quantity

...     def total(self):
...         return self.price * self.quantity

>>> apple_order = Order('apple', 1, 10)
>>> apple_order.total()
10
>>> apple_order.price = -10
ValueError: Cannot be negative
>>> apple_order.quantity = -10
ValueError: Cannot be negative
```

Пример взят из [этой статьи](https://webdevblog.ru/chto-takoe-deskriptory-i-ih-ispolzovanie-v-python-3-6/). В данном случае мы определили класс дескриптора и класс владельца, в котором класс дескриптора используется для инициализации атрибутов. При попытке установить неприемлемое значение в атрибут будет вызван `ValueError`. Такая реализация является типичной для до python 3.6.

В настоящий момент это можно реализовать короче (измененеия отмечены #):

```python
>>> class NonNegative:

...     def __get__(self, instance, owner):
...         return instance.__dict__[self.name] #

...     def __set__(self, instance, value):
...         if value < 0:
...             raise ValueError('Cannot be negative.')
...         instance.__dict__[self.name] = value

...     def __set_name__(self, owner, name): #
...         self.name = name #

>>> class Order:

...     price = NonNegative() #
...     quantity = NonNegative() #

...     def __init__(self, name, price, quantity):
...         self._name = name
...         self.price = price
...         self.quantity = quantity

...     def total(self):
...         return self.price * self.quantity
```

`object.__get__(self, instance, owner=None)` вызывается для получения атрибута класса-владельца (доступ к атрибуту класса) или экземпляра этого класса (доступ к атрибуту экземпляра). Необязательный аргумент `owner` - это класс владельца, в то время как `instance` - это экземпляр, через который был осуществлен доступ к атрибуту, или `None`, если доступ к атрибуту осуществляется через владельца. Этот метод должен возвращать вычисленное значение атрибута или вызывать исключение `AttributeError`. PEP 252 указывает, что `__get __()` вызывается с одним или двумя аргументами. Собственные встроенные дескрипторы python поддерживают эту спецификацию; однако вполне вероятно, что некоторые сторонние инструменты имеют дескрипторы, требующие обоих аргументов. Собственная реализация Python `__getattribute __()` всегда передает оба аргумента независимо от того, требуются они или нет.

`object.__set__(self, instance, value)` вызывается для установки в экземпляре `instance` класса-владельца нового значения атрибута `value`. Добавление `__set __()` или `__delete __()` изменяет тип дескриптора на «дескриптор данных».

`object.__delete__(self, instance)` вызывается для удаления атрибута в экземпляре `instance` класса владельца. Атрибут `__objclass__` интерпретируется модулем проверки как указывающий класс, в котором был определен этот объект (соответствующая установка этого параметра может помочь в самоанализе динамических атрибутов класса во время выполнения). Для вызываемых объектов это может указывать на то, что экземпляр данного типа (или подкласса) ожидается или требуется в качестве первого позиционного аргумента (например, CPython устанавливает этот атрибут для несвязанных методов, реализованных в C).

Данные методы применяются только тогда, когда экземпляр класса, содержащего метод (так называемый класс дескриптора), появляется в классе владельца (дескриптор должен находиться либо в словаре класса владельца, либо в словаре классов для одного из его родителей).

Про порядок вызовов методов дескриптора в разных чулаях [читай подробнее тут](https://docs.python.org/3/reference/datamodel.html#invoking-descriptors)

Методы python (в том числе `@staticmethod` и `@classmethod`) реализованы как non-data дескрипторы. Соответственно, экземпляры могут переопределять декорированные таким образом методы. Это позволяет отдельным экземплярам приобретать поведение, которое отличается от поведения других экземпляров того же класса.

Функция `property()` реализована как дескриптор данных. Соответственно, экземпляры не могут переопределить поведение property.

## [Descriptor HowTo](https://docs.python.org/3/howto/descriptor.html#descriptorhowto)

В статье документации приводистя большое число примеров реализации и обьъясняются особенности констирукции.

Например здесь класс `Person` имеет два экземпляра дескриптора, `name` и `age`. Когда класс `Person` определяется, он выполняет обратный вызов `__set_name__()` в `LoggedAccess`, чтобы можно было определить имена полей, давая каждому дескриптору собственное имя `public_name` и `private_name`:

```python
>>> 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

>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_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
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}
```

[Более близкий к практике пример](https://docs.python.org/3/howto/descriptor.html#complete-practical-example) - определен дескриптор как абстрактный класс, наследующие классы обязаны определять ряд методов. Их инстансы используются в классе-владельце.

Тут рассматривается [вызов дескрипторов.](https://docs.python.org/3/howto/descriptor.html#overview-of-descriptor-invocation):

- из инстанса
- из класса
- с помощью `super()`

Общие принципы:

- Механизм дескрипторов встроен в методы `__getattribute__()` для `object`, `type` и `super() `
- Дескрипторы вызываются методом `__getattribute__()`
- Классы наследуют этот механизм от `object`, `type` или `super()`
- Переопределение предотвращает автоматические вызовы дескриптора, потому что вся логика дескриптора находится в этом методе
- `object.__ getattribute__()` и `type.__getattribute__()` по-разному вызывают `__get__()`. Первый включает экземпляр и может включать класс. Второй определяет `None` для экземпляра и всегда включает класс
- Дескрипторы данных всегда имеют приоритет над словарями экземпляров
- Дескрипторы, не относящиеся к данным, могут быть переопределены словарями экземпляров

[Пример для ОРМ](https://docs.python.org/3/howto/descriptor.html#orm-example)

Далее описываются [встроенные конструкции python на дескрипторах](https://docs.python.org/3/howto/descriptor.html#pure-python-equivalents)

Properties, bound methods, static methods, class methods и `__slots__` основаны на протоколе дескриптора:

- [property](https://docs.python.org/3/howto/descriptor.html#properties) Вызов `property()` - это краткий способ создания дескриптора данных, который запускает вызов функции при доступе к атрибуту. Встроенная функция `property()` помогает всякий раз, когда пользовательский интерфейс предоставляет доступ к атрибутам, а затем для последующих изменений требуется вмешательство метода
- [функции и методы](https://docs.python.org/3/howto/descriptor.html#functions-and-methods) Используя дескрипторы, не относящиеся к данным, эти два элемента легко объединяются. Функции, хранящиеся в словарях классов, при вызове превращаются в методы. Методы отличаются от обычных функций только тем, что экземпляр объекта добавляется к другим аргументам. По соглашению экземпляр называется self, но может называться этим или любым другим именем переменной
- [static methods](https://docs.python.org/3/howto/descriptor.html#static-methods) Возвращают возвращают базовую функцию без изменений. Хорошими кандидатами на статические методы являются методы, которые не ссылаются на переменную self.
- [class methods](https://docs.python.org/3/howto/descriptor.html#class-methods) В отличие от статических методов, методы класса добавляют ссылку на класс к списку аргументов перед вызовом функции. Этот формат одинаков для экземпляра или класса. Это поведение полезно, когда методу нужна только ссылка на класс, и он не полагается на данные, хранящиеся в конкретном экземпляре. Одно из применений методов класса - создание альтернативных конструкторов класса

Кроме того, дескрипторы [реализуют](https://docs.python.org/3/howto/descriptor.html#member-objects-and-slots) логику `__clots__`

Когда класс определяет `__slots__`, он заменяет словари экземпляров массивом значений слотов фиксированной длины. С точки зрения пользователя, это имеет несколько эффектов:

Во-первых обеспечивает немедленное обнаружение ошибок из-за неправильного написания атрибутов, т.к. разрешены только имена атрибутов, указанные в `__slots__`

```python
>>> 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'
```

Во-вторых помогает создавать неизменяемые объекты, в которых дескрипторы управляют доступом к закрытым атрибутам, хранящимся в `__slots__`

```python
>>> 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: can't set attribute
>>> mark.location = 'Mars'
Traceback (most recent call last):
    ...
AttributeError: 'Immutable' object has no attribute 'location'
```

Кроме того, это `__slots__`:

- Экономит память. В 64-битной сборке Linux экземпляр с двумя атрибутами занимает 48 байтов с `__slots__` и 152 байта без. Этот шаблон проектирования легковесного проекта, вероятно, имеет значение только тогда, когда будет создано большое количество экземпляров.
- Повышает скорость. Чтение переменных экземпляра выполняется на 35% быстрее из `__slots__` (по измерениям с Python 3.10 на процессоре Apple M1)
- Но это блокирует такие инструменты, как `functools.cached_property()`, которым для правильной работы требуется словарь экземпляра

Смотри еще:

- [[python-standart-library]]
- [[python-datamodel]]
- [[sqlalchemy]]
- [[functools]]
- [[python-patterns]] паттерн strategy с дескриптором


[python-standart-library]: ../lists/python-standart-library "Стандартная библиотека python и полезные ресурсы"
[python-datamodel]: ../lists/python-datamodel "Python datamodel"
[sqlalchemy]: ../lists/sqlalchemy "Sqlalchemy"
[functools]: functools "Functools"
[python-patterns]: python-patterns "Python patterns programming"