1. 程式人生 > >Week 9: 描述器及其應用

Week 9: 描述器及其應用

一、描述器

用到魔術方法__get__()、set()、delete()

1、用非資料描述器寫staticmethod裝飾器:

class StaticMethod:
    def __init__(self,fn):
        self.fn=fn

    def __get__(self,instance,owner):
        return self.fn

class A:
    @StaticMethod  # 要點一:等價式 foo=StaticMethod(foo),foo變成了StaticMethod類的例項
    def foo():
        print('static method called')

a=A()
a.foo()  # 要點二:例項呼叫的形式,反推出dec=a.foo,使用為dec()

# 執行結果:
static method called

2、用非資料描述器寫classmethod裝飾器

# 錯誤寫法:

class ClassMethod:
    def __init__(self,fn):
        self.fn=fn

    def __get__(self,instance,owner):
        return self.fn(owner)


class A:
    @ClassMethod  # 等價式 foo=ClassMethod(foo),foo變成了ClassMethod類的例項
    def foo(cls):
        print(cls,'class method called')

    def __repr__(self):
        return '< A >'

a=A()
a.foo   # 不加括號就已完成函式呼叫
A.foo

可考慮用偏函式將cls這個引數固定,返回的新fn其cls引數已固定,也就是說變成了一個無參函式、直接fn()就可實現呼叫,其中新fn需變成使用者輸入a.foo或A.foo返回的一個可呼叫物件

from functools import partial

class ClassMethod:
    def __init__(self,fn):
        self.fn=fn

    def __get__(self,instance,owner):
        return partial(self.fn,owner)


class A:
    @ClassMethod  # 等價式 foo=ClassMethod(foo),foo變成了ClassMethod類的例項
    def foo(cls):
        print(cls,'class method called')

a=A()
a.foo()
A.foo()

# 執行結果:
<class '__main__.A'> class method called
<class '__main__.A'> class method called

3、用資料描述器寫property屬性裝飾器

重點:例項的屬性對應一個方法時,不會有繫結效果

class Property:
    def __init__(self,fget,fset=None):
        self.fget=fget
        self.fset=fset

    def __get__(self,instance,owner): # instance: a, owner: A
        if instance is not None:
            # 例項調方法、要看這個方法是註冊在類上還是例項上,類上才有繫結效果,例項上則是普通的函式呼叫,需傳參
            return self.fget(instance) # 例項調例項的方法,沒有繫結效果,需手動傳第一引數
        return self

    def __set__(self,instance,value):
        if instance is not None:
            self.fset(instance,value)  # fset對應下面的data函式,需正常傳兩個引數

    def setter(self,fset): # fset對應data函式,是例項a的例項方法,呼叫時依然無繫結效果
        self.fset=fset
        return self

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

    @Property  # data=Property(data) 即Property例項
    def data(self):
        return self._data
    
    # data=data.setter(data) 即data=Property例項.setter(data),右邊返回data=Property例項
    # 如果data的返回值不是Property例項,相當於把data=Property(data)這個類屬性沖掉,a.data將不再對應描述器
    # 上下必須一致,保證data屬性是描述器,所以需設立setter方法
    @data.setter
    def data(self,value):
        self._data=value

# 使用方法,先例項化、傳參,再呼叫a.data,a是A的例項
# data是A的類屬性,對應另一個類Property的例項,Property實現了描述器協議
# data屬性現在是描述器,描述器一旦被訪問,即print(a.data),就呼叫__get__
a=A('jerry')
print(a.data)
# data本是對應另一個類的例項,如果沒有set方法就是覆蓋了
a.data='tom'
print(a.data)

分析:加了這兩個裝飾器後,A類中以下兩個函式的等價式均為:data=Property例項,且下面覆蓋上面

    @Property  
    def data(self):
        return self._data
    
    @data.setter
    def data(self,value):
        self._data=value

然後A類的定義就相當於:

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

原來的上下兩個data函式,分別被fset和fget記住,類屬性data等於資料描述器Property的例項,對類屬性data的訪問相當於對描述器的訪問

二、描述器應用

1、對實參進行例行檢查

# 有硬編碼的描述器

class TypeCheck:
    def __init__(self,name,type):
        self.name=name
        self.type=type

    def __get__(self,instance,owner):
        if instance is not None:
            return instance.__dict__[self.name]

    def __set__(self,instance,value):
        if not isinstance(value,self.type):
            raise TypeError()
        instance.__dict__[self.name]=value 


class Person:
    name=TypeCheck('name',str)
    age=TypeCheck('age',int)

    def __init__(self,name:str,age:int):
        self.name=name  # 只要初始化賦值,就送到描述器類的set處先攔截
        self.age=age

p1=Person('tom',18)
print(p1.__dict__)
print(p1.name)

改進:將硬編碼name=TypeCheck(‘name’,str) 和 age=TypeCheck(‘age’,int) 提到外面寫成一個函式裝飾器

class TypeCheck:
    def __init__(self,name,type):
        self.name=name
        self.type=type
        self.data={}

    def __get__(self,instance,owner):
        if instance is not None:
            #print(self.data)
            return self.data[self.name]
        return self

    def __set__(self,instance,value):
        if not isinstance(value,self.type):
            raise TypeError()
        self.data[self.name]=value

import inspect

def typeassert(cls):
    sig=inspect.signature(cls)
    params=sig.parameters
    for name,param in params.items():
    	if param.annotation != param.empty:
    		setattr(cls,name,TypeCheck(name,param.annotation))
    return cls

@typeassert
class Person:
    def __init__(self,name:str,age:int):
        self.name=name
        self.age=age

p1=Person('tom',18)
print(p1.__dict__)
print(p1.name,p1.age)

進一步改進:將這個函式裝飾器改成類裝飾器 要點1、等價式Person=TypeAssert(Person),Person變成了TypeAssert的例項; 要點2、實際呼叫時,p1=Person(‘tom’,18),也就是p1=TypeAssert例項(‘tom’,18),即TypeAssert的例項必須實現可呼叫

class TypeCheck:
    def __init__(self,name,type):
        self.name=name
        self.type=type
        self.data={}

    def __get__(self,instance,owner):
        if instance is not None:
            #print(self.data)
            return self.data[self.name]
        return self

    def __set__(self,instance,value):
        if not isinstance(value,self.type):
            raise TypeError()
        self.data[self.name]=value

import inspect

class TypeAssert:
	def __init__(self,cls):
		self.cls=cls
		sig=inspect.signature(cls)
		params=sig.parameters
		for name,param in params.items():
			if param.annotation != param.empty:
				setattr(cls,name,TypeCheck(name,param.annotation))
	
	def __call__(self,*args,**kwargs):
		return self.cls(*args,**kwargs) # 返回一個新的Person的例項

@TypeAssert
class Person:  # Person=TypeAssert(Person),Person變成了TypeAssert的例項
    def __init__(self,name:str,age:int):
        self.name=name
        self.age=age

p1=Person('tom',18)
print(p1.__dict__)
print(p1.name,p1.age)