Python中的@property修饰器是如何工作的?

1349

我希望了解内置函数 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 的参数是 getxsetxdelx 和文档字符串。

在下面的代码中,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.setterx.deleter修饰符是如何创建的?


29
另请参见:Python 属性是如何工作的? - Martin Thoma
8
property实际上是一个类(不是函数),当你创建一个对象时,它可能会调用__init__()方法。从终端使用help(property)可以获得深入了解。令人惊讶的是,help也是一个类。 - Brōtsyorfuzthrāx
4
我认为这个链接提供了一个很好的例子:[property] (https://www.journaldev.com/14893/python-property-decorator) - Sheng Bi
9
@Shule 两年前的帖子,但仍然适用:一切都是类。甚至类本身也是类。 - Artemis
5
我也感到困惑。最终我找到了一篇文章,能够为我分解清楚。希望能对其他人有所帮助。 https://www.programiz.com/python-programming/property 我与该网站没有任何关联。 - jjwdesign
显示剩余4条评论
15个回答

1280
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().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` 方法,它返回一个新的属性副本,但这次将 setter 函数替换为装饰的方法。 以下序列也通过使用这些装饰器方法创建了一个完整的属性。 首先,我们创建一些函数:
>>> def getter(self): print('Get!')
... 
>>> def setter(self, value): print('Set to {!r}!'.format(value))
... 
>>> def deleter(self): print('Delete!')
... 
然后,我们创建一个只有getter方法的property对象:
>>> prop = property(getter)
>>> prop.fget is getter
True
>>> prop.fset is None
True
>>> prop.fdel is None
True
接下来,我们使用.setter()方法来添加一个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!
The Descriptor 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__)

16
很好。您可以补充一下这样一个事实,即在执行 Foo.prop = prop 后,您可以使用 Foo().prop = 5; print Foo().prop; del Foo().prop 实现所需的结果。 - glglgl
16
方法对象是即时创建的,如果可用,它们可以重复使用相同的内存位置。 - Martijn Pieters
4
@MarkusMeskanen:我更喜欢使用type(),因为访问双下划线属性和方法是由标准函数和运算符用作扩展点。 - Martijn Pieters
5
因为这个对象是不可变的,如果你在原地改变它,你就不能将它专门化为一个子类。 - Martijn Pieters
12
@MarkusMeskanen:请参考Python overriding getter without setter;如果@human.name.getter改变了property对象而不是返回一个新的对象,那么human.name属性将被更改,从而改变超类的行为。 - Martijn Pieters
显示剩余6条评论

360

文档说它只是创建只读属性的快捷方式。所以

@property
def x(self):
    return self._x

等同于

def getx(self):
    return self._x
x = property(getx)

79
全文背景(最赞的答案)很好,但这个回答对于弄清别人为什么在他们的类中使用@property作为装饰器是非常有用的。 - ijoseph
7
"…创建只读属性的快捷方式。" 百万美元的答案! - Saptadeep
它不会创建只读属性,而是创建一个“标准”的getter方法。这个语句仍然会按预期工作:obj.x = 5。 - Federico Razzoli
4
@FedericoRazzoli:obj._x = 5 可以工作。obj.x = 5 不行(除非你在使用不完全支持描述符的旧式类的 Python 2),因为没有定义 setter。属性本身是只读的(除非你尝试在类本身上修改它,而不是类的实例),只是 Python 没有明显支持只读 属性(你最好可以子类化 tuple 并使用 tuple 的存储值,使用命名属性提供更友好的访问;这就是 collections.namedtuple/typing.NamedTuple 为你做的事情)。 - ShadowRanger
我最近没有尝试过。但是,很有可能我正在使用Python 2。 - Federico Razzoli

179

以下是如何实现@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'

2
如果需要在__init__中定义word()函数/属性,那么这个例子会是什么样子? - J.J
20
为什么我要在这里创建一个属性装饰器,而不是只有 self.word = my_word,这样做的效果就和 print( Thing('ok').word ) = 'ok' 一样了? - Silver
2
@SilverSlash 这只是一个简单的例子,真正的用例会涉及到更复杂的方法。 - Alex
2
请问您能否解释一下,如何在运行时内部调用打印 Thing('ok').word 函数? - Vicrobot
@property 隐藏了它是一个函数的事实?如果没有它,你只需要用括号来调用它? - Michael
https://dev59.com/BFUK5IYBdhLWcg3wwSLg#51342825 解释了为什么变量如 '_word' 以下划线开头。 - Tom J

115
以下是另一个示例说明如何使用@property帮助重构代码的方法,这个示例来自于这里(我仅在下面进行了总结):https://powerfulpython.com/blog/python-properties-refactoring/ 假设你创建了一个名为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类,并摆脱dollarscents属性,而是决定仅跟踪美分的总金额:

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))

会导致错误

属性错误:'Money'对象没有'dollars'属性

这意味着现在所有依赖您原始的Money类的人都必须更改使用了dollarscents的所有代码行,这可能非常痛苦...那么,如何避免这种情况?通过使用@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,我在这里提供了一个最简示例。


6
您的总结非常好,该网站提供的例子有点奇怪。。一个初学者可能会问..为什么我们不能只使用self.dollar = dollars?我们已经在@property方面做了很多努力,但似乎没有增加任何额外的功能。 - Sheng Bi
6
不要过于关注实际示例,而是更加关注底层原则:如果由于任何原因你必须重构代码,你可以这样做而不影响其他人的代码。 - Cleb
4
@cleb你是真正的MVP。其他人都使用像这个https://www.programiz.com/python-programming/property的getter/setter示例。但是只有你能够实际解释为什么我们想要属性。因为当我们编写一些许多人会在其基础上进行构建的东西时,我们希望能够修改基类,而不会对后继者在实现上使用或构建我们的工作产生真正影响。 - Chen Lizi
2
@Cleb 我当然做到了! :) - Chen Lizi

96

第一部分很简单:

@property
def x(self): ...

等同于

def x(self): ...
x = property(x)
  • 这反过来是创建只有getter的property的简化语法。

下一步是使用适当的方法扩展此属性,包括设置器和删除器:

@x.setter
def x(self, value): ...

返回一个新属性,该属性从旧的x继承所有内容,并添加指定的setter。

x.deleter的工作方式相同。


67

以下内容:

class C(object):
    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

与以下相同:

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x

    def _x_set(self, value):
        self._x = value

    def _x_del(self):
        del self._x

    x = property(_x_get, _x_set, _x_del, 
                    "I'm the 'x' property.")

与之相同:

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x

    def _x_set(self, value):
        self._x = value

    def _x_del(self):
        del self._x

    x = property(_x_get, doc="I'm the 'x' property.")
    x = x.setter(_x_set)
    x = x.deleter(_x_del)

与以下表述等价:

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x
    x = property(_x_get, doc="I'm the 'x' property.")

    def _x_set(self, value):
        self._x = value
    x = x.setter(_x_set)

    def _x_del(self):
        del self._x
    x = x.deleter(_x_del)

这与以下内容相同:

class C(object):
    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

6
第一个和最后一个代码示例是完全相同的(逐字逐句)。 - Adomas Baliuka
4
我认为这是有意为之。总之,这对我来说是最有用的例子,因为我可以从这些例子中领会含义。感谢@Bill Moore。 - Jérémie

48

让我们从Python装饰器开始。

Python装饰器是一种函数,它帮助在已定义的函数中添加一些附加功能。

在Python中,一切皆为对象。Python中的函数是一等公民对象,这意味着它们可以由变量引用,在列表中添加,作为参数传递给另一个函数等。

考虑以下代码片段。

def decorator_func(fun):
    def wrapper_func():
        print("Wrapper function started")
        fun()
        print("Given function decorated")
        # Wrapper function add something to the passed function and decorator 
        # returns the wrapper function
    return wrapper_func

def say_bye():
    print("bye!!")

say_bye = decorator_func(say_bye)
say_bye()

# Output:
#  Wrapper function started
#  bye!!
#  Given function decorated
 

在这里,我们可以说装饰器函数修改了我们的say_bye函数,并向其中添加了一些额外的代码行。

Python中的装饰器语法

def decorator_func(fun):
    def wrapper_func():
        print("Wrapper function started")
        fun()
        print("Given function decorated")
        # Wrapper function add something to the passed function and decorator 
        # returns the wrapper function
    return wrapper_func

@decorator_func
def say_bye():
    print("bye!!")

say_bye()

让我们通过一个案例来说明一切。但在此之前,让我们谈谈一些面向对象编程的原则。

Getter和Setter在许多面向对象编程语言中被用来确保数据封装的原则(这被视为将数据与操作这些数据的方法捆绑在一起的做法)。

当然,这些方法就是获取数据的getter和更改数据的setter。

根据这个原则,类的属性被设为私有来隐藏并保护它们免受其他代码的侵犯。

没错,@property基本上是使用getter和setter的“Python风格”。

Python有一个很棒的概念,称为property,使得面向对象程序员的生活变得更加简单。

假设您决定创建一个可以存储摄氏度温度的类。

class Celsius:
    def __init__(self, temperature = 0):
        self.set_temperature(temperature)

    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32

    def get_temperature(self):
        return self._temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        self._temperature = value

重构代码,以下是我们如何使用“property”实现它的方法。

在Python中,property()是一个内置函数,用于创建并返回一个属性对象。

属性对象有三个方法:getter()、setter()和delete()。

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    def get_temperature(self):
        print("Getting value")
        return self.temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self.temperature = value

temperature = property(get_temperature,set_temperature)

在这里,

temperature = property(get_temperature,set_temperature)

可以拆分为以下内容:

# make empty property
temperature = property()
# assign fget
temperature = temperature.getter(get_temperature)
# assign fset
temperature = temperature.setter(set_temperature)

注意事项:

  • get_temperature仍然是一个属性而不是方法。

现在,您可以通过编写代码来访问温度的值。

C = Celsius()
C.temperature
# instead of writing C.get_temperature()
我们可以进一步简化代码,不定义名称get_temperatureset_temperature,因为它们是不必要的,会污染类命名空间。 处理上述问题的Pythonic方式是使用@property
class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    @property
    def temperature(self):
        print("Getting value")
        return self.temperature

    @temperature.setter
    def temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self.temperature = value

注意事项 -

  1. 用于获取值的方法使用"@property"进行装饰。
  2. 必须使用"@temperature.setter"来装饰作为setter的方法,如果函数名为"x",则必须使用"@x.setter"进行装饰。
  3. 我们编写了两个名称相同但参数数量不同的方法,"def temperature(self)"和"def temperature(self,x)"。

可以看出,代码显然不够优雅。

现在,让我们谈谈一个真实的实际场景。

假设您设计了一个如下所示的类:

class OurClass:

    def __init__(self, a):
        self.x = a


y = OurClass(10)
print(y.x)

现在,让我们进一步假设我们的类在客户中间很受欢迎,并且他们开始在他们的程序中使用它,对对象进行各种任务分配。

有一天,一个值得信赖的客户来找我们建议,“x”必须是0到1000之间的值;这真是一个可怕的情况!

由于属性的存在,这很容易:我们创建一个“x”的属性版本。

class OurClass:

    def __init__(self,x):
        self.x = x

    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 1000:
            self.__x = 1000
        else:
            self.__x = x

这太棒了,不是吗:你可以从最简单的实现开始,而且以后可以自由地迁移到属性版本,而无需更改接口!因此,属性不仅仅是getter和setter的替代品!

您可以在此处检查此实现。


7
当设置温度时(也就是在实例化时),你的 Celsius 类将会无限递归。 - Ted Petrou
1
人们在问它是如何工作的,而不是为什么它能工作?@ShengBi - Divyanshu Rawat
1
这只是一条评论,是我的个人观点。你的答案可能非常好,所以不要在意。 - Sheng Bi
2
相比于被投票赞成的答案,这个回答是为人类设计的;谢谢。 - Info5ek
2
你应该将获取和设置温度的方法从self.temperature更改为self._temperature,否则会出现递归运行的情况。 - Ang Yiwei
显示剩余2条评论

31

我阅读了这里的所有帖子,意识到我们可能需要一个真实的例子。为什么我们需要@property呢? 考虑一个使用身份验证系统的Flask应用程序。 您在models.py中声明一个User模型:

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))

    ...

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

在这段代码中,我们使用 @property 隐藏了属性 password ,如果您直接访问它,则会触发 AttributeError 断言,而我们使用 @property.setter 来设置实际的实例变量 password_hash

现在在 auth/views.py 中,我们可以使用以下方式实例化一个用户:

...
@auth.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        db.session.commit()
...

注意属性password,它来自用户填写表单时的注册表单。密码确认在前端进行,使用EqualTo('password', message='Passwords must match')(如果您想知道,但这是与Flask表单相关的不同主题)。

我希望这个例子会很有用。


21

很多人已经澄清了这一点,但这里有一个我正在寻找的直接观点。

这就是我认为从@property修饰符开始很重要的原因。

例如:

class UtilityMixin():
    @property
    def get_config(self):
        return "This is property"

函数"get_config()"的调用将按照以下方式工作。

util = UtilityMixin()
print(util.get_config)

如果您注意到我没有使用“()”括号来调用函数。这是我一直在寻找@property装饰器的基本功能。这样,您就可以像使用变量一样使用您的函数。


16

2
目前来看,这是最好的资源。 - thanos.a

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,