Шаг 138.
Python: сборник рецептов.
Классы и объекты. Создание нового типа атрибута класса или экземпляра

    На этом шаге мы рассмотрим особенности создания такого атрибута.

Задача

    Вы хотите создать новый тип атрибута экземпляра с некой дополнительной функциональностью - например, с проверкой типа.

Решение

    Если вы хотите создать полностью новый тип атрибута экземпляра, определите его функциональность в форме класса-дескриптора. Вот пример:

>>> # Дескриптор атрибута для целочисленного атрибута с проверкой типа
>>> class Integer:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance (value, int):
            raise TypeError('Expected an int')
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]

    Дескриптор - это класс, который реализует три ключевые операции доступа к атрибутам (получение, присваивания значения и удаления) в форме специальных методов __get ()__, __set__() и __delete__(). Эти методы работают путем получения экземпляра на вход. Затем производятся манипуляции над словарем экземпляра.

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

>>> class Point:
    x = Integer('x')
    y = Integer('y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

    Когда вы это делаете, все попытки доступа к атрибуту дескриптора (то есть x или у) перехватываются методами __get()__, __set__() и __delete__(). Например:

>>> p = Point(2, 3)
>>> p.x # Вызывает Point.x.__get__(p,Point)
2
>>> p.y = 5 # Вызывает Point.y.__set__(p, 5)
>>> p.x = 2.3 # Вызывает Point.x.__set__(p, 2.3)
Traceback (most recent call last):
  File "<pyshell#70>", line 1, in <module>
    p.x = 2.3 # Вызывает Point.x.__set__(p, 2.3)
  File "<pyshell#65>", line 13, in __set__
    raise TypeError('Expected an int')
TypeError: Expected an int
>>> 

    На вход каждый метод дескриптора получает экземпляр, над которым нужно провести какие-то манипуляции. Чтобы провести запрошенную операцию, соответствующим образом меняется словарь экземпляра (атрибут __dict__). Атрибут дескриптора self.name содержит ключ словаря, который используется для хранения реальных данных в словаре экземпляра.

Обсуждение

    Дескрипторы являются основой "подкапотной" магии большинства возможностей классов Python, включая @classmethod, @staticmethod, @property и даже __slots__.

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

    Путаница с дескрипторами иногда возникает по причине того, что они могут быть определены только на уровне класса, но не на уровне экземпляра. Поэтому вот такой код работать не будет:

# Не работает
class Point:
    def __init__(self, x, y):
        self.x = Integer('x') # Нет! Должна быть переменной класса
        self.y = Integer('y')
        self.x = x
        self.y = y

    А реализация метода __get__() сложнее, чем может показаться:

# Дескриптор атрибута для целочисленного атрибута с проверкой типа
class Integer:
    .  .  .
    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]
    .  .  .

    Причина того, что get () выглядит довольно сложным, заключается в различии между переменными экземпляра и переменными класса. Если доступ к дескриптору осуществляется как к переменной класса, аргумент instance имеет значение None. В этом случае стандартным подходом будет просто вернуть сам экземпляр дескриптора (хотя разрешается также любой тип нестандартной обработки). Например:

>>> p = Point(2, 3)
>>> p.x	# Вызывает Point.x.__get__(p, Point)
2
>>> Point.x # Вызывает Point.x.__get__(None, Point)
<__main__.Integer object at 0x000001C5E92CFEB0>
>>> 

    Дескрипторы часто являются одним из компонентов крупного фреймворка, использующего декораторы или метаклассы. В этом случае их использование может быть практически незаметно. В качестве примера приведем более продвинутый код, основанный на дескрипторах, использующий декоратор класса:

# Дескриптор для атрибута с проверкой типа
class Typed:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError('Expected ' + str(self.expected_type))
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]

# Декоратор класса, который применяет его к выбранным атрибутам
def typeassert(**kwargs):
    def decorate(cls):
        for name, expected_type in kwargs.items():
            # Attach a Typed descriptor to the class
            setattr(cls, name, Typed(name, expected_type))
        return cls
    return decorate

# Пример использования
@typeassert(name=str, shares=int, price=float)
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

if __name__ == '__main__':
    s = Stock('ACME', 100, 490.1)
    print(s.name, s.shares, s.price)
    s.shares = 50
    try:
        s.shares = 'a lot'
    except TypeError as e:
        print(e)
Архив с файлом можно взять здесь.

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

    На следующем шаге мы рассмотрим использование лениво вычисляемых свойств.




Предыдущий шаг Содержание Следующий шаг