Как работает декоратор @property в Python?
Я хотел бы понять, как работает встроенная функция property
. Что меня смущает, так это то, что property
также может использоваться как декоратор, но он принимает аргументы только при использовании в качестве встроенной функции, а не при использовании в качестве декоратора.
Этот пример взят из документации:
class C:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
property
аргументами являются getx
, setx
, delx
и строка документа.
В приведенном ниже коде property
используется в качестве декоратора. Его объектом является x
функция, но в приведенном выше коде в аргументах нет места объектной функции.
class C:
def __init__(self):
self._x = None
@property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
Как в этом случае создаются декораторы x.setter
и x.deleter
?
Переведено автоматически
Ответ 1
property()
Функция возвращает специальный объект-дескриптор:
>>> property()
<property object at 0x10ff07940>
Именно этот объект имеет дополнительные методы:
>>> property().getter
<built-in method getter of property object at 0x10ff07998>
>>> property().setter
<built-in method setter of property object at 0x10ff07940>
>>> property().deleter
<built-in method deleter of property object at 0x10ff07998>
Они тоже действуют как декораторы. Они возвращают новый объект property:
>>> property().getter(None)
<property object at 0x10ff079f0>
это копия старого объекта, но с заменой одной из функций.
Помните, что @decorator
синтаксис - это просто синтаксический сахар; синтаксис:
@property
def foo(self): return self._foo
на самом деле означает то же самое, что
def foo(self): return self._foo
foo = property(foo)
итак, foo
функция заменяется на property(foo)
, которая, как мы видели выше, является специальным объектом. Затем, когда вы используете @foo.setter()
, вы вызываете тот property().setter
метод, который я показал вам выше, который возвращает новую копию свойства, но на этот раз с заменой функции установки на оформленный метод.
Следующая последовательность действий также создает полноценное свойство с использованием этих методов декоратора.
Сначала мы создаем несколько функций:
>>> def getter(self): print('Get!')
...
>>> def setter(self, value): print('Set to {!r}!'.format(value))
...
>>> def deleter(self): print('Delete!')
...
Затем мы создаем property
объект, используя только метод получения:
>>> prop = property(getter)
>>> prop.fget is getter
True
>>> prop.fset is None
True
>>> prop.fdel is None
True
Далее мы используем метод .setter()
для добавления установщика:
>>> prop = prop.setter(setter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is None
True
Наконец, мы добавляем средство удаления с помощью .deleter()
метода:
>>> prop = prop.deleter(deleter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is deleter
True
И последнее, но не менее важное: property
объект действует как объект-дескриптор, поэтому у него есть .__get__()
, .__set__()
и .__delete__()
методы для подключения к получению, установке и удалению атрибута экземпляра:
>>> class Foo: pass
...
>>> prop.__get__(Foo(), Foo)
Get!
>>> prop.__set__(Foo(), 'bar')
Set to 'bar'!
>>> prop.__delete__(Foo())
Delete!
Дескриптор Howto включает в себя пример реализации на чистом Python типа property()
:
class Property:
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
Ответ 2
В документации говорится, что это просто ярлык для создания свойств, доступных только для чтения. Итак
@property
def x(self):
return self._x
эквивалентно
def getx(self):
return self._x
x = property(getx)
Ответ 3
Вот минимальный пример того, как @property
может быть реализован:
class Thing:
def __init__(self, my_word):
self._word = my_word
@property
def word(self):
return self._word
>>> print( Thing('ok').word )
'ok'
В противном случае word
остается методом, а не свойством.
class Thing:
def __init__(self, my_word):
self._word = my_word
def word(self):
return self._word
>>> print( Thing('ok').word() )
'ok'
Ответ 4
Ниже приведен еще один пример того, как @property
может помочь при необходимости рефакторинга кода, который взят из здесь (я только кратко изложу его ниже):
Представьте, что вы создали класс Money
, подобный этому:
class Money:
def __init__(self, dollars, cents):
self.dollars = dollars
self.cents = cents
и пользователь создает библиотеку в зависимости от этого класса, где он / она использует, например
money = Money(27, 12)
print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 27 dollar and 12 cents.
Теперь давайте предположим, что вы решили изменить свой Money
класс и избавиться от dollars
и cents
атрибутов, но вместо этого решили отслеживать только общее количество центов:
class Money:
def __init__(self, dollars, cents):
self.total_cents = dollars * 100 + cents
Если вышеупомянутый пользователь теперь пытается запустить свою библиотеку, как раньше
money = Money(27, 12)
print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
это приведет к ошибке
AttributeError: объект 'Money' не имеет атрибута 'dollars'
Это означает, что теперь каждому, кто полагается на ваш исходный Money
класс, придется изменять все строки кода, в которых используются dollars
и cents
, что может быть очень болезненным... Итак, как этого можно избежать? Используя @property
!
Вот как:
class Money:
def __init__(self, dollars, cents):
self.total_cents = dollars * 100 + cents
# Getter and setter for dollars...
@property
def dollars(self):
return self.total_cents // 100
@dollars.setter
def dollars(self, new_dollars):
self.total_cents = 100 * new_dollars + self.cents
# And the getter and setter for cents.
@property
def cents(self):
return self.total_cents % 100
@cents.setter
def cents(self, new_cents):
self.total_cents = 100 * self.dollars + new_cents
когда мы теперь вызываем из нашей библиотеки
money = Money(27, 12)
print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 27 dollar and 12 cents.
все будет работать так, как ожидалось, и нам не пришлось изменять ни одной строки кода в нашей библиотеке! Фактически, нам даже не нужно было бы знать, что изменилась библиотека, от которой мы зависим.
Также setter
работает нормально:
money.dollars += 2
print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 29 dollar and 12 cents.
money.cents += 10
print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 29 dollar and 22 cents.
Вы можете использовать @property
также в абстрактных классах; Я привожу минимальный пример здесь.