描述器(Descriptor)
描述器(又称描述符)(Descriptor)
描述器:如果一个类中实现了__get__, __set__, __delete__
三个方法中的任何一个,那么这样的类的实例就称为描述器。 当某一个类的类属性是一个描述器的时候,通过这个类或者类的实例来访问、修改或删除这个类属性时,就会分别触发描述器的__get__, __set__和__delete__
方法。
class Descriptor():
def __get__(self, instance, owner):
return {'instance': instance, 'owner': owner, 'value': 12345}
glb_descriptor = Descriptor() # 在模块全局声明一个描述器
class A:
prop = glb_descriptor
class B:
def __init__(self):
self.prop = glb_descriptor # 由于描述器不是B类的类属性,所以不会起作用。
class C:
cls_descriptor = Descriptor() # 在类定义中声明一个描述器
def __init__(self):
# 如下三种方式都能将描述器的__get__返回内容添加到实例的__dict__属性中,但内容略有不同,主要区别在于instance不同。
self.prop = self.cls_descriptor # c.prop: {'instance': <__main__.C object at 0x00000235799B6210>, 'owner': <class '__main__.C'>, 'value': 12345}
# self.prop = C.cls_descriptor # c.prop: {'instance': None, 'owner': <class '__main__.C'>, 'value': 12345}
# self.prop = type(self).cls_descriptor # c.prop: {'instance': None, 'owner': <class '__main__.C'>, 'value': 12345}
a = A()
b = B()
c = C()
print('a.prop:', a.prop)
print('b.prop:', b.prop)
print('c.prop:', c.prop)
print('-' * 25, '分隔线', '-' * 25)
print(A.prop)
print(C.cls_descriptor)
print('-' * 25, '分隔线', '-' * 25)
print(a.__dict__) # a实例的__dict__没有内容
print(c.__dict__) # c实例的__dict__中存储了描述器__get__方法的返回值。因为在c的实例化过程中,`__init__`方法里就已经通过"实例.属性"(self.cls_descriptor)的方式触发了描述器的`__get__`方法。
输出结果:
a.prop: {'instance': <__main__.A object at 0x0000017589326190>, 'owner': <class '__main__.A'>, 'value': 12345}
b.prop: <__main__.Descriptor object at 0x00000175893256D0>
c.prop: {'instance': <__main__.C object at 0x0000017589326210>, 'owner': <class '__main__.C'>, 'value': 12345}
------------------------- 分隔线 -------------------------
{'instance': None, 'owner': <class '__main__.A'>, 'value': 12345}
{'instance': None, 'owner': <class '__main__.C'>, 'value': 12345}
------------------------- 分隔线 -------------------------
{}
{'prop': {'instance': <__main__.C object at 0x0000017589326210>, 'owner': <class '__main__.C'>, 'value': 12345}}
非数据描述器: 只实现了__get__
方法,未实现__set_
_和__delete_
_方法中的任何一个。这种情况下,实例优先访问自身的属性,如果不存在,再找描述器。
class Descriptor():
def __get__(self, instance, owner) :
return {'instance': instance, 'owner': owner, 'value': 12345}
class C:
cls_descriptor = Descriptor() # 在类定义中声明一个描述器
def __init__(self, prop):
self.cls_descriptor = prop # cls_descriptor是非数据描述器,prop值会保存在实例自己的同名属性中。
c = C('88888')
print('c.cls_descriptor:', c.cls_descriptor)
print('-' * 25, '分隔线', '-' * 25)
print(C.cls_descriptor)
print('-' * 25, '分隔线', '-' * 25)
print(c.cls_descriptor)
print(c.__dict__) # 由于是非数据描述器,给实例中与类属性描述器同名的属性赋值不会触发描述器,而是直接保存在实例属性中。
输出结果:
c.cls_descriptor: 88888
------------------------- 分隔线 -------------------------
{'instance': None, 'owner': <class '__main__.C'>, 'value': 12345}
------------------------- 分隔线 -------------------------
88888
{'cls_descriptor': '88888'}
数据描述器: 描述器实现了__set__或__delete__方法,此时通过实例访问属性时,优先触发描述器,而不是优先访问实例属性。
class Descriptor():
def __get__(self, instance, owner) :
return {'instance': instance, 'owner': owner, 'value': 12345}
def __set__(self, instance, value):
instance.papapa = value
class C:
cls_descriptor = Descriptor() # 在类定义中声明一个描述器
def __init__(self, prop):
self.cls_descriptor = prop # 类属性cls_descriptor是描述器,通过实例访问这个属性会触发描述器的__set__方法,而不会在实例中存储一个同名属性。
c = C('88888')
print('c.cls_descriptor:', c.cls_descriptor)
print('-' * 25, '分隔线', '-' * 25)
print(C.cls_descriptor)
print('-' * 25, '分隔线', '-' * 25)
print(c.cls_descriptor)
print(c.__dict__) # 由于cls_descriptor是数据描述器,所以实例在初始化时`self.cls_descriptor = prop`实际上会触发描述器的`__set__`方法,这样,实例自身的`__dict__`中并没有成功填加与描述器同名的属性。。
输出结果:
c.cls_descriptor: {'instance': <__main__.C object at 0x000001BBE5075B90>, 'owner': <class '__main__.C'>, 'value': 12345}
------------------------- 分隔线 -------------------------
{'instance': None, 'owner': <class '__main__.C'>, 'value': 12345}
------------------------- 分隔线 -------------------------
{'instance': <__main__.C object at 0x000001BBE5075B90>, 'owner': <class '__main__.C'>, 'value': 12345}
{'papapa': '88888'}
下面为了说明属性查找的优先级,我直接通过实例的__dict__字典来添加实例属性,但在通过实例.属性的方式来查找属性时,仍然会显示调用描述器的结果。
属性查找顺序: 数据描述器(通过mro查找,即包含父类) > 实例属性 > 非数据描述器(通过mro查找,即包含父类) > 普通类属性(通过mro查找,即包含父类)
class Descriptor():
def __get__(self, instance, owner) :
return {'instance': instance, 'owner': owner, 'value': 12345}
def __set__(self, instance, value):
instance.papapa = value
class C:
cls_descriptor = Descriptor() # 在类定义中声明一个描述器
def __init__(self, prop):
self.cls_descriptor = prop # 类属性cls_descriptor是描述器,通过实例访问这个属性会触发描述器的__set__方法,而不会在实例中存储一个同名属性。
c = C('88888')
c.__dict__['cls_descriptor'] = '这是实例自己的属性' # 为了不触发描述器的__set__方法,直接通过实例的__dict__字典为其添加实例属性。
print('c.cls_descriptor:', c.cls_descriptor) # 这里仍然触发了描述器方法,而不是显示实例自身的属性的值
print('-' * 25, '分隔线', '-' * 25)
print(C.cls_descriptor)
print('-' * 25, '分隔线', '-' * 25)
print(c.cls_descriptor)
print(c.__dict__) # 这里保存了实例初始化时触发描述器时生成的属性,以及直接通过__dict__字典添加的实例属性
输出结果:
c.cls_descriptor: {'instance': <__main__.C object at 0x000002189035BAD0>, 'owner': <class '__main__.C'>, 'value': 12345}
------------------------- 分隔线 -------------------------
{'instance': None, 'owner': <class '__main__.C'>, 'value': 12345}
------------------------- 分隔线 -------------------------
{'instance': <__main__.C object at 0x000002189035BAD0>, 'owner': <class '__main__.C'>, 'value': 12345}
{'papapa': '88888', 'cls_descriptor': '这是实例自己的属性'}
__getattribute__魔术方法的等价实现:
__getattribute__
方法是用c实现的,如非必需要,尽量不要覆写此方法。此方法的内部实现也体现了python内部属性访问机制的复杂性。 但其内部本身不包含对 __getattr__
方法的调用。即,只有当__getattribute__
返回AttributeError时, __getattr__
方法才会被触发。
如下为官方文档中使用python写的__getattribute__
的内部机制的模拟实现。注意,这里只针对通过实例对象访问属性的情况,通过类对象及super()来访问的情况不在此代码中实现。
def find_name_in_mro(cls, name, default):
"Emulate _PyType_Lookup() in Objects/typeobject.c"
for base in cls.__mro__:
if name in vars(base):
return vars(base)[name]
return default
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = find_name_in_mro(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
下面是我写的一个类似的等价实现,当然没有官方写的好。
def find_in_mro(cls_obj, name):
for cls in cls_obj.mro():
if name in vars(cls):
return cls, vars(cls)[name]
else:
return None, None
def object_getattribute(obj, name):
cls_obj = type(obj)
cls_prop, prop_obj = find_in_mro(cls_obj, name) # 查找类属性
if prop_obj: # 存在同名类属性
_, descriptior_get = find_in_mro(type(prop_obj), '__get__')
_, descriptior_set = find_in_mro(type(prop_obj), '__set__')
_, descriptior_del = find_in_mro(type(prop_obj), '__delete__')
if descriptior_get: # 类属性是描述器
if descriptior_set or descriptior_del: # 类属性是数据描述器
return descriptior_get(prop_obj, obj, cls_obj)
else: # 类属性是 非数据描述器
if name in vars(obj): # 存在同名实例属性
return vars(obj)[name]
else: # 不存在同名实例属性
return descriptior_get(prop_obj, obj, cls_obj)
else:
if name in vars(obj): # 存在同名实例属性
return vars(obj)[name]
else:
return prop_obj
else: # 不存在同名类属性
if name in vars(obj): # 存在同名实例属性
return vars(obj)[name]
else: # 类和实例中都未找到该属性
raise AttributeError(name)
下面是对自己写的方法的一个测试:
class Descriptor:
def __get__(self, instance, owner):
print('描述器 __get__ 被调用')
return '描述器get返回值'
def __set__(self, instance, value):
print('描述器 __set__ 被调用')
pass # 让其成为 数据描述器
class A:
cls_prop = Descriptor()
pass
class B(A):
b_cls_prop = 'value of b_cls_prop'
pass
输出结果: 如果注释掉描述器的__set__
方法,结果将会不同。
描述器 __get__ 被调用
描述器get返回值
{}
------------------------- 分隔线 -------------------------
描述器 __set__ 被调用
描述器 __get__ 被调用
描述器get返回值
{}
------------------------- 分隔线 -------------------------
描述器 __get__ 被调用
描述器get返回值
value of b_cls_prop
描述器实现ORM
我们常用的ORM模块,比如sqlalchemy,也使用了描述器来实现。 下面是我写的一个模拟实现代码:
import sqlite3
con = sqlite3.connect('example.db')
cur = con.cursor()
# Create table
# cur.execute('''CREATE TABLE scores
# (study_no number, name text, english number, math number)''')
#
# cur.execute("INSERT INTO scores VALUES ('002','Harry',90,95)")
# cur.execute("INSERT INTO scores VALUES ('002','Harry',85,95)")
# cur.execute("INSERT INTO scores VALUES ('003','Cong',95,85)")
# con.commit()
# for row in cur.execute('SELECT * FROM scores ORDER BY study_no'):
# print(row)
# 定义描述器,意义为表的字段
class Field():
def __set_name__(self, owner, name):
self.col_name = name
self.table = owner.table
def __get__(self, instance, owner):
query_statement = f'''select {self.col_name} from {self.table} where {owner.key} = \'{instance.key}\''''
con.execute(query_statement)
return cur.fetchone()[0]
def __set__(self, instance, value):
update_statement = f'''update {self.table} set {self.col_name} = ? where {type(instance).key} = ?'''
con.execute(update_statement, [value, instance.key])
con.commit()
# 数据模型 data model
class Score:
table = 'scores'
key = 'name'
english = Field()
math = Field()
def __init__(self, key):
self.key = key
print(Score('Roland').math)
Score('Roland').math = 100
print(Score('Roland').math)
输出结果:
95
100
当然,还是下面官方的例子更好:
定义描述器
class Field:
def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]
def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()
定义数据库表模型
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()
def __init__(self, key):
self.key = key
class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()
def __init__(self, key):
self.key = key
使用这个模拟ORM
import sqlite3
conn = sqlite3.connect('entertainment.db')
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'
>>> Song('Country Roads').artist
'John Denver'
>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'
描述器实现property类装饰器
property是python提供的内置类(C语言实现的),可以用于装饰器,将类中的方法变成属性。 有点类似于C Sharp语言中,类的属性都默认有get和set方法,用于实现复杂的计算逻辑,而不是简单的修改和返回一个固定值。
如下是我对property类的模拟实现过程:
定义Property类做为描述器的类。
class Property:
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, instance, owner):
return self.fget(instance)
def __set__(self, instance, value):
if self.fset:
self.fset(instance, value)
def __delete__(self, instance):
if self.fdel:
self.fdel(instance)
def setter(self, fset):
self.fset = fset
自定义一个类,类中包含要对属性进行get和set的方法。
将prop类属性设置为描述器,并将对属性的get和set方法的引用传递给描述器。
class Foo:
def __init__(self, name):
self.__name = name
def get_prop(self):
print(f'{self.__name} get_prop 被执行')
return self.__prop * 2
def set_prop(self, value):
print(f'{self.__name} set_prop 被执行')
self.__prop = value
prop = Property(get_prop, set_prop)
测试对属性的值的修改和读取
foo = Foo('Roland')
foo.prop = 10
print(foo.prop)
输出结果:
Roland set_prop 被执行
Roland get_prop 被执行
20
使用装饰器语法实现:
@Property相当于调用Property类的实例化方法,并把get_prop方法做为函数引用传递给__init_方法,生成一个描述器,变量get_prop将指向这个描述器。
@get_prop.setter,相当于调用了前一步创建的描述器的setter方法,而这个方法只是指这个set_prop函数引用保存到描述器对象中。
class Bar:
def __init__(self, name):
self.__name = name
@Property
def get_prop(self):
print(f'{self.__name} get_prop 被执行')
return self.__prop * 2
@get_prop.setter
def set_prop(self, value):
print(f'{self.__name} set_prop 被执行')
self.__prop = value
print(locals())
测试使用:
bar = Bar('bar la')
bar.get_prop = 11
print(bar.get_prop)
输出结果:
{'__module__': '__main__', '__qualname__': 'Bar', '__init__': <function Bar.__init__ at 0x0000020F45AF3B00>, 'get_prop': <__main__.Property object at 0x0000020F45AF4C90>, 'set_prop': None}
bar la set_prop 被执行
bar la get_prop 被执行
22
如下为官方的property类的模拟实现:
定义一个Property类,用于模拟property类的实现机制。 注意,这个类的getter和setter方法会返回不同的装饰器实例。
class Property:
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
self._name = ''
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"property '{self._name}' has no getter")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(f"property '{self._name}' has no setter")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"property '{self._name}' has no deleter")
self.fdel(obj)
def getter(self, fget):
prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def setter(self, fset):
prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def deleter(self, fdel):
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
prop._name = self._name
return prop
自定义一个类,使用装饰器来对get和set方法进行装饰,注意,官方这个模拟实现要求get与set方法必须使用相同的方法名。
@Property相当于使用实例化一个Property描述器,并将get_prop方法保存在描述器中。此时,变量名get_prop也就指向了这个描述器。
@get_prop.setter相当于调用了描述器的setter方法,而这个方法将返回一个新的描述器实例,而这个新的描述器实例同时保存了原来的get_prop方法以及新的set_prop方法。同时变量名get_prop转而指向了这个新的描述器。
class Bar:
def __init__(self, name):
self.__name = name
@Property
def get_prop(self):
print(f'{self.__name} get_prop 被执行')
return self.__prop * 2
@get_prop.setter
def get_prop(self, value):
print(f'{self.__name} set_prop 被执行')
self.__prop = value
print(locals())
测试使用效果:
bar = Bar('bar la')
bar.get_prop = 11
print(bar.get_prop)
输出结果:CSS
{'__module__': '__main__', '__qualname__': 'Bar', '__init__': <function Bar.__init__ at 0x0000026FA8413CE0>, 'get_prop': <__main__.Property object at 0x0000026FA8415010>}
bar la set_prop 被执行
bar la get_prop 被执行
22
描述器实现将类中定义的函数绑定为实例的方法
为什么类中声明的函数,在被类实例调用的时候,就自动将实例本身做为第一个参数传递给了函数的第一个参数(self)? 这背后的机制也是由描述器实现的。
最简单的示例
class Foo:
def bar(self, x):
print(f'self is {self}, x is {x}')
return x
print(Foo.bar('roland', 'abc'))
print('-' * 25, '分隔线', '-' * 25)
foo = Foo()
print(foo.bar('abc'))
输出结果:
self is roland, x is abc
abc
------------------------- 分隔线 -------------------------
self is <__main__.Foo object at 0x0000020D860752D0>, x is abc
abc
将函数声明在类的外部,然后用类属性指向这个函数
不难发现,其实将函数定义在类外部,完全可以达到相同的效果。
def fun(self, x):
print(f'self is {self}, x is {x}')
return x
class Foo:
bar = fun
print(Foo.bar('roland', 'abc'))
print('-' * 25, '分隔线', '-' * 25)
foo = Foo()
print(foo.bar('abc'))
输出结果:
self is roland, x is abc
abc
------------------------- 分隔线 -------------------------
self is <__main__.Foo object at 0x000001F5047E51D0>, x is abc
abc
模拟实现实例方法绑定机制
python内部已经给我们做好了这种将类函数绑定成实例方法的机制。如下我们来自己模拟一下这个机制,以深入了解其内部实现过程。
# 在类外部定义一个独立函数
def fun(self, x):
print(f'self is {self}, x is {x}')
return x
# 定义一个类,实现call方法,用于替换对函数的直接调用
class MethodType:
def __init__(self, func, instance):
self.func = func
self.instance = instance
def __call__(self, *args, **kwargs): # call方法调用时,直接调用原函数,并将函数所属类实例做为第一个参数传递给函数,其他参数也通过万能形参传进去。
return self.func(self.instance, *args, **kwargs)
# 定义一个描述器类
class Function:
def __init__(self, func): # 将真正的函数保存起来
self.func = func
def __get__(self, instance, owner):
if instance is None: # 如果该函数是由类直接调用,则直接返回该函数
return self.func
else: # 若函数是由所属的类的实例调用,则将其包装到一个MethodType类实例中,并转而使用该实例的call方法来代替直接调用函数
return MethodType(self.func, instance)
class Foo:
bar = Function(fun) # 实例化描述器,并把真正的函数保存在描述器中
print(Foo.bar('roland', 'abc')) # 类调用属性时,描述器的__get__方法判断其为类调用后,直接返回原函数
print('-' * 25, '分隔线', '-' * 25) # 类实例调用属性时,描述器的__get__方法判断将其先封装一个MethodType实例,然后调用其call方法来替代直接调用原函数。
foo = Foo()
print(foo.bar('abc'))
输出结果:
self is roland, x is abc
abc
------------------------- 分隔线 -------------------------
self is <__main__.Foo object at 0x00000210EA394A10>, x is abc
abc
改用装饰器语法也能达到同样效果
# 定义一个类,实现call方法,用于替换对函数的直接调用
class MethodType:
def __init__(self, func, instance):
self.func = func
self.instance = instance
def __call__(self, *args, **kwargs): # call方法调用时,直接调用原函数,并将函数所属类实例做为第一个参数传递给函数,其他参数也通过万能形参传进去。
return self.func(self.instance, *args, **kwargs)
# 定义一个描述器类
class Function:
def __init__(self, func): # 将真正的函数保存起来
self.func = func
def __get__(self, instance, owner):
if instance is None: # 如果该函数是由类直接调用,则直接返回该函数
return self.func
else: # 若函数是由所属的类的实例调用,则将其包装到一个MethodType类实例中,并转而使用该实例的call方法来代替直接调用函数
return MethodType(self.func, instance)
class Foo:
@Function
def bar(self, x):
print(f'self is {self}, x is {x}')
return x
print(Foo.bar('roland', 'abc')) # 类调用属性时,描述器的__get__方法判断其为类调用后,直接返回原函数
print('-' * 25, '分隔线', '-' * 25) # 类实例调用属性时,描述器的__get__方法判断将其先封装一个MethodType实例,然后调用其call方法来替代直接调用原函数。
foo = Foo()
print(foo.bar('abc'))
输出结果:
self is roland, x is abc
abc
------------------------- 分隔线 -------------------------
self is <__main__.Foo object at 0x000002189D9E4A10>, x is abc
abc
描述器实现内置classmethod装饰器
内置classmethod装饰器用法:
class Foo:
@classmethod
def test_class_method(cls, x):
return cls.__name__, x
foo = Foo()
print(foo.test_class_method('abc')) # 通过实例调用类方法
print(Foo.test_class_method('abc')) # 通过类调用类方法
输出结果:
('Foo', 'abc')
('Foo', 'abc')
使用装饰器模拟实现classmethod内置装饰器
class MethodType:
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
class ClassMethod:
def __init__(self, f):
self.f = f
def __get__(self, obj, cls=None):
return MethodType(self.f, cls)
class Foo:
@ClassMethod
def test_class_method(cls, x):
return cls.__name__, x
foo = Foo()
print(foo.test_class_method('abc')) # 通过实例调用类方法
print(Foo.test_class_method('abc')) # 通过类调用类方法
输出结果:
('Foo', 'abc')
('Foo', 'abc')
描述器实现内置staticmethod装饰器
内置staticmethod装饰器用法:
class Bar:
@staticmethod
def test_static_method(x):
return x
bar = Bar()
print(bar.test_static_method('abc')) # 通过对象调用静态方法
print(Bar.test_static_method('abc')) # 通过类调用静态方法
输出结果:
abc
abc
使用装饰器模拟实现staticmethod内置装饰器
class StaticMethod:
def __init__(self, f):
self.f = f
def __get__(self, instance, owner):
return self.f
def __call__(self, *args, **kwargs):
return self.f(*args, **kwargs)
class Bar:
@StaticMethod
def test_static_method(x):
return x
bar = Bar()
print(bar.test_static_method('abc')) # 通过对象调用静态方法
print(Bar.test_static_method('abc')) # 通过类调用静态方法
输出结果:
abc
abc