函数装饰与语法糖 @ 在说函数的装饰之前,先说说内嵌函数的运行机制。 内嵌函数 内嵌函数就是在函数中定义的函数。 内嵌函数,除非明确的调用,它是不会按照函数中的代码顺序运行的。比如下面的代码中 def foo(): def bar(): print "bar() is running" print
函数装饰与语法糖 @
在说函数的装饰之前,先说说内嵌函数的运行机制。
内嵌函数
内嵌函数就是在函数中定义的函数。
内嵌函数,除非明确的调用,它是不会按照函数中的代码顺序运行的。比如下面的代码中
def foo():
def bar():
print "bar() is running"
print "foo() is running"
运行上述代码,不会输出 "bar() is running" ,原因就是内嵌函数必须要在函数中显式的调用,否则它不会按照脚本语言的规则,按顺序执行的。
下面是上述代码的修改版:
def foo():
def bar():
print "bar() is running"
bar()
print "foo() is running"
运行上面的代码,得到的输出结果如下:
>>> foo()
bar() is running
foo() is running
内嵌函数,作用域仅限于它定义所在的外层函数。
所以,不能从外部被调用。下面的调用就会报错。
>>> bar()
Traceback (most recent call last):
File "", line 1, in
NameError: name 'bar' is not defined
>>> foo().bar()
foo() is running
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'NoneType' object has no attribute 'bar'
函数的理解与装饰
在 python 中,函数也是对象,既然是对象,那么就可以传递、赋值给其他变量并被调用。
这就是函数能够被装饰的基础。装饰一个函数,这个时候,内嵌函数就派上了用场了。
通常情况下,我们直接调用一个函数。现在假设我们想要在函数调用的前后打印两个日志,记录这个函数被调用了。一般的方法就是在函数的入口和出口调用两次打印语句,这样做也行,但是如果我们不是一个函数,而是几个,几十个呢?
所以,如果一个函数 foo() 传递给另外一个函数 bar(),函数 bar() 做的事是打印一条入口日志,然后调用 foo(),然后再打印一条出口日志,这样来说,不就行了么?即 bar(foo),那么实现的代码就可以这样写:
def bar(func):
print "before call %s" % func.__name__
func()
print "after call %s" % func.__name__
def foo():
print "called foo()"
上面的代码就可以采用 bar(foo) 来调用,实现了打印日志的目的。那么我们在我们已经调用了 foo() 的地方,是不是都要改成 bar(foo) 的形式呢?
这样做也太脏了。那么是不是我们在 bar() 中加一个返回,赋值给被传入的函数,是不是就行了呢?先看下面的代码,和上面相比,只是在 bar() 中增加了一个返回。
def bar(func):
print "before call %s" % func.__name__
func()
print "after call %s" % func.__name__
return func
def foo():
print "called foo()"
我们使用如下调用方式:
foo = bar(foo)
foo()
不起作用!单独调用的 foo() 并没有按照我们期望的那样,在输出 foo() 的内容前先输出 bar() 的两个打印语句。问题出在哪里?
分析上面的代码,可以看出来,在 bar() 中的 return 语句,返回的 func 在 bar() 中的传入、传出都没有一点改变,所以上述方法不起作用。
也就是说,如果要使一个函数 foo() 传入另外一个函数 bar(),返回时函数 foo() 被修改了,就要求 bar() 中的代码能够修改 foo() 的运行形式,在返回时 foo() 已经不是原来传入时的 foo() 了,而是被修改过的函数了。
自然的(其实是不自然的,看了书和网上的代码才知道的)我们就想到了内嵌函数,我们在内嵌函数中调用传入的函数,并且返回内嵌函数,这样就达到了修改传入函数的的目的。看下面的例子:
def bar(foo):
def wrap():
print "before call %s" % func.__name__
foo()
print "after call %s" % func.__name__
return wrap
def foo():
print "called foo()"
现在我们再使用如下的调用方法:
foo = bar(foo)
foo()
输出如下内容
before call foo
called foo
after call foo
现在,我们看到,我们修改函数的运行方式的目的已经达到了,那么,我们是不是要在每一个我们需要进行 bar() 修改的地方都来一句类似于 foo = bar(foo) 的调用呢?
不是的,我们有语法糖 "@" 符号!使用上面的例子,我们可以使用下面的方法来修改函数:
@bar
def foo():
print "called foo()"
这样的效果和上面的一致的。
稍微复杂点的例子(有参数)
先说一下 python 中实现变参的两种方式
python 中的变参
被装饰的函数如果自身是有参数的怎么办呢?如何来写一个通用的可处理参数的包装函数?
python 中提供了两种可变参数, *args, **kwargs,形参名字随便取的,不过为了简洁与明晰表达意思,所以一般都用 args, kwargs。
*args 表示的是:无关键字的,参数长度可变的参数列表(非python 意义上的list());
**kwargs 表示的是:有关键字的,参数长度可变的参数列表(非python 意义上的list());
通俗点说,一个函数定义了 *args 这样的参数,那么就可以直接传递值或者一个列表的值进去;如果定义了 **kwargs 这样的参数,就可以传递类似字典参数进去。
比如有下面的函数:
def foo(*args, **kwargs):
for arg in args:
print arg
for k,v in kwargs.items():
print k,v
调用方法就可以使用:
foo(1, 2, 3, d = 4, e = 5, f = 6)
args = [1, 2, 3]
kwargs = {"d":4, "e":5, "f":6}
foo(*args, **kwargs)
foo(*args)
foo(**kwargs)
foo(1, 2) #args
foo(d = 4, e = 5, f = 6) # kwargs
上面就是可变参数的实现和调用方法。
函数装饰器中的参数
我们要编写一个通用点的装饰器,就不能假设被装饰的函数的参数的是什么样的,所以就需要用到上述的两种变参方法。下面是修改后的例子:
def bar(foo):
def wrap(*args, **kwargs):
print "before call %s" % func.__name__
foo(*args, **kwargs)
print "after call %s" % func.__name__
return wrap
@bar
def foo(arg0 = None, arg1 = None):
print "called foo()", arg0, arg1
结语
理解装饰器的关键在于理解内嵌函数的运行机制以及 python 中函数也是对象这个事实。
装饰器主要用在哪些地方呢?都有些什么作用?python 自带的 property, staticmethond, classmethod 的使用场景以及作用是什么?请看本博接下来的文章。