ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python 类,property属性(简化属性的操作),@property,property()用

Python 类,property属性(简化属性的操作),@property,property()用 前言property是 Python 里最常用的「语法糖」之一它的官方定义是把方法调用伪装成属性访问。官方文档对它的描述是「返回一个 property 属性」本质是构建了一个数据描述符data descriptor读c.x时调 getter写c.x v时调 setterdel c.x时调 deleter。它有两种完全等价的写法这也是标题里同时出现property和property()的原因装饰器写法propertyx.setterx.deleter函数式写法x property(fget, fset, fdel, doc)。很多人只会其中一种见到另一种就发懵也有人以为property有某种隐藏状态其实它就是把几个函数装进一个描述符对象而已。本文专讲property的语法两种写法各自长什么样、如何一一对应、__set_name__在其中起什么作用、以及几种常见误用。全篇只有这一处使用函数式写法以便把两种形式放在一起对照。代码按 Python 3.8 给出。一、property 是什么一个数据描述符先用一段最小例子感受它的作用——把「取值时要做的事」写进一个方法对外却仍然用属性语法访问# 适用于 Python 3.8class Product:def __init__(self, name, price):self.name nameself.price price # 走下面的 setterpropertydef price(self):商品单价元。return self._priceprice.setterdef price(self, value):if value 0:raise ValueError(价格不能为负)self._price valuep Product(笔记本, 9.9)print(p.price) # 推演输出9.9p.price 12.5 # 走的不是赋值而是 setter 调用print(p.price) # 推演输出12.5关键点p.price 12.5表面看是赋值实际执行的是Product.price.fset(p, 12.5)。这也是为什么 getter 和 setter 里用的是self._price而不是self.price——后者会再次触发 setter无限递归。property之所以能接管赋值是因为它属于数据描述符同时定义了__get__和__set__优先级高于实例字典。所以哪怕实例字典里已经有price属性访问依然会走描述符的方法。二、装饰器写法getter / setter / deleterproperty装饰的是 getter随后用属性名.setter和属性名.deleter注册另外两个访问器。官方提醒这几个被装饰的函数必须和原来的属性同名例子里的price因为x.setter是拿「名字x上的 property 对象」去派生一个新的 property。# 适用于 Python 3.8class Product:def __init__(self, name, price):self.name nameself.price pricepropertydef price(self):商品单价元。return self._priceprice.setterdef price(self, value):if value 0:raise ValueError(价格不能为负)self._price valueprice.deleterdef price(self):del self._pricep Product(笔记本, 9.9)print(p.price) # 推演输出9.9p.price 12.5print(p.price) # 推演输出12.5del p.price # 触发 deleterprint(hasattr(p, _price)) # 推演输出False装饰器写法的好处是「三件套挨着写读起来像一段声明」缺点是每一个属性都要写三遍名字属性多了会显得啰嗦。三、函数式写法property(fget, fset, fdel, doc)官方给出的签名是property(fgetNone, fsetNone, fdelNone, docNone)四个参数依次是取值的函数、赋值的函数、删除的函数以及属性自身的文档字符串。任何一项都可以省略省略 getter 时读取会抛AttributeError省略 setter 时赋值会抛AttributeError。若doc为Noneproperty会去拷贝fget的文档字符串。把它写成一句就是「先定义三个普通函数再把它们交给property()」# 适用于 Python 3.8class Product:def __init__(self, name, price):self.name nameself.price price # 走下面的 fsetdef get_price(self):return self._pricedef set_price(self, value):if value 0:raise ValueError(价格不能为负)self._price valuedef del_price(self):del self._price# 三个普通函数在这里被打包成一个属性price property(get_price, set_price, del_price, 商品单价元。)p Product(笔记本, 9.9)print(p.price) # 推演输出9.9p.price 12.5print(p.price) # 推演输出12.5del p.priceprint(hasattr(p, _price)) # 推演输出False这段代码与上一节的装饰器版本在行为上完全等价——官方文档原话是「这段代码与第一个例子完全等价」。区别只在于组织方式维度装饰器写法函数式写法语法形态property/x.setter/x.deleterx property(fget, fset, fdel, doc)函数命名三个函数都叫x三个函数各有名字如get_x文档字符串写在 getter 的 docstring 里作为第四个参数传入或由 getter 自动带过来适用场景新写代码可读性最好动态拼装、包装已有函数、老代码风格返回对象每步都返回新的 property一次构造完成四、两种写法的等价关系与 __set_name__x.setter为什么能拿到「上一个」property看官方描述符指南里给出的纯 Python 等价实现就明白了setter是一个方法它返回一个新的property 对象把对应位置换成新函数其余原样保留。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(fget, fset, fdel, doc)。第三件值得知道的事是__set_name__。Python 3.6 起PEP 487创建类对象时type.__new__会扫描类命名空间里定义了__set_name__的对象并把「所属类」和「属性名」回调给它。property也实现了这个钩子效果是把属性名记进自己的__name__# 适用于 Python 3.8class C:def get_v(self):return self._vdef set_v(self, value):self._v valuev property(get_v, set_v)c C()c.v 5print(c.v) # 推演输出5print(C.v.__name__) # 推演输出v —— 3.6 由 __set_name__ 写入print(C.v.fget.__name__) # 推演输出get_vprint(C.v.fset.__name__) # 推演输出set_v顺带说明两点版本信息__set_name__是 Python 3.6 才加入的PEP 487而「property 的文档字符串可写」是 3.5 的变化。至于 Python 2它已于2020 年 1 月 1 日停止维护PEP 373Python 2 的官方文档还写明property只对新式类显式继承object即class C(object):有效旧式类上挂property不会生效。迁移到 Python 3 后所有类都是新式类写class C:即可。常见坑点1. setter 里又把值赋给同名属性。❌def price(self, value): self.price value一赋值就递归报RecursionError。 ✅ 内部存到另一个名字上比如self._price value。2. 装饰器写法里三个函数取了不同的名字。❌ 写成def get_price(self)和price.setter def set_price(self, value)price.setter找不到price这个名字。 ✅ 三个函数必须同名这里是pricex.setter的x就是属性名。3. 以为省略 setter 会得到一个「安静忽略赋值」的属性。❌ 只写 getter 后用p.x 1期待赋值被默默丢弃。 ✅ 省略fset时赋值会抛AttributeError只读属性这是官方定义的行为。4. 用函数式写法时把doc写成了位置参数却顺序记错。❌property(get_x, 说明, set_x, del_x)字符串被当成了fset。 ✅ 顺序固定为fget, fset, fdel, doc拿不准就用关键字传参或直接给 getter 写 docstring。5. 在__init__里给只读 property 赋值。❌ 定义只读total之后写self.total 0实例化时直接抛AttributeError。 ✅ 初始化的是底层字段如self._items []派生值由 getter 现算。6. 把property用成「带副作用的方法」。❌ getter 里发网络请求、写日志、改全局状态。 ✅ 属性语义意味着「读取低成本、无副作用」重活请写成显式方法。7. 以为 property 会把名字自动暴露给外部拼装逻辑。❌ 在类创建之后才把 property 挂上去期待__set_name__自动跑。 ✅__set_name__只在类创建时由type.__new__调用事后手动赋值需要自己调用一次或者改用装饰器定义在类体内。总结问题答案property是什么一个数据描述符把方法伪装成属性函数式签名property(fgetNone, fsetNone, fdelNone, docNone)装饰器三件套property、x.setter、x.deleter两种写法关系完全等价装饰器是函数式写法的语法包装只读属性怎么做只提供fget赋值会抛AttributeError属性名从哪来3.6 起由__set_name__自动写入__name__内部存哪里存到另一个名字如_x绝不能存回同名属性一句话收尾property和property()是同一件事的两种写法——前者把三个同名函数声明成属性后者把三个普通函数打包交给property()理解「setter 返回新 property」和__set_name__这两点两种写法就能随手互换。
返回列表