简单 12 步理解 Python 装饰器

好吧,我标题党了。作为 Python 教师,我发现理解装饰器是学生们从接触后就一直纠结的问题。那是因为装饰器确实难以理解!想弄明白装饰器,需要理解一些函数式编程概念,并且要对Python中函数定义和函数调用语法中的特性有所了解。使用装饰器非常简单(见步骤10),但是写装饰器却很复杂。

虽然我没法让装饰器变得简单,但也许通过将问题进行一步步的讲解,可以帮助你更容易理解装饰器。由于装饰器较为复杂,文章会比较长,请坚持住!我会尽量使每个步骤简单明了,这样如果你理解了各个步骤,就能理解装饰器的原理。本文假定你具备最基础的 Python 知识,另外本文对工作中大量使用 Python 的人将大有帮助。

此外需要说明的是,本文中 Python 代码示例是用 doctest 模块来执行的。代码看起来像是交互式 Python 控制台会话(>>> 表示 Python 语句,输出则另起一行)。偶然有以“doctest”开头的“奇怪”注释——那些只是 doctest 的指令,可以忽略。

1. 函数

在 Python 中,使用关键字 def 和一个函数名以及一个可选的参数列表来定义函数。函数使用 return 关键字来返回值。定义和使用一个最简单的函数例子:

1234 >>> def foo():...     return 1>>> foo()1

函数体(和 Python 中所有的多行语句一样)由强制性的缩进表示。在函数名后面加上括号就可以调用函数。

2. 作用域

在 Python 函数中会创建一个新的作用域。Python 高手也称函数有自己的命名空间。也就是说,当在函数体中遇到变量时,Python 会首先在该函数的命名空间中寻找变量名。Python 有几个函数用来查看命名空间。下面来写一个简单函数来看看局部变量和全局变量的区别。

1234567 >>> a_string = \”This is a global variable\”>>> def foo():...     print locals()>>> print globals() # doctest: +ELLIPSIS{..., \’a_string\’: \’This is a global variable\’}>>> foo() # 2{}

内建函数 globals 返回一个包含所有 Python 能识别变量的字典。(为了更清楚的描述,输出时省略了 Python 自动创建的变量。)在注释 #2 处,调用了 foo 函数,在函数中打印局部变量的内容。从中可以看到,函数 foo 有自己单独的、此时为空的命名空间。

3. 变量解析规则

当然,以上并不意味着我们不能在函数内部使用全局变量。Python 的作用域规则是, 变量的创建总是会创建一个新的局部变量但是变量的访问(包括修改)在局部作用域查找然后是整个外层作用域来寻找匹配。所以如果修改 foo 函数来打印全部变量,结果将是我们希望的那样:

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

#1 处,Python 在函数 foo 中搜索局部变量 a_string,但是没有找到,然后继续搜索同名的全局变量。

另一方面,如果尝试在函数里给全局变量赋值,结果并不是我们想要的那样:

12345678 >>> 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\’

从上面代码可见,全部变量可以被访问(如果是可变类型,甚至可以被修改)但是(默认)不能被赋值。在函数 #1 处,实际上是创建了一个和全局变量相同名字的局部变量,并且“覆盖”了全局变量。通过在函数 foo 中打印局部命名空间可以印证这一点,并且发现局部命名空间有了一项数据。在 #2 处的输出可以看到,全局命名空间里变量 a_string 的值并没有改变。

4. 变量生命周期

值得注意的是,变量不仅是在命名空间中有效,它们也有生命周期。思考下面的代码:

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

这个问题不仅仅是因为 #1 处的作用域规则(虽然那是导致 NameError 的原因),也与 Python 和很多其他语言中函数调用的实现有关。没有任何语法可以在该处取得变量 x 的值——它确确实实不存在!函数 foo 的命名空间在每次函数被调用时重新创建,在函数结束时销毁。

5. 函数的实参和形参

Python 允许向函数传递参数。形参名在函数里为局部变量。

1234 >>> def foo(x):...     print locals()>>> foo(1){\’x\’: 1}

Python 有一些不同的方法来定义和传递函数参数。想要深入的了解,请参考 Python 文档关于函数的定义。来说一个简单版本:函数参数可以是强制的位置参数或者可选的有默认值的关键字参数。

好吧,我标题党了。作为 Python 教师,我发现理解装饰器是学生们从接触后就一直纠结的问题。那是因为装饰器确实难以理解!想弄明白装饰器,需要理解一些函数式编程概念,并且要对Python中函数定义和函数调用语法中的特性有所了解。使用装饰器非常简单(见步骤10),但是写装饰器却很复杂。

虽然我没法让装饰器变得简单,但也许通过将问题进行一步步的讲解,可以帮助你更容易理解装饰器。由于装饰器较为复杂,文章会比较长,请坚持住!我会尽量使每个步骤简单明了,这样如果你理解了各个步骤,就能理解装饰器的原理。本文假定你具备最基础的 Python 知识,另外本文对工作中大量使用 Python 的人将大有帮助。

此外需要说明的是,本文中 Python 代码示例是用 doctest 模块来执行的。代码看起来像是交互式 Python 控制台会话(>>> 表示 Python 语句,输出则另起一行)。偶然有以“doctest”开头的“奇怪”注释——那些只是 doctest 的指令,可以忽略。

1. 函数

在 Python 中,使用关键字 def 和一个函数名以及一个可选的参数列表来定义函数。函数使用 return 关键字来返回值。定义和使用一个最简单的函数例子:

1234 >>> def foo():...     return 1>>> foo()1

函数体(和 Python 中所有的多行语句一样)由强制性的缩进表示。在函数名后面加上括号就可以调用函数。

2. 作用域

在 Python 函数中会创建一个新的作用域。Python 高手也称函数有自己的命名空间。也就是说,当在函数体中遇到变量时,Python 会首先在该函数的命名空间中寻找变量名。Python 有几个函数用来查看命名空间。下面来写一个简单函数来看看局部变量和全局变量的区别。

1234567 >>> a_string = \”This is a global variable\”>>> def foo():...     print locals()>>> print globals() # doctest: +ELLIPSIS{..., \’a_string\’: \’This is a global variable\’}>>> foo() # 2{}

内建函数 globals 返回一个包含所有 Python 能识别变量的字典。(为了更清楚的描述,输出时省略了 Python 自动创建的变量。)在注释 #2 处,调用了 foo 函数,在函数中打印局部变量的内容。从中可以看到,函数 foo 有自己单独的、此时为空的命名空间。

3. 变量解析规则

当然,以上并不意味着我们不能在函数内部使用全局变量。Python 的作用域规则是, 变量的创建总是会创建一个新的局部变量但是变量的访问(包括修改)在局部作用域查找然后是整个外层作用域来寻找匹配。所以如果修改 foo 函数来打印全部变量,结果将是我们希望的那样:

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

#1 处,Python 在函数 foo 中搜索局部变量 a_string,但是没有找到,然后继续搜索同名的全局变量。

另一方面,如果尝试在函数里给全局变量赋值,结果并不是我们想要的那样:

12345678 >>> 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\’

从上面代码可见,全部变量可以被访问(如果是可变类型,甚至可以被修改)但是(默认)不能被赋值。在函数 #1 处,实际上是创建了一个和全局变量相同名字的局部变量,并且“覆盖”了全局变量。通过在函数 foo 中打印局部命名空间可以印证这一点,并且发现局部命名空间有了一项数据。在 #2 处的输出可以看到,全局命名空间里变量 a_string 的值并没有改变。

4. 变量生命周期

值得注意的是,变量不仅是在命名空间中有效,它们也有生命周期。思考下面的代码:

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

这个问题不仅仅是因为 #1 处的作用域规则(虽然那是导致 NameError 的原因),也与 Python 和很多其他语言中函数调用的实现有关。没有任何语法可以在该处取得变量 x 的值——它确确实实不存在!函数 foo 的命名空间在每次函数被调用时重新创建,在函数结束时销毁。

5. 函数的实参和形参

Python 允许向函数传递参数。形参名在函数里为局部变量。

1234 >>> def foo(x):...     print locals()>>> foo(1){\’x\’: 1}

Python 有一些不同的方法来定义和传递函数参数。想要深入的了解,请参考 Python 文档关于函数的定义。来说一个简单版本:函数参数可以是强制的位置参数或者可选的有默认值的关键字参数。