Python 25 面向对象进阶 描述符及应用 上下文管理

Python 25 面向对象进阶 描述符及应用 上下文管理

旧博客Python系列

描述符

将一个类属性指向一个类的调用,就是一个描述符.之后这个类属性的各项行为,将由这个类来代理.
描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),set(),delete()中的一个,这也被称为描述符协议.(正常使用的类是无须定义这三个方法的,是采取之前的attr和items系列方法来控制行为)
get():调用一个属性时,触发
set():为一个属性赋值时,触发
delete():采用del删除属性时,触发

描述符分两种:
一 数据描述符:至少实现了__get__()和__set__()
二 非数据描述符:没有实现__set__()

描述符的基本操作

# 建立一个满足描述符协议的类
class Foo: #在Python3中Foo是新式类,它实现了三种方法,这个类就被称作一个描述符
    def __get__(self, instance, owner):
        print('get方法被触发')
    def __set__(self, instance, value):
        print('set方法被触发')
    def __delete__(self, instance):
        print('delete方法被触发')

如何使用描述符,只需要将一个类属性指向描述符即可.

class Bar:
    x = Foo()

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


bar = Bar('test')
print(bar.x) # get方法被触发

bar.x = 4  # set方法被触发
print(bar.__dict__) 

del bar.x  # delete方法被触发

描述符的优先级

如果运行如下代码:

Bar.x = 30  # 没有触发描述符的动作

这是因为描述符有优先级,数据描述符的优先级小于类属性,所以直接设置类属性的时候,会覆盖原来的类属性,将x设置为常量30.可以理解为描述符是一个伪装的类属性,无法覆盖类属性的直接赋值,但在描述符存在的状态下,对描述符代理的类属性进行取值和删除是可以触发描述符动作的.
描述符的优先级如下:

类属性大于数据描述符 给类的属性直接赋值的时候,会覆盖数据描述符
数据描述符>实例属性 由于数据描述符是类属性,实例的属性没有的时候会去找类属性,因此数据描述符的属性要比实例属性高,即实例里边引用描述符,都会触发描述符操作/td>
实例属性>非数据描述符 其实函数就是一个非数据描述符对象,字符串也是,实例里如果有同名的属性,肯当会优先选择实例的,不管是赋值还是调用
非数据描述符>找不到(触发__getattr__) 如果所有描述符都找不到,实际上就是找不到该属性,会报错或者触发__getattr__
可见一个类内定义的所有属性,都是某种意义上的描述符(因为一切皆对象,所有的赋值,取值,删除都是对象的行为).一部分是数据描述符,一部分是非数据描述符,只不过一些有用户自定,称为描述符,一些系统内置,赋值的时候就称为属性或者方法.描述符的优先级,实际上就是自定义描述符和默认的字段行为的比较.

描述符的应用

描述符的标准应用

# 基本应用,解释如何使用描述符:
class Check:

    def __init__(self, key):
        self.key = key

    def __get__(self, instance, owner):
        print('__get__ is triggered: instance--->{}  owner--->{}'.format(instance, owner))
        return instance.__dict__[self.key]

    def __set__(self, instance, value):
        print('set is triggered: instance--->{}  owner--->{}'.format(instance, value))
        instance.__dict__[self.key] = value
            
    def __delete__(self, instance):
        print('__delete__ is triggered: instance --> {}'.format(instance))
        instance.__dict__.pop(self.key)


class People:
    name = Check('name')

    def __init__(self, name, age, salary):
        self.name = name  # 触发代理,不会把name加入到对象的__dict__中
        self.age = age
        self.salary = salary

    def show(self):
        print('My name is {}, my age is {}'.format(self.name, self.age))

p1 = People('cony', 4, 999999)

编写描述符的__set__方法并且使用的思路如下:
编写__set__函数:

顺序 操作内容
1 建立People类,定义了类属性name由class类进行代理,init定义了三个初始属性.
2 p1实例化传入三个参数,此时self.name 指向的是代理,由于数据类型代理优先于实例属性,因此p1的属性字典不包含name,而是把p1这个对象和name的值转交代理.此时查看p1的属性字典,会发现没有name键
3 是赋值操作,描述符类触发set方法;instance指的就是传入的对象p1,而value就是赋的值'cony'.拿到值之后,要去操作对象的属性字典(这里一定不能直接写成instance.name = value,因为数据描述符优先于实例属性,又会触发描述符导致无限循环),写入同名name和对应的值value
4 此时查看属性字典,发现三个项目都完备,新增了name键,值就是'cony'
此时还没有编写__get__方法,结果发现,即使p1的字典内已经有了'name':'cony'键值对,但是不管是外部p1.name还是内部的show方法取到的还是None,这是因为实例属性优先级低于数据描述符,如果不通过字典的形式,则去取p1.name实际上是触发了描述符的__get__函数,因为没有编写内容,自然显示为None,搞清楚这一步,就知道编写__get__方法与__set__方法类似,都要操作instance的字典,所以编写返回值,应该是p1的字典里由刚刚set方法写入的值.

设置好__get__属性之后的取值流程:

顺序 操作内容
1 p1.name去调用p1的name属性,虽然此时p1的属性字典里有name键,但优先级低于数据描述符,所以依然被描述符代理
2 描述符的get参数从p1属性字典内取到name对应的值,返回
3 p1.name返回属性字典内的值
4 结果看起来和直接p1.name相似,但内部却经过了描述符代理
__delete__方法也与之前的方法类似,由于del p1.name依然会由描述符代理触发__delete__方法,因此将name从p1的属性字典里去除即可. 最后发现键如果写死,每次就修改同样的字典键,如果可以将其作为一个参数传入,就很方便的可以修改对象的属性名称,所以最后用了初始化函数增加了一个self.key. 从外部看起来,p1的name属性与不使用描述符的情况下,行为完全相同,但其内部机制却都经过了描述符代理.因此就可以根据这个原理,采取各种各样的控制措施.

描述符的应用

描述符的应用非常广泛,如果要自行开发框架或者复杂程序,描述符是一定会用到的,下边是描述符作为装饰器的几种应用,顺便回头用面向对象的知识解释一下原来的知识.

类的装饰器

由于装饰器的本质是返回和传入的对象一致,所以装饰器也可以对类来起作用.
所以可以定义一些装饰器来修饰类.
带参的装饰器,可以在类开始的时候做各种工作,比较典型的就是给类添加一系列参数,如果把一系列参数和描述符联系起来,一个限定类的描述符可以这样来写:

# 不加装饰器之前的代码,通过描述符限定了三个属性
class Typed:

    def __init__(self, name, target_type):
        self.key_name = name
        self.t_type = target_type

    def __get__(self, instance, owner):
        return instance.__dict__[self.key_name]

    def __set__(self, instance, value):
        if isinstance(value, self.t_type):
            instance.__dict__[self.key_name] = value
        else:
            raise AttributeError

    def __delete__(self, instance):
        return instance.__dict__.pop(self.key_name)


class People:
    name = Typed('name', str)
    age = Typed('age', int)
    gender = Typed('gender', bool)

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

cony = People('cony', 32, True)

用一个带参装饰器一次性解决类型限定的问题:

class Typed:

    def __init__(self, name, target_type):
        self.key_name = name
        self.t_type = target_type

    def __get__(self, instance, owner):
        return instance.__dict__[self.key_name]

    def __set__(self, instance, value):
        if isinstance(value, self.t_type):
            instance.__dict__[self.key_name] = value
        else:
            raise AttributeError

    def __delete__(self, instance):
        return instance.__dict__.pop(self.key_name)


def type_check(**kwargs):
    def wrapper(cls):
        for x, y in kwargs.items():
            setattr(cls, x, Typed(x, y))
        return cls

    return wrapper


@type_check(name=str, age=int, gender=bool)
class People:

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


cony = People('cony', 32, True)

可见只要按照被装饰对象的init方法里的参数给出类型限定,直接用一个装饰器就可以达成效果,而且适合于各种类.

描述符实现静态属性

现在有一个类如下:

class Room:
    def __init__(self,width,length):
        self.width= width
        self.length = length

    @property      # 加上装饰器,实际上就是把area改成了一个属性
    def area(self):
        return self.width * self.length

来试着用描述符来玩一下静态属性:
装饰器这个时候注意,还可以是一个类:

class Myproperty:
    def __init__(self,func):
        print('Myproperty初始化运行')
        self.func = func

    def __get__(self, instance, owner):   # 2 知道了加上装饰器,就相当于描述符,所以加上一个__get__方法就可以返回想要的东西了.
        return self.func(instance)    # 由于init执行之后area方法已经被self.func接收,所以只要传area的运行结果就可以了,这时候要把instance传进去.

class Room:
    def __init__(self,name,width,length):
        self.width= width
        self.length = length
        self.name = name

    @Myproperty      # 1  加上装饰器,就是area = Myproperty(area),结果发现,这就是属性名 = 类对象,所以就是一个描述符.
    def area(self):
        return self.width * self.length


room = Room('test',1,10)
print(room.area)

这样就自己制作了一个相当于python内置的@property装饰器.
可见装饰器的@,其实就是一个var = func(var),如果func是一个高阶函数,就是装饰器,如果func是一个类,那么就成了描述符.这就是一切皆对象的统一之处.
对象调用描述符OK了,但是用类调用描述符的时候出错,这是因为用类调用的时候,__get__方法的instance是None.最好还是用实例运行.
那么用系统内置的property来测试一下,发现类调用的时候返回的是property对象,所以在get方法里加上一个判断,如果instance == None的时候,就返回self
所以最终自行编写类似@property的Myproperty描述符如下:

class Myproperty:
    def __init__(self, func):
        print('Myproperty初始化运行')
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return self.func(instance)  # 由于装饰过以后area方法已经被self.func接受,所以只要传area的运行结果就可以了,这时候要把instance传进去.


class Room:
    def __init__(self, name, width, length):
        self.width = width
        self.length = length
        self.name = name

    @Myproperty  # 加上装饰器,就是area = Myproperty(area),结果发现,这就是属性名 = 类对象,所以就是一个描述符.
    def area(self):
        return self.width * self.length


r1 = Room('test', 3.7, 4.1)
print(r1.area)   # 显示15.17
print(Room.area) # 显示<__main__.Myproperty object at 0x000001B562686AC8>

@property 研究

已经知道了@property其实就是一个描述符类.但是用@property装饰的静态属性是无法赋值的,如果要给静态属性赋值如何操作呢?

class Room:
    def __init__(self, name, width, length):
        self.name = name
        self.width = width
        self.length = length

    @property
    def area(self):
        return self.width * self.length


r1 = Room('alex', 4.3, 3.6)
print(r1.area)
r1.area = 16  # 报错,AttributeError: can't set attribute

这个时候就要在@property修饰的基础上,用@被装饰属性名.getter 和 @被装饰属性名.deleter:

class Room:
    def __init__(self, name, width, length):
        self.name = name
        self.width = width
        self.length = length

    @property
    def area(self):
        return self.width * self.length

    @area.setter
    def area(self,value):
        print('setter is triggered')

    @area.deleter
    def area(self):
        print('setter is triggered')


r1 = Room('alex', 4.3, 3.6)
print(r1.area)
r1.area = 16  # 触发了setter 下边修饰的同名函数
del r1.area   # 触发了deleter下边的同名函数

当然,这里是操作触发函数,并没有真正的实现赋值,这只是让针对静态属性的赋值和删除成为可能,具体如何操作要编写后续代码.
注意这三段的顺序必须这么摆放,而且必须要有最前边的@property及正常的函数,否则后边的setter和deleter无法生效.

上下文管理协议

在操作文件对象的时候,为了免去每次关闭文件的动作,写成如下:

with open('test.txt','r+') as file:

由于文件句柄是一个对象,那么可以想到,支持with进行上下文管理的对象,起内部一定有支持上下文协议的方法.所以open类内一定有支持with上下文的方法.

__enter__和__exit__方法

__enter__是进入上下文的方法,当有with语句时,触发该方法,该方法的返回值会被赋给 as 之后的变量. __exit__是结束上下文的方法,在with内的语句块执行完毕的时候,会触发__exit__函数,结束上下文管理过程.

# 定义自己的Open类
class Open:
    def __init__(self,name, open_type,encoding = 'utf-8'):
        self.open_type = open_type
        self.name = name
        self.encoding = encoding
    def __enter__(self):
        print('__enter__ is triggered')
        self.f = open(self.name,self.open_type,encoding=self.encoding)
        return self.f

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.f.close()
        print('__exit__ is tirggered')

# 用with来使用该类
with Open('table.xml','r+') as file:
    print(file.read())

这里要特别说的是__exit__方法.如果with之后的代码块内发生错误,会直接触发__exit__方法(相当于也退出了上下文).
如果__exit__的返回值设置为True,这个错误信息会被__exit__里的三个参数捕捉到;如果返回值不是True,这错误会报出来

class Open:
    def __init__(self,name, open_type,encoding = 'utf-8'):
        self.open_type = open_type
        self.name = name
        self.encoding = encoding
    def __enter__(self):
        print('__enter__ is triggered')
        self.f = open(self.name,self.open_type,encoding=self.encoding)
        return self.f

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.f.close()
        print(exc_type,exc_val,exc_tb,sep = '\n')
        print('__exit__ is tirggered')
        return True

with Open('table.xml','r+') as file:
    file.read()
    file.test  # 添加一条引起错误的信息

异常不会抛出,被__exit__捕捉到,记录到三个默认参数里

# 执行结果
__enter__ is triggered
<class 'AttributeError'>   # 异常类型,exc_type的值
'_io.TextIOWrapper' object has no attribute 'test'   # 异常值 exc_cal的值
<traceback object at 0x000001FA8A1A6248>  # 追溯信息 exc_tb
__exit__ is tirggered

with在后边的网络编程中经常会遇到,将一些每次都要做的内容写入__enter__和__exit__方法内,会方便很多.

当一个类定义了上下文协议方法,对对象也是可以使用with的:

class Open:
    def __init__(self, name, open_type, encoding='utf-8'):
        self.open_type = open_type
        self.name = name
        self.encoding = encoding

    def __enter__(self, name, open_type, encoding='utf-8'):
        print('__enter__ is triggered')
        self.f = open(self.name, self.open_type, encoding=self.encoding)
        return self.f

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.f.close()
        print(exc_type, exc_val, exc_tb, sep='\n')
        print('__exit__ is tirggered')
        return True

fa = Open('table.xml', 'r+')

with fa as f:
    print(f.read())

当然这也写不是太好,还是将with把类的初始化和类的__enter__方法在一起使用比较方便.

LICENSED UNDER CC BY-NC-SA 4.0
Comment