zoukankan      html  css  js  c++  java
  • 深入理解「描述符」

    1.17 深入理解「描述符」

    学习 Python 这么久了,说起 Python 的优雅之处,能让我脱口而出的, Descriptor(描述符)特性可以排得上号。

    描述符 是Python 语言独有的特性,它不仅在应用层使用,在语言的基础设施中也有涉及。

    我可以大胆地猜测,你对于描述符的了解是始于诸如 Django ORM 和 SQLAlchemy 中的字段对象,是的,它们都是描述符。你的它的认识,可能也止步于此,如果你没有去深究,它为何要如此设计?也就加体会不到 Python 给我们带来的便利与优雅。

    由于 描述符的内容较多,长篇大论,容易让你倦怠,所以我打算分几篇来讲。

    1.17.1 为什么要使用描述符?

    假想你正在给学校写一个成绩管理系统,并没有太多编码经验的你,可能会这样子写。

    class Student:
        def __init__(self, name, math, chinese, english):
            self.name = name
            self.math = math
            self.chinese = chinese
            self.english = english
    
        def __repr__(self):
            return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                    self.name, self.math, self.chinese, self.english
                )
    

    看起来一切都很合理

    >>> std1 = Student('小明', 76, 87, 68)
    >>> std1
    <Student: 小明, math:76, chinese: 87, english:68>
    

    但是程序并不像人那么智能,不会自动根据使用场景判断数据的合法性,如果老师在录入成绩的时候,不小心录入了将成绩录成了负数,或者超过100,程序是无法感知的。

    聪明的你,马上在代码中加入了判断逻辑。

    class Student:
        def __init__(self, name, math, chinese, english):
            self.name = name
            if 0 <= math <= 100:
                self.math = math
            else:
                raise ValueError("Valid value must be in [0, 100]")
    
            if 0 <= chinese <= 100:
                self.chinese = chinese
            else:
                raise ValueError("Valid value must be in [0, 100]")
    
            if 0 <= chinese <= 100:
                self.english = english
            else:
                raise ValueError("Valid value must be in [0, 100]")
    
    
        def __repr__(self):
            return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                    self.name, self.math, self.chinese, self.english
                )
    

    这下程序稍微有点人工智能了,能够自己明辨是非了。

    image0

    程序是智能了,但在__init__里有太多的判断逻辑,很影响代码的可读性。巧的是,你刚好学过 Property 特性,可以很好的应用在这里。于是你将代码修改成如下,代码的可读性瞬间提升了不少

    class Student:
        def __init__(self, name, math, chinese, english):
            self.name = name
            self.math = math
            self.chinese = chinese
            self.english = english
    
        @property
        def math(self):
            return self._math
    
        @math.setter
        def math(self, value):
            if 0 <= value <= 100:
                self._math = value
            else:
                raise ValueError("Valid value must be in [0, 100]")
    
        @property
        def chinese(self):
            return self._chinese
    
        @chinese.setter
        def chinese(self, value):
            if 0 <= value <= 100:
                self._chinese = value
            else:
                raise ValueError("Valid value must be in [0, 100]")
    
        @property
        def english(self):
            return self._english
    
        @english.setter
        def english(self, value):
            if 0 <= value <= 100:
                self._english = value
            else:
                raise ValueError("Valid value must be in [0, 100]")
    
        def __repr__(self):
            return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                    self.name, self.math, self.chinese, self.english
                )
    

    程序还是一样的人工智能,非常好。

    image1

    你以为你写的代码,已经非常优秀,无懈可击了。

    没想到,人外有天,你的主管看了你的代码后,深深地叹了口气:类里的三个属性,math、chinese、english,都使用了 Property 对属性的合法性进行了有效控制。功能上,没有问题,但就是太啰嗦了,三个变量的合法性逻辑都是一样的,只要大于0,小于100 就可以,代码重复率太高了,这里三个成绩还好,但假设还有地理、生物、历史、化学等十几门的成绩呢,这代码简直没法忍。去了解一下 Python 的描述符吧。

    经过主管的指点,你知道了「描述符」这个东西。怀着一颗敬畏之心,你去搜索了下关于 描述符的用法。

    其实也很简单,一个实现了 描述符协议 的类就是一个描述符。

    什么描述符协议:实现了 __get__()__set__()__delete__() 其中至少一个方法的类,就是一个描述符。

    • __get__: 用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常。
    • __set__:将在属性分配操作中调用。不会返回任何内容。
    • __delete__:控制删除操作。不会返回内容。

    对描述符有了大概的了解后,你开始重写上面的方法。

    如前所述,Score 类是一个描述器,当从 Student 的实例访问 math、chinese、english这三个属性的时候,都会经过 Score 类里的三个特殊的方法。这里的 Score 避免了 使用Property 出现大量的代码无法复用的尴尬。

    class Score:
        def __init__(self, default=0):
            self._score = default
    
        def __set__(self, instance, value):
            if not isinstance(value, int):
                raise TypeError('Score must be integer')
            if not 0 <= value <= 100:
                raise ValueError('Valid value must be in [0, 100]')
    
            self._score = value
    
        def __get__(self, instance, owner):
            return self._score
    
        def __delete__(self):
            del self._score
    
    class Student:
        math = Score(0)
        chinese = Score(0)
        english = Score(0)
    
        def __init__(self, name, math, chinese, english):
            self.name = name
            self.math = math
            self.chinese = chinese
            self.english = english
    
    
        def __repr__(self):
            return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                    self.name, self.math, self.chinese, self.english
                )
    

    实现的效果和前面的一样,可以对数据的合法性进行有效控制(字段类型、数值区间等)

    image2

    以上,我举了下具体的实例,从最原始的编码风格到 Property ,最后引出描述符。由浅入深,一步一步带你感受到描述符的优雅之处。

    通过此文,你需要记住的只有一点,就是描述符给我们带来的编码上的便利,它在实现保护属性不受修改属性类型检查 的基本功能,同时有大大提高代码的复用率。


    1.17.2 描述符的访问规则

    描述符分两种:

    • 数据描述符:实现了__get__ 和 __set__ 两种方法的描述符
    • 非数据描述符:只实现了__get__ 一种方法的描述符

    你一定会问,他们有什么区别呢?网上的讲解,我看过几个,很多都把一个简单的东西讲得复杂了。

    其实就一句话,数据描述器和非数据描述器的区别在于:它们相对于实例的字典的优先级不同

    如果实例字典中有与描述器同名的属性,如果描述器是数据描述器,优先使用数据描述器,如果是非数据描述器,优先使用字典中的属性。

    这边还是以上节的成绩管理的例子来说明,方便你理解。

    # 数据描述符
    class DataDes:
        def __init__(self, default=0):
            self._score = default
    
        def __set__(self, instance, value):
            self._score = value
    
        def __get__(self, instance, owner):
            print("访问数据描述符里的 __get__")
            return self._score
    
    # 非数据描述符
    class NoDataDes:
        def __init__(self, default=0):
            self._score = default
    
        def __get__(self, instance, owner):
            print("访问非数据描述符里的 __get__")
            return self._score
    
    
    class Student:
        math = DataDes(0)
        chinese = NoDataDes(0)
    
        def __init__(self, name, math, chinese):
            self.name = name
            self.math = math
            self.chinese = chinese
    
        def __getattribute__(self, item):
            print("调用 __getattribute__")
            return super(Student, self).__getattribute__(item)
    
        def __repr__(self):
            return "<Student: {}, math:{}, chinese: {},>".format(
                    self.name, self.math, self.chinese)
    

    需要注意的是,math 是数据描述符,而 chinese 是非数据描述符。从下面的验证中,可以看出,当实例属性和数据描述符同名时,会优先访问数据描述符(如下面的math),而当实例属性和非数据描述符同名时,会优先访问实例属性(__getattribute__

    >>> std = Student('xm', 88, 99)
    >>>
    >>> std.math
    调用 __getattribute__
    访问数据描述符里的 __get__
    88
    >>> std.chinese
    调用 __getattribute__
    99
    

    讲完了数据描述符和非数据描述符,我们还需要了解的对象属性的查找规律。

    当我们对一个实例属性进行访问时,Python 会按 obj.__dict__ → type(obj).__dict__ → type(obj)的父类.__dict__ 顺序进行查找,如果查找到目标属性并发现是一个描述符,Python 会调用描述符协议来改变默认的控制行为。

    1.17.3 基于描述符如何实现property

    经过上面的讲解,我们已经知道如何定义描述符,且明白了描述符是如何工作的。

    正常人所见过的描述符的用法就是上篇文章提到的那些,我想说的是那只是描述符协议最常见的应用之一,或许你还不知道,其实有很多 Python 的特性的底层实现机制都是基于 描述符协议 的,比如我们熟悉的@property 、@classmethod 、@staticmethod 和 super 等。

    先来说说 property 吧。

    有了第一篇的基础,我们知道了 property 的基本用法。这里我直接切入主题,从第一篇的例子里精简了一下。

    class Student:
        def __init__(self, name):
            self.name = name
    
        @property
        def math(self):
            return self._math
    
        @math.setter
        def math(self, value):
            if 0 <= value <= 100:
                self._math = value
            else:
                raise ValueError("Valid value must be in [0, 100]")
    

    不防再简单回顾一下它的用法,通过property装饰的函数,如例子中的 math 会变成 Student 实例的属性。而对 math 属性赋值会进入 使用 math.setter 装饰函数的逻辑代码块。

    为什么说 property 底层是基于描述符协议的呢?通过 PyCharm 点击进入 property 的源码,很可惜,只是一份类似文档一样的伪源码,并没有其具体的实现逻辑。

    不过,从这份伪源码的魔法函数结构组成,可以大体知道其实现逻辑。

    这里我自己通过模仿其函数结构,结合「描述符协议」来自己实现类 property 特性。

    代码如下:

    class TestProperty(object):
    
        def __init__(self, fget=None, fset=None, fdel=None, doc=None):
            self.fget = fget
            self.fset = fset
            self.fdel = fdel
            self.__doc__ = doc
    
        def __get__(self, obj, objtype=None):
            print("in __get__")
            if obj is None:
                return self
            if self.fget is None:
                raise AttributeError
            return self.fget(obj)
    
        def __set__(self, obj, value):
            print("in __set__")
            if self.fset is None:
                raise AttributeError
            self.fset(obj, value)
    
        def __delete__(self, obj):
            print("in __delete__")
            if self.fdel is None:
                raise AttributeError
            self.fdel(obj)
    
    
        def getter(self, fget):
            print("in getter")
            return type(self)(fget, self.fset, self.fdel, self.__doc__)
    
        def setter(self, fset):
            print("in setter")
            return type(self)(self.fget, fset, self.fdel, self.__doc__)
    
        def deleter(self, fdel):
            print("in deleter")
            return type(self)(self.fget, self.fset, fdel, self.__doc__)
    

    然后 Student 类,我们也相应改成如下

    class Student:
        def __init__(self, name):
            self.name = name
    
        # 其实只有这里改变
        @TestProperty
        def math(self):
            return self._math
    
        @math.setter
        def math(self, value):
            if 0 <= value <= 100:
                self._math = value
            else:
                raise ValueError("Valid value must be in [0, 100]")
    

    为了尽量让你少产生一点疑惑,我这里做两点说明:

    1. 使用TestProperty装饰后,math 不再是一个函数,而是TestProperty 类的一个实例。所以第二个math函数可以使用 math.setter 来装饰,本质是调用TestProperty.setter 来产生一个新的TestProperty 实例赋值给第二个math
    2. 第一个 math 和第二个 math 是两个不同 TestProperty 实例。但他们都属于同一个描述符类(TestProperty),当对 math 对于赋值时,就会进入 TestProperty.__set__,当对math 进行取值里,就会进入 TestProperty.__get__。仔细一看,其实最终访问的还是Student实例的 _math 属性。

    说了这么多,还是运行一下,更加直观一点。

    # 运行后,会直接打印这一行,这是在实例化 TestProperty 并赋值给第二个math
    in setter
    >>>
    >>> s1.math = 90
    in __set__
    >>> s1.math
    in __get__
    90
    

    对于以上理解 property 的运行原理有困难的同学,请务必参照我上面写的两点说明。如有其他疑问,可以加微信与我进行探讨。

    1.17.4 基于描述符如何实现staticmethod

    说完了 property ,这里再来讲讲 @classmethod 和 @staticmethod 的实现原理。

    我这里定义了一个类,用了两种方式来实现静态方法。

    class Test:
        @staticmethod
        def myfunc():
            print("hello")
    
    # 上下两种写法等价
    
    class Test:
        def myfunc():
            print("hello")
        # 重点:这就是描述符的体现
        myfunc = staticmethod(myfunc)
    

    这两种写法是等价的,就好像在 property 一样,其实以下两种写法也是等价的。

    @TestProperty
    def math(self):
        return self._math
    
    math = TestProperty(fget=math)
    

    话题还是转回到 staticmethod 这边来吧。

    由上面的注释,可以看出 staticmethod 其实就相当于一个描述符类,而myfunc 在此刻变成了一个描述符。关于 staticmethod 的实现,你可以参照下面这段我自己写的代码,加以理解。

    image3

    调用这个方法可以知道,每调用一次,它都会经过描述符类的 __get__ 。

    >>> Test.myfunc()
    in staticmethod __get__
    hello
    >>> Test().myfunc()
    in staticmethod __get__
    hello
    

    1.17.4 基于描述符如何实现classmethod

    同样的 classmethod 也是一样。

    class classmethod(object):
        def __init__(self, f):
            self.f = f
    
        def __get__(self, instance, owner=None):
            print("in classmethod __get__")
    
            def newfunc(*args):
                return self.f(owner, *args)
            return newfunc
    
    class Test:
        def myfunc(cls):
            print("hello")
    
        # 重点:这就是描述符的体现
        myfunc = classmethod(myfunc)
    

    验证结果如下

    >>> Test.myfunc()
    in classmethod __get__
    hello
    >>> Test().myfunc()
    in classmethod __get__
    hello
    

    讲完了 propertystaticmethodclassmethod 与 描述符的关系。我想你应该对描述符在 Python 中的应用有了更深的理解。对于 super 的实现原理,就交由你来自己完成。

    1.17.5 所有实例共享描述符

    若要合理使用描述符,利用描述符给我们带来的编码上的便利。有一个坑,需要注意,比如下面这个Student我们没有定义构造函数

    class Score:
        def __init__(self, default=0):
            self._value = default
    
        def __get__(self, instance, owner):
            return self._value
    
        def __set__(self, instance, value):
            if 0 <= value <= 100:
                self._value = value
            else:
                raise ValueError
    
    
    class Student:
        math = Score(0)
        chinese = Score(0)
        english = Score(0)
    
        def __repr__(self):
            return "<Student math:{}, chinese:{}, english:{}>".format(self.math, self.chinese, self.english)
    

    看一下会出现什么样的问题,std2 居然共享了std1 的属性值,因为它被当成了一个类变量了,而每个实例都没有自己的实例变量,自然访问的是同一个变量。这样是很难达到我们使用描述符的初衷。

    >>> std1 = Student()
    >>> std1
    <Student math:0, chinese:0, english:0>
    >>> std1.math = 85
    >>> std1
    <Student math:85, chinese:0, english:0>
    >>> std2 = Student()
    >>> std2 # std2 居然共享了std1 的属性值
    <Student math:85, chinese:0, english:0>
    >>> std2.math = 100
    >>> std1 # std2 也会改变std1 的属性值
    <Student math:100, chinese:0, english:0>
    

    而正确的做法应该是,所有的实例数据只属于该实例本身(通过实例初始化传入),具体写法可参考上一节。

    http://python-online.cn/zh_CN/latest/c01/c01_17.html

  • 相关阅读:
    Coursera机器学习week11 单元测试
    关于 TypeReference 的解释
    getModifiers 方法解释。
    instanceof isInstance isAssignableFrom 比较
    elasticsearch 基础 语法总结
    kibana 启动 关闭 和进程查找
    MD5 SHA1 SHA256 SHA512 SHA1WithRSA 的区别
    spring boot 项目 热启动
    java zip 压缩文件
    Packet for query is too large (1660 > 1024). You can change this value on the server by setting the max_allowed_packet' variable.
  • 原文地址:https://www.cnblogs.com/leijiangtao/p/4161168.html
Copyright © 2011-2022 走看看