Вопрос-Ответ

How does the @property decorator work in Python?

Как работает декоратор @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 также в абстрактных классах; Я привожу минимальный пример здесь.

python