【Python之路】特别篇--Python装饰器

 

前情提要

1. 作用域

  在python中,函数会创建一个新的作用域。python开发者可能会说函数有自己的命名空间,差不多一个意思。这意味着在函数内部碰到一个变量的时候函数会优先在自己的命名空间里面去寻找。

python中的作用域分4种情况:

  • L:local,局部作用域,即函数中定义的变量;

  • E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的;

  • G:globa,全局变量,就是模块级别定义的变量;

  • B:built-in,系统固定模块里面的变量,比如int, bytearray等。

搜索变量的优先级顺序依次是:局部作用域 > 外层作用域 > 当前模块中的全局 > python内置作用域,也就是LEGB。

当然,local和enclosing是相对的,enclosing变量相对上层来说也是local。

x = int(2.9)  # int built-in
 
g_count = 0  # global
def outer():
    o_count = 1  # enclosing
    def inner():
        i_count = 2  # local
        print(o_count)
    # print(i_count) 找不到
    inner() 
outer()
 
# print(o_count) #找不到

在Python中,只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如if、try、for等)是不会引入新的作用域的。

让我们写一个简单的函数看一下 global 和 local 有什么不同:

>>> a_string = "This is a global variable"
>>> def foo():
...     print locals()
>>> print globals()
{..., 'a_string': 'This is a global variable'}
>>> foo() # 2
{}

内置的函数globals返回一个包含所有python解释器知道的变量名称的字典。

在#2我调用了函数 foo 把函数内部本地作用域里面的内容打印出来。

我们能够看到,函数foo有自己独立的命名空间,虽然暂时命名空间里面什么都还没有。

global关键字 

当内部作用域想修改外部作用域的变量时,就要用到global和nonlocal关键字了,当修改的变量是在全局作用域(global作用域)上的,就要使用global先声明一下,代码如下:

count = 10
def outer():
    global count
    print(count) 
    count = 100
    print(count)
outer()
#10
#100

nonlocal关键字 

global关键字声明的变量必须在全局作用域上,不能嵌套作用域上,当要修改嵌套作用域(enclosing作用域,外层非全局作用域)中的变量怎么办呢,这时就需要nonlocal关键字了

def outer():
    count = 10
    def inner():
        nonlocal count
        count = 20
        print(count)
    inner()
    print(count)
outer()
#20
#20 

2. 变量解析规则

当然这并不是说我们在函数里面就不能访问外面的全局变量。

在python的作用域规则里面,创建变量一定会一定会在当前作用域里创建一个变量,

但是访问或者修改变量时会先在当前作用域查找变量,没有找到匹配变量的话会依次向上在闭合的作用域里面进行查看找

所以如果我们修改函数foo的实现让它打印全局的作用域里的变量也是可以的:

>>> a_string = "This is a global variable"
>>> def foo():
...     print a_string # 1
>>> foo()
This is a global variable

在#1处,python解释器会尝试查找变量a_string,当然在函数的本地作用域里面是找不到的,所以接着会去上层的作用域里面去查找。

但是另一方面,假如我们在函数内部给全局变量赋值,结果却和我们想的不一样:

>>> a_string = "This is a global variable"
>>> def foo():
...     a_string = "test" # 1
...     print locals()
>>> foo()
{'a_string': 'test'}
>>> a_string # 2
'This is a global variable'

我们能够看到,全局变量能够被访问到(如果是可变数据类型(像list,dict这些)甚至能够被更改)但是赋值不行。

在函数内部的#1处,我们实际上新创建了一个局部变量,隐藏全局作用域中的同名变量。

我们可以通过打印出局部命名空间中的内容得出这个结论。

我们也能看到在#2处打印出来的变量a_string的值并没有改变。

3. 变量生存周期

值得注意的一个点是,变量不仅是生存在一个个的命名空间内,他们都有自己的生存周期,请看下面这个例子:

>>> def foo():
...     x = 1
>>> foo()
>>> print x # 1
Traceback (most recent call last):
  ...
NameError: name 'x' is not defined

#1处发生的错误不仅仅是因为作用域规则导致的(尽管这是抛出了NameError的错误的原因)

它还和python以及其它很多编程语言中函数调用实现的机制有关。

在这个地方这个执行时间点并没有什么有效的语法让我们能够获取变量x的值,因为它这个时候压根不存在!

函数foo的命名空间随着函数调用开始而开始,结束而销毁。

4. 嵌套函数

Python允许创建嵌套函数。这意味着我们可以在函数里面定义函数而且现有的作用域和变量生存周期依旧适用。

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()
1

python解释器需找一个叫x的本地变量,查找失败之后会继续在上层的作用域里面寻找。

这个上层的作用域定义在另外一个函数里面。

对函数outer来说,变量x是一个本地变量,但是如先前提到的一样,

函数inner可以访问封闭的作用域(至少可以读和修改)。

在#2处,我们调用函数inner,非常重要的一点是:

inner仅仅是一个遵循python变量解析规则的变量名,python解释器会优先在outer的作用域里面对变量名inner查找匹配的变量.

5. 闭包

我们先不急着定义什么是闭包,先来看看一段代码:

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo.func_closure
(<cell at 0x...: int object at 0x...>,)

inner作为一个函数被outer返回,保存在一个变量foo,并且我们能够对它进行调用foo()

不过它会正常的运行吗?我们先来看看作用域规则。

所有的东西都在python的作用域规则下进行工作:“x是函数outer里的一个局部变量。

当函数inner在#1处打印x的时候,python解释器会在inner内部查找相应的变量,当然会找不到,

所以接着会到封闭作用域里面查找,并且会找到匹配。

但是从变量的生存周期来看,该怎么理解呢?

我们的变量x是函数outer的一个本地变量,这意味着只有当函数outer正在运行的时候才会存在。

根据我们已知的python运行模式,我们没法在函数outer返回之后继续调用函数inner,在函数inner被调用的时候,变量x早已不复存在,可能会发生一个运行时错误。

万万没想到,返回的函数inner居然能够正常工作。

Python支持一个叫做函数闭包的特性,用人话来讲就是:

嵌套定义在非全局作用域里面的函数能够记住它在被定义的时候它所处的封闭命名空间。

这能够通过查看函数的func_closure属性得出结论,这个属性里面包含封闭作用域里面的值

(只会包含被捕捉到的值,比如x,如果在outer里面还定义了其他的值,封闭作用域里面是不会有的)

记住,每次函数outer被调用的时候,函数inner都会被重新定义。

闭包用途:

# 用途1:当闭包执行完后,仍然能够保持住当前的运行环境。
# 比如说,如果你希望函数的每次执行结果,都是基于这个函数上次的运行结果。我以一个类似棋盘游戏的例子
# 来说明。假设棋盘大小为50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向
# (direction),步长(step),该函数控制棋子的运动。棋子运动的新的坐标除了依赖于方向和步长以外,
# 当然还要根据原来所处的坐标点,用闭包就可以保持住这个棋子原来所处的坐标。

origin = [0, 0] # 坐标系统原点
legal_x = [0, 50] # x轴方向的合法坐标
legal_y = [0, 50] # y轴方向的合法坐标
def create(pos=origin):
    def player(direction,step):
        # 这里应该首先判断参数direction,step的合法性,比如direction不能斜着走,step不能为负等
        # 然后还要对新生成的x,y坐标的合法性进行判断处理,这里主要是想介绍闭包,就不详细写了。
        new_x = pos[0] + direction[0]*step
        new_y = pos[1] + direction[1]*step
        pos[0] = new_x
        pos[1] = new_y
        #注意!此处不能写成 pos = [new_x, new_y],原因在上文有说过
        return pos
    return player

player = create() # 创建棋子player,起点为原点
print (player([1,0],10)) # 向x轴正方向移动10步
print (player([0,1],20)) # 向y轴正方向移动20步
print (player([-1,0],10)) # 向x轴负方向移动10步
用途1
# 用途2:闭包可以根据外部作用域的局部变量来得到不同的结果,这有点像一种类似配置功能的作用,我们可以
# 修改外部的变量,闭包根据这个变量展现出不同的功能。比如有时我们需要对某些文件的特殊行进行分析,先
# 要提取出这些特殊行。

def make_filter(keep):
    def the_filter(file_name):
        file = open(file_name)
        lines = file.readlines()
        file.close()
        filter_doc = [i for i in lines if keep in i]
        return filter_doc
    return the_filter

# 如果我们需要取得文件"result.txt"中含有"pass"关键字的行,则可以这样使用例子程序
filter = make_filter("pass")
filter_result = filter("result.txt")
用途2

 


装饰器需要掌握知识

  • 作用域知识(LEGB)
  • 函数知识

    • 函数名可以作为参数输入

    • 函数名可以作为返回值
  • 闭包
    • 在内部函数里,对外部作用域的变量引用,那么内部函数就是一个闭包

写代码要遵循开发封闭原则,虽然在这个原则是用的面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即:

  • 封闭:已实现的功能代码块

  • 开放:对扩展开发

如果需要在  函数执行前 额外执行其他功能的话,我们就可以用到装饰器来实现~

def index():
    print("Hello !")
    return True
    
index()

如果我们需要在函数 f1执行前先输出一句 Start ,函数执行后输出一句 End  ,那么我们可以这样做:

def outer(func):
    def inner():
        print("Start")
        result = func()
        print("End")
        return result
    return inner

@outer     # index = outer(index)
def index():
    print("Hello !")
    return True

result = index()
 
# Start
# Hello !
# End

当写完这段代码后(函数未被执行、未被执行、未被执行),python解释器就会从上到下解释代码,步骤如下:

1.先把 def outer(func) 函数加载到内存

2.执行@outer

执行@outer 时 , 先把 index 函数 加载到内存 ! 并且内部会执行如下操作: 

1.执行 outer 函数,将 index 作为参数传递   ( 此时 func = index )

2.将 outer 函数返回值 ( return inner ),重新赋值给 index  (index = inner)

然后执行下面的 result = index()  => 相当于 执行了 inner() 函数!!!


 

问题:如果被装饰的函数如果有参数呢?

def outer(func):
    def inner(a1):
        print("start")
        result = func(a1)
        print("end")
        return result
    return inner


def index(a1):
    print(a1)
    return True

result = index(1)
print(result)
一个参数
def outer(func):
    def inner(a1,a2):
        print("start")
        result = func(a1,a2)
        print("end")
        return result
    return inner


def index(a1,a2):
    a3 = a1 + a2
    print(a3)
    return True

result = index(1,2)
print(result)
两个参数

问题:可以装饰具有处理n个参数的函数的装饰器吗?

def outer(func):
    def inner(*args,**kwargs):
        print("start")
        result = func(*args,**kwargs)
        print("end")
        return result
    return inner

@outer
def index1(a1,):
    print(a1)
    return True

@outer
def index2(a1,a2):
    print(a1,a2)
    return True

@outer
def index3(a1,a2,a3):
    print(a1,a2,a3)
    return True


index1(5)
index2(5,6)
index3(5,6,7)

 

多层装饰器

问题:一个函数可以被多个装饰器装饰吗?

def outer_0(func):
    def inner(*args,**kwargs):
        print("0.5")
        result = func(*args,**kwargs)
        return result
    return inner


def outer_1(func):
    def inner(*args,**kwargs):
        print("123")
        result = func(*args,**kwargs)
        print("456")
        return result
    return inner

@outer_0
@outer_1
def index(a1,a2):
    print(a1,a2)
    return True

index(1,2)

1.先把 outer_0 、outer_1 和 index 加载到内存

2.执行 @outer_0 时, func = @outer_1 和 index 函数的结合体

3.然后执行 @outer_0 的 inner , 其中包含了 @outer_1 和index  =>  所以先执行 @outer_1

4.当执行 @outer_1 时, func = index , 并执行 inner -> 再执行 func  即 index 函数

(原理同 只有一个装饰器一样 , 可以 把 @outer_1 和 def index 看成一个 结成的函数  => 当作只有一个 装饰器@outer_0)

 

带参数的装饰器

  装饰器还有更大的灵活性,例如带参数的装饰器:在上面的装饰器调用中,比如@show_time,

  该装饰器唯一的参数就是执行业务的函数。

  装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。

import time
def time_logger(flag=0):
 
    def show_time(func):
 
            def wrapper(*args,**kwargs):
                start_time=time.time()
                func(*args,**kwargs)
                end_time=time.time()
                print('spend %s'%(end_time-start_time))
 
                if flag:
                    print('将这个操作的时间记录到日志中')

            return wrapper

    return show_time
 
@time_logger(3)
def add(*args,**kwargs):
    time.sleep(1)
    sum=0
    for i in args:
        sum+=i
    print(sum)
 
add(2,7,5)

  @time_logger(3) 做了两件事:

  (1)time_logger(3):得到闭包函数show_time,里面保存环境变量flag

  (2)@show_time   :add=show_time(add)

  上面的time_logger是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器(一个含有参数的闭包函数)。

  当我们使用@time_logger(3)调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。

 

类装饰器

  再来看看类装饰器,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。

  使用类装饰器还可以依靠类内部的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

import time
class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        start_time=time.time()
        self._func()
        end_time=time.time()
        print('spend %s'%(end_time-start_time))

@Foo  #bar=Foo(bar)
def bar():
    print ('bar')
    time.sleep(2)

bar()    #bar=Foo(bar)()>>>>>>>没有嵌套关系了,直接active Foo的 __call__方法

注意 :

  使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、__name__、参数列表,:

  我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器函数中,这使得装饰器函数也有和原函数一样的元信息了。

from functools import wraps
 
def logged(func):
 
    @wraps(func)
    def wrapper(*args, **kwargs):
        print (func.__name__ + " was called")
        return func(*args, **kwargs)
    return wrapper
 
@logged
def cal(x):
   return x + x * x
 
print(cal.__name__)         #cal

 

posted @ 2016-11-16 14:47  5_FireFly  阅读(362)  评论(0编辑  收藏  举报
web
counter