Python的Property详细档案
今天我们就来好好聊聊Python3里面的Property
特性的引入
特性和属性的区别是什么?
在python 中 属性 这个 实例方法, 类变量 都是属性.
属性, attribute
在python 中 数据的属性 和处理数据的方法 都可以叫做 属性.
简单来说 在一个类中, 方法是属性, 数据也是属性 .
class Animal: name = 'animal' def bark(self): print('bark') pass @classmethod def sleep(cls): print('sleep') pass @staticmethod def add(): print('add')
在命令行里面执行
>>> animal = Animal() >>> animal.add() add >>> animal.sleep() sleep >>> animal.bark() bark >>> hasattr(animal,'add') #1 True >>> hasattr(animal,'sleep') True >>> hasattr(animal,'bark') True
可以看出#1 animal 中 是可以拿到 add ,sleep bark 这些属性的.
特性: property 这个是指什么? 在不改变类接口的前提下使用
存取方法 (即读值和取值) 来修改数据的属性.
什么意思呢?
就是通过 obj.property 来读取一个值,
obj.property = xxx ,来赋值
还以上面 animal 为例:
class Animal: @property def name(self): print('property name ') return self._name @name.setter def name(self, val): print('property set name ') self._name = val @name.deleter def name(self): del self._name
这个时候 name 就是了特性了.
>>> animal = Animal() >>> animal.name='dog' property set name >>> animal.name property name 'dog' >>> >>> animal.name='cat' property set name >>> animal.name property name 'cat'
肯定有人会疑惑,写了那么多的代码, 还不如直接写成属性呢,多方便.
比如这段代码:
直接把name 变成类属性 这样做不是很好吗,多简单. 这样写看起来 也没有太大的问题.但是 如果给name 赋值成数字 这段程序也是不会报错. 这就是比较大的问题了.
>>> class Animal: ... name=None ... >>> animal = Animal() >>> animal.name >>> animal.name='frank' >>> animal.name 'frank' >>> animal.name='chang' >>> animal.name 'chang' >>> animal.name=250 >>> animal <Animal object at 0x10622b850> >>> animal.name 250 >>> type(animal.name) <class 'int'>
这里给 animal.name 赋值成 250, 程序从逻辑上来说 没有问题. 但其实这样赋值是毫无意义的.
我们一般希望 不允许这样的赋值,就希望 给出 报错或者警告 之类的.
animal= Animal() animal.name=100 property set name Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 13, in name ValueError: expected val is str
其实当name 变成了property 之后,我们就可以对name 赋值 进行控制. 防止一些非法值变成对象的属性.
比如说name 应该是这个字符串, 不应该是数字 这个时候 就可以在 setter 的时候 进行判断,来控制 能否赋值.
要实现上述的效果, 其实也很简单 setter 对value进行判断就好了.
class Animal: @property def name(self): print('property name ') return self._name @name.setter def name(self, val): print('property set name ') # 这里 对 value 进行判断 if not isinstance(val,str): raise ValueError("expected val is str") self._name = val
感受到 特性的魅力了吧,可以通过 赋值的时候 ,对 值进行校验,方式不合法的值,进入到对象的属性中. 下面 看下 如何设置只读属性, 和如何设置读写 特性.
假设 有这样的一个需求 , 某个类的属性一个初始化之后 就不允许 被更改,这个 就可以用特性这个问题 , 比如一个人身高是固定, 一旦 初始化后,就不允许改掉.
设置只读特性
class Frank: def __init__(self, height): self._height = height @property def height(self): return self._height >>> frank = Frank(height=100) >>> frank.height 100 >>> frank.height =150 Traceback (most recent call last): File "<input>", line 1, in <module> AttributeError: can't set attribute
这里初始化 frank后 就不允许 就修改 这个 height 这个值了. (实际上也是可以修改的)
重新 给 height 赋值就会报错, 报错 AttributeError ,这里 不实现 setter 就可以了.
设置读写特性 class Frank: def __init__(self, height): self._height = height @property def height(self): return self._height @height.setter def height(self, value): """ 给特性赋值 """ self._height = value
比如对人的身高 在1米 到 2米之间 这样的限制
>>> frank = Frank(height=100) >>> frank.height 100 >>> frank.height=165 >>> frank.height 165 对特性的合法性进行校验 class Frank: def __init__(self, height): self.height = height # 注意这里写法 @property def height(self): return self._height @height.setter def height(self, value): """ 判断逻辑 属性的处理逻辑 定义 了 setter 方法之后就 修改 属性 了. 判断 属性 是否合理 ,不合理直接报错. 阻止赋值,直接抛异常 :param value: :return: """ if not isinstance(value, (float,int)): raise ValueError("高度应该是 数值类型") if value < 100 or value > 200: raise ValueError("高度范围是100cm 到 200cm") self._height = value >>> frank = Frank(100) >>> frank.height 100 >>> frank.height='aaa' Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 21, in height ValueError: 高度应该是 数值类型 >>> frank.height=250 Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 23, in height ValueError: 高度范围是100cm 到 200cm
这样就可以进行严格的控制, 一些特性的方法性 ,通过写setter 方法 来保证数据 准确性,防止一些非法的数据进入到实例中.
Property是什么?
实际上是一个类 , 然后就是一个装饰器. 让一个方法变成 一个特性.
假设某个类的实例方法 bark 被property修饰了后, 调用方式就会发生变化.
其实特性模糊了方法和数据的界限.
方法是可调用的属性 , 而property 是 可定制化的'属性' . 一般方法的名称是一个动词(行为). 而特性property 应该是名词.
如果我们一旦确定了属性不是动作, 我们需要在标准属性 和 property 之间做出选择 .
一般来说你如果要控制 property 的 访问过程,就要用property. 否则用标准的属性即可 .
attribute属性和property特性的区别在于当property被读取, 赋值, 删除时候, 自动会执行某些特定的动作.
peroperty 详解
特性都是类属性,但是特性管理的其实是实例属性的存取。
----- 摘自 fluent python
下面的例子来自 fluent python
看一下几个例子来说明几个特性和属性区别
>>> class Class: """ data 数据属性和 prop 特性。 """ ... data = 'the class data attr' ... ... @property ... def prop(self): ... return 'the prop value' ... >>> >>> obj= Class() >>> vars(obj) {} >>> obj.data 'the class data attr' >>> Class.data 'the class data attr' >>> obj.data ='bar' >>> Class.data 'the class data attr'
实例属性遮盖类的数据属性 , 就是说如果obj.data重新修改了 , 类的属性不会被修改 .
下面尝试obj 实例的prop特性
>>> Class.prop <property object at 0x110968ef0> >>> obj.prop 'the prop value' >>> obj.prop ='foo' Traceback (most recent call last): File "<input>", line 1, in <module> AttributeError: can't set attribute >>> obj.__dict__['prop'] ='foo' >>> vars(obj) {'data': 'bar', 'prop': 'foo'} >>> obj.prop #1 'the prop value' >>> Class.prop ='frank' >>> obj.prop 'foo'
我尝试修改 obj.prop 会直接报错 ,这个容易理解, 因为property没有实现 setter 方法 . 我直接修改obj.dict
然后 在#1的地方, 发现 还是正常调用了特性 ,而没有属性的值.
当我改变Class.prop变成一个属性的时候 .
再次调用obj.prop才调用到了 实例属性.
再看一个例子 添加 特性
>>> Class.prop <property object at 0x110968ef0> >>> obj.prop 'the prop value' >>> obj.prop ='foo' Traceback (most recent call last): File "<input>", line 1, in <module> AttributeError: can't set attribute >>> obj.__dict__['prop'] ='foo' >>> vars(obj) {'data': 'bar', 'prop': 'foo'} >>> obj.prop #1 'the prop value' >>> Class.prop ='frank' >>> obj.prop 'foo'
改变 data 变成特性后, obj.data也改变了. 删除这个特性的时候 , obj.data 又恢复了.
本节的主要观点是, obj.attr 这样的表达式不会从 obj 开始寻找 attr,而是从
obj.__class__ 开始,而且,仅当类中没有名为 attr 的特性时, Python 才会在 obj 实
例中寻找。这条规则适用于特性 .
property 实际上 是一个类
def __init__(self, fget=None, fset=None, fdel=None, doc=None): pass # known special case of property.__init__
完成 的要实现一个特性 需要 这 4个参数, get , set ,del , doc 这些参数.但实际上大部分情况下,只要实现 get ,set 即可.
Property的两种写法
第一种写法
使用 装饰器 property 来修饰一个方法
# 方法1 class Animal: def __init__(self, name): self._name = name @property def name(self): print('property name ') return self._name @name.setter def name(self, val): print('property set name ') if not isinstance(val, str): raise ValueError("expected val is str") self._name = val @name.deleter def name(self): del self._name
第二种写法
直接 实现 set get delete 方法 即可, 通过property 传入 这个参数
# 方法二 class Animal2: def __init__(self, name): self._name = name def _set_name(self, val): if not isinstance(val, str): raise ValueError("expected val is str") self._name = val def _get_name(self): return self._name def _delete_name(self): del self._name name = property(fset=_set_name, fget=_get_name,fdel= _delete_name,doc= "name 这是特性描述") if __name__ == '__main__': animal = Animal2('dog') >>> animal = Animal2('dog') >>> >>> animal.name 'dog' >>> animal.name 'dog' >>> help(Animal2.name) Help on property: name 这是特性描述 >>> animal.name='cat' >>> animal.name 'cat'
替换背景的新方法:选择背景图设置后,左边模板、收藏、剪贴板和图库都可以点击,根据选择的内容,设置背景到当前的编辑布局上。如果选择了的是图片,都把图片设置被背景图。如果选择了一个带背景的模板,就把这个模板的背景给复制过来。
常见的一些例子
A、对一些值进行合法性校验.
在举一个小例子 比如 有一个货物, 有重量 和 价格 ,需要保证 这两个属性是正数 不能是 0 , 即>0 的值
基础版本的代码:
class Goods: def __init__(self, name, weight, price): """ :param name: 商品名称 :param weight: 重量 :param price: 价格 """ self.name = name self.weight = weight self.price = price def __repr__(self): return f"{self.__class__.__name__}(name={self.name},weight={self.weight},price={self.price})" @property def weight(self): return self._weight @weight.setter def weight(self, value): if value < 0: raise ValueError(f"expected value > 0, but now value:{value}") self._weight = value @property def price(self): return self._price @price.setter def price(self, value): if value < 0: raise ValueError(f"expected value > 0, but now value:{value}") self._price = value >>> goods = Goods('apple', 10, 30) ... >>> goods Goods(name=apple,weight=10,price=30) >>> goods.weight 10 >>> goods.weight=-10 Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 26, in weight ValueError: expected value > 0, but now value:-10 >>> goods.price 30 >>> goods.price=-3 Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 37, in price ValueError: expected value > 0, but now value:-3 >>> goods Goods(name=apple,weight=10,price=30) >>> goods.price=20 >>> goods Goods(name=apple,weight=10,price=20)
代码 可以正常的判断出来 ,这些非法值了. 这样写 有点问题是什么呢? 就是 发现 weight ,price 判断值的逻辑 几乎是一样的代码… 都是判断是 大于 0 吗? 然而我却写了 两遍相同的代码 .
优化后的代码
有没有更好的解决方案呢?
是有的, 我们可以写一个 工厂函数 来返回一个property , 这实际上是两个 property 而已.
下面 就是工厂函数 ,用来生成一个 property 的.
def validate(storage_name): """ 用来验证 storage_name 是否合法性 , weight , price :param storage_name: :return: """ pass def _getter(instance): return instance.__dict__[storage_name] def _setter(instance, value): if value < 0: raise ValueError(f"expected value > 0, but now value:{value}") instance.__dict__[storage_name] = value return property(fget=_getter, fset=_setter) class Goods: weight = validate('weight') price = validate('price') def __init__(self, name, weight, price): """ :param name: 商品名称 :param weight: 重量 :param price: 价格 """ self.name = name self.weight = weight self.price = price def __repr__(self): return f"{self.__class__.__name__}(name={self.name},weight={self.weight},price={self.price})" >>> goods = Goods('apple', 10, 30) >>> goods.weight 10 >>> goods.weight=-10 Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 16, in _setter ValueError: expected value > 0, but now value:-10 >>> goods Goods(name=apple,weight=10,price=30) >>> goods.price=-2 Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 16, in _setter ValueError: expected value > 0, but now value:-2 >>> goods Goods(name=apple,weight=10,price=30) B、缓存某些值 ... from urllib.request import urlopen ... class WebPage: ... ... def __init__(self, url): ... self.url = url ... ... self._content = None ... ... @property ... def content(self): ... if not self._content: ... print("Retrieving new page") ... self._content = urlopen(self.url).read()[0:10] ... ... return self._content ... >>> >>> >>> url = 'http://www.baidu.com' >>> page = WebPage(url) >>> >>> page.content Retrieving new page b'<!DOCTYPE ' >>> page.content b'<!DOCTYPE ' >>> page.content b'<!DOCTYPE '
可以看出 第一次调用了 urlopen 从网页中读取值, 第二次就没有调用urlopen 而是直接返回content 的内容.
总结
python的特性算是python的高级语法,不要因为到处都要用这个特性的语法.实际上大部分情况是用不到这个语法的. 如果代码中,需要对属性进行检查就要考虑用这样的语法了. 希望你看完之后不要认为这种语法非常常见, 事实上不是的. 其实更好的做法对属性检查可以使用描述符来完成. 描述符是一个比较大的话题,本文章暂未提及,后续的话,可能 会写一下 关于描述的一些用法 ,这样就能更好的理解python,更加深入的理解python.
参考文档
fluent python(流畅的Python)
Python3面向对象编程
Python为什么要使用描述符?
https://juejin.im/post/5cc4fbc0f265da0380437706
https://tech-summary.readthedocs.io/en/latest/python_property.html