浅析基于类及类的特殊方法的一种Python修饰器

最近在做有关Django有关的项目的时候,本人看到源代码里有大量修饰器(decorator)的身影,尤其是一种基于类及类的特殊方法的修饰器吸引了我的注意,遂研究并记录其中的一些关键点于此。

最简单的修饰器

先看一个最简单的python修饰器的例子:

python 复制代码
def MyFirstDecorator(func):
    def wrapper(*arg, **kwargs):
            print("before calling func")
            func(*arg, **kwargs)
            print("after calling func")
            
    return wrapper;
    
@MyFirstDecorator
def MyFunc():
    print('Hello World')
    
MyFunc()

以上代码的输出结果如下:

go 复制代码
before calling func
Hello World
after calling func

相信在其他编程语言中接触过函数柯里化概念的同学已经看出了其中的端倪。我们可以将上面的代码理解为如下的操作:

python 复制代码
def MyFirstDecorator(func):
    def wrapper(*arg, **kwargs):
        print("before calling func")
        func(*arg, **kwargs)
        print("after calling func")
            
    return wrapper;
    
def MyFunc():
    print('Hello World')

# 修饰器的真面目:函数柯里化!!!
MyFunc = MyFirstDecorator(MyFunc)
    
MyFunc();

理解了这点之后,我们还可以结合函数柯里化的思想,实现定制修饰器。譬如下面这个例子:

python 复制代码
def createDecorator(name):

    def decorator(func):
        def wrapper(*arg, **kwargs):
            print('hello, %s' % name)
            print("before calling func")
            func(*arg, **kwargs)
            print("after calling func")
        return wrapper
            
    return decorator
    
@createDecorator("JiangNanGame")
def MyFunc():
    print('test MyFunc')
    
MyFunc();

输出结果:

go 复制代码
hello, JiangNanGame
before calling func
test MyFunc
after calling func

基于类的修饰器

下面来看一个我在实际项目源码中遇到的例子。这里为了方便讲解,我已对原先的代码进行了高度提炼和精简。

python 复制代码
class my_property:

    def __get__(self, obj, cls):
        return self.func(obj)

    def __call__(self, func, *args, **kwargs):
        self.func = func
        return self
        
class SysOptions:
    @my_property()
    def website_base_url(cls):
        return 'http://www.jiangnangame.com'

# 输出:http://www.jiangnangame.com
print(SysOptions.website_base_url)

这个输出结果乍一看是十分令人困惑的,为什么直接访问SysOptions.website_base_url就可以得到经过修饰器修饰的website_base_url函数的返回值?

除此之外还令人费解的是,前文中的修饰器都是以单个函数的形式出现的,而在这个例子中,名为my_property的类怎么就发挥起修饰器的作用了?

类的特殊方法

为了搞清楚这段代码的原理,首先必须明白在my_property类中定义的两个特殊成员__get__和__call__的作用。这里通过简单的例子来理解。

get

先来看__get__。这里直接上一个取自于Python官方文档的例子:

python 复制代码
# __get__定义于描述器中,这里的Ten就是一个描述器
class Ten:
    def __get__(self, obj, cls):
        print(self)
        print(obj)
        print(cls)
        return 10

class A:
    x = 5
    # 只有将描述器实例化后存储在另一个类中,它才会发挥作用
    y = Ten()

a = A()
# 输出:5。可见含有描述器实例的类在实例化后,不会影响实例对象的其他属性
print(a.x)
# 输出:
# <__main__.Ten object at 0x0000021F8724B5E0>
# <__main__.A object at 0x0000021F872831C0>
# <class '__main__.A'>
# 10
print(a.y)

可见当访问某个类(上例中的A)的实例化对象(上例中的a)中的值为某个描述器实例的属性(上例中的y),就会触发描述器的__get__方法,实现对取值操作的劫持。

同时__get__方法中的三个参数的意义也昭然若揭了。self代表描述器实例自身,obj表示描述器实例所处类的实例对象,cls表示描述器实例所处的类。

call

__call__理解起来就更简单。它可以使得类的实例对象如同函数一般被调用:

python 复制代码
class MyClass:
    def __call__(self, someResult):
        print('hook call!')
        return someResult
       
myObj = MyClass()
# 输出:
# hook call!
# 2023
print(myObj(2023)) 

解决开始的问题

搞明白了前面的两个特殊方法的作用,我们回过头来分析一下起先的代码。

首先第11行@my_property()实际上是对类my_property进行了一次实例化,得到了一个my_property实例对象。而由于类my_property上设置了__call__方法,因此其实例对象也可以如同函数一般进行调用,故而可以充当修饰器。

接下来,python解释器在发现了11行调用修饰器的命令后,会将原函数website_base_url传入修饰器(即前述的my_property实例对象)以计算出经修饰器包装后的函数。这个过程便会触发修饰器的__call__方法,在第7,8行中可以看到该方法被触发后会将原函数储存为修饰器对象自己的一个属性func,并且将修饰器自身返回作为"包装后的新函数"。

至此为止,SysOptions.website_base_url真实指向的便是前述的my_property对象了。而该对象拥有一个__get__方法,且其作为类SysOptions的一个属性存在,符合前文提到的__get__方法被触发的条件。因此当第16行对SysOptions.website_base_url的值进行访问时,get 方法被触发,其调用在修饰器包装原函数时,储存到修饰器对象中的原函数func,并返回其返回值(字符串"JiangNanGame"),实现了对访问值操作的劫持全过程。

尾声

在本文中重点分析的基于类的,针对其他类方法的修饰器my_property是有一定实际意义的。本文中展示的的精简代码虽然只实现了对取值操作的劫持,但只要对该修饰器进行适当的拓展,我们就可以很方便地对类中的方法进行劫持,使其转变为可以直接读写的类属性,大大简化了代码的编写和维护工作。

相关推荐
武当王丶也2 分钟前
从零构建基于 RAG 的 AI 对话系统:Ollama + Python + 知识库实战
人工智能·python
38242782732 分钟前
python:正则表达式
前端·python·正则表达式
锐学AI44 分钟前
从零开始学LangChain(二):LangChain的核心组件 - Agents
人工智能·python
风送雨1 小时前
多模态RAG工程开发教程(上)
python·langchain
棒棒的皮皮1 小时前
【OpenCV】Python图像处理形态学之膨胀
图像处理·python·opencv·计算机视觉
小草cys1 小时前
HarmonyOS Next调用高德api获取实时天气,api接口
开发语言·python·arkts·鸿蒙·harmony os
爬山算法1 小时前
Netty(25)Netty的序列化和反序列化机制是什么?
开发语言·python
未知数Tel1 小时前
Dify离线安装插件
python·阿里云·pip·dify
龘龍龙1 小时前
Python基础学习(六)
开发语言·python·学习
热爱专研AI的学妹1 小时前
【搭建工作流教程】使用数眼智能 API 搭建 AI 智能体工作流教程(含可视化流程图)
大数据·数据库·人工智能·python·ai·语言模型·流程图