python描述器介紹

小賤_L發表於2020-10-02

簡介

描述器功能強大,應用廣泛,它可以控制我們訪問屬性、方法的行為,是@property、super、靜態方法、類方法、甚至屬性、例項背後的實現機制,是一種比較底層的設計,因此理解起來也會有一些困難。它們在 Python 內部被廣泛使用來實現自 2.2 版中引入的新式類

定義

一個描述器是一個包含 “繫結行為” 的物件,對其屬性的訪問被描述器協議中定義的方法覆蓋。這些方法有:__get__()__set__() 和 __delete__()。如果某個物件中定義了這些方法中的任意一個,那麼這個物件就可以被稱為一個描述器。

型別

  1. 描述符有__get__和__set__2個方法
  2. 非資料描述符只有一個__get__方法,通常用於方法。非資料描述符的優先順序低於例項屬性。

簡單例項

class RevealAccess(object):
    """A data descriptor that sets and returns values
       normally and prints a message logging their access.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print('Retrieving', self.name)
        return self.val

    def __set__(self, obj, val):
        print('Updating', self.name)
        self.val = val

class MyClass(object):
    x = RevealAccess(10, 'var "x"')
    y = 5

m = MyClass()
print("="*20)
print(m.x)
print("="*20)
m.x = 20
print(m.x)
print("="*20)
print(m.y)

輸出:

====================
Retrieving var "x"
10
====================
Updating var "x"
Retrieving var "x"
20
====================
5

上面這個例子中:

  • 建立m例項和普通類沒什麼區別,我們從m.x開始看
  • m.x是m例項呼叫了x這個類屬性,然而這個類屬性不是普通的值,而是一個描述器,所以我們從訪問這個類屬性變成了訪問這個描述器
  • 如果呼叫時得到的是一個描述器,python內部就會自動觸發一套使用機制
  • 訪問的話自動觸發描述器的__get__方法
  • 修改設定的話就自動觸發描述器的__set__方法
  • 這裡就是m.x觸發了__get__方法,得到的是self.value的值,在前面__init__中定義的為10
  • m.x = 20則觸發了__set__方法,賦的值20傳到value引數之中,改變了self.value的值,所以下一次m.x呼叫的值也改變了

描述器的訪問

整個描述器的核心是__getattribute__(),因為對像任何屬性的訪問都會呼叫到這個特殊的方法。這個方法被用來查詢屬性,同時也是你的一個代理,呼叫它可以進行屬性的訪問操作。
一般我們的類的__getattribute__()方法都是繼承自object,自己改寫__getattribute__()是很危險的,也會阻止正常的描述器呼叫。__getattribute__()的Python描述原型如下:

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
       return v.__get__(None, self)
    return v

 如果通過例項ins訪問描述器,由__getattribute__()轉化為:
type(ins).__dict__['attr'].__get__(ins, type(ins)
如果通過類Class訪問描述器,由__getattribute__()轉化為:
Class.__dict__['attr'].__get__(None, Class)

class Descriptor(object):
    def __init__(self):
        self.aaaa = 'anonymous'

    def __get__(self, instance, owner):
        print('instance: %s' % instance)
        print('owner: %s' % owner)
        print("Invoke __get__: %s" % self.aaaa)
        return self.aaaa
 
    def __set__(self, instance, name):
        print("invoke __set__: %s" % name)
        self.aaaa = name.title()
 
    def __delete__(self, instance):
        print("Invoke __delete__: %s" % self.aaaa)
        del self.aaaa


class Person(object):
    name = Descriptor()

# 通過類Person訪問
print(Person.name)
# instance: None
# owner: <class '__main__.Person'>
# Invoke __get__: anonymous
# anonymous

print(Person.__dict__['name'].__get__(None, Person))
# instance: None
# owner: <class '__main__.Person'>
# Invoke __get__: anonymous
# anonymous

user = Person()

# 通過例項user訪問, `owner`訪問描述器例項的物件。`instance`則是訪問描述器例項的例項
print(user.name)
# instance: <__main__.Person object at 0x7f88c5472dd0>
# owner: <class '__main__.Person'>
# Invoke __get__: anonymous
# anonymous

print(type(user).__dict__['name'].__get__(user, type(user)))
# instance: <__main__.Person object at 0x7f0873fb5d90>
# owner: <class '__main__.Person'>
# Invoke __get__: anonymous
# anonymous

user.name = 'jack'
# invoke __set__: jack

del user.name
# Invoke __delete__: Jack

另外通過super訪問,如SubPersonPerson的子類,super(SubPerson, subins).name)訪問通過subins.__class__.__mro__查詢到Person類,然後呼叫:
Person.__dict__['name'].__get__(subins, Person)

class SubPerson(Person):
    pass

subins = SubPerson()

print(subins.__class__.__mro__)
# (<class '__main__.SubPerson'>, <class '__main__.Person'>, <class 'object'>)

# 通過super訪問
print(super(SubPerson, subins).name)
# instance: <__main__.SubPerson object at 0x7f30b1537f28>
# owner: <class '__main__.Person'>
# Invoke __get__: anonymous
# anonymous

print(Person.__dict__['name'].__get__(subins, Person))
# instance: <__main__.SubPerson object at 0x7f30b1537f28>
# owner: <class '__main__.Person'>
# Invoke __get__: anonymous
# anonymous

訪問的優先順序

例項

上面提到例項ins訪問描述器,實際是由__getattribute__()訪問: type(ins).__dict__['attr'].__get__(ins, type(ins)
具體實現是依據這樣的優先順序是:資料描述器 > 例項屬性 > 非資料描述符 -> __getter__() 方法
如下,我們user.name = 'andy'我們通過例項對屬性name賦值,但由於資料描述器優先順序高於例項屬性。賦值操作被資料描器中的__set__方法截獲,我們在__set__忽略了重新賦值(當然也可以在其中更新賦值,但實質不是通過例項屬性繫結的方式)。易見例項user的屬性字典__dict__還是空的。

class Descriptor(object):
    def __init__(self, name):
        self.aaaa = name

    def __get__(self, instance, owner):
        print("Invoke __get__: %s" % self.aaaa)
        return self.aaaa
 
    def __set__(self, instance, name):
        print("invoke __set__, ignore assignment.")
 
    def __delete__(self, instance):
        print("Invoke __delete__: %s" % self.aaaa)
        del self.aaaa

class Person(object):
    name = Descriptor('jack')
user = Person()

print(user.name)
# Invoke __get__: jack
# jack
print(user.__dict__)
# {}

user.name = 'andy' # 例項屬性賦值
# invoke __set__, ignore assignment.

print(user.name)
# Invoke __get__: jack
# jack
print(user.__dict__)
# {}

 再看非資料描述器和例項屬性比較。user.name = 'andy'成功的把屬性name繫結到user.__dict__中。

class Descriptor(object):
    def __init__(self, name):
        self.aaaa = name

    def __get__(self, instance, owner):
        print("Invoke __get__: %s" % self.aaaa)
        return self.aaaa

class Person(object):
    name = Descriptor('jack')

user = Person()

print(user.name)
# Invoke __get__: jack
# jack
print(user.__dict__)
# {}

user.name = 'andy'

print(user.name)
# andy
print(user.__dict__)
# {'name': 'andy'}

如果通過類Class訪問描述器,由__getattribute__()訪問:Class.__dict__['attr'].__get__(None, Class)
優先順序是:類屬性 > 描述器。
通過類物件Person.name = 'andy'更新屬性name,並沒有進入到描述器的__set__方法中,而且Person.__dict__中的屬性name也由描述器<__main__.Descriptor object at 0x7f1a72df9710>更新為字串'andy'。可見類屬性的優先順序高於描述器。

class Descriptor(object):
    def __init__(self, name):
        self.aaaa = name

    def __get__(self, instance, owner):
        print("Invoke __get__: %s" % self.aaaa)
        return self.aaaa
 
    def __set__(self, instance, name):
        print("invoke __set__, ignore assignment.")
 
    def __delete__(self, instance):
        print("Invoke __delete__: %s" % self.aaaa)
        del self.aaaa

class Person(object):
    name = Descriptor('jack')

print(Person.__dict__)
# {'__module__': '__main__', 'name': <__main__.Descriptor object at 0x7f1a72df9710>, 
# '__dict__': <attribute '__dict__' of 'Person' objects>, '__doc__': None, '__weakref__': <attribute '__weakref__' of 'Person' objects>}
# Invoke __get__: jack

print(Person.name)
# jack
Person.name = 'andy'

print(Person.__dict__)
# {'__module__': '__main__', 'name': 'andy', '__dict__': <attribute '__dict__' of 'Person' objects>, 
# '__doc__': None, '__weakref__': <attribute '__weakref__' of 'Person' objects>}
print(Person.name)
# andy

類屬性 > 資料描述器 > 例項屬性 > 非資料描述符 > __getter__() 方法
如果有__getattribute__方法,當__getattribute__出現異常時可能會呼叫__getter__()

函式都是非資料描述器

類字典將方法儲存為函式。在類定義中,方法是用 def 或 lambda 這兩個建立函式的常用工具編寫的。方法與常規函式的不同之處僅在於第一個引數是為物件例項保留的。按照 Python 約定,例項引用稱為 self ,但也可以稱為 this 或任何其他變數名稱。

為了支援方法呼叫,函式包含 __get__() 方法用於在訪問屬性時將其繫結成方法。這意味著所有函式都是非資料描述器,當從物件呼叫它們時,它們返回繫結方法。在純 Python 中,它的工作方式如下:

class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

 描述器的使用

描述器就是屬性訪問的代理,通過描述器來訪問屬性,需要把描述器(例項)作為一個類的屬性(作為例項的屬性沒啥用),通過內部的__get__,__set__,__delete__方法處理對一個屬性的操作。

常規類方法建立描述器

class Descriptor(object):
    def __init__(self, name):
        self.aaaa = name

    def __get__(self, instance, owner):
        print("Invoke __get__: %s" % self.aaaa)
        return self.aaaa
 
    def __set__(self, instance, name):
        print("invoke __set__, ignore assignment.")
 
    def __delete__(self, instance):
        print("Invoke __delete__: %s" % self.aaaa)
        del self.aaaa

class Person(object):
    name = Descriptor('jack')

user = Person()

user.name = 'andy'
# invoke __set__, ignore assignment.
print(user.name)
# Invoke __get__: jack
# jack
del user.name
# Invoke __delete__: jack

使用property類建立描述器

class property(fget=None, fset=None, fdel=None, doc=None)fget是獲取屬性的函式,fset是設定屬性的函式,fdel是刪除屬性的函式,doc是這個屬性的文件字串。

class C:
    def __init__(self):
        self._x = None

    def getx(self):
        print('invoke getx')
        return self._x

    def setx(self, value):
        print('invoke setx')
        self._x = value

    def delx(self):
        print('invoke delx')
        del self._x

    x = property(getx, setx, delx, "I'm the 'x' property.")

ins = C()

ins.x = 'property'
# invoke setx

print(ins.x)
# invoke getx
# property

print(C.x.__doc__)
# I'm the 'x' property.

del ins.x
# invoke delx

使用 @property 裝飾器建立描述器

這種使用很廣泛,在python原始碼中經常遇見。

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

ins = C()

ins.x = 'property'

print(ins.x)
# property

print(C.x.__doc__)
# I'm the 'x' property.

del ins.x

Property純Python的等價實現

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError, "unreadable attribute"
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError, "can't set attribute"
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError, "can't delete attribute"
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

實現StaticMethod

非資料描述器 StaticMethod 的 Python版本:

class StaticMethod(object):
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

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

    def __get__(self, obj, objtype=None):
        return self.f

class E(object):

    @staticmethod
    def f(x):
          print(x)
    # f = staticmethod(f)

E.f(3)
# 3
E().f(3)
# 3

實現ClassMethod

非資料描述器 ClassMethod 的 Python版本:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

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

    def __get__(self, obj, klass=None):
        if klass is None:
               klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

class E(object):
    def f(klass, x):
        return klass.__name__, x
    f = classmethod(f)

print(E.f(3))
# ('E', 3)
print(E().f(3))
# ('E', 3)

print(vars(E))
# {'__module__': '__main__', 'f': <classmethod object at 0x028DAAF0>, 
# '__dict__': <attribute '__dict__' of 'E' objects>, '__weakref__': 
# <attribute '__weakref__' of 'E' objects>, '__doc__': None}

print(vars(E()))
# {}

 

參考連結

python 描述符解析

描述器使用指南

Python 描述符簡介

 

 

毫無理想而又優柔寡斷是一種可悲的心理。   -----培根

 

相關文章