个人吐血整理,完全弄懂描述符到底是神马

描述符

描述符的定义

描述符的作用是用来代理另外一个类的属性的 (必须把描述符定义成这个类的类属性,不能定义到构造函数中)

前面的铺垫-各个优先级

https://www.cnblogs.com/linhaifeng/articles/6204014.html#sr-toc-10
关于数据描述符和非数据描述以及他们和类属性实例属性之间的优先级请看上面的博文链接,这里不会赘述太多。这里只会分享一下我的心得和发现。

你只要知道这些:
在一般情况下,

一 描述符本身应该定义成新式类, 被代理的类也应该是新式类

二 必须把描述符定义成这个类的类属性,不能为定义到构造函数中

三 要严格遵循该优先级, 优先级由高到底分别是

  1. 类属性
  2. 数据描述符
  3. 实例属性
  4. 非数据描述符
  5. 找不到的属性触发__getattr__()

但这并不是完全正确,我在下面会将,请耐心看完

描述符的使用

描述符配合类的装饰器控制实例属性类型
  • 代码
class Typed:
    def __init__(self,name,expected_type):
        self.name=name
        self.expected_type=expected_type
    def __get__(self, instance, owner):
        print('get--->',instance,owner)
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        print('set--->',instance,value)
        if not isinstance(value,self.expected_type):
            raise TypeError('Expected %s' %str(self.expected_type))
        instance.__dict__[self.name]=value
    def __delete__(self, instance):
        print('delete--->',instance)
        instance.__dict__.pop(self.name)

def typeassert(**kwargs):
    def decorate(cls):
        print('类的装饰器开始运行啦------>',kwargs)
        for name,expected_type in kwargs.items():
            setattr(cls,name,Typed(name,expected_type))
        return cls
    return decorate
@typeassert(name=str,age=int,salary=float) #有参:1.运行typeassert(...)返回结果是decorate,此时参数都传给kwargs 2.People=decorate(People)
class People:
    def __init__(self,name,age,salary):
        self.name=name
        self.age=age
        self.salary=salary

#有参:1.运行typeassert(...)返回结果是decorate,此时参数都传给kwargs
# 2.People=decorate(People)

print('People-----',People.__dict__)  # 在decorate(People)装饰符的函数中加了数据装饰符属性
p1=People('egon',18,3333.3)
print('People.name',People.name)
print('p1-----',p1.__dict__) # 因为 instance.__dict__[self.name]=value的缘故,p1中也添加了这些实例属性
print('People-----',People.__dict__)
print(p1.name) # 因为数据装饰符的优先级大于实例属性,
                # 所有这里的name其实是描述符,然后调用了描述符中的get方法,
                # 返回了实例中的实例属性name
p1.name = 'das'
print(p1.name)

  • 运行结果为:
类的装饰器开始运行啦------> {'name': <class 'str'>, 'age': <class 'int'>, 'salary': <class 'float'>}
People----- {'__module__': '__main__', '__init__': <function People.__init__ at 0x0000024CE7A98730>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None, 'name': <__main__.Typed object at 0x0000024CE7A73588>, 'age': <__main__.Typed object at 0x0000024CE7A735F8>, 'salary': <__main__.Typed object at 0x0000024CE7A735C0>}
set---> <__main__.People object at 0x0000024CE7A73A58> egon
set---> <__main__.People object at 0x0000024CE7A73A58> 18
set---> <__main__.People object at 0x0000024CE7A73A58> 3333.3
get---> None <class '__main__.People'>
People.name <__main__.Typed object at 0x0000024CE7A73588>
p1----- {'name': 'egon', 'age': 18, 'salary': 3333.3}
People----- {'__module__': '__main__', '__init__': <function People.__init__ at 0x0000024CE7A98730>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None, 'name': <__main__.Typed object at 0x0000024CE7A73588>, 'age': <__main__.Typed object at 0x0000024CE7A735F8>, 'salary': <__main__.Typed object at 0x0000024CE7A735C0>}
get---> <__main__.People object at 0x0000024CE7A73A58> <class '__main__.People'>
egon
set---> <__main__.People object at 0x0000024CE7A73A58> das
get---> <__main__.People object at 0x0000024CE7A73A58> <class '__main__.People'>
das
  • 分析运行过程

1.首先这里是有参数的类修饰符,typeassert函数返回的是decorate函数,当编译到

@typeassert(name=str,age=int,salary=float) #有参:1.运行typeassert(...)返回结果是decorate,此时参数都传给kwargs 2.People=decorate(People)
class People:

时,执行的为

#有参:1.运行typeassert(...)返回结果是decorate,此时参数都传给kwargs
# 2.People=decorate(People)

2.在decorate函数中,将**kwargs中key转换为People类中的同名的数据描述符

setattr(cls,name,Typed(name,expected_type))

所以我们可以看到People打印出来的结果为

People----- {'__module__': '__main__', '__init__': <function People.__init__ at 0x0000024CE7A98730>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None, 'name': <__main__.Typed object at 0x0000024CE7A73588>, 'age': <__main__.Typed object at 0x0000024CE7A735F8>, 'salary': <__main__.Typed object at 0x0000024CE7A735C0>}

他是包含name,age,salary的,并且可以出现在类的__dict__中
3.实例化People类,创建p1实例,执行了init方法,但由于数据描述符的优先级大于实例属性,所以self.name,这个name其实是数据描述符。age和salary也是同理,所以会打印出如下信息:

set---> <__main__.People object at 0x0000024CE7A73A58> egon
set---> <__main__.People object at 0x0000024CE7A73A58> 18
set---> <__main__.People object at 0x0000024CE7A73A58> 3333.3

全都触发了,描述符类中的set方法,再看set方法

    def __set__(self, instance, value):
        print('set--->',instance,value)
        if not isinstance(value,self.expected_type):
            raise TypeError('Expected %s' %str(self.expected_type))
        instance.__dict__[self.name]=value

这里的instance指的就是传过来的p1实例对象,set中做的事就是让符合条件的数据编程p1的实例属性(我们都知道实例的__dict__中代表的是实例属性)

可能有人会问,传过来的参数那么多,set怎么就知道每个值对应的self.name呢?

这位同学犯了一个理解性错误,在decorate函数中,他是为每一个**kwargs中的都key创建了一个描述符,所有name.age.salary都只会对应一个描述符类的实例。self.name是他们自身的实例属性,有且只有
一个,当我们在init使用self.name = name时,它实际上只的就是name这个描述符实例。

经过set方法,p1中也会包含name,age,salary这样名称的实例属性,可以看出p1.__dict__的打印结果为:

p1----- {'name': 'egon', 'age': 18, 'salary': 3333.3}

而与此同时People类中还是会包含name,age,salary描述符类属性,可以看People.__dict__的打印结果:

People----- {'__module__': '__main__', '__init__': <function People.__init__ at 0x000001DBCD9E8730>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None, 'name': <__main__.Typed object at 0x000001DBCD9C3588>, 'age': <__main__.Typed object at 0x000001DBCD9C35F8>, 'salary': <__main__.Typed object at 0x000001DBCD9C35C0>}

之后当我们执行p1.name获取name时,因为数据描述符的优先级大于实例属性,所有获取的仍然是name数据描述符类,他仍然会执行get方法:

get---> <__main__.People object at 0x000001DBCD9C3A58> <class '__main__.People'>

而由于get方法的返回值是instance.dict[self.name],所有他返回的还是p1的实例属性name,于是就打印出:

egon

这里顺便插一句,get方法中的instance指的就是调用它的实例,owner指的就是这个描述符所在的类,所有当我们使用People.name直接调用时,instance的值就是None:

get---> None <class '__main__.People'>

而我们直接返回描述符实例:

People.name <__main__.Typed object at 0x000001DBCD9C3588>

叨叨几句,如果这里使用People.name = 'clearlove’会触发描述符中的set方法吗?答案是不会,因为类属性的优先级大于实例属性,他会在类的__dict__创建同名的name覆盖掉原来的name,同样,删除也不会触发__delete__,在使用类删除他只会把数据描述符当做类属性删除,所以最后不要用类去操作描述符

我们接着往下看,当我们执行到:

p1.name = 'das'

因为数据描述符的优先级大于实例属性,所以还是会触发set方法,打印:

set---> <__main__.People object at 0x000001AF6F043AC8> das

然后我们将p1.name打印出来又会触发get方法,打印出来

get---> <__main__.People object at 0x000001AF6F043AC8> <class '__main__.People'>

使用描述符自定制@porperty

到了这一步,我们算是对了描述符有了些基本的认识,但千万别以为自己已经掌握了描述符的应用,不信?我们看下面的例子:

class myproperty(object):
    def __init__(self, func):
        print('init....')
        self.name = func.__name__

    def __get__(self, instance, owner):
        print('执行了get', instance, owner)
        # print('in dict',instance.__dict__)
        if instance is None:
            return self
        if self.name not in instance.__dict__:
            raise AttributeError(instance, 'object no attribute', self.name)
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        print('执行了set', instance, value)
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        print('执行了delete')
        instance.__dict__.pop(self.name)

    def setter(self, func):
        print('执行了setter', func)
        return self

class People(object):
    # job = 6
    def __init__(self, name, age, salary):
        self.name = name
        self.age = age
        self.salary = salary

    @property  # city = property(city)
    def city(self):
        return self.__city  # 报错,因为会无限递归,所以porperty中最好使用私有变量

    @city.setter  # city = property(city).setter
    def city(self, value):
        self.__city = value

    @myproperty  # job = myproperty(job)
    def job(self):
        return self.__job

    print(job) #myproperty对象 <__main__.myproperty object at 0x0000025C69B33550>
    @job.setter  # job = job<myproperty object>.setter(job)
    def job(self, value):
        print('set __ job')
        self.__job = value

    print('-----2',job) # <__main__.myproperty object at 0x00000244592C3B38>
    def xxx(self):
        return 1

    def xxx(self, value):
        return 2

    def __repr__(self):
        return str(self.__class__)

p1 = People('张三', 18, 2000.00)
# p1.city = '上海'
# print(p1.city)

# print(p1.xxx)
print(People.__dict__)
print(p1.__dict__)
# print(p1.job)  # 如果myproperty是只实现了get方法的非数据描述符,那么p1.job会找到下面重载的job函数(他会覆盖上面的job函数),他相当于类属性,但是实例也可以调用
# 如果把重载job函数注释掉,那么他就会找到这个非数据描述符,并执行其中的get方法,而且因为get中没有设置返回值,所有打印出来是None
# 如果把重载job函数注释掉,在描述符类中添加set那么就会找到这个优先级较高的描述符,并执行相应的方法
# 如果重载job函数,那么即使在描述符类中添加set,也只会找到job函数,因为类属性的优先级大于描述符,但是调用类的方法()却是相当于调用非数据描述符
# 问题?如果说函数是非数据描述符的话,为什么这里的优先级会比数据描述符大呢
# 不管了,反正只要不同名就行了
# print(p1.xxx())
# print(p1.xxx(2))
p1.job = 'dsa'
print('p1-------',p1.job)
print('People------',People.job)
print(p1.__dict__)
print(People.__dict__)

他的运行结果为:

init....
<__main__.myproperty object at 0x000001FDE76B36A0>
执行了setter <function People.job at 0x000001FDE76D8840>
-----2 <__main__.myproperty object at 0x000001FDE76B36A0>
{'__module__': '__main__', '__init__': <function People.__init__ at 0x000001FDE76D86A8>, 'city': <property object at 0x000001FDE76ACE08>, 'job': <__main__.myproperty object at 0x000001FDE76B36A0>, 'xxx': <function People.xxx at 0x000001FDE76D88C8>, '__repr__': <function People.__repr__ at 0x000001FDE76D8840>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None}
{'name': '张三', 'age': 18, 'salary': 2000.0}
执行了set <class '__main__.People'> dsa
执行了get <class '__main__.People'> <class '__main__.People'>
p1------- dsa
执行了get None <class '__main__.People'>
People------ <__main__.myproperty object at 0x000001FDE76B36A0>
{'name': '张三', 'age': 18, 'salary': 2000.0, 'job': 'dsa'}
{'__module__': '__main__', '__init__': <function People.__init__ at 0x000001FDE76D86A8>, 'city': <property object at 0x000001FDE76ACE08>, 'job': <__main__.myproperty object at 0x000001FDE76B36A0>, 'xxx': <function People.xxx at 0x000001FDE76D88C8>, '__repr__': <function People.__repr__ at 0x000001FDE76D8840>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None}

这是我自己摸索着创建的描述符,并不是很完善,但依然可以发现许多有趣的现象,让我们来分析一下运行流程:

1.首先当编译到这里时:

    @myproperty  # job = myproperty(job)
    def job(self):
        return self.__job

实际上运行的是 job = myproperty(job),所以才会打印出init的信息,
我们把job打印出来可以看到:

<__main__.myproperty object at 0x000001F2F61B3550>
# 他其实就是一个myproperty描述符类对象

而当我们编译到这里时:

    @job.setter  # job = job<myproperty object>.setter(job)
    def job(self, value):
        print('set __ job')
        self.__job = value

他实际上执行的是:job = job.setter(job)
,是执行上面创建的myproperty对象的setter方法:所以这里我们需要在setter方法中返回自身的实例,好让job继续保持描述符对象的身份:

  def setter(self, func):
        print('执行了setter', func)
        return self

明确一点,当使用@area修饰一个class或者def时,他的运行流程其实是 class_name = area(class_name)

3.我们创建p1实例之后,到这一步我们把People.dictp1.__dict都打印出来看看

{'__module__': '__main__', '__init__': <function People.__init__ at 0x000001FDE76D86A8>, 'city': <property object at 0x000001FDE76ACE08>, 'job': <__main__.myproperty object at 0x000001FDE76B36A0>, 'xxx': <function People.xxx at 0x000001FDE76D88C8>, '__repr__': <function People.__repr__ at 0x000001FDE76D8840>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None}
{'name': '张三', 'age': 18, 'salary': 2000.0}

可以看到job是作为数据描述符放入到类的dict中的,并且只有一份,而p1的实例属性只有在init中创建的三个。

后面的p1.job = ‘dsa’ 就会触发set方法,打印出来就会出发get方法,而至于People.job,他会先去类属性当中找有没有job,没有就去数据描述符中找,找到了,就会触发get方法:

执行了set <class '__main__.People'> dsa
执行了get <class '__main__.People'> <class '__main__.People'>
p1------- dsa
执行了get None <class '__main__.People'>
People------ <__main__.myproperty object at 0x000001FDE76B36A0>

只有一点值得注意,对象调用时打印出来的是People类,但是测试发现确实是p1实例,这是个未解之谜,搞不懂也没事,可以忽略这段话

最后再次把实例和类的dict打印出来

{'name': '张三', 'age': 18, 'salary': 2000.0, 'job': 'dsa'}
{'__module__': '__main__', '__init__': <function People.__init__ at 0x000001FDE76D86A8>, 'city': <property object at 0x000001FDE76ACE08>, 'job': <__main__.myproperty object at 0x000001FDE76B36A0>, 'xxx': <function People.xxx at 0x000001FDE76D88C8>, '__repr__': <function People.__repr__ at 0x000001FDE76D8840>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None}

可以看到job出现在了实例属性中,这是因为get方法中做了处理,处理方式和上一个例子相同,这里就不再多赘述了。

但是我们发现,这样子子写,不会执行job函数中的方法,所有这不是正规的property,我在网上找了分正规的,如下:

class Property:
    def __init__(self, fget):
        self.fget = fget    # 为实例增加方法,这里的方法是未绑定实例的,不会自动传入实例self
        self.fset = None    # 同上,未绑定实例

    def __get__(self, instance, owner):
        if instance is not None:
            return self.fget(instance)  # 调用原方法,传入实例self
        return self

    def __set__(self, instance, value):
        self.fset(instance, value)  # 调用原方法,传入实例self和value

    def setter(self, func):
        self.fset = func  # 更新属性
        return self

class A:
    def __init__(self, data):
        self._data = data

    @Property  # data = Property(data) 描述符实例
    def data(self):
        return self._data

    @data.setter  # data = data.setter(data) 更新属性,并返回描述符实例
    def data(self, value):
        self._data = value

可以看到,他的思路和我们原来的大体相同,最核心的内容还是要把data转换成另一个类的实例(描述符),主要的区别是,他在这个类的实例中分别保存了自定义的getset方法,这样我们在调用描述符时,他其实就是调用的get/set方法,而后又在get/set方法中调用了之前保存的自定义方法,所以达到了和@property 相同的效果

到这里,我们必须明确的三个概念是:

1.使用装饰器的本质就是:

    @Property  # data = Property(data) 描述符实例
    @data.setter  # data = data.setter(data)

2.描述符的触发时机

a = A()
a.data   # 触发get
a.data = '111'  # 触发set
del a  # 触发delete

A.data  # 触发get
A.data = '222' # 不仅不会触发,还会同名的类属性data覆盖掉描述符
del A.data  # 不会触发,类只会将data当做类属性删掉

3.@property的本质

我们知道python中是不会重载函数的,后面编译的同名函数(包括类属性)会覆盖掉上面的,之所以@property看起来像是突破了这一限制,同时保留了data同名方法,但时机上却是将data转换成了一个描述符,这一点我们可以从打印类的dict中看到,他只会保留一个data,他就是描述符类的实例。而在这个实例中,保存了我们自定义的get/set方法,并分别放在了描述符类的get/set方法中,这样,就可以达到以假乱真的地步

实例属性的优先级补充

如果你看到了,这里,你已经是一个很有毅力的人了,但是,千万不要以为自己已经掌握python优先级的魔法,我们还没有得出最后的结论,因为我发现了这个例子。

class Person(object):
    address = 'zhejiang'
    def __init__(self, name):
        self.name = name
 
p1 = Person('Bob')
p2 = Person('Alice')
 
print 'Person.address = ' + Person.address  # zhejiang
 
p1.address = 'shanxi'
print 'p1.address = ' + p1.address  # shanxi
 
print 'Person.address = ' + Person.address   # zhejiang
print 'p2.address = ' + p2.address  # zhejiang

可见,千万不要在实例上修改类属性,它实际上并没有修改类属性,而是给实例绑定了一个实例属性。因为:

当实例属性和类属性重名时,实例属性优先级高,它将屏蔽掉对类属性的访问。

那么问题来了,不是说好的,类属性>数据描述符>实例属性>非数据描述符>找不到属性的getattr方法吗???

少年,我也很无奈啊,所以经过测试,得出以下结论:


在一般情况下,

一 描述符本身应该定义成新式类, 被代理的类也应该是新式类

二 必须把描述符定义成这个类的类属性,不能为定义到构造函数中

三 要严格遵循该优先级, 优先级由高到底分别是

  1. 类属性
  2. 数据描述符
  3. 实例属性
  4. 非数据描述符
  5. 找不到的属性触发__getattr__()

但是,当我们使用实例来调用时,如果这时候同时存在与实例属性同名的类属性或函数,实例属性的优先级大于一切!!!


如果你都沒有看懂以上的所有内容,那么也没有关系,你只需要在实际应用中牢记以下4点:

1.不要用类去调用描述符!!!
2.不要用实例去调用类属性!!!
3.类属性请不要用和(实例属性and描述符)相同的名字!!! # 注意这里的逻辑顺序
4.清楚的认识到描述符的本质就是用一个类的实例去充分充当另一个类中的属性(存放在类dict中),并且明白对象调用它时,get/set/delete的执行时机就可以了!!!

补充描述符应用,自定制@classmethod,@staticmethod

最后,补充描述符实现@classmethod,和@staticmethod的应用,可以先自己试一下,再看源码:

class myclassmethod(object):
    def __init__(self,func):
        self.func = func

    def __get__(self, instance, owner):
        def inner(*args, **kwargs):
            result = self.func(owner,*args,**kwargs)
            return result
        return inner

    def __set__(self, instance, value):
        pass

class mystaticmethod(object):
    def __init__(self,func):
        self.func = func

    def __get__(self, instance, owner):
        def inner(*args,**kwargs):
            result = self.func(*args,**kwargs)
            return result
        return inner

    def __set__(self, instance, value):
        pass

class People(object):
    job = 'cooker'

    def __init__(self,name,age,salary):
        self.name = name
        self.age = age
        self.salary = salary

    @classmethod
    def achievejob(cls,name):
        return name+cls.job

    @myclassmethod  # obtainjob = myclassmethod(obtainjob)
    def obtainjob(cls,name):
        return name+cls.job

    @staticmethod
    def gainAdress(city):
        return 'live in'+city

    @mystaticmethod
    def getAdress(city):
        return 'live in'+city


p1 = People('李白',20,66666.66)
print(p1.achievejob('tom')) # tomcooker
print(People.achievejob('tom')) # tomcooker

print(p1.obtainjob('faker')) # fakercooker
print(People.obtainjob('faker')) # fakercooker

print(p1.gainAdress('成都')) # live in成都
print(People.gainAdress('成都')) # live in成都

print(p1.getAdress('常州')) # live in常州
print(People.getAdress('常州')) # live in常州

打完收工

本篇是我研究了一天,吐血整理出来的,希望大家能不吝啬的点个赞,不是为了积分,只想收到来自您的鼓励和肯定,作为我日后继续发表优质文章的动力~~~~~0.0在这里插入图片描述

  • 31
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值