python--基础知识点--

    技术2025-04-02  24

    一、属性查找策略

    1. python 属性

    属性:python中,对象的方法也可以认为是属性,所以下面所说的属性包含方法在内。使用dir()列出对象所有有效属性。

    属性分类:属性可以分为两类,一类是Python自动产生的,如__class__,__hash__等,另一类是我们自定义的。我们只关心自定义属性。

    类和实例对象(实际上,Python中一切都是对象,类是type的实例)都有__dict__属性,里面存放它们的自定义属性(对与类,里面还存放了别的东西)。

    # 示例 class Test: def __init__(self): self.name = "zhangsan" def a(self): pass test = Test() print(Test.__dict__) print(dir(test)) print(test.__dict__) """ 运行结果: {'__module__': '__main__', '__init__': <function Test.__init__ at 0x0000019D6E109C80>, 'a': <function Test.a at 0x0000019D6E109BF8>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None} ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'name'] {'name': 'zhangsan'} """

    有些内建类型,如list和string,它们没有__dict__属性,随意没办法在它们上面附加自定义属性。

    2、描述符

    (1) 初步概念理解

    查找属性时,如obj.attr,如果Python发现这个属性attr有个__get__方法,Python会调用attr的__get__方法,返回__get__方法的返回值,而不是返回attr(这一句话并不准确,我只是希望你能对descriptor有个初步的概念)。

    (2) 迭代器 VS 描述符

    (i) 迭代器是实现了iterator协议的对象,也就是说它实现了下面两个方法__iter__和__next__。类似的,描述符是实现了descriptor协议的对象,也是实现了某些特定方法的对象。descriptor的特定方法是__get__,__set__和__delete__,其中__set__和__delete__方法是可选的。

    (ii) iterator必须依附某个对象而存在(由所依附的对象的__iter__方法返回)。descriptor也必须依附某个对象,作为所依附对象的一个属性,而不能单独存在。

    (iii) 还有一点,descriptor必须存在于类的__dict__中(跟属性查找策略有关),这句话的意思是只有在类的__dict__中找到属性,python才会去看看它有没有__get__等方法,对一个在实例的__dict__中找到的属性,Python根本不理会它有没有__get__等方法,直接返回属性本身。

    descriptor到底是什么呢:简单的说,descriptor是对象的一个属性,只不过它存在于类的__dict__中并且有特殊方法__get__(可能还有__set__和__delete)而具有一点特别的功能,为了方便指代这样的属性,我们给它起了个名字叫descriptor属性。

    (3) 详解描述符
    (i) 函数原型
    __get__(self, instance, owner) __set__(self, instance, value) __delete__(self, instance)
    (ii) 参数说明

    self:指当前Descriptor的实例。

    instance:指拥有Descriptor实例对象作为属性的对象。

    owner:指实现obj的类。

    value:指"="右边所需赋的值

    (iii) 触发调用方式

    obj.attr obj.attr = value del obj.attr

    [以上attr都是Descriptor类的实例对象]

    # 示例 class Descriptor(object): def __get__(self, obj, type=None): print("obj:", obj) print("type:", type) return 'get', self, obj, type def __set__(self, obj, val): print('set', self, obj, val) def __delete__(self, obj): print('delete', self, obj) class Test: descriptor = Descriptor() # 只能定义为类属性,不能定义为实例属性,因为属性查找策略,在实例中的属性只能被认为是普通属性。定义为实例属性时不会有任何返回结果。 def __init__(self): self.name = "zhangsan" test = Test() test.descriptor """ 运行结果: obj: <__main__.Test object at 0x0000020CF6064080> type: <class '__main__.Test'> Process finished with exit code 0 """

    这里__set__和__delete__其实可以不出现,不过为了后面的说明,暂时把它们全写上。

    如果将descriptor(Descriptor实例对象)定义为实现obj的类的类属性,然后直接通过实现obj的类访问descriptor,此时obj是None,type就是实现obj的类本身。也就是说__get__的参数obj只接收实例对象,不接收类对象,对于类对象默认为None。

    # 直接通过实现obj的类访问descriptor class Descriptor(object): def __get__(self, obj, type=None): print("obj:", obj) print("type:", type) return 'get', self, obj, type def __set__(self, obj, val): print('set', self, obj, val) def __delete__(self, obj): print('delete', self, obj) class Test: descriptor = Descriptor() def __init__(self): self.name = "zhangsan" Test.descriptor """ 运行结果: obj: None type: <class '__main__.Test'> Process finished with exit code 0 """

    设置属性时,test.descriptor = value,实际上调用descriptor.__set__(test, value),Test.descriptor = value,这是真正的赋值,test.descriptor的值从此变成value。删除属性和设置属性类似。

    iv) data descriptor VS non-data descriptor

    data descriptor:同时具有__get__和__set__方法的descriptor。

    non-data descriptor:只有__get__方法的descriptor。

    容易想到,由于non-data descriptor没有__set__方法,所以在通过实例对属性赋值时,例如上面的test.descriptor = ‘hello world!!!’,不会再调用__set__方法,会直接把test.descriptor的值变成’hello world!!!’。

    class Descriptor(object): def __get__(self, obj, type=None): print("obj:", obj) print("type:", type) return 'get', self, obj, type class Test: descriptor = Descriptor() def __init__(self): self.name = "zhangsan" test = Test() print(test.descriptor) test.descriptor = "hello world!!!" print(test.descriptor) """运行结果: obj: <__main__.Test object at 0x0000028517AF84A8> type: <class '__main__.Test'> ('get', <__main__.Descriptor object at 0x0000028517AF8400>, <__main__.Test object at 0x0000028517AF84A8>, <class '__main__.Test'>) hello world!!! Process finished with exit code 0 """

    在实例上对non-data descriptor赋值隐藏了实例上的non-data descriptor!

    3、属性查找策略
    (1) 获取属性值时属性查找策略 。对于obj.attr(注意:obj可以是一个类):

    (i) 如果attr是一个Python自动产生的属性,找到!(优先级非常高!)

    (ii) 查找obj.__class__.__dict__,如果attr存在并且是data descriptor,返回data descriptor的__get__方法的结果,如果没有继续在obj.__class__的父类以及祖先类中寻找data descriptor

    (iii) 在obj.__dict__中查找,这一步分两种情况,第一种情况是obj是一个普通实例,找到就直接返回,找不到进行下一步。第二种情况是obj是一个类,依次在obj和它的父类、祖先类的__dict__中查找,如果找到一个descriptor就返回descriptor的__get__方法的结果,否则直接返回attr。如果没有找到,进行下一步。

    (iv) 在obj.__class__.__dict__中查找,如果找到了一个descriptor(插一句:这里的descriptor一定是non-data descriptor,如果它是data descriptor,第二步就找到它了)descriptor的__get__方法的结果。如果找到一个普通属性,直接返回属性值。如果没找到,进行下一步。

    (v) 如果实现obj的类中定义了__getattr__方法,则会调用__getattr__方法;如果没有定义则很不幸,Python终于受不了。在这一步,它raise AttributeError。

    __getattribute__:属性访问拦截器,就是当类的实例对象的属性被访问时,会自动调用类的__getattribute__方法。所以以上属性查找过程,也是解释器默认调用__getattribute__后的默认的属性查找过程。 __getattribute__详解-请点击

    (2)对属性赋值时的查找策略 。对于obj.attr = value:

    (i) 查找obj.__class__.dict,如果attr存在并且是一个data descriptor,调用attr的__set__方法,结束。如果不存在,会继续到obj.__class__的父类和祖先类中查找,找到 data descriptor则调用其__set__方法。没找到则进入下一步。

    (ii) 直接在obj.__dict__中加入obj.__dict__[“attr”] = value

    class Descriptor(object): def __get__(self, obj, type=None): return 'get', self, obj, type class Test: descriptor = Descriptor() def __init__(self): self.name = "zhangsan" test = Test() print(test.descriptor) """ 通过赋值时的属性查找策略没有找到满足要求的descriptor属性,之后会为test添 加一个名为descriptor的普通属性,但Test的类属性descriptor依然存在。 """ test.descriptor = "hello world!!!" print(test.descriptor) print(test.__dict__) print(Test.descriptor) """ 运行结果: ('get', <__main__.Descriptor object at 0x0000019899BD8438>, <__main__.Test object at 0x0000019899BD8470>, <class '__main__.Test'>) hello world!!! {'name': 'zhangsan', 'descriptor': 'hello world!!!'} ('get', <__main__.Descriptor object at 0x0000019899BD8438>, None, <class '__main__.Test'>) Process finished with exit code 0 """

    在test的__dict__里出现了descriptor这个属性。根据对属性赋值的查找策略,第1步,确实在test.class.__dict__也就是Test.__dict__中找到了属性descriptor,但它是一个non-data descriptor,不满足data descriptor的要求,进入第2步,直接在test的__dict__属性中加入了属性和属性值。

    当获取test.descriptor时,执行获取属性值时的查找策略,第2步在Test.__dict__中找到了descriptor,但它是non-data descriptor,步满足要求,进行第3步,在test的__dict__中找到了descriptor这个普通属性,直接返回了它的值’hello world!!!’。

    二、描述符的使用场景

    场景:属性校验

    1、基本的调用 set 和get 方法
    class Student: def __init__(self): self.age = 0 def get(self): return self.age def set(self, age): if isinstance(age, int) and 0 < age < 100: self.age = age else: print("请输入合法的年龄") stu = Student() stu.set(110) # 请输入合法的年龄 stu.set(10) print(stu.get()) # 10 """ 运行结果: 请输入合法的年龄 10 Process finished with exit code 0 """

    有的小伙伴该说了,你这太低级了,现在都是用property 了

    2、利用@property

    对,正确的做法就是 用**@property 装饰器**,可以把方法封装成属性。

    class Student: def __init__(self): self.value = 0 @property def age(self): return self.value @age.setter def age(self, age): if isinstance(age, int) and 0 < age < 100: self.value = age else: print("请输入合法的年龄") stu = Student() stu.age = 110 # 请输入合法的年龄 stu.age = 10 print(stu.age) # 10 """ 运行结果: 请输入合法的年龄 10 Process finished with exit code 0 """

    恭喜你明白了@property 的用法。前面都是引子,现在开始我们的正题。

    现在又有一个场景: 像age属性 一共有十几个,你该如何做呢

    用@property 是可以做到,你知道你需要写多少方法吗,得写20多个,这代码量也实在太大了吧,这个难道不了我们的优秀的高级开发测试员。

    3.利用属性描述符

    属性描述符的原理利用的是抽象的方法, 把十几个字段共同的特性抽出来,每个字段都用这个特性,达到节省代码的目的。 看下边的例子:

    class IntValidation: def __init__(self): self.value = None def __get__(self, instance, owner): print(instance, owner) return self.value def __set__(self, instance, value): if isinstance(value, int) and 0 < value < 100: self.value = value else: print("请输入合法的年龄") def __delete__(self, instance): del instance.abc # 该语句没有任何意义,只是为了测试__delete__函数的用法 class Student: age = IntValidation() def __init__(self): self.abc = "zah" # 该语句没有任何意义,只是为了测试__delete__函数的用法 stu = Student() stu.age = 110 # 请输入合法的年龄 stu.age = 10 print(stu.age) # 10 print(stu.__dict__) del stu.age print(stu.__dict__) """ 运行结果: <__main__.Student object at 0x000001E178E18F60> <class '__main__.Student'> 10 {'abc': 'zah'} {} Process finished with exit code 0 """

    [参考博客] https://blog.csdn.net/sjyttkl/article/details/80655421 https://blog.csdn.net/qq_34979346/article/details/83758447 【多为拼凑整合,修改少部分内容】

    Processed: 0.009, SQL: 9