python--基础知识点--__get__、__set__、__delete__和描述符

一、属性查找策略

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__': , 'a': , '__dict__': , '__weakref__': , '__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: 

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: 

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: 
('get', <__main__.Descriptor object at 0x0000028517AF8400>, <__main__.Test object at 0x0000028517AF84A8>, )
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>, )
hello world!!!
{'name': 'zhangsan', 'descriptor': 'hello world!!!'}
('get', <__main__.Descriptor object at 0x0000019899BD8438>, None, )

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> 
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
【多为拼凑整合,修改少部分内容】

你可能感兴趣的:(python,#,基础知识点)