Когда мы считываем данные с объекта, неважно, сколько раз вы их считываете, пока вы не измените данные, результат останется тем же. Что если мы хотим, чтобы считанные данные менялись со временем или с другими данными в объекте?
Использование декораторов @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
в основном используется в декораторах, а не для создания отдельных объектов. Знание этого поможет вам быстро понять, когда вы встречаете то или иное имя, чтобы не сломать внутренний механизм.