Python学习之路39-特性property

《流畅的Python》笔记。

本篇主要讨论Python中的特性property。

1. 前言

上一篇介绍了如何动态创建属性(Attribute),在最后一个例子中我们使用了@property装饰器实现了只读特性。本篇将介绍如何使用特性(Property)来验证属性。我会通过一个Food类来演示property的用法和行为。

2. property基本用法

Food是个食品类,论公斤卖,以下是它的定义和用法:

# 代码2.1
class 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.weightself.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.1
property(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,特性是类属性
<property object at 0x000002BBD1963C78>
>>> obj.prop          # 通过实例访问特性prop
'the prop value'
>>> obj.prop = "foo"  # 没有定义set方法,所以不能对特性设置值,也不能像上面那样创建同名实例属性
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          # 依然显示的是特性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.a
This is a
'a'
>>> del t.a
Delete 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类的代码。


迎大家关注我的微信公众号"代码港" & 个人网站 www.vpointer.net ~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值