На этом шаге мы рассмотрим особенности создания такого атрибута.
Вы хотите создать новый тип атрибута экземпляра с некой дополнительной функциональностью - например, с проверкой типа.
Если вы хотите создать полностью новый тип атрибута экземпляра, определите его функциональность в форме класса-дескриптора. Вот пример:
>>> # Дескриптор атрибута для целочисленного атрибута с проверкой типа >>> 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 шаге. Дескрипторы более полезны в ситуациях, где предполагается многократно переиспользовать код (то есть если вы хотите использовать функциональность, предоставленную дескриптором, в сотнях мест в вашем коде или предоставить ее в качестве возможности библиотеки).
На следующем шаге мы рассмотрим использование лениво вычисляемых свойств.