Python学习之路39-特性property
发布日期:2021-08-31 13:57:52 浏览次数:19 分类:技术文章

本文共 4817 字,大约阅读时间需要 16 分钟。

《流畅的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.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.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类的代码。

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

转载于:https://juejin.im/post/5b2e10476fb9a00e6678ae72

转载地址:https://blog.csdn.net/weixin_34077371/article/details/93174722 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:servlet
下一篇:【BZOJ】2134: 单选错位 期望DP

发表评论

最新留言

关注你微信了!
[***.104.42.241]2024年03月28日 15时11分51秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章