Python面向对象之描述器理解


Python面向对象编程中的魔术方法中的描述器,学习过程中有很多的疑惑,现在将自己的理解过程记录下来。

反射

关于描述器,python官方文档广义的放在了自定义属性访问小节中介绍,为了更清楚实例属性的访问顺序,所以先对反射相关的魔术方法复习一下。

概述

反射名字有点抽象,就是程序被加载到内存中运行时,动态的给实例或者类增加或者修改属性。区别于类或者实例定义时。

反射相关的函数和方法

内建函数含义
getattr(obj, name[,default])通过name返回obj的属性值,当属性不存在,返回default。如果没有default,则跑出AttributeError。name类型必须是str
setattr(obj, name, value)为obj添加属性。属性存在则覆盖,name类型必须是str
hasattr(obj, name)判断对象是否存在name属性, name类型必须是str
delattr(obj, name)删除name属性, 如果obj没有name属性,抛出AttributeError
class A:
    m = 100
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __repr__(self):
        return "{} {}".format(self.x, self.y)
    
a = A(1,2)
getattr(a, "x") # 1
getattr(a, "n", "default") # default  
setattr(a, "z", 100)
hasattr(a, "z") # True
delattr(a, "n") # AttributeError


# 动态为类绑定属性, 类属性的第一个参数会默认绑定到实例本身    
if not hasattr(A, "get"):
    setattr(A, "get", lambda self: self.x)
A.__dict__    

# 动态为实例绑定属性, 实例属性的第一个参数,不会绑定实例本身,仅仅是实例的一个属性,而这个属性是一个函数罢了
if not hasattr(a, "set"):
    setattr(a, "add", lambda self, other: self.x + other.x)
a.__dict__

这种动态增加属性的方式和装饰器修饰一个类、Mixin方式的差异?
这种动态增加属性的方式是运行时改变类和实例的访问,但是装饰器或mixin都是自定义时实现的,因此反射具有更大的灵活性。

反射相关的魔术方法

可以实现下列方法来实现自定义对类的实例属性访问

object.getattr(self, name)

name为属性名字
当默认属性访问因AttributeError而失败是被调用,此方法应该返回找到的属性值或者引发一个AttributeError异常

class A:
    def __init__(self):
        self.x = 100
        
    def __getattr__(self, item, default = None):
        if default is None:
            raise AttributeError
        return self.item
    
a = A()
a.x # 100
getattr(a, "y", "default") # default
a.y # AttributeError

属性会按照正常机制(所谓正常机制就是属性的继承关系)寻找,如果找到就不会调用__getattr__方法,否则就会调用__getattr__方法

属性的查找顺序:
instance.dict => instance.class.dict => 继承的祖先类,一直到object的__dict__ => 调用__getattr__方法

object.getattribute(self, name)

name为属性的名字
此方法会无条件的调用以实现对类实力属性的访问。

class A:
    def __init__(self):
        self.x = 100
        
    def __getattr__(self, item):
        return 123
    
    
    def __getattribute__(self, name):
        # return name
        raise AttributeError
    
a = A()
a.x  # 123

所有的实例属性访问,第一个都会调用__getattribute__方法,他阻止了属性的查找
该方法应该返回一个属性值或者抛出一个AttributeError异常。

  • 它的return值将作为属性查找的结果
  • 如果抛出AttributeError,则会调用__getattr__方法
class A:
    def __init__(self):
        self.x = 100
        
    def __getattr__(self, name):
        return 123
    
    def __getattribute__(self, name):
        return object.__getattribute__(self, name)

为了避免此方法中的无限递归,其实现应该总是调用具有相同名称的基类方法来访问他所需要的任何属性,例如object.getattribute(self, name)

object.setattr(self, name, value)

name为属性名称,value为要赋给属性的值
此方法在一个实例属性被尝试赋值定义时被调用,包括实例初始化时的属性定义。这个调用会取代正常机制(即将值保存到实例字典)。

class A:
    d = {}
    def __init__(self):
        self.x = 100
    
    def __getattr__(self, name):
        return self.d[name]
        
    def __setattr__(self, name, value):
        self.d[name] = value # 实例属性定义不会再装入实例__dict__中,而是装在类属性d中  
        
a = A()
setattr(a, "y", 100) 
a.d  # {'x': 100, 'y': 100}

可以拦截对实例属性的增加、修改操作,如果要设置生效,需要自己操作实例的__dict__

object.delattr(self, name)

name为属性名
可以阻止通过实现来删除实例属性的操作,与类无关

class A:
    def __init__(self):
        self.x = 100
        
    def __delattr__(self, name):
        print("can not delete!") # 定义一个与删除无关的操作即可实现阻止删除  
        
a = A()
del a.x  # can not delete!

描述器

描述器介绍

描述器是一种创建托管属性的方法。描述器具有诸多优点:保护属性不受修改、属性类型检查和自动更新某个依赖属性的值等。
通俗的讲,如果一个类实现了__get__、__set__、__delete__三个方法的任意一个方法就是描述器,实现了这三个中的某些方法,就只吃了描述器协议。

1.描述器定义

当一个描述器类的实例出现在一个其他类中的时候才会起作用(或者说描述器类的实例必须在所有者类或者某个上级类的字典中),这个其他类就是owner所有者类

2.实现描述器的方法

  • __get__(self, instance, owner)
    调用此方法以获取所有者类的属性(对于非数据描述器)或者实例属性访问(对于数据描述器)
    owner类调用自己的描述器属性来说,instance为None
  • __set__(self, instance, value)
    调用此方法以设置instance指定的所有者类的实例的属性为新值value
    定义或者修改实例属性会调用此方法
    实例化的时候如果触发描述器,也会调用此方法
  • __delete__(self, instance)
    调用此方法以删除instance指定所有者类的实例的属性
  • __set_name__(self, owner, name)
    python3.6新增的方法
    描述器类实例化时调用,但是描述器类创建的时候不会执行

instance为属主实例
owner为属主类
value为类属性值

3.数据描述器和非数据描述器

  • 只实现了__get__方法的描述器称为非数据描述器
  • 在非数据描述器的基础上,实现其他三个方法中的任意方法就是数据描述器
class A:  # 非数据描述器类
    def __init__(self):
        self.x = 100
        
    def __get__(self, instance, owner):
        return self.x
    
class B:  # 数据描述器类
    def __init__(self):
        self.x = 1000
    
    def __get__(self, instance, owner):
        return self.x
    
    def __set__(self, instance, value):
        self.x += 1000
        
class C:
    a = A() # 非数据描述器
    b = B() # 数据描述器
    def __init__(self, z):
        self.a = 1
        self.b = 2 # C类的实例初始化,因为给属性赋值定义,所以会调用数据描述器
        self.z = z
        
c = C(3)
C.a  # 100 # 实例调用类的属性描述器,调用a非数据描述器的\_\_get__方法
c.a  # 1
c.b  # 2000
c.b = 3  # 修改描述器同名属性值,会调用数据描述器的\_\_set__和\_\_get__方法  
c.b  # 3000
c.z  # 3 z只是个普通属性,所以会按照正常机制寻找属性值z,结果为3

注意:只有调用owner的类属性才会触发描述器,所以一个实例属性和类属性描述器重名,就触发描述器

  1. 对于属主实例instance,只有实例属性名和类的属性描述器同名时才会触发描述器
  2. 对于属主owner来说,直接调用类内属性描述器就可以直接触发描述器

4.属性访问优先级

  • 触发数据描述器的属性访问,会先调用描述器,然后再按照正常机制
  • 触发非数据描述器的属性访问, 会先按照正常机制查找属性,最后再调用描述器

使用实例对象访问属性时,都会先调用__getattribute__内建函数,优先级如下:
1.数据描述器
2.实例属性
3.非数据描述器
4.__getattr__

Python中的描述器

python的方法,包括staticmethod()和classmethod()都实现为非数据描述器,因此实例可以重新定义和覆盖方法

Property()函数实现为一个数据描述器,因此实例不能实现覆盖类属性的行为

一旦了解描述器的原理,结合之前描述器的知识,理解它们非常轻松

1. classmethod、staticmethod以及普通方法的非数据描述器的实现

1.staticmethod

class StaticMethod:  # 非数据描述器
    def __init__(self, fn):
        self.fn = fn
    
    def __get__(self, instance, owner):
        return self.fn
    
class A:
    
    @StaticMethod  # f = StaticMethod(f) # 相当于f为StaticMethod的实例,非数据描述器
    def f(*args, **kwargs):
        print("hello")

a = A()
a.f() # 调用了非数据描述器
  1. classmethod
from functools import partial

class ClassMethod:
    def __init__(self, fn):
        self.fn = fn
    
    def __get__(self, instance, owner):
        newfun = partial(self.fn, owner)
        return newfun # 将参数cls固定住
     
class B:
    @ClassMethod  # f = ClassMethod(f) 
    def f(cls, *args, **kwargs): # 经过装饰的f是其实类方法,而且是个非描述器
        print(cls, args, kwargs)
        # return (args, kwargs)
    
b = B()
b.f(1,2)  # 实例调用类描述器,触发描述器
  1. 普通方法实际上也是一个非数据描述器
class Common: # 将普通方法改成调用描述器实现
    def __init__(self, fn):
        self.fn = fn
    
    def __get__(self, insance, owner):
        print("Common")
        return partial(self.fn, self = insance)

class A:
    def __init__(self):
        self.age = 100    
        
    @Common  # fn = Common(fn) fn是一个非数据描述器  
    def fn(self):
        return self.age

2. property数据描述器的实现

class Property:
    def __init__(self, fn, fset=None, fdel=None):
        print("init====")
        self.fn = fn
        self.fset = fset
        self.fdel = fdel
        
    def __get__(self, instance, owner):
        print("get====")
        return self.fn(instance) # 只负责获取实例的属性,没有其他功能  
    
    def __set__(self, instance, value):
        print("set====")
        print(self.fset)
        self.fset(instance, value) # 调用原A 的设置属性函数,,将属性更改
        print("123123123")
    
    def __delete__(self, instance):
        print("delete====")
        self.fdel(instance)
        
        
    def setter(self, fset):
        print("setter===")
        self.fset = fset
        return self # 如果不返回self, x.setter的返回值为None, 
    # 相当于A的类属性为None,就不是数据描述器了,所以返回值必须为self
    
    def deleter(self, fdel):
        self.fdel = fdel
        return self # 
    
class A:
    def __init__(self, x):
        self.__x = x
        # self.x = 1000000
        
    @Property
    def x(self): # Property的实例 ,也是A的类属性描述器  
        return self.__x
    
    @x.setter # setter装饰完的了之后,必须还是个装饰器   
    def x(self, value):
        self.__x = value
    
    @x.deleter
    def x(self):
        del self.__x
    
    y = Property(lambda self: self.__x)
    

3. 对实例的数据进行校验

  • 要是想校验实例的数据,必须要构造实例
  • 目的:将不符合参数注解的参数的不保存在实例的字典中

三种思路:

  1. 写函数,在__init__中先检查,如果不合格,直接抛异常
    耦合度太高,只适合当前类使用
  2. 写装饰器,使用inspect完成
    缺点是:被装饰完的类再也不是之前的类,类属性不可以从外部调用
  3. 描述器

下面分别实现这三种思路:

# 1.写函数,在__init__中先检查,如果不合格,直接抛异常  

class A:
    def __init__(self, name:str, age:int):
        params = ((name,str), (age,int))
        if not self.check(params): # 为什么可以被调用
            raise TypeError()
        self.name = name
        self.age = age
        
    def check(self, params):
        for param, type in params:
            if not isinstance(param, type):
                return Fasle
            return True
        
a = A("john",18)
# 2.函数装饰器方式:  
import inspect

def decorator(cls):
    def wrapper(*args, **kwargs):
        sig = inspect.signature(cls)
        params = sig.parameters # 原始参数注解
        bind_value = sig.bind(*args, **kwargs) # 类实例化形参和实参的绑定关系
        for name, value in bind_value.arguments.items():
            if not isinstance(value, params[name].annotation):
                raise TypeError
        print(bind_value.arguments)
        return cls(*args, **kwargs) #返回值为实例对象, 被装饰完的类,再也不能使用之前的类的属性了
    return wrapper

@decorator
class A:
    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age

    def get(self): 
        return 123123123
a = A("1",2)
a.name
a.get()

最后使用描述器的方式实现参数检查

# 描述器方式:
# 肯定是个数据描述器  
# 思路整理:
import inspect
class Check:
    def __init__(self, name, typ):
        self.typ = typ
        self.name = name
    
    def __get__(self, instance, owner):
        if instance:
            return instance.__dict__[self.name]
    
    def __set__(self, instance, value):
        if isinstance(value, self.typ):
            instance.__dict__[self.name] = value
            
class B:
    
    def __call__(self, cls):
        sig = inspect.signature(cls)
        params = sig.parameters
        for name, v in params.items():  
            cls.name = Check(name, v.annotation)
            # __setattr__
            # cls.__setattr__()
            # setattr(cls, name, Check(name, v.annotation)) # 给cls增加类属性  
        return cls

@B()
class A:
#     name = Check("name", str) # 描述器
#     age = Check("age", int) # 描述器 ,硬编码,不好,搞一个类装饰器  
    def __init__(self, name:str, age:int):
        self.name = name  # 定义这里的属性会调用上面的描述器
        self.age = age # 定义这里的属性会调用上面的描述器  
    
    def get(self): 
        return 123123123
    
a = A("john", 20)
a.__dict__
a.name, a.age
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值