python描述器的研究和总结(__get__, __set__, ...)
概要
- 描述器
- @语法
- property
注意,代码中所有#的注释,为print输出结果;所有""""""的注释,为说明和结论文字
描述器
__get__, __set__, __delete__
class NoPermissionError(object):
def __str__(self):
return "Has No Permission, You Cannot Do That!"
class NoDataDescription(object):
def do_something(self):
return "Yes, I have do something."
def __str__(self):
return "==>NoDataDescription<=="
def __get__(self, instance, owner):
print(f"instance:{instance};owner:{owner}")
# instance:==>ForTest<==;owner:<class '__main__.ForTest'>
# :param instance: 调用该属性的对象, 在小例中就是ForTest实例obj
# :param owner: instance的类对象, 在小例中就是ForTest
if getattr(instance, "allowed", False):
return self
else:
return NoPermissionError()
class ForTest(object):
Attr = NoDataDescription()
def __init__(self):
self.allowed = False
def __str__(self):
return "==>ForTest<=="
"""
obj.allowed 是false
ForTest.Attr 只定义了__get__方法,并根据调用对象instance.allowed决定返回对象
"""
obj = ForTest()
print(obj.Attr)
# Has No Permission, You Cannot Do That!
obj.allowed = True
print(obj.Attr)
# ==>NoDataDescription<==
print(f"dict of ForTest: {ForTest.__dict__}")
# dict of ForTest: {
# '__module__': '__main__',
# 'Attr': <__main__.NoDataDescription object at 0x000002146D0A6D60>,
# '__init__': <function ForTest.__init__ at 0x000002146D110C10>,
# '__dict__': <attribute '__dict__' of 'ForTest' objects>,
# '__weakref__': <attribute '__weakref__' of 'ForTest' objects>,
# '__doc__': None}
print(f"dict of obj: {obj.__dict__}")
# dict of obj: {'allowed': True}
"""
首先看下ForTest类和obj对象的__dict__内容,发现Attr只出现在ForTest中,作为实例对象的obj并没有这个属性
:: __dict__是对象的属性字典,类的属性并不会出现在实例对象的属性字典中,因为类属性不是实例属性!
:: 这在后面会提到
从执行结果看
:: 作为ForTest的类属性的 Attr = NoDataDescription()
:: 在实例对象调用该属性时,执行了 NoDataDescription 的__get__方法
"""
class ForTestTwo(ForTest):
def __init__(self):
super(ForTestTwo, self).__init__()
self.Attr = "12345"
obj = ForTestTwo()
print(obj.Attr)
# 12345
print(obj.__dict__)
# {'allowed': False, 'Attr': '12345'}
"""
注意 obj.__dict__
:: 此时obj.__dict__中已经包含Attr
:: 当类实例有自己定义的与类属性同名的Attr属性时,再调用Attr,发现不再执行类属性Attr = NoDataDescription()的__get__方法
当类属性是一个定义了__get__方法时,该属性就成为了一个=描述器=,如果只定义__get__方法,则是 非数据描述器
当具有描述器作为类属性的类
- 没有定义与该属性同名的实例属性时,该类的实例调用该属性,就会执行该描述器的__get__方法
__get__方法返回的值就是调用的结果
- 定义了与该属性同名的实例属性时,该类的实例调用该属性,会直接返回实例属性
这里可以确定一个结论, python中使用.方式调用对象属性时,会优先从对象的__dict__中扫描,如果扫描不到,就会扫描
对象的类的__dict__
至于__get__和描述器, 还要进一步验证
"""
下面引入两个新的魔法方法__set__, __set_name__
class DataDescription(object):
def __get__(self, instance, owner):
return "DataDescription with String Format"
def __set__(self, instance, value):
print(f"{instance} Set Value {value}")
instance.More = value
instance.String = value
def __set_name__(self, owner, name):
print(f"Owner:{owner}, name: {name}")
# Owner:<class '__main__.ForTest'>, name: Desc
class ForTest(object):
Desc = DataDescription()
def __init__(self):
self.More = "=None="
self.Desc = ">Just String"
def __str__(self):
return "==>ForTest Obj<=="
# Owner:<class '__main__.ForTest'>, name: Desc
obj = ForTest()
# ==>ForTest Obj<== Set Value >Just String
print(obj.Desc)
# DataDescription with String Format
print(obj.More)
# >Just String
print(obj.String)
# >Just String
print(obj.__dict__)
# {'More': '>Just String', 'String': '>Just String'}
"""
ForTest对象累计操作了三个属性,Desc为类属性描述器, More为实例自己的实例属性,String为__get__方法中额外定义的属性
实例化对象obj时,在self.Desc = ">Just String"处触发描述器的__set__方法,在该方法中,进行两步操作
将对象More属性改为">Just String"
额外添加属性String为">Just String"
结果More属性,String属性均为修改的值
同时在__set_name__中发现调用者为ForTest. 说明
:: 描述器在这个例子中分两步被触发, 在被声明在ForTest时,触发__set_name__, 在__init__中被赋值时,触发__set__方法
:: 在__set_name__中,owner为描述器所被定义在的类,name为描述器被设定的变量名
结合上面的两个例子,得出结论
:: 定义了__get__\__set__方法的类为描述器,当被作为属性赋值给类的时候会触发__set_name__方法
:: 当实例对描述器同名的属性做修改时,会触发描述器的__set__方法
:: 当实例调用描述器同名的属性时,会触发__get__方法
"""
关于描述器,结论如下:
如果只定义了__get__方法, 如果
实例定义了自己的实例属性, 那么调用时会使用实例的属性
实例未定义自己的实例属性, 那么调用时会使用描述器
结合下面的__set__方法来看的话,由于只定义__get__方法,没有__set__方法,当实例定义与类描述器属性同名的属性时,
会直接将值作为实例属性定义在__dict__中, python.方式调用会优先扫描对象自己的__dict__,所以后续的调用都会是
新定义或修改后的值
如果定义了__set__方法,实例对同名属性做的任何定义和修改,都会触发描述器的__set__方法,该属性不会出现在实例的__dict__中
任何操作都是对类描述器属性的操作
定义了__set__方法的是数据描述器,未定义的是非数据描述器,如果是非数据描述器,当对类实例做重新赋值时,对象继承的描述器属性就会被覆盖,赋值之后就没有该类属性了,变成了实例属性,只要明确一点,__dict__中的永远是对象的属性。
那么如果在__set__方法中将同名属性改为实例想要赋予的值呢?
比如:
obj.Attr = 13
def __set__(instance, value):
instance.Attr = value
setattr(instance, "Attr", value)
这样会替换描述器吗?
答案:不会,会报错,递归深度报错
分析这个操作,instance.Attr = value\ setattr(instance, "Attr", value)实际上和obj.Attr = 13没有任何区别
会一直递归调用描述器的__set__方法
由此可以得出描述器的作用: 对象属性的代理或者管理对象属性
python检索扫描对象属性的优先级
实例查找通过命名空间链进行扫描,数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,最后是 __getattr__() (如果存在的话)。
结论
描述器,就是作为类属性声明且定义了__get__, __set__, __delete__方法的类
作为声明描述器类属性的类实例的属性代理或管理器
通常情况下都用不到自己编写描述器,但是实际开发中许多第三方包都是基于描述器实现的,包括property
property作为一个被用为装饰器的类,通常用于将实例的方法属性作为数值属性调用
在property中,就是通过描述器来实现的
装饰器
装饰器一般形式:
def dec(func):
def inside(*args, **kwargs):
ret = func(*args, **kwargs)
return ret
return inside
@dec
def one():
pass
one()
执行被装饰的函数,执行的流程是什么呢?
当使用@dec对函数one进行装饰时,再调用one函数,实际上就是调用dec(one)
one = dec(one)
one() = dec(one)()
dec(one) = inside
dec(one)() = inside() = ret
即: one() = ret
这是被装饰函数的执行流程
装饰器其实就是定义了闭包的函数,这里引入作用域的概念更好理解,闭包就是在函数内部定义的函数,内部函数的作用域仅限于函数内部
闭包的意义在于可以讲函数内部作用域的变量对外输出,这里不对闭包做深究,这里主要是研究@语法
那么装饰器如何把被装饰的函数作为参数传入的。
这就是@语法
@语法
看一个例子
def fake_decoration(func):
print(f"In fake_decoration {func}, {type(func)}")
return func
@fake_decoration
def my_function():
print("function has been called")
return 12000
print(my_function())
"""
输出
In fake_decoration <function my_function at 0x00000117008CB310>, <class 'function'>
function has been called
12000
fake_decoration并不是定义了闭包的装饰器,只是一个普通的函数,但是仍然能通过@语法获得被‘装饰’的函数,从这里可以看出,@不只限于装饰器,或者说,
装饰器并不是一定实现了闭包的函数,而是通过@对另一个函数施加影响的函数都可以算作装饰器。
这个执行流程,相当于:
func = fake_decoration(my_function)
print(func())
"""
@做了什么
当用@语法将一个函数D放置在另一个函数A上时,python做了一个事情,即:
A = D(A)
函数被装饰后,函数名称重新指向装饰器执行的结果,即装饰器的闭包
装饰器的另外一种形式可能会让上面的结论有些不确定,如下:
def outside(name=""):
def dec(func):
def inside(*args):
print(name)
ret = func(*args)
return ret
return inside
return dec
@outside(name="Yes")
def funcT(a):
print(f"Got a: {a}")
这是一种带参数的装饰器,其实和上面的说法一致,@outside(name="Yes")看起来没有把funcT传给outside,那是因为装饰器不是outside,而是outside(name="Yes")
即dec,@outside(name="Yes")要先执行outside(name="Yes"),return的dec才是作为装饰器的函数。
如果理解了闭包的含义和意义,就能很好的理解上面的情况。
property 装饰器和描述器的结合
在上面描述器部分留有一个问题,上面说了如果一个类声明了数值型描述器,那么在对实例的同名属性做重新负值时,怎么让实例在调用该属性时让修改生效呢。
比如
让 obj.Desc = 1234
在上面的例子里,我在__set__方法里并没有去操作Desc,这就导致无法让实例修改Desc的值,如果修改了,就会导致递归报错。
这显然是不合理的
上面说了,描述器,实际是实例属性的代理或管理,用于监管控制对实例属性的操作。
也就是说,如果定义了描述器,那么对实例属性的一切操作,都不应该直接作用在实例的该属性上,而是应该让描述器来决定如何操作。
如下:
class DataDescription(object):
def __init__(self):
self.public_value = None
def __get__(self, instance, owner):
return self.public_value
def __set__(self, instance, value):
self.public_value = value
class ForTest(object):
Desc = DataDescription()
def __init__(self):
self.Desc = ">Just String"
def __str__(self):
return "==>ForTest Obj<=="
print(ForTest().Desc)
# >Just String
那么描述器到底有什么用呢?看下面:
class MustString(object):
def __init__(self, max_length=0):
self.max_length = max_length
self.value = None
self.name = ""
def __set_name__(self, owner, name):
self.name = name
self.owner = owner
def __set__(self, instance, value):
assert isinstance(value, str), f"{self.owner}.{self.name} must be type String"
assert len(value) < self.max_length, f"Length of {self.owner}.{self.name} must less than {self.max_length}"
self.value = value
def __get__(self, instance, owner):
return self.value
class Document(object):
text = MustString(max_length=5)
doc = Document()
try:
doc.text = 12
except Exception as e:
print(e)
# <class '__main__.Document'>.text must be type String
try:
doc.text = "hello world"
except Exception as e:
print(e)
# Length of <class '__main__.Document'>.text must less than 5
doc.text = "YES"
print(doc.text)
# YES
是不是很熟悉,像不像ORM,除此之外,还有更多玩法。
编程中常用的,将方法转换为属性(其实方法也是属性,是方法属性)形式的装饰器property
我们可以自己写一个了,从上面的装饰器研究可以得出,@语法,就是把被装饰的函数传给装饰器
那么如下:
class Property(object):
def __init__(self, func_get):
self.func_get = func_get
self.func_set = None
def __get__(self, instance, owner):
return self.func_get()
def __set__(self, instance, value):
self.func_set(value)
def setter(self, func_set):
self.func_set = func_set
class Obj(object):
def __init__(self):
self.v = None
@Property
def value(self):
return self.v
@value.setter
def value(self, v):
self.v = v
obj = Obj()
obj.value = "Value"
print(obj.value)
obj.value = "New Value"
print(obj.value)
输出结果:
Value
New Value
value方法被装饰之后,value = Property(Obj.value),这时新的value属性就成了类的描述器属性,再使用@value.setter时,实际是@Property(Obj.value).setter
这样我们就自己写了一个property装饰器。