查看原文
其他

Python高级编程——描述符Descriptor超详细讲解(上篇之属性优先级)

草yang年华 机器学习与python集中营 2021-09-10

送你小心心记得关注我哦!!

进入正文

全文摘要

描述符descriptor,是python语言的重要特性,上上一个系列文章专门讲解python高级编程系列之装饰器decorator,从英文上看起来它们还的确有那么一点点像,不过它们确实也是有关系的,后面会讲解到。本次系列文章将带各位小伙伴深入详解python描述符descriptor,鉴于我自己的文章风格,我会和前面的文章一样,依然从它的诞生背景开始,到它的深入应用,由浅入深,逐层深入。

重新声明,python高级编程——描述符descriptor系列文章一共分为上、中、下、补充篇,本文讲解上篇,后续会陆续更新系列文章哦,请记得持续关注哦!文章偏长,阅读全文约30min。

上篇请参考:

全文目录

01 从python对象的__dict__属性开始说起

    1.1 类属性or实例属性

    1.2 子类属性or父类属性

    1.3 python对象的__dict__属性

02 python属性优先级的实验验证

    2.1 简单的实现代码

03 python的“属性拦截器”

    3.1 __getattibute__魔术方法

    3.2 __getattibute__方法的升级实现

    3.3 __getattibute__方法的使用陷阱

    3.4 __getattibute__方法实用总结

04 python高级编程——描述符descriptor(中篇)(预告


python描述符详解(上篇)



python的描述符descriptor,这是属于python高级编程的一些概念和实现方法,可能有很多的小伙伴还并没有用到过,甚至每听说过,但是在Python的面试过程中有可能会出现,究竟什么是python描述符,有什么作用,使用有什么意义,它的诞生背景是什么,很少有文章专门介绍这一块,有的文章介绍的太过粗浅,以至于看过之后依然不能够理解描述符的本质。鉴于此,我寻思着出一期专门讲解python描述符的系列文章,跟前面的python装饰器系列文章一样,因为涉及到的内容偏多,本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第一篇——上篇,介绍Python的对象的属性访问优先级与对象属性的简单控制(属性拦截器)。


01

从对象的__dict__属性开始说起

声明:本文所采用的面向对象设计并不是十分严格,所以很多可能觉得面向对象的设计不合理,本文仅仅借助简单的示例讲清楚一些python的语言特性。

01 对象的__dict__属性1.1 类属性or实例属性?


我们都知道,在Python的面向对象里面,他的灵活程度是比其他语言更加灵活的,为什么这么说呢?因为以下几点:

(1)python面向对象中,类属性,我既可以通过类名访问,也可以通过实例访问(这与C#不同,C#的静态属性只能够通过类访问,不能够通过实例访问)

(2)python中的类方法(@classmethod装饰的方法),我也可以通过类名访问,也可以通过实例访问(这与C#中的静态方法也不一样)

我们可以通过一个例子来说明,代码如下:

class Animal:
    name='老虎'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def eat(self):
        return '我需要吃东西!'

    @classmethod
    def sleep(cls):
        return '我需要睡觉'

a=Animal('老虎',5)   #构造对象
print(a.name)        
print(a.age)
print(a.eat())      #实例访问实例方法
print(a.sleep())    #实例也可以访问类方法哦!
#----------------------------------
print(Animal.name)
print(Animal.sleep())

上面的运行结果为:

老虎
5
我需要吃东西!
我需要睡觉
老虎
我需要睡觉


细心的小伙伴应该发现了一个问题,在Animal中我定义了两个属性name,一个是类属性、一个是实例属性,这里因为这二者的值都是“老虎”,我没有办法辨别,a.name 到底是输出的类属性还是实例属性啊,如果我们改一下,改成如下:

a=Animal('狮子',5)   #构造对象
print(a.name)     

发现运行结果为:

狮子 

得到的结论是,a.name 访问的是实例属性。那到底怎么判断呢?通过我的__dict__属性去判断。

01 对象的__dict__属性1.2 子类属性or父类属性?


先看一个例子,代码如下:

class Animal:
    name='老虎'
    def __init__(self,name='老虎',age=5):
        self.name=name
        self.age=age
    def eat(self):
        return '我需要吃东西!'

    @classmethod
    def sleep(cls):
        return '我需要睡觉'

class Dog(Animal):
    #name_='小狗'
    def __init__(self,age):
        self.age=age

d=Dog(8)
print(d.name)  
print(d.age)

运行结果为:

老虎  #说明子类继承了父类的类属性

8


这里通过两个简单的例子,说明了两种关系

关系一:类成员与实例成员的关系(包括属性和方法这两种成员)

关系二:父类和子类的关系

那这二者具体的关系到底是怎么样的呢?先告诉你答案吧!

对于关系一而言,实例是可以访问类的成员的,但是类不能访问实例成员;对于关系二而言,子类继承父类的类成员和实例成员。


01 对象的__dict__属性1.3 python对象的__dict__属性


上面的两个例子所反映出的实际上是一个“属性的优先级问题”,即什么样的属性优先访问。

Python一切皆对象,故而都有__dict__属性,包括对象实例、函数、类本身。我们可以通过对象的__dict__查看,它返回的是一个字典对象,对于下面的代码,

class Animal:
    name='老虎'
    def __init__(self,name='老虎',age=5):
        self.name=name
        self.age=age
        self.weight=200
    def eat(self):
        self.height=100
        return '我需要吃东西!'

    @classmethod
    def sleep(cls):
        return '我需要睡觉'

class Dog(Animal):
    def __init__(self,age):
        self.age=age

a=Animal()
d=Dog(8)
print(Animal.__dict__.keys())
print(a.__dict__.keys())
print(Dog.__dict__.keys())
print(d.__dict__.keys())

运行结果为:

dict_keys(['__module__', 'name', '__init__', 'eat', 'sleep', '__dict__', '__weakref__', '__doc__'])

dict_keys(['name', 'age', 'weight'])

dict_keys(['__module__', '__init__', '__doc__'])

dict_keys(['age'])


由此可见,实例a最先直接访问的属性就只有name,age,weight,并不包含类属性name,我们可以这么,name、age、weight是属于a的,但是类属性name是不属于a的。

那么,对象对属性的访问到底遵循怎样一个优先级呢?先告诉你答案!

①.实例属性

②.类属性

③.父类的类属性

④.__getattr__()方法(访问一个不存在的属性的时候发生的行为)

注意:这里的优先级只是最基本的,没有涉及到任何的属性设置和属性控制,后面还会讲到属性的优先级。


02

属性优先级的实验验证 

02 属性优先级的实验验证2.1 优先级的代码实现


根据上面的描述,我们来做一个实验,

 class Animal:
    name='老虎'
    def __init__(self):
        pass

class Dog(Animal):
    age=10
    def __init__(self,height):
        self.height=height
    def __getattr__(self,propertyname):
        if propertyname=='weight':
            return 200
        else:
            raise AttributeError

d=Dog(150)
print(d.height)
print(d.age)
print(d.name)
print(d.weight)
运行结果为: 150   #第一优先级访问,访问实例属性 10     #第二优先级访问,访问类属性 老虎  #第三优先级访问,访问父类的类属性 200   #第四优先级访问,如果这里改成d.sex,则会抛出异常总结:对象属性的访问优先级顺序为:①.实例属性 ②.类属性 ③.父类的类属性 ④.__getattr__()方法

注意:这里的优先级只是暂时的哦!!!后面还会讲到属性的控制与代理,情况还会更加复杂一点。


总结

对象属性的访问优先级顺序为:(这只是暂时的哦)

①.实例属性

②.类属性

③.父类的类属性

④.__getattr__()方法


03

python的“属性拦截器” 


概述

我们经常看见下面的一些魔术方法,比如__getattribute__、__getattr__、__setattr__、__delattr__,那么他们每一个函数代表什么意思呢,在什么样的使用场景下使用呢?他们到底有什么样的作用,使用这些方法有什么意义?本系列文章都将一一讨论,但是,限于篇幅,本文只讲第一个__getattribute__函数。

03 python的属性拦截器3.1 __getattribute__魔术方法


当一个属性被访问的时候发生的行为,称之为“属性拦截器”。其实这是对属性的一种高级控制,我们也常称之为“为属性绑定行为”

Python中只要定义了继承object的类,就默认存在属性拦截器,只不过是拦截后没有进行任何操作,而是直接返回。所以我们可以自己改写__getattribute__方法来实现相关功能,比如查看权限、打印log日志等

Python的属性访问方式很直观,使用点属性运算符。在新式类中,对对象属性的访问,都会调用特殊方法__getattribute__。__getattribute__允许我们在访问对象属性时自定义访问行为,但是使用它特别要小心无限递归的问题。

通过一个简单的例子来说明:

class Animal(object):
    run = '我会跑'
    def die(self):
        return '我会死'
class Dog(Animal):
    color='Blue'
    def __init__(self, name,age):
        self.name=name
        self.age = age
    # 重写__getattribute__。需要注意的是重写的方法中不能
    # 使用对象的点运算符访问属性,否则使用点运算符访问属性时,
    # 会再次调用__getattribute__。这样就会陷入无限递归。
    # 可以使用super()方法避免这个问题。
    def __getattribute__(self, key):
        print('调用了__getattribute__属性')
        return super(Dog, self).__getattribute__(key)
    def sound(self):
        return "汪汪汪"

dog=Dog('泰迪',4)
print(dog.name,end='\n------------------------------\n'#调用实例属性
print(dog.age,end='\n------------------------------\n')
print(dog.color,end='\n------------------------------\n'#调用方法属性
print(dog.run,end='\n------------------------------\n')   #调用父类的类属性
print(dog.sound(),end='\n------------------------------\n'#调用实例方法
print(dog.die(),end='\n------------------------------\n')  #调用父类的方法

运行结果为:

调用了__getattribute__属性

泰迪

------------------------------

调用了__getattribute__属性

4

------------------------------

调用了__getattribute__属性

Blue

------------------------------

调用了__getattribute__属性

我会跑

------------------------------

调用了__getattribute__属性

汪汪汪

------------------------------

调用了__getattribute__属性

我会死

------------------------------

从上面的例子中,我们可以看出,不管是访问对象的实例属性、类属性、父类的类属性、对象方法、父类方法,总是需要先过__getattribute__着一关的,即如果定义了__getattribute__魔法方法,他总是优先调用,要不怎么称之为“属性拦截器”呢。


03 python的属性拦截器3.2 __getattribute__方法的升级实现


事实上,关于__getattribute__的重写,并不是一定要按照上面的格式,他可以有更加灵活的方式去实现的,上面的拦截方式太简单了,不管我做什么都是打印同样一句话,这很没特点,我要使得调用不同的属性,显示不同的信息该怎么做呢?比如我也可以这样,将上面的__getattribute__函数体重新定义如下:

 def __getattribute__(self, key):
        if key=='name':
            print('name属性被调用了')
            return super(Dog, self).__getattribute__(key)
        elif key=='age':
            print('age属性被调用了')
            return super(Dog, self).__getattribute__(key)
        elif key=='color':
            print('color属性被调用了')
            return super(Dog, self).__getattribute__(key)
        elif key=='run':
            print('run属性被调用了')
            return super(Dog, self).__getattribute__(key)
        elif key=='sound':
            print('sound方法被调用了')
            return super(Dog, self).__getattribute__(key)
        elif key=='die':
            print('die方法属性被调用了')
            return super(Dog, self).__getattribute__(key)
        else:
            print('调用了__getattribute__属性')
            return super(Dog, self).__getattribute__(key)
    def sound(self):
        return "汪汪汪"

dog=Dog('泰迪',4)
print(dog.name,end='\n------------------------------\n'#调用实例属性
print(dog.age,end='\n------------------------------\n')
print(dog.color,end='\n------------------------------\n'#调用方法属性
print(dog.run,end='\n------------------------------\n')   #调用父类的类属性
print(dog.sound(),end='\n------------------------------\n'#调用实例方法
print(dog.die(),end='\n------------------------------\n')  #调用父类的方法
print(dog.height,end='\n------------------------------\n')  #调用一个不存在的属性

运行结果为:

name属性被调用了

泰迪

------------------------------

age属性被调用了

4

------------------------------

color属性被调用了

Blue

------------------------------

run属性被调用了

我会跑

------------------------------

sound方法被调用了

汪汪汪

------------------------------

die方法属性被调用了

我会死

------------------------------

调用了__getattribute__属性

Traceback (most recent call last):显示没有定义height属性。


注意:没一个条件分支一定要返回父类的__getattribute__方法哦,如果没有返回,会发现虽然打印了相关的提示信息,但是并没有返回属性或方法的值,而是返回的一个None。

03 python的属性拦截器3.3 __getattribute__魔术方法的使用陷阱


     def __getattribute__(self, key):
        if key=='sound':
            print('sound方法被调用了')
            return self.sound()
        else:
            print('调用了__getattribute__属性')
            return super(Dog, self).__getattribute__(key)
    def sound(self):
        return "汪汪汪"

比如改成如上代码:上面的代码运行并不会出现预期的结果,即

sound方法被调用了

汪汪汪


为什么呢?因为调用self.sound()方法的时候一定会先调用__getattribute__方法,然后__getattribute__里面又调用sound方法,然后又调用__getattribute__方法。。。。。。如此递归循环下去,而且没有退出机制,直到程序崩溃。

03 python的属性拦截器3.4 __getattribute__方法的实用总结


(1)一定要在每一个需要访问的属性里面设置返回值,否则会返回None,一般有两种做法:

即:return super(Dog, self).__getattribute__(key)这种形式或:return object.__getattribute__(self,key)

即返回父类的__getattribute__方法。

(2)不要再在__getattribute__方法的定义内部显示使用self.成员 这种方法,这样可能会造成无限递归,导致系统崩溃。


04

“描述符”下篇预告 


下一篇预告:

下面篇文章的重点是python类中属性访问、修改、删除的控制方法:

限于篇幅,关于python高级编程之描述符descriptor系列文章的第一篇预热就到这里,在本文还没有真正涉及到python的描述符,按照我一贯的风格,这里只是介绍python面向对象中属性的访问优先级,属性的控制等基础知识,关于python描述符,欢迎继续关注哦!

有兴趣的小伙伴持续关注哦!


推 荐 阅 读

程序员8大终极杀器,你get到几个?
Python高级编程——装饰器Decorator超详细讲解(补充篇——嵌套装饰器)
Python高级编程——装饰器Decorator超详细讲解(下篇之python闭包详解)

Python高级编程——装饰器Decorator超详细讲解(中篇)

Python高级编程——装饰器Decorator超详细讲解(上篇)


2018/12/07

Friday

小伙伴们,如果你耐心的看完了关于python描述符的系列文章(上篇),是不是有所收获呢?是不是对python复杂的属性访问搞得有点模糊不清?没关系,后面还有更加模糊的,不过没关系,当你静下心来,认真看完本系列文章,我相信一定会大有收获的。欢迎小伙伴们继续关注和支持。如果你有需要,就添加我的公众号哦,里面分享有海量资源,包含各类数据、教程等,后面会有更多面经、资料、数据集等各类干货等着大家哦,重要的是全都是免费、无套路分享,有兴趣的小伙伴请持续关注!











: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存