Этот материал дает базовый обзор дескриптора класса, плавно переходя от простых примеров, добавляя по одной функции за раз. Начните здесь, если вы новичок в дескрипторах.
Смотрите также раздел "Как работают дескрипторы классов в Python".
Класс Ten
- это дескриптор, чей метод __get__()
всегда возвращает константу 10:
class Ten: """Класс дескриптора""" def __get__(self, obj, objtype=None): return 10
Чтобы использовать дескриптор, он должен храниться как переменная класса в другом классе:
class A: # Обычный атрибут класса x = 5 # Экземпляр дескриптора y = Ten()
Сеанс интерпретатора Python показывает разницу между обычным поиском атрибутов и поиском дескриптора:
# экземпляр класса A >>> a = A() # поиск атрибута >>> a.x # 5 # поиск дескриптора >>> a.y # 10
При поиске атрибута a.x
оператор точки находит 'x': 5
в словаре класса. При поиске a.y
оператор точки находит экземпляр дескриптора, который распознается по методу __get__
. Вызов этого метода возвращает 10.
Обратите внимание, что значение 10 не сохраняется ни в словаре класса, ни в словаре экземпляра. Значение 10 вычисляется по требованию.
Этот пример показывает, как работает простой дескриптор, но он не очень полезен. Для извлечения констант лучше использовать обычные атрибуты класса.
Полезные дескрипторы обычно выполняют вычисления, а не возвращают константы:
import os class DirectorySize: """Класс дескриптора""" def __get__(self, obj, objtype=None): return len(os.listdir(obj.dirname)) class Directory: # Экземпляр дескриптора size = DirectorySize() def __init__(self, dirname): # Обычный атрибут экземпляра self.dirname = dirname
Интерактивный сеанс интерпретатора показывает, что поиск является динамическим - он каждый раз вычисляет разные обновленные ответы:
>>> s = Directory('songs') >>> g = Directory('games') # папка `songs` содержит 20 файлов >>> s.size # 20 # папка `games` содержит 3 файла >>> g.size # 3 # например удалим файл `chess` из папки `games` >>> os.remove('games/chess') # кол-во файлов обновляется автоматически >>> g.size # 2
Помимо демонстрации того, как дескрипторы могут выполнять вычисления, этот пример показывает назначение аргументов для метода __get__()
:
self
- это экземпляр дескриптора DirectorySize
, то есть атрибут size
. obj
является либо экземпляром g
, либо s
, который позволяет методу __get__()
узнать целевой экземпляр класса, в данном случае это Directory
. objtype
- это класс Directory
.Популярным применением дескрипторов является управление доступом к данным экземпляра. Дескриптор назначается общедоступному атрибуту в словаре класса, в то время как фактические данные хранятся как частный атрибут в словаре экземпляра. Методы дескриптора __get__()
и __set__()
запускаются при доступе к общедоступному атрибуту.
В следующем примере age
является общедоступным атрибутом, а _age
- приватным атрибутом. При доступе к общедоступному атрибуту дескриптор регистрирует поиск или обновление:
import logging logging.basicConfig(level=logging.INFO) class LoggedAgeAccess: """Класс дескриптора""" def __get__(self, obj, objtype=None): value = obj._age logging.info('Доступ к %r дающий %r', 'age', value) return value def __set__(self, obj, value): logging.info('Обновление %r до %r', 'age', value) obj._age = value class Person: # Экземпляр дескриптора age = LoggedAgeAccess() def __init__(self, name, age): # Обычный атрибут экземпляра self.name = name # Вызов метода `__set__()` self.age = age def birthday(self): # Вызывает как `__get__()`, так и `__set__()` self.age += 1
Интерактивный сеанс показывает, что все обращения к возрасту управляемого атрибута регистрируются, но имя обычного атрибута не регистрируется:
# Записывается начальное обновление возраста >>> mary = Person('Mary M', 30) # INFO:root:Обновление 'age' до 30 >>> dave = Person('David D', 40) # INFO:root:Обновление 'age' до 40 # Фактические данные находятся в приватном атрибуте >>> vars(mary) # {'name': 'Mary M', '_age': 30} >>> vars(dave) # {'name': 'David D', '_age': 40} # Доступ к данным и регистрация поиска >>> mary.age # INFO:root:Доступ к 'age' дающий 30 # 30 # Обновления также регистрируются >>> mary.birthday() # INFO:root:Доступ к 'age' дающий 30 # INFO:root:Обновление 'age' до 31 # Обычный поиск атрибутов не регистрируется >>> dave.name # 'David D' # Регистрируется только управляемый атрибут >>> dave.age # INFO:root:Доступ 'age' дающий 40 # 40
Основная проблема, связанная с этим примером заключается в том, что приватное имя _age
жестко запрограммировано в классе дескриптора LoggedAgeAccess
. Это означает, что каждый экземпляр этого дескриптора может иметь только один зарегистрированный атрибут и что его имя остается неизменным. Другими словами, класс дескриптора LoggedAgeAccess
заточен только на работу с атрибутом obj._age
и не может, например хранить name
в приватной переменной obj._name
.
Когда класс использует дескрипторы, он может информировать каждый дескриптор о том, какое имя переменной было использовано.
В следующем примере класс Person
имеет два экземпляра дескриптора LoggedAccess
- это name
и age
. Когда класс Person
определен, он выполняет обратный вызов метода __set_name__()
, который определяется в классе LoggedAccess
, присваивая каждому дескриптору собственное имя public_name
и private_name
:
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('Доступ %r дающий %r', self.public_name, value) return value def __set__(self, obj, value): logging.info('Обновление %r до %r', self.public_name, value) setattr(obj, self.private_name, value) class Person: # Первый экземпляр дескриптора name = LoggedAccess() # Второй экземпляр дескриптора age = LoggedAccess() def __init__(self, name, age): # Вызывает первый дескриптор self.name = name # Вызывает второй дескриптор self.age = age def birthday(self): self.age += 1
Изменено в Python 3.12: исключения, возникающие в методе
__set_name__
класса или типа, больше не переносятся с помощьюRuntimeError
. Контекстная информация добавляется к исключению в виде примечания. (Предоставлено Ирит Катриэль.)
При использовании интерактивного сеанса видно, что класс Person
вызвал __set_name__()
для записи имен полей. Для поиска дескриптора, без его физического вызова, можно использовать встроенную функцию vars()
:
>>> 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:Обновление 'name' до 'Peter P' # INFO:root:Обновление 'age' до 10 >>> kate = Person('Catherine C', 20) # INFO:root:Обновление 'name' до 'Catherine C' # INFO:root:Обновление 'age' до 20
Два экземпляра Person
содержат только частные имена:
>>> vars(pete) # {'_name': 'Peter P', '_age': 10} >>> vars(kate) # {'_name': 'Catherine C', '_age': 20}
__get__()
, __set__()
или __delete__()
.__set_name__()
. Этот метод используется только в тех случаях, когда дескриптору необходимо знать либо класс, в котором он был создан, либо имя переменной класса, которой он был присвоен. (Этот метод, если он присутствует, вызывается, даже если класс не является дескриптором.)vars(some_class)[descriptor_name]
, экземпляр дескриптора возвращается без его фактического вызова.classmethod()
, staticmethod()
, property()
и functools.cached_property()
, реализованы как дескрипторы.