【由浅入深】例解python装饰器执行顺序

一直以来只是单纯地使用装饰器,并没有深究过其执行过程,或者说之前没有死磕,这两天重拾python的基础学习,在这一块儿花了点功夫,把此时的理解记下。

在谈装饰器之前,先理解闭包(closure)的概念:如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包。(这一段以后再来补充)
仔细观察装饰器的结构,无非是在一个函数内部定义了另外一个函数,因此,先来说明这种内嵌函数的执行(调用)过程。

例1.内嵌函数例子

def foo():
	def bar():
		print('bar() called')
	print ('foo() called')
	bar()

>>>foo()
foo() called
bar() called

上述过程总的来说是很自然的,其中bar()是在foo()中通过显式调用的方式执行的。为此,做一点小小的改变,将该过程改为通过return来调用。

例2.内嵌函数变型1

def foo():
	def bar():
		print('bar() called')
	print ('foo() called')
	return bar()

>>>foo()
foo() called
bar() called

结果和例1是一样的,有了上述基础,上面foo()函数返回的是bar的调用,我们也可以返回其引用,并在需要的时候调用它,如例3所示。

例3.内嵌函数变型2

def foo():
	def bar():
		print('bar() called')
	print ('foo() called')
	return bar

>>>f = foo()
foo() called
>>>f()
bar() called

例3中,先将foo()的引用赋给f,即f=bar;然后执行f(),即执行bar()。有了这个基础,现在看一个最简单的装饰器例子。

例4.简单的装饰器

def dec(func):
    @functools.wraps(func)            # 加这句是为了防止装饰器对被装饰函数的影响
    def wrapper(*args,**kwargs):
        print('this is a wrapper')
        return func(*args,**kwargs)
    return wrapper
    
@dec
def foo():
    print('foo() called')

foo()

保存为test.pypython test.py运行结果如下:

this is a wrapper
foo() called

首先说明@dec的含义,这可以看作是foo=dec(foo)的一种简写(这其实类似于数学中的函数复用),既然如此,那么在foo()前加上@dec相当于foo=wrapper。剩下的就是类似例3的过程了,首先执行装饰器内的打印语句,然后返回foo(),执行真实的foo()内容。实际上上述过程省略了一个重要的点,那就是装饰器函数在被装饰函数定义好后立即执行,这个如何理解呢,可以理解为当被装饰函数定义好后,即执行了foo=dec(foo)操作,因此实际上在例4中,可以将上例稍做修改,再去掉最后一行的foo()

例5.简单的装饰器变形

def dec(func):
    print('this is dec')
    @functools.wraps(func)            # 加这句是为了防止装饰器对被装饰函数的影响
    def wrapper(*args,**kwargs):
        print('this is a wrapper')
        return func(*args,**kwargs)
    return wrapper
    
@dec
def foo():
    print('foo() called')



然后python test.py,其结果如下:

this is dec

在例5中,我们并没有调用foo(),但是装饰器的外部函数其实已经执行了,也即foo=dec(foo)=wrapper操作其实已经执行完成,之后进行foo()的调用就是执行该装饰器内部函数的过程。至此,一个不带参数的简单的装饰器执行过程已经说清楚了。

有时候,需要装饰器有参数,比如设计一个计时程序,用来测试网络训练和测试的时间,此时需要指定当前执行的是训练或测试过程,为此,需要传入一个状态参数。此处说明一下装饰器传参的方式:@dec(args1,args2)等价于func=dec(args1,args2)(func),值得说明的是此处所谓的参数是指装饰器的参数而不是被装饰函数的参数,被装饰函数的参数是通过*args**kwargs自然地传递的。

例6.带参数的装饰器

def timing(status='Train'):
    def dec(func):
        @functools.wraps(func)
        def wrapper(*args,**kwargs):
            start = time.time()
            func1 = func(*args,**kwargs)   # 此处做了一个变形
            print('[%s] time: %.3f s '%(status,time.time()-start))
            return func1
        return wrapper
    return dec

@timing(status='Train')
def Training():
    time.sleep(3)

@timing(status='Test')
def Testing():
    time.sleep(2)
>>>Training()
[Train] time: 3.000 s 
>>>Testing()
[Test] time: 2.000 s 

上面这个例子,相对于例4,多了一层函数嵌套,但是理解起来应该并不困难,但是为了深入了解每一步执行过程,加入一些打印信息。

例7.深入探究装饰器执行过程

def timing(status='Train'):
    print('this is timing')
    def dec(func):
        print('this is dec in timing')
        @functools.wraps(func)
        def wrapper(*args,**kwargs):
            start = time.time()
            func1 = func(*args,**kwargs)
            print('[%s] time: %.3f s '%(status,time.time()-start))
            return func1
        return wrapper
    return dec
    
@timing(status='Train')
def Training():
    time.sleep(3) 
>>>Training()
this is timing
this is dec in timing
[Train] time: 3.000 s 

对上述过程分步描述:
0.首先说明,@timing(status='Train')在此处等价于Traing=timing('Train')(Traing)

  1. 第一步,打印第一句话,然后Traing=timing('Train')(Traing)=dec(Traing)
  2. 第二步,打印第二句话,且dec(Testing)返回wrapper,即Traing=wrapper。(到这一步都只是定义Traing所产生的操作,即即使不调用该函数,上两句话也会被打印)
  3. 调用wrapper函数,记下起始时间start,执行Traing函数,延时2秒,打印所用时间,返回func1

值得说明的是,这里返回的func1None,因为这个返回值实际是Traing函数的返回值,而这个函数我并没有定义返回值。

现在讨论多个装饰器的情况,多个装饰器类似于数学中的多个函数复用,看下面一个简单的例子,说明多个装饰器的执行顺序。

例8.多个装饰器模型

@dec1(args)
@dec2
@dec3
def foo():
    pass
    

这等价于func = dec1(args)(dec2(dec3(func)),此处特意将第一个装饰器设置为带参数的。
从上面这些例子中,我们不难看出,装饰器功能是先于被装饰函数执行的,为验证这个猜想,用一个实例说明。

例9.多个装饰器实例

def timing(status='Train'):
    print('this is timing')
    def dec(func):
        print('this is dec in timing')
        @functools.wraps(func)
        def wrapper3(*args,**kwargs):
            start = time.time()
            func1 = func(*args,**kwargs)
            print('[%s] time: %.3f s '%(status,time.time()-start))
            return func1
        return wrapper3
    return dec


def dec1(func):
    print('this is dec1')
    @functools.wraps(func)
    def wrapper1(*args,**kwargs):
        print('this is a wrapper in dec1')
        return func(*args,**kwargs)
    return wrapper1


def dec2(func):
    print('this is dec2')
    @functools.wraps(func)
    def wrapper2(*args,**kwargs):
        print('this is a wrapper in dec2')
        return func(*args,**kwargs)
    return wrapper2
    
@dec1
@dec2
@timing(status='Test')
def fun():
    time.sleep(2)

为方便分析,先不调用函数,其输出为

this is timing
this is dec in timing
this is dec2
this is dec1

从这个例子的输出来看,是从下而上地执行。首先写出此处的“复用”规则,fun = dec1(dec2(timing('Test')(fun))),为了方便叙述,此处将三个装饰器的内部函数分别命名为wrapper1wrapper2wrapper3,上面这个过程其实是一个逐步“解包”的过程,除输出信息外,其最终达成一种其他的引用关系,在最外层(全局变量空间),fun=wrapper1,而在wrapper1的作用域内fun=wrapper2,在wrapper2的作用域内fun=wrapper3。按照这个设定,先假设调用fun会有什么输出。首先,把上面四个输出信息省略。由于在全局变量空间fun=wrapper1,故执行fun()时,会先打印wrapper1内的信息,然后由于在wrapper1中,fun=wrapper2,故继续执行wrapper2内的内容,以此类推,(这段文字比较绕,但是应该仔细推测)故推测其输出为:

this is a wrapper in dec1
this is a wrapper in dec2
[Test] time: 2.000 s 

现在,调用fun函数测试是否正确。

this is timing
this is dec in timing
this is dec2
this is dec1
this is a wrapper in dec1
this is a wrapper in dec2
[Test] time: 2.000 s 

上述结果和猜测是一致的,从这个结果来看,这个过程有点像自底向上地完成装饰器的定义,然后自顶向下地执行装饰器的功能。所以,如果从使用的角度来看,装饰器其实是自顶向下的(毕竟实际使用过程中一般不会在外层打印信息)

总结

情形“复用公式”
单个无参fun=dec(fun)
单个含参fun=dec(args1,args2)(fun)
多个无参fun=dec1(dec2(fun))
多个含参fun=dec1(args1,args2)(dec2((fun)))
  • 29
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
Python爬虫的学习可以从浅到深逐步进行,以下是一个可能的学习路径: 1. 网络基础知识:了解HTTP协议和HTML基础知识,理解网页结构和标签的含义。 2. requests库:学习使用Python的requests库发送HTTP请求,获取网页内容。可以使用该库来实现简单的网页爬取。 3. BeautifulSoup库:学习使用BeautifulSoup库解析HTML文档,提取所需的信息。该库可以帮助我们以更方便的方式处理网页数据。 4. 数据存储:学习使用CSV、JSON、数据库等方式来存储爬取到的数据,以便后续使用和分析。 5. 爬虫进阶:学习处理JavaScript渲染的页面,使用Selenium库模拟浏览器行为。还可以学习使用代理IP、验证码处理等技术来应对一些反爬机制。 6. 爬虫框架:了解并学习使用一些流行的爬虫框架,如Scrapy,可以大幅度提高爬虫的开发效率和稳定性。 7. 反爬虫策略:学习了解常见的反爬虫策略,如User-Agent伪装、IP封禁等,并学会应对这些策略,提高爬虫的鲁棒性。 8. 数据清洗和分析:学习使用Python的数据处理和分析库,如Pandas、NumPy、Matplotlib等,对爬取到的数据进行清洗和分析。 9. 分布式爬虫:学习使用分布式爬虫框架,如Scrapy-Redis,来提高爬虫的并发能力和抓取速度。 10. 伦理和法律问题:了解爬虫的伦理和法律问题,遵守相关法律法规,不进行非法、恶意的爬取行为。 以上是一个学习Python爬虫的大致路径,根据自己的兴趣和实际需求可以自行调整学习的深度和广度。注意,在进行爬虫时请遵守网站的相关规定和爬虫道德准则。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值