在Python中@属性装饰器是如何工作的?

我想了解内置函数property是如何工作的。让我困惑的是property也可以用作装饰器,但它只在用作内置函数时接受参数,而不是用作装饰器时。

这个例子来自留档

class C:def __init__(self):self._x = None
def getx(self):return self._xdef setx(self, value):self._x = valuedef delx(self):del self._xx = property(getx, setx, delx, "I'm the 'x' property.")

property的参数是getxsetxdelx和一个doc字符串。

在下面的代码中,property被用作装饰器。它的对象是x函数,但在上面的代码中,参数中没有对象函数的位置。

class C:def __init__(self):self._x = None
@propertydef x(self):"""I'm the 'x' property."""return self._x
@x.setterdef x(self, value):self._x = value
@x.deleterdef x(self):del self._x

在这种情况下,x.setterx.deleter装饰器是如何创建的?

689033 次浏览

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

@propertydef x(self):return self._x

相当于

def getx(self):return self._xx = property(getx)

第一部分很简单:

@propertydef x(self): ...

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

下一步是使用setter和deleter扩展此属性。这会通过适当的方法发生:

@x.setterdef x(self, value): ...

返回一个新属性,该属性继承了旧x加上给定setter的所有内容。

x.deleter以相同的方式工作。

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语法只是语法糖;语法:

@propertydef foo(self): return self._foo

真的意味着和

def foo(self): return self._foofoo = property(foo)

所以foo函数被property(foo)替换,我们在上面看到它是一个特殊对象。然后当你使用@foo.setter()时,你所做的就是调用我在上面向你展示的property().setter方法,它返回属性的一个新副本,但这次是用装饰方法替换的setter函数。

以下序列还通过使用这些装饰器方法创建一个完整的属性。

首先,我们创建一些函数和一个只有getter的property对象:

>>> def getter(self): print('Get!')...>>> def setter(self, value): print('Set to {!r}!'.format(value))...>>> def deleter(self): print('Delete!')...>>> prop = property(getter)>>> prop.fget is getterTrue>>> prop.fset is NoneTrue>>> prop.fdel is NoneTrue

接下来我们使用.setter()方法添加一个setter:

>>> prop = prop.setter(setter)>>> prop.fget is getterTrue>>> prop.fset is setterTrue>>> prop.fdel is NoneTrue

最后,我们使用.deleter()方法添加一个删除器:

>>> prop = prop.deleter(deleter)>>> prop.fget is getterTrue>>> prop.fset is setterTrue>>> prop.fdel is deleterTrue

最后但并非最不重要的是,property对象充当描述符对象,因此它有#1#2#3方法来挂钩实例属性获取、设置和删除:

>>> class Foo: pass...>>> prop.__get__(Foo(), Foo)Get!>>> prop.__set__(Foo(), 'bar')Set to 'bar'!>>> prop.__delete__(Foo())Delete!

描述符Howto包含property()类型的纯Python示例实现

class Property:"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):self.fget = fgetself.fset = fsetself.fdel = fdelif 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 selfif 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__)

以下是如何实现@property的最小示例:

class Thing:def __init__(self, my_word):self._word = my_word@propertydef word(self):return self._word
>>> print( Thing('ok').word )'ok'

否则word仍然是一个方法而不是一个属性。

class Thing:def __init__(self, my_word):self._word = my_worddef word(self):return self._word
>>> print( Thing('ok').word() )'ok'

如下:

class C(object):def __init__(self):self._x = None
@propertydef x(self):"""I'm the 'x' property."""return self._x
@x.setterdef x(self, value):self._x = value
@x.deleterdef 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._xx = property(_x_get, doc="I'm the 'x' property.")
def _x_set(self, value):self._x = valuex = x.setter(_x_set)
def _x_del(self):del self._xx = x.deleter(_x_del)

这与:

class C(object):def __init__(self):self._x = None
@propertydef x(self):"""I'm the 'x' property."""return self._x
@x.setterdef x(self, value):self._x = value
@x.deleterdef x(self):del self._x

属性可以通过两种方式声明。

  • 为属性创建getter、setter方法,然后将这些方法作为参数传递给财产函数
  • 使用@刘德华装饰器。

你可以看看我写的关于python中的属性的几个例子。

我阅读了这里所有的帖子,意识到我们可能需要一个现实生活中的例子。为什么,实际上,我们有@属性?因此,考虑一个使用身份验证系统的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))
...
@propertydef password(self):raise AttributeError('password is not a readable attribute')
@password.setterdef 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中,我们可以实例化一个User:

...@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表单相关的不同主题)。

我希望这个例子会有用

下面是另一个例子:

#### Python Properties Example##class GetterSetterExample( object ):## Set the default value for x ( we reference it using self.x, set a value using self.x = value )__x = None

#### On Class Initialization - do something... if we want..##def __init__( self ):## Set a value to __x through the getter / setter... Since __x is defined above, this doesn't need to be set...self.x = 1234
return None

#### Define x as a property, ie a getter - All getters should have a default value arg, so I added it - it will not be passed in when setting a value, so you need to set the default here so it will be used..##@propertydef x( self, _default = None ):## I added an optional default value argument as all getters should have this - set it to the default value you want to return..._value = ( self.__x, _default )[ self.__x == None ]
## Debugging - so you can see the order the calls are made...print( '[ Test Class ] Get x = ' + str( _value ) )
## Return the value - we are a getter afterall...return _value

#### Define the setter function for x...##@x.setterdef x( self, _value = None ):## Debugging - so you can see the order the calls are made...print( '[ Test Class ] Set x = ' + str( _value ) )
## This is to show the setter function works.... If the value is above 0, set it to a negative value... otherwise keep it as is ( 0 is the only non-negative number, it can't be negative or positive anyway )if ( _value > 0 ):self.__x = -_valueelse:self.__x = _value

#### Define the deleter function for x...##@x.deleterdef x( self ):## Unload the assignment / data for xif ( self.__x != None ):del self.__x

#### To String / Output Function for the class - this will show the property value for each property we add...##def __str__( self ):## Output the x property data...print( '[ x ] ' + str( self.x ) )

## Return a new line - technically we should return a string so it can be printed where we want it, instead of printed early if _data = str( C( ) ) is used....return '\n'
######_test = GetterSetterExample( )print( _test )
## For some reason the deleter isn't being called...del _test.x

基本上,与C(对象)示例相同,除了我使用我也不初始化__init-…嗯…我做的,但它可以被删除,因为__x被定义为类的一部分。

输出是:

[ Test Class ] Set x = 1234[ Test Class ] Get x = -1234[ x ] -1234

如果我注释掉init中的self. x=1234,则输出为:

[ Test Class ] Get x = None[ x ] None

如果我在getter函数中将_default=无设置为_default=0(因为所有getter都应该有一个默认值,但它不是由我所看到的属性值传入的,所以你可以在这里定义它,它实际上并不坏,因为你可以定义一次默认值并在任何地方使用它)即:def x(self,_default=0):

[ Test Class ] Get x = 0[ x ] 0

注意:getter逻辑的存在只是为了让值被它操纵,以确保它被它操纵——对于print语句也是如此。

注意:我习惯了Lua,并且能够在调用单个函数时动态创建10多个助手,并且我在不使用属性的情况下为Python做了类似的东西,并且它在一定程度上可以工作,但是,即使函数在使用之前被创建,有时在创建之前调用它们仍然存在问题,这很奇怪,因为它不是这样编码的……我更喜欢Lua元表的灵活性,以及我可以使用实际的setter/getter而不是直接访问变量的事实……虽然我正在设计的一个可能不可能没有很多额外的库-如果我在AutoHotkey中编写它,我可以直接访问我需要的dll调用,同样可以在Java,C#,C++等中完成-也许我还没有找到合适的东西,但对于那个项目,我可能会从Python切换。

注意:此论坛中的代码输出已损坏-我必须在代码的第一部分添加空格才能正常工作-当复制/粘贴确保将所有空格转换为制表符时……我在Python中使用制表符,因为在10,000行的文件中,文件大小可以是512KB到1MB的空格和100到200KB的制表符,这相当于文件大小的巨大差异,并减少流转时长……

标签也可以根据每个用户进行调整-因此,如果您喜欢2个空格宽度,4个,8个或任何您可以做的事情,这意味着它对于视力缺陷的开发人员来说是深思熟虑的。

注意:由于论坛软件中的bug,类中定义的所有函数都没有正确缩进-确保在复制/粘贴时缩进

让我们从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 functionreturn 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 functionreturn wrapper_func
@decorator_funcdef say_bye():print("bye!!")
say_bye()

让我们用一个案例场景来完成所有事情。但在此之前,让我们谈谈一些OOP原则。

Getter和setter在许多面向对象的编程语言中使用,以确保数据封装的原则(将数据与操作这些数据的方法捆绑在一起)。

当然,这些方法是用于检索数据的getter和用于更改数据的setter。

根据这个原则,类的属性是私有的,以隐藏和保护它们免受其他代码的影响。

@刘德华基本上就是使用getter和setter的Pythonic方式。

Python有一个叫做属性的伟大概念,它使面向对象程序员的生活变得更加简单。

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

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

重构代码,这是我们如何用“属性”实现它。

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

属性对象有三个方法,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 propertytemperature = property()# assign fgettemperature = temperature.getter(get_temperature)# assign fsettemperature = temperature.setter(set_temperature)

注意事项:

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

现在您可以通过写入来访问温度值。

C = Celsius()C.temperature# instead of writing C.get_temperature()

我们可以继续下去,不定义名称get_temperatureset_temperature,因为它们是不必要的并且污染了类的命名空间。

处理上述问题的Pythonic方式是使用@刘德华

class Celsius:def __init__(self, temperature = 0):self.temperature = temperature
def to_fahrenheit(self):return (self.temperature * 1.8) + 32
@propertydef temperature(self):print("Getting value")return self.temperature
@temperature.setterdef temperature(self, value):if value < -273:raise ValueError("Temperature below -273 is not possible")print("Setting value")self.temperature = value

注意事项-

  1. 用于获取值的方法用“@属性”装饰。
  2. 必须作为setter函数的方法用“@temperature.setter”修饰,如果函数被称为“x”,我们必须用“@x.setter”修饰它。
  3. 我们编写了具有相同名称和不同数量参数的“两个”方法,“def温度(self)”和“def温度(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
@propertydef x(self):return self.__x
@x.setterdef x(self, x):if x < 0:self.__x = 0elif x > 1000:self.__x = 1000else:self.__x = x

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

您可以检查此实现这里

下面是另一个例子,说明当必须重构取自这里的代码时,@property如何提供帮助(我只在下面总结它):

假设你创建了一个类Money,如下所示:

class Money:def __init__(self, dollars, cents):self.dollars = dollarsself.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...@propertydef dollars(self):return self.total_cents // 100
@dollars.setterdef dollars(self, new_dollars):self.total_cents = 100 * new_dollars + self.cents
# And the getter and setter for cents.@propertydef cents(self):return self.total_cents % 100
@cents.setterdef 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 += 2print("I have {} dollar and {} cents.".format(money.dollars, money.cents))# prints I have 29 dollar and 12 cents.
money.cents += 10print("I have {} dollar and {} cents.".format(money.dollars, money.cents))# prints I have 29 dollar and 22 cents.

您也可以在抽象类中使用@property;我举了一个最小的例子这里

这一点已经被很多人清除了,但这里有一个我正在搜索的直接点。这是我觉得从@属性装饰器开始很重要的。例如:-

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

函数“get_config()”的调用将像这样工作。

util = UtilityMixin()print(util.get_config)

如果你注意到我没有使用“()”括号来调用函数。这是我搜索@属性装饰器的基本内容。这样你就可以像变量一样使用你的函数。

property@property装饰器后面的类。

你总是可以检查这个:

print(property) #<class 'property'>

我重写了help(property)的示例,以显示@property语法

class C:def __init__(self):self._x=None
@propertydef x(self):return self._x
@x.setterdef x(self, value):self._x = value
@x.deleterdef x(self):del self._x
c = C()c.x="a"print(c.x)

在功能上与property()语法相同:

class C:def __init__(self):self._x=None
def g(self):return self._x
def s(self, v):self._x = v
def d(self):del self._x
prop = property(g,s,d)
c = C()c.x="a"print(c.x)

正如你所看到的,我们如何使用财产没有区别。

要回答的问题@property装饰器是通过property类实现的。


所以,问题是解释一下property类。此行:

prop = property(g,s,d)

是初始化。我们可以像这样重写它:

prop = property(fget=g,fset=s,fdel=d)

fgetfsetfdel的含义:

 |    fget|      function to be used for getting an attribute value|    fset|      function to be used for setting an attribute value|    fdel|      function to be used for del'ing an attribute|    doc|      docstring

下一张图片显示了我们拥有的三胞胎,来自类property

在此处输入图片描述

__get____set____delete__覆盖。这是Python中描述符模式的实现。

一般来说,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。

我们还可以使用属性settergetterdeleter方法将函数绑定到属性。检查下一个示例。类C的方法s2将设置属性翻了一番

class C:def __init__(self):self._x=None
def g(self):return self._x
def s(self, x):self._x = x
def d(self):del self._x
def s2(self,x):self._x=x+x

x=property(g)x=x.setter(s)x=x.deleter(d)

c = C()c.x="a"print(c.x) # outputs "a"
C.x=property(C.g, C.s2)C.x=C.x.deleter(C.d)c2 = C()c2.x="a"print(c2.x) # outputs "aa"

最好的解释可以在这里找到:Python@属性解释-如何使用以及何时使用?(完整示例)作者:塞尔瓦·普拉巴卡兰|发表于2018年11月5日

它帮助我理解了为什么,而不仅仅是如何。

https://www.machinelearningplus.com/python/python-property/

装饰器是一种接受函数作为参数并返回闭包的函数。闭包是一组内部函数和自由变量。内部函数对自由变量关闭,这就是为什么它被称为“闭包”。自由变量是位于内部函数之外并通过docorator传递给内部的变量。

顾名思义,装饰器正在装饰接收到的函数。

function decorator(undecorated_func):print("calling decorator func")inner():print("I am inside inner")return undecorated_funcreturn inner

这是一个简单的装饰器函数。它接收“undecorated_func”并将其作为自由变量传递给内部(),内部()打印“我在内部”并返回undecorated_func。当我们调用decorator(undecorated_func)时,它返回inner。这是键,在装饰器中,我们将内部函数命名为我们传递的函数的名称。

   undecorated_function= decorator(undecorated_func)

现在内部函数被称为“undecorated_func”。由于内部现在被命名为“undecorated_func”,我们将“undecorated_func”传递给装饰器,我们返回“undecorated_func”并打印出“我在内部”。所以这个打印语句装饰了我们的“undecorated_func”。

现在让我们定义一个带有属性装饰器的类:

class Person:def __init__(self,name):self._name=name@propertydef name(self):return self._name@name.setterdef name(self.value):self._name=value

当我们将name()修饰为@属性()时,发生了这样的事情:

name=property(name) # Person.__dict__ you ll see name

属性()的第一个参数是getter。这是在第二次装饰中发生的事情:

   name=name.setter(name)

正如我上面提到的,装饰器返回内部函数,我们用我们传递的函数的名称命名内部函数。

这里有一件重要的事情要注意。“name”是不可变的。在第一个装饰中,我们得到了这个:

  name=property(name)

在第二个我们得到了这个

  name=name.setter(name)

我们不是在修改名称obj。在第二次装饰中,python看到这是属性对象并且它已经有getter。所以python创建了一个新的“name”对象,从第一个obj添加“fget”,然后设置“fset”。

在下面,我举了一个例子来澄清@属性

考虑一个名为Student的类,它有两个变量:nameclass_number,您希望class_number位于范围为1至5中。

现在我将解释两种错误的解决方案,最后是正确的解决方案:


下面的代码是错误,因为它不验证class_number(在1到5的范围内)

class Student:def __init__(self, name, class_number):self.name = nameself.class_number = class_number

尽管经过验证,这个解决方案也是错误

def validate_class_number(number):if 1 <= number <= 5:return numberelse:raise Exception("class number should be in the range of 1 to 5")
class Student:def __init__(self, name, class_number):self.name = nameself.class_number = validate_class_number(class_number)

因为class_number验证仅在创建类实例时检查,之后不检查(可以使用范围1到5之外的数字更改class_number):

student1 = Student("masoud",5)student1.class_number = 7

正确解决方案是:

class Student:def __init__(self, name, class_number):self.name = nameself.class_number = class_number        
@propertydef class_number(self):return self._class_number
@class_number.setterdef class_number(self, class_number):if not (1 <= class_number <= 5): raise Exception("class number should be in the range of 1 to 5")self._class_number = class_number