python闭包与装饰器

发布时间 2023-06-27 21:43:00作者: wancy
1.  闭包

  闭包定义:在函数嵌套的前提下;内部函数使用了外部函数的变量;并且外部函数返回了内部函数;我们把这个使用外部函数变量的内部函数称为闭包。

  闭包有三大特点:

  1.有内函数与外函数,即函数是嵌套的。

  2.内函数使用了外函数的变量与参数。

  3.外部函数的返回值为内部函数名。

  例子:

def func01():
    a=10
    def func02(b):
        print(a)
        print(b)
    # return func02()#注意写括号就是直接调用,一般不这么做
    return func02
#调用外函数,接受内函数
result=func01()
#调用内函数
result(2)
#或者func01()(2)

2.  装饰器

  采用了闭包的思路,在不改变原函数功能的情况下,为函数增添新的功能。下面给个例子:

def new_func(func):
    def wrapper():
        print("新功能")
        func()
    return wrapper

def old_func():
    print("旧功能")
#此时同时执行了新旧功能
old_func=new_func(old_func)
#调用旧功能
old_func()#调用了new_func的wrapper()

  分析以上代码:定义了两个函数,new_func的函数参数为func,将来调用它传入函数名。new_func(old_func)返回了wrapper函数名。最后一行old_func()调用了wrapper函数,而不是直接调用了定义的old_func()函数。

   除了使用上面的方法外,python还提供了另外一种写法:@装饰器函数名写在被装饰函数的上方,实现两者的联系。如下:

def new_func(func):
    def wrapper():
        print("新功能")
        res=func()
        return res
    return wrapper
@new_func
def old_func(): print("旧功能") return True @new_func def old_func2(): print("旧功能2") return True #调用旧功能 r=old_func()#调用了new_func的wrapper() print("r=",r)#True r2=old_func2()

  通过上面,基本对装饰器有了初步认识,但是仔细思考,发现如果def old_func()与def old_func2()加入函数参数又该怎么办处理。如果定义old_func时加入参数,那么装饰函数new_func里面的wrapper(参数)就应该加入参数(思考一下如何调用的)。比如下面这样:

def new_func(func):
    def wrapper(a):
        print("新功能")
        res=func(a)
        return res
    return wrapper

@new_func
def old_func(n):
    print("旧功能")
    print(n)
    return n**2

@new_func
def old_func2(m):
    print("旧功能2")
    print(m)
    return m**2

#调用旧功能
r1=old_func(3)#调用了new_func的wrapper()
print("r1=",r1)#
r2=old_func2(4)
print("r2=",r2)

  运行结果如下:

   似乎没有问题了,但是在newfunc装饰不同函数的情况下,如果被装饰函数参数个数不同呢?比如下面情况如何处理:

@new_func
def old_func(n):
    print("旧功能")
    print(n)
    return n**2

@new_func
def old_func2(m,k):
    print("旧功能2")
    print(m,k)
    return m+k

  由于old_func与old_func2的参数不同,那么def wrapper(参数):中的参数个数似乎不好处理,别忘了还有元组形参与字典形参的写法。

def new_func(func):
    def wrapper(*args,**kwargs):#星号元组形参,把实参合并为一个元组,也可以不给传参数,字典实参也是
        print("新功能")
        res=func(*args,**kwargs)
        return res
    return wrapper

@new_func
def old_func(n):
    print("旧功能")
    print(n)
    return n**2

@new_func
def old_func2(m,k=2):
    print("旧功能2")
    print(m,k)
    return m+k

#调用旧功能
r1=old_func(3)#调用了new_func的wrapper
print("r1=",r1)#
r2=old_func2(4,6)
print("r2=",r2)

  运行结果:

   因此,不管有参没参,不管参数个数,统统使用wrapper(*args,**kwargs)这种写法,满足所有需求。

3.  带参数的装饰器

  所谓带参数的装饰器语法格式:@函数名(参数)写在被装饰函数上方,又因为如果是装饰器的话@后面跟函数名,所以函数名(参数)必定返回一个函数名。如果不做说明,装饰器就指函数装饰器,这里引用网上的一个例子:

# 添加输出日志的功能
def logging(flag):
    def decorator(fn):
        def inner(num1, num2):
            if flag == "+":
                print("--正在努力加法计算--")
            elif flag == "-":
                print("--正在努力减法计算--")
            result = fn(num1, num2)
            return result
        return inner
    # 返回装饰器
    return decorator

# 使用装饰器装饰函数
@logging("+")
def add(a, b):
    result = a + b
    return result

@logging("-")
def sub(a, b):
    result = a - b
    return result

result = add(1, 2)
print(result)
result = sub(1, 2)
print(result)

  运行结果:

4. 多个装饰器

  先直接看一段代码:

def new_func1(func):
    print(1)
    def wrapper(*args,**kwargs):#星号元组形参,把实参合并为一个元组,也可以不给传参数,字典实参也是
        print(2)
        print("新功能1")
        res=func(*args,**kwargs)
        print(3)
        return res
    print(4)
    return wrapper

def new_func2(func):
    print(5)
    def wrapper(*args,**kwargs):#星号元组形参,把实参合并为一个元组,也可以不给传参数,字典实参也是
        print(6)
        print("新功能2")
        res=func(*args,**kwargs)
        print(7)
        return res
    print(8)
    return wrapper

@new_func2
@new_func1
def old_func(n):
    print(9)
    print("旧功能")
    return n**2

  右键运行程序后猜一猜结果,结果如下:

   也就是说加上了@装饰器后,定义的函数有些代码执行了。换句话说,装饰器函数在被装饰函数定义好后立即执行。多个装饰器的调用顺序是自下往上的(装饰器装饰函数时的上下顺序)。再看下面这个例子:

"""
多个装饰器装饰的顺序是从里到外(就近原则),而调用的顺序是从外到里(就远原则)
装饰器函数在被装饰函数定义好后立即执行。多个装饰器的调用顺序是自下往上的(装饰器装饰函数时的上下顺序)。
"""
def new_func1(func):
    print(1,end=" ")
    def wrapper1(*args,**kwargs):#星号元组形参,把实参合并为一个元组,也可以不给传参数,字典实参也是
        print(2,end=" ")
        print("新功能1",end=" ")
        res=func(*args,**kwargs)
        print(3,end=" ")
        return res
    print(4,end=" ")
    return wrapper1

def new_func2(func):
    print(5,end=" ")
    def wrapper2(*args,**kwargs):#星号元组形参,把实参合并为一个元组,也可以不给传参数,字典实参也是
        print(6,end=" ")
        print("新功能2",end=" ")
        res=func(*args,**kwargs)
        print(7,end=" ")
        return res
    print(8,end=" ")
    return wrapper2

@new_func2
@new_func1
def old_func(n):
    print(9,end=" ")
    print("旧功能",end=" ")
    return n**2

#调用旧功能
r1=old_func(3)#调用了new_func的wrapper()
print("r1=",r1)#

  至于为什么先输出1 4 5  8上面的例子与图讲了,接下来我们分析执行最后两句代码后的执行顺序。@new_func1看作a=new_func1(old_func),这里a的值为new_func1中的wrapper1的函数地址值,(还未执行);@new_func2看作 b=new_func2(a),此时将wrapper1函数名(地址值)传给了new_func2,此时b的值为new_func2中的wrapper2的地址值(函数名)。最后相当于执行了b(3)。此时,函数名+括号,函数内容开始执行了,首先执行wrapper2(3),所以输出了6 新功能2,然后到这里了,res=func(*args,**kwargs),这一句中的func就是a值,也就是执行a(3),即wrapper1(3),此时输出2 新功能1,又到这里了res=func(*args,**kwargs),此刻func是old_func,也就是old_func(3),输出 9 旧功能(然后返回给3**3的结果给res),然后就行执行wrapper1中的代码,输出3,然后返回res给了wrapper2中的res,然后输出 7,最后返回res给r1,输出r1=9.

  再看下面这个例子可能更懂一些了:

def decorator1(func):
    def wrapper():
        print('decorator1 start')
        func()
        print('decorator1 end')
    return wrapper

def decorator2(func):
    def wrapper():
        print('decorator2 start')
        func()
        print('decorator2 end')
    return wrapper

@decorator1
@decorator2
def my_func():
    print('Original function')

my_func()

  在Python中,装饰器可以串联在一起使用,即一个函数可以同时应用多个装饰器。但是,需要注意的是装饰器应用顺序是从下往上执行。在上面的代码中,我们定义了两个装饰器decorator1decorator2,其中decorator1decorator2之上。然后我们将它们同时应用在my_func函数上面,并且在my_func函数中添加一些输出语句。当我们调用my_func函数时,它将输出以下内容:

5. 类装饰器装饰函数

  类也可以作为装饰器,可以装饰函数。

class Decorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("Decorator")
        self.func(*args, **kwargs)

@Decorator
def some_function():
    print("Some function")

print(type(some_function))#Decorator类型,相当于some_function=Decorator(some_function); 
some_function.__call__() #或者 some_function()

 

 

 

  运行结果:

 6.  函数装饰器装饰类

  函数装饰器装饰类是一种很有趣的技巧,可用于编写更具有动态性和灵活性的代码。

def add_method(cls):
    def add(self, x, y):
        return x + y
    cls.add = add  # 在类上添加新方法
    return cls

@add_method
class MyClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

my_obj = MyClass(5, 10)
print(my_obj.add(2, 3))  # Output: 5

  在上面的代码中,add_method函数接收一个类作为参数,然后定义了一个新方法add并将它添加到类上面。使用@add_method装饰器应用这个函数到MyClass类上面。最后,我们创建了一个MyClass对象,并且使用add方法。因为我们已经在类上定义了add方法,所以我们可以直接使用它。

7.  类装饰器装饰类

  Python类也可以作为装饰器来装饰其他类。这种装饰器常常被称为类装饰器。这种装饰器不同于函数装饰器,因为它们可以为类添加属性和方法,并且可以在整个类的层次结构中重复使用。例子如下:

class AddMethod:
    def __init__(self, cls):
        self.cls = cls

    def __call__(self):
        self.cls.new_method = lambda x, y: x + y
        return self.cls

@AddMethod
class MyClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

my_obj = MyClass()
print(my_obj.new_method(3, 5))  # Output: 8

  在上面的代码中,我们首先定义了一个AddMethod类,该类接收一个类作为参数并对其进行修改。在__call__方法中,我们将类的new_method属性设置为一个新的lambda函数。然后我们将整个类返回。接着,我们使用@AddMethod装饰器应用了该类装饰器到MyClass类上面。最后,我们创建了一个MyClass对象,并且使用添加的new_method方法。

小结:使用带参数的装饰器,其实也就是再在外层嵌套一层函数,注意@后面一定是装饰器(函数名)或者返回值是装饰器(函数名)。如果有多个装饰器,要注意多个装饰器的调用顺序。另外,要注意wrapper的返回值与func()一致,不能增加或者减少返回值的个数(装饰器不改变原函数功能,所以返回值不能改可以直接return func(*args,**kwargs)。文章对函数作为装饰器装饰函数,也装饰了类,而且将类也作为装饰器对类进行装饰。关于闭包与装饰器介绍的可能不全面,但是可以作为基础入门。

 

  不足或存在错误之处,欢迎指正与评论!觉得有用,请帮忙点个赞!

参考资料:

https://blog.csdn.net/zzh_love/article/details/129007703

https://zhuanlan.zhihu.com/p/451374770

https://blog.csdn.net/weixin_44992737/article/details/125868592

https://www.jb51.net/article/258254.htm