Python - 装饰器(Decorator) - 可调用的(Callable)

其实这个是个大家都已比较熟悉的话题,Callable,也就是可调用的。但当接触到装饰器(decorator)的时候,表现形式复杂些时,我发现还是有必要理清一下的。

什么是Callable

任何可以使用括号操作符的目标都是可调用的。

如果我们在Python使用某个目标具有以下特点,我们就说这个目标是可调用的。

  • 目标名

在Python中目标名往往都是绑定某个目标的内存地址。我们就是通过目标名最终引用到目标本身。

  • 括号

当标签名后面跟着括号时,那就是说有代码被执行了

  • 输入和输出

输入:也就实参,在调用是会把实参拷贝给形参,请注意,在Python中,都是拷贝的内存地址

输出:也就是返回值,在Python都是有返回值的,没有明确返回什么的情况下,返回的默认是None

注意:输入和输出也有可能是callable,但级联使用时可能一时之间有点迷糊!

怎么验证Callable

在Python中有个内置函数(built-in funtion)callable。

Help on built-in function callable in module builtins:

callable(obj, /)
    Return whether the object is callable (i.e., some kind of function).
    
    Note that classes are callable, as are instances of classes with a
    __call__() method.

接下来我们callable函数运行的一些结果。

注意:对于(1).bit_length,为什么我们不能写成1.bit_length? 那是因为如果我们写成1.bit_length,Python会当成浮点型字面量去解析,碰到小数点后面的字母时,当然解析出错。此处把1放进括号里,就是告诉Python解析器,1就是一个token了。

注意:此处callable去调用callable本身也是返回True。

Callable的种类

  • 内置函数

print, len, callable

  • 内置方法

'abc'.upper, [1].append, (1).bit_length

  • 自定义函数

我们通过def和lambda自定义的函数

  • 方法

绑定到对象的方法

类也是可以调用的,通过调用类,我们生成了类的实例。

  • 实例

实例也可以调用?哈哈,Python提供了一种机制,让类的实例也可以直接调用。请看以下例子。

  • 其他(生成器, 协程,异步生成器等等)

Callalbe的级联调用

之前所说的内容,大家都是完全理解和掌握的。但接触装饰器(decorator)时,callable的级联调用被@语法糖隐藏了,就有可能有点犯迷糊。我们直接看一些例子吧。

  • 例: @语法糖 - 不带参数 

Q:print可以用作装饰器吗?

A:当然可以。只要用作装饰器的函数能解释一个函数作为输入,就可以用作装饰器,就可以用@语法糖。

Q:打印出“<function func at 0x......>”到底在什么时候运行?

A:在用@语法糖的时候,打印出“<function func at 0x......>”相当于被装饰函数的定义阶段就执行了。因为有print(func)调用,有调用就有代码被执行了,此处就是打印函数本身。

Q:为什么func变成了None? 原来func不是函数吗?

A:我们也展示了不用@语法糖时的同等代码。func原来确实是绑定到一个函数,但后来func却又绑定到由print(func)函数执行过后的返回值,print函数的返回值就是None。执行过func=print(func)代码过后,func绑定的内容变了。

Q:为什么没有“func running”输出?

A:当func绑定到一个函数时,我们从来没有调用过这个函数,也就是说这个函数体内的代码从来没被执行过,也就是说“func running”从来没被输出过。当func绑定到None后,None是不可调用的,如果这时执行func()就会产生异常了。

例:执行顺序

from functools import wraps

def dec1(fn):
    print('dec1.outer: start')
    
    @wraps(fn)
    def inner(*args, **kwargs):
        print('dec1.inner: start')
        result = fn(*args, **kwargs)
        print('dec1.inner: end')
        return result
    
    print('dec1.outer: end')
    return inner

def dec2(fn):
    print('dec2.outer: start')
    
    @wraps(fn)
    def inner(*args, **kwargs):
        print('dec2.inner: start')
        result = fn(*args, **kwargs)
        print('dec2.inner: end')
        return result
    
    print('dec2.outer: end')
    return inner

print('*' * 50)

@dec1
@dec2
def add(a, b): # 相当于 add = dec1(dec2(add))
    print('add: start')
    result = a + b
    print(f'add reult: {result}')
    print('add: end')
    return result

print('*' * 50)
add(1, 2)
print('*' * 50)
add(3, 4)

输出如下:

请仔细参考这张内存中的关系图。

Q:dec函数中除了返回inner函数的其他代码在实际中有没有用?

A:只有当装饰其他函数的时候,这部分代码才会被运行一次。除了日志等目的之外,例如记录哪些函数被当前装饰器装饰之外,还真没见过其他特别的用途。

Q:如果不用@语法糖,直接运行dec1(dec2(add))(5, 6),代码到底是怎么运行的?

A:这个表达式(expression)是不是有点复杂?我们只要根据之前了解的关于callable的基础之上,一点一点来分析。

add

add就是一个绑定函数的变量名!当然可以作为dec2(fn)的输入

dec2(add)

此处有调用,有调用就有代码被执行。输入是add函数,输出是一个闭包函数(closure)。

dec1(dec2(add))

dec2(add)的返回值就是一个函数,dec1()函数的输入就要一个函数。

另外,此处有调用,有调用就有代码被执行。

dec1(dec2(add))输出也是一个闭包函数(closure)。

dec1(dec2(add))(5, 6)

因为dec1(dec2(add))的输出就是一个函数,我们当然可以调用啦!此处的返回是11.

我们可以把dec1(dec2(add))(5, 6)执行分成2部分,这样便于理解

  • 首先是求得inner函数的部分

dec1(dec2(add))根据表达式的求值顺序,当然是先执行dec2.outer,然后在执行dec1.outer

  • 然后是inner函数的执行部分

dec1(dec2(add))指向的是dec.inner函数,执行起来当然先执行dec1.inner代码,dec1.inner函数又闭包了dec2.inner函数,然后又可以执行dec2.inner函数,后来dec2.inner函数有闭包了add函数,然后有执行了add函数,最后在一步一步回退,把add的返回值回退给dec1.inner当做返回值!

在实际应用中,可能应用多个装饰器的机会也不多,即使有多个装饰器,需要特别注意顺序的机会也不多,但如果理解了这些执行顺序的细节,那我们就更加全面的了解了装饰器。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值