《流畅的Python》笔记。
本篇主要讨论Python中的特性property。
1. 前言
介绍了如何动态创建属性(Attribute),在最后一个例子中我们使用了@property
装饰器实现了只读特性。本篇将介绍如何使用特性(Property)来验证属性。我会通过一个Food
类来演示property
的用法和行为。
2. property基本用法
Food
是个食品类,论公斤卖,以下是它的定义和用法:
# 代码2.1class Food: def __init__(self, weight, price): self.weight = weight self.price = price def subtotal(self): return self.weight * self.price# 实例>>> food = Food(10, 10)>>> food.subtotal()100>>> food.weight = -20>>> food.subtotal()-200复制代码
这个类很简单,但有一个问题:可以将self.weight
和self.price
的值设为负数。解决这个问题很好办,为每个属性设置get/set
方法,在设置值之前对传入的值进行验证,Java就是这么做的。但比起直接访问和设置属性来说,通过get/set
方法操作属性并不自然。并且,如果这个代码已经上线运行,存取值就是直接操作属性,现在要把它改成用get/set
方法操作属性,那要改的地方就太多了。此时,符合Python风格的做法是:将属性替换成特性。
现在我们使用@property
装饰器来修改上述代码:
# 代码2.2 subtotal()不变class Food: def __init__(self, weight, price): self.weight = weight # 这里已经在使用特性了,而不是创建一个名为weight的属性 self.price = price @property # get方法 def weight(self): return self.__weight @weight.setter # set方法 def weight(self, value): if value > 0: self.__weight = value else: raise ValueError("Value must be > 0")复制代码
我们将真正的值存储在self.__weight
属性中,并且在设置weight
的值之前进行了验证,使其必须为正数。
这里留了一个坑:price
依然可以设置为负数。之所以没有改price
,因为如果要改,也就只是把上面get/set
方法再抄一遍:把self.__weight
换为self.__price
,再把方法名给换了。这不就重复造轮子了吗?要是get/set
方法的代码量比较大,那整个文件一大半内容都被存取值方法给占了。如果这个类再多一些属性,这些属性的要求都一样,这得写多少个@property
?
避免这种情况的方法大家都知道:抽象。对特性进行抽象有两种方式:使用特性工厂函数,或者使用描述符类。后者更灵活,下一篇再介绍。本篇介绍特性工厂函数,不过在此之前,先深入了解一下特性。
3. property解析
虽然内置的property
经常被用作装饰器,但它其实是一个类(在Python中,类和函数经常互换,不用纠结)。它的构造方法的完整签名如下:
# 代码3.1property(fget=None, fset=None, fdel=None, doc=None)复制代码
所有参数都是可选的,比如Food
中,特性weight
设置了前两个参数,后两个没有设置。
3.1 用法
property
有两种用法,将其用作装饰器是现在主流的用法,但它还有一个“经典”的用法:
# 代码3.2 class Example: def get_a(self): return self.__a def set_a(self, value): self.__a = value a = property(get_a, set_a)复制代码
某些情况下,这种写法比装饰器写法要好,比如后面用到的特性工厂函数,但装饰器更加明显且常用。
3.2 特性覆盖实例属性
类属性会被实例属性覆盖,特性也是类属性,但特性管理的是实例属性的存取,它不会被实例属性覆盖。 下面来看一个例子:
# 代码3.3>>> class Test: # 定义一个测试类... data = "the class data attr" # 这是个类属性... @property... def prop(self): # prop是特性,特性也是类属性!... return "the prop value"... >>> obj = Test() # 新建一个Test实例>>> vars(obj) # 查看实例属性,没有任何实例属性{} # 特性prop和类属性data都不在其中>>> obj.data # 访问的是类属性'the class data attr'>>> obj.data = "bar" # 添加实例属性,与类属性同名>>> obj.data # 覆盖了类属性'bar'>>> vars(obj) # 现在有一个实例属性{'data': 'bar'}>>> Test.data # 类属性的值并没有被改变'the class data attr'>>> Test.prop # 通过类访问特性prop,特性是类属性>>> obj.prop # 通过实例访问特性prop'the prop value'>>> obj.prop = "foo" # 没有定义set方法,所以不能对特性设置值,也不能像上面那样创建同名实例属性Traceback (most recent call last): File " ", line 1, in AttributeError: can't set attribute>>> obj.__dict__["prop"] = "foo" # 创建也特性同名的普通实例属性,上一篇文章中用到了此法>>> vars(obj) # 现在有两个实例属性{'data': 'bar', 'prop': 'foo'}>>> obj.prop # 依然显示的是特性prop的值,而不是刚才设置的值'the prop value'>>> Test.prop = "baz" # 这里不是调用特性的set方法,而是把特性给删除了,prop变为了str类型的类属性>>> obj.prop # 访问普通实例属性prop,它不再被覆盖'foo'>>> Test.data = property(lambda self: "the 'data' prop value") # 将之前的类属性data变为特性>>> obj.data # 之前这个属性覆盖了类属性,现在类属性变为了特性,于是这个实例属性被特性覆盖"the 'data' prop value">>> del Test.data # 删除这个特性>>> obj.data # 实例属性不再被覆盖'bar'复制代码
上述代码也展示了一个技巧:如果想添加与特性同名的实例属性,可以直接操作__dict__
。
3.3 特性删除操作
从property
的签名可以看出,它的第三个参数是fdel
,当删除特性时,就会调用它。虽然使用Python编程时不常删除属性,但Python为我们提供了删除方法del
。以下是删除特性的一个例子:
# 代码3.4>>> class Test:... @property... def a(self):... print("This is a")... return "a" ... @a.deleter... def a(self):... print("Delete a")... >>> t = Test()>>> t.aThis is a'a'>>> del t.aDelete a复制代码
3.4 特性的文档
__doc__
属相相当于类或方法的使用说明,当用户需要了解某个类或方法时,Python会从这个属性获取值,并返回给用户。
从property
的签名可以看出,它有一个参数doc
,用于设置特性的__doc__
属性。如果使用“经典”方法创建特性,我们可以手动传入这个参数。但如果使用的是装饰器方式,则读值方法的文档字符串将作为特性的文档。
4. 特性工厂函数
现在来定义一个特性工厂函数,实现特性的抽象。延续前面Food
类的例子:
def quantity(name): # 工厂函数,这个单词表示正数量。这个函数使用到了闭包 def qty_getter(instance): # 统一的get方法 return instance.__dict__[name] # name是自由变量 def qty_setter(instance, value): # 统一的set方法 if value > 0: instance.__dict__[name] = value else: raise ValueError("value must be > 0") return property(qty_getter, qty_setter)class Food: weight = quantity("weight") # 同一单词重复输入了两次,这是特性工厂方式的一个不足,很难避免 price = quantity("price") def __init__(self, weight, price): self.weight = weight self.price = price def subtotal(self): return self.weight * self.price复制代码
当一个类中有多个属性采用相同的验证方法时(比如100个属性有50个都要求为正数),使用此法可以节省大量代码。
5. 总结
本篇内容并不多。首先我们介绍了特性property
的常用方式,并引出了特性工厂的概念,但并没有马上展开这个概念,转而介绍特性本身的相关内容。最后,使用特性工厂函数改写了之前的Food
类的代码。
迎大家关注我的微信公众号"代码港" & 个人网站 ~