python教程(三)·函数进阶(下)

下半部分果然很快到来,这次介绍函数的更高级用法,装饰器!

函数嵌套

先来说说函数嵌套,python中的函数是可以嵌套的,也就是说可以将一个函数放在另一个函数里面,比如:

>>> def outer(name):
...     def inner():
...             print(name)
...     return inner
... 
>>> func1 = outer('feather')
>>> func2 = outer('Lee')
>>> func1()
feather
>>> func2()
Lee
>>> 

你没看错,我们在函数里面定义了另一个函数,并把这个函数返回了?

返回出来的函数是带着它的所在的作用域的,这就是为什么返回出来的函数仍能访问外层函数的变量name,而且可以看到func1()func2()结果不一样,这表明每次调用外层函数都会重新定义内层函数,事实也是如此。

装饰器

再来看看下面这个函数,<( ̄︶ ̄)↗

def func():
    for i in range(100000):
        pass  # pass的意思是什么都不做

这段函数只是空转100000次没什么意义,只是做个例子,假如我们想要运使用这个函数并输出运行了多长时间,我们可以这么做:

import time

def func():
    for i in range(100000):
        pass  # pass的意思是什么都不做

# 获取当前时间戳,时间戳就是当前到1970年所经过的秒数
start = time.time()  
func()
end = time.time()

print(end-start)

我们可以在函数前后分别获取时间,两个时间的差就是函数的运行时间,这很简单。可是,如果我们有很多个函数都需要统计时间怎么办?每个函数调用前都加上同样的代码吗?

当然不是,受函数嵌套的启发,我们可以像下面这样定义一个函数:

def count(func):
    def inner():
        start = time.time()
        result = func()
        end = time.time()
        print(end-start)
        return result
    return inner()

然后把要计算运行时间的函数作为参数传给count函数,就是把count函数当一个代理人,替我们调用函数。

但是这样的话,调用func函数不能写func(),要写count(func),有点别扭,再改进下:

def count(func):
    def inner():
        start = time.time()
        result = func()
        end = time.time()
        print(end-start)
        return result
    return inner    # 返回这个内层函数,而不是调用

如果再执行了这样的代码:

func = count(func)

这个什么意思?

其实就是把func这个变量(函数也可理解成一种变量)重新绑定成count里面定义的inner()函数。这个时候我们再执行func()就可以达到效果了,完整代码如下:

import time

def func():
    for i in range(100000):
        pass  # pass的意思是什么都不做

def count(func):
    def inner():
        # 获取当前时间戳,时间戳就是当前到1970年所经过的秒数
        start = time.time()  
        result = func()
        end = time.time()
        print(end-start)
        return result
    return inner

func = count(func)
func()

装饰器的雏形出来了!装饰器就是像count这样的函数,用来给其它函数增加额外的功能,count函数在里面定义了一个inner函数用来包裹(装饰)被装饰的函数func。

不过这仅仅是雏形,并不是真正的装饰器,现在该让真正的装饰器登场了!

import time

def count(func):
    def inner():
        # 获取当前时间戳,时间戳就是当前到1970年所经过的秒数
        start = time.time()  
        result = func()
        end = time.time()
        print(end-start)
        return result
    return inner
    
@count
def func():
    for i in range(100000):
        pass  # pass的意思是什么都不做
        
func()

在函数func()的上面多了样东西:@count,这个东西就是装饰器,它的本质等同于func = count(func)。这样以后,每当我们需要为哪个函数的增加输出运行时间的功能,就在这个函数的定义前加上@count

万能的***

如果func函数的参数数量不确定,我们可以使用参数收集和分配参数的技巧,代码如下:

def count(func):
    # “百搭”的参数定义
    def inner(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)  # 展开参数
        end = time.time()
        print(end-start)
        return result
    return inner

这样的count装饰器对于参数数量任意的函数都可以匹配了。

带参数的装饰器

装饰器也可以有参数!

import time

def count(switch):
    # switch为True则计时,否则关闭计时功能
    def wrapper(func):
        def inner(*args, **kwargs):
            if switch:
                start = time.time()  
                result = func()
                end = time.time()
                print(end-start)
                return result
            else:
                return func()
        return inner
    return wrapper

switch = bool(input('是否计时?(y/n)') == 'y')

@count(switch)
def func():
    for i in range(100000):
        pass  # pass的意思是什么都不做

func()

看完以后是不是有点晕?(@_@;)

我来说说这个运行的过程:首先,程序等待输入,假定输入y,这是switch变量赋值为True,然后遇到@count(switch),这一行相当于调用了count函数,这个函数返回了一个装饰器,就是里面的wrapper,然后对func函数使用这个装饰器。

(°ー°〃)

嗯?我应该说清楚了吧。。。

对!还有一个问题 (°ー°〃)

函数签名

每个函数都有一个属性,叫做函数签名,就是函数的名字,比如print函数的函数签名可以这样获得:

>>> print.__name__
'print'
>>> 

一个函数使用了装饰器,表面上还是使用这个函数的名字,实际上真的是这样吗?看下面代码:

>>> def outer(func):
...     def inner():
...             pass
...     return inner
... 
>>> @outer
... def func():
...     pass
... 
>>> func.__name__
'inner'
>>> 

函数的签名已经改变了!

可是…

这个能有什么问题?

( ̄︶ ̄)↗ 有些时候我们是需要使用这个函数签名的的,比如:根据输入的函数名字使用对应的函数(根据字符串获取函数的方法在后面的小实例有用到),有些模块也可能使用到了函数签名,如果不注意到这个细节,出错了也找不出什么问题来!

那么怎么解决?

解决这个问题的方法很简单,我们只需使用一个内置模块的装饰器:

>>> import functools
>>> def outer(func):
...     @functools.wraps(func)
...     def inner():
...             pass
...     return inner
... 
>>> @outer
... def func():
...     pass
... 
>>> func.__name__
'func'
>>> 

代码简洁明了,至于原理,我们奉行 “拿来主义”,只需要知道在用于包裹的函数上方加上@functools.wraps()即可,不必纠结functools.wraps的内部实现

多个装饰器

装饰器可以使用多个,多个装饰器的执行顺序是从下往上的,看下面这个例子:

>>> def wrapper_1(func):
...     def inner():
...             pass
...     print('wrapper_1')
...     return inner
... 
>>> def wrapper_2(func):
...     def inner():
...             pass
...     print('wrapper_2')
...     return inner
... 
>>> @wrapper_1
... @wrapper_2
... def func(): 
...     pass
... 
wrapper_2
wrapper_1
>>>

这很好理解,我们在包装东西的时候,都是先从最里面的一层开始,一层一层包裹,直到最外层(就像“俄罗斯套娃“),所以先执行@wrapper_2再执行外面的@wrapper_1

与此同时,这个例子中并没有调用func函数,但是却有执行的print语句,这更进一步证明了,@wrapper这样的语句相当于执行了func=wrapper(func),这是在函数定义的时候马上就执行的,而不是调用的时候才执行的,要注意了


到目前,我们对函数的讲解就此结束,内容很多,读者们有得是时间消化了,也为我准备后面小实例提供了准备时间,读者们期待吧!(提示一下,我们将使用微信控制电脑!)

ヾ( ̄▽ ̄)Bye~Bye~