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装饰器。

posted @ 2023-08-05 08:59  华腾海神  阅读(25)  评论(0编辑  收藏  举报