Что это за магия?

Когда мы считываем данные с объекта, неважно, сколько раз вы их считываете, пока вы не измените данные, результат останется тем же. Что если мы хотим, чтобы считанные данные менялись со временем или с другими данными в объекте?

Использование декораторов @property для создания свойств

Но Python предоставляет волшебный механизм, который позволяет вам использовать тот же синтаксис, что и для чтения данных из объектов, называемый объектными методами, и поскольку он фактически называется объектными методами, вы можете генерировать возвращаемое значение посредством вычислений и использовать его так же, как и при чтении данных из объектов, но прочитанное значение будет Считанное значение изменится. Этот механизм называется свойством и может быть реализован с помощью декораторов, таких как @property.

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

>>> time.time()
1646532639.2636657
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

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

>>> class C:
...     def __init__(self):
...         self.start = time.time()
...     @property
...     def age(self):
...         return int(time.time() - self.start)
...     @age.setter
...     def age(self, new_age):
...         self.start = time.time() - new_age
...
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Есть несколько моментов, которые следует отметить в этой категории.

  • В __init__() записывается время создания объекта, а затем на основе этого момента времени может быть рассчитано время жизни объекта.

Я знаю, что у вас могут возникнуть некоторые вопросы, но давайте рассмотрим, как использовать этот класс.

>>> c = C()
>>> c.age
6
>>> c.age
9
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

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

>>> c.age = 0
>>> c.age
1
Войдите в полноэкранный режим Выход из полноэкранного режима

Это действительно можно сделать с помощью описательной части присвоения, как и с обычными данными в объекте, и новое время жизни рассчитывается после установки.

Хотя все работает правильно, мы остаемся в недоумении, что за магия, @property, была применена? Почему два метода с одинаковыми именами, записанные в классе, работают правильно?

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

>>> C.__dict__['age']
<property object at 0x0000018CD4B64040>
>>> vars(C)['age']
<property object at 0x0000018CD4B64040>
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

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

Использование дескрипторов для создания свойств

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

Ниже приведен пример создания дескриптора, начиная с чтения данных. Дескриптор — это не определенный класс объекта, а объект с определенным методом, и для того, чтобы дескриптор мог читать данные, он должен иметь метод __get__().

>>> class Age:
...     def __get__(self, obj, objType=None):
...         return int(time.time() - obj.start)
...
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Когда вызывается __get__(), obj — это присоединенный объект, а objType — класс, к которому принадлежит объект, и obj может использоваться для доступа к данным внутри присоединенного объекта. Доступ к данным внутри присоединенного объекта можно получить через obj, в данном случае путем чтения start для расчета времени выживания.

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

>>> class D:
...     age = Age()
...     def __init__(self):
...         self.start = time.time()
...
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

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

>>> d = D()
>>> d.age
3
>>> d.age
4
>>> d.age
6
Войдите в полноэкранный режим Выход из полноэкранного режима

Когда . Когда оператор обнаруживает, что age является дескриптором с __get_(), вместо того, чтобы рассматривать дескриптор как результат операции, он вызывает дескриптор __get__() и использует его возвращаемое значение как результат операции, в данном случае В этом случае выполняется __get__() класса Age, возвращая время жизни объекта.

Чтобы дескриптор можно было использовать для задания данных, он должен иметь метод __set__(), который будет вызван автоматически, как только в описании задания будет обнаружен предмет задания — дескриптор. Например

>>> class Age:
...     def __get__(self, obj, objType=None):
...         return int(time.time() - obj.start)
...     def __set__(self, obj, value):
...         obj.start = time.time() - value
...
Войдите в полноэкранный режим Выход из полноэкранного режима

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

>>> class D:
...     age = Age()
...     def __init__(self):
...         self.start = time.time()
...
Войдите в полноэкранный режим Выход из полноэкранного режима

В дополнение к чтению age, теперь можно установить age, и фактический доступ осуществляется методом внутри дескриптора

>>> d = D()
>>> d.age
2
>>> d.age
9
>>> d.age = 0
>>> d.age
2
>>> d.age
3
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Таким образом, мы создали те же свойства, которые только что реализовали с помощью декоратора @property.

Важно отметить, что если вы реализуете свойство только для чтения, вы должны добавить __set__() к дескриптору и вызвать исключение AttributeError в нем, иначе, если вы присвоите его, оно не будет рассматриваться как дескриптор, потому что нет __set__(). метод не рассматривается как дескриптор и рассматривается как обычные данные, дескриптор будет удален. Например

>>> class Age:
...     def __get__(self, obj, objType):
...         return int(time.time() - obj.start)
...
>>> class D:
...     age = Age()
...     def __init__(self):
...         self.start = time.time()
...
>>> d = D()
>>> d.age
5
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Поскольку Age имеет __get__(), он рассматривается как дескриптор при чтении, но Age не имеет __set__() и не рассматривается как дескриптор при присвоении, он становится новым элементом объекта Данные с именем age.

>>> d.__dict__
{'start': 1646556621.0307684}
>>> d.age = 0
>>> d.__dict__
{'start': 1646556621.0307684, 'age': 0}
>>> d.age
0
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Видно, что до присвоения, поскольку age является классом, он не появляется в словаре объекта d. Однако после присвоения элемент age появляется в словаре объекта d, и отныне доступ к age считывается из age в объекте d, а не из D Объект age в классе D больше не читается, поэтому, сколько бы раз он ни был прочитан, он получает 0, который был только что присвоен, и оригинальный дескриптор не работает. Однако это касается только объектов d, и если будет создан другой объект класса D, он все равно будет работать следующим образом

>>> d1 = D()
>>> d1.age
7
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Ниже приведен правильный способ использования свойств только для чтения.

>>> class Age:
...     def __get__(self, obj, objType):
...         return int(time.time() - obj.start)
...     def __set__(self, obj, value):
...         raise AttributeError("read only.")
...
>>> class D:
...     age = Age()
...     def __init__(self):
...         self.start = time.time()
...
>>> d = D()
>>> d.age
3
>>> d.age = 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __set__
AttributeError: read only.
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Как только вы попытаетесь присвоить новое значение свойству, доступному только для чтения, это вызовет исключение, чтобы избежать только что описанной проблемы.

Использование объектов свойств в качестве дескрипторов

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

>>> class E:
...     def __init__(self):
...         self.start = time.time()
...     def getAge(self):
...         return int(time.time() - self.start)
...     def setAge(self, new_age):
...         self.start = time.time() - new_age
...     age = property(getAge, setAge)
...
Войдите в полноэкранный режим Выход из полноэкранного режима

property Первые два параметра в методе construct являются методами, отвечающими за чтение и запись соответственно, и результат выглядит следующим образом

>>> e = E()
>>> e.age
2
>>> e.age
3
>>> e.age
5
>>> e.age = 0
>>> e.age
2
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Это точно так же, как и предыдущая реализация с использованием самого дескриптора.

Класс property предоставляет getter() и setter(), которые могут быть установлены индивидуально в __get__() и __set__(). методы, которые будут вызываться, поэтому вы также можете сегментировать свою работу, например, так.

>>> class E:
...     def __init__(self):
...         self.start = time.time()
...     def getAge(self):
...         return int(time.time() - self.start)
...     def setAge(self, new_age):
...         self.start = time.time() - new_age
...     age = property(getAge)
...     age = age.setter(setAge)
...
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

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

>>> e = E()
>>> e.age
2
>>> e.age
3
>>> e.age
4
>>> e.age = 0
>>> e.age
1
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

@property = декоратор + дескриптор

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

>>> class F:
...     def __init__(self):
...         self.start = time.time()
...     def age(self):
...         return int(time.time() - self.start)
...     age = property(age)
...     age_setter = age.setter
...     def age(self, new_age):
...         self.start = time.time() - new_age
...     age = age_setter(age)
...
Войдите в полноэкранный режим Выход из полноэкранного режима

Вы можете видеть, что оба столбца обернуты в age() и передаются обратно с тем же именем age: age.

age = property(age)
...
age = age_setter(age)
Войдите в полноэкранный режим Выход из полноэкранного режима

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

>>> class G:
...     def __init__(self):
...         self.start = time.time()
...     @property
...     def age(self):
...         return int(time.time() - self.start)
...     @age.setter
...     def age(self, new_age):
...         self.start = time.time() - new_age
...
>>>
Войдите в полноэкранный режим Выход из полноэкранного режима

Если вы вернетесь к примеру в начале статьи, то увидите, что это одна и та же программа. На данном этапе мы поняли, как работают атрибуты в Python.

Заключение

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

Наконец, вы также можете увидеть примеры так называемых конвенций Python вместо правил, таких как __get__(), который представляет собой специальный метод с двумя подчеркиваниями впереди и позади него, в основном для того, чтобы система вызывала его автоматически в определенное время, и обычно с механизмом, который нам не нужно вызывать непосредственно в наших собственных программах. Кроме того, хотя по соглашению имена классов пишутся с большой буквы, например, property полностью строчная, потому что property в основном используется в декораторах, а не для создания отдельных объектов. Знание этого поможет вам быстро понять, когда вы встречаете то или иное имя, чтобы не сломать внутренний механизм.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *