内层函数——它们的优点是什么?
让我们看一下写内层函数的三个常见原因。
记住:在Python中,函数是“一等公民”,这意味着它们和其他对象平起平坐(例如:整型,字符串,列表,模块等)。你可以动态地创建和销毁它们,把它们传递给其他函数,把它们作为值返回,等等。
本文使用Python 3.4.1版本
1.封装
你使用内层函数来保护它们不受函数外部变化的影响,也就是说把它们从全局作用域藏起来。
这里有一个简单的例子来强调这一概念:
1
2
3
4
5
6
7
8
|
def
outer
(
num1
)
:
def
inner_increment
(
num1
)
:
# hidden from outer code
return
num1
+
1
num2
=
inner_increment
(
num1
)
print
(
num1
,
num2
)
inner_increment
(
10
)
# outer(10)
|
尝试调用inner_increment()
函数
1
2
3
4
|
Traceback
(
most
recent
call
last
)
:
File
"inner.py"
,
line
7
,
in
<
module
>
inner_increment
(
)
NameError
:
name
'inner_increment'
is
not
defined
|
现在,把inner_increment
的调用注释掉,再把对外部函数调用的注释取消,outer(10)
,把10作为参数传入:
1
|
10
11
|
请记住这仅仅是一个例子,尽管代码得到了期望的结果,但是使用一个前置的下划线把
inner_increment()
函数变为“私有”函数:_inner_increment()
更好。
下面这个递归的例子与使用嵌套函数相比稍微好一些:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
def
factorial
(
number
)
:
# error handling
if
not
isinstance
(
number
,
int
)
:
raise
TypeError
(
"Sorry. 'number' must be an integer."
)
if
not
number
&
gt
;
=
0
:
raise
ValueError
(
"Sorry. 'number' must be zero or positive."
)
def
inner_factorial
(
number
)
:
if
number
&
lt
;
=
1
:
return
1
return
number
*
inner_factorial
(
number
-
1
)
return
inner_factorial
(
number
)
# call the outer function
print
(
factorial
(
4
)
)
|
同样测试一下这段代码。使用这种设计模式的一个主要优势在于:在外部函数中对全部参数执行了检查,你可以在内部函数中跳过全部的检查过程。
关于这个递归更加详细的解释请看Problem Solving with Algorithms and Data Structures
2.贯彻DRY(Don’t Repeat Yourself )原则
也许你有一个巨型函数,在很多很多地方执行一大段的代码。比如,你可能写了一个函数用来处理文件,并且你希望它既可以接受一个打开文件对象或是一个文件名:
1
2
3
4
5
6
7
8
9
|
def
process
(
file_name
)
:
def
do_stuff
(
file_process
)
:
for
line
in
file_process
:
print
(
line
)
if
isinstance
(
file_name
,
str
)
:
with
open
(
file_name
,
'r'
)
as
f
:
do_stuff
(
f
)
else
:
do_stuff
(
file_name
)
|
同样,通常会把
do_stuff()
作为一个顶层私有函数,但是如果你想要把它作为一个内层函数隐藏起来,你也可以这样做。
来个实际的例子怎么样?
让我们假设你想知道纽约市全部WiFi热点的数量。而且确实有提供这些信息的原始数据: 数据。
访问这个网站并下载CSV.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
def
process
(
file_name
)
:
def
do_stuff
(
file_process
)
:
wifi_locations
=
{
}
for
line
in
file_process
:
values
=
line
.
split
(
','
)
# Build the dict, and increment values
wifi_locations
[
values
[
1
]
]
=
wifi_locations
.
get
(
values
[
1
]
,
0
)
+
1
max_key
=
0
for
name
,
key
in
wifi_locations
.
items
(
)
:
all_locations
=
sum
(
wifi_locations
.
values
(
)
)
if
key
&
gt
;
max_key
:
max_key
=
key
business
=
name
print
(
'There are {0} WiFi hot spots in NYC and {1} has the most with {2}.'
.
format
(
all_locations
,
business
,
max_key
)
)
if
isinstance
(
file_name
,
str
)
:
with
open
(
file_name
,
'r'
)
as
f
:
do_stuff
(
f
)
else
:
do_stuff
(
file_name
)
process
(
"NAME_OF_THE.csv"
)
|
执行函数:
1
|
There
are
1251
WiFi
hot
spots
in
NYC
and
Starbucks
has
the
most
with
212.
|
3.闭包和工厂函数
现在我们要谈到使用内层函数最重要的原因了。目前为止我们看到的全部内层函数的例子都是普通函数,只不过它们凑巧嵌套在另外的函数之中了。换句话说,我们本可以用另外的方式定义这些函数(正如我们谈到的那样),并没有什么特别的理由去嵌套它们。
但是当我们谈到闭包的时候,情况就不一样了:你必须要利用嵌套函数。
什么是闭包?
闭包无非是使内层函数在调用时记住它当前环境的状态。初学者经常认为闭包就是内层函数,而且实际上它是由内层函数导致的。闭包在栈上“封闭”了局部变量,使其在栈创建执行结束后仍然存在。
一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
def
generate_power
(
number
)
:
"""
Examples of use:
>>raise_two = generate_power(2)
>>raise_three = generate_power(3)
>>print(raise_two(7))
128
>>print(raise_three(5))
243
"""
# define the inner function ...
def
nth_power
(
power
)
:
return
number
*
*
power
# ... which is returned by the factory function
return
nth_power
|
此处发生了什么?
- generate_power()函数是一个工厂方法——简单说就是意味着它每次调用的时候会创建一个新函数,并返回这个新创建的函数,因此
raise_two
和raise_three
都是新创建的函数。 - 这个新的内层函数做了什么呢?它接受一个单独的参数,power,并且返回
number**power
。 - 内层函数从哪得到
number
的值呢?这就该闭包登场了:nth_power()
从外层函数得到power
的值,让我们逐步查看这一过程:- 调用外层函数:
generate_power(2)
- 生成一个
nth_power()
函数,它接受一个单一的参数power - 保存
nth_power()
的状态快照,其中number=2 - 把这个快照传入
generate_power()
函数 - 返回
nth_power()
函数
- 调用外层函数:
换句话说,闭包函数“初始化”了nth_power()
函数并将其返回。现在无论你何时调用这个新返回的函数,它都会去查看其私有的快照,也就是包含power=2
的那一个。
现实世界
来一个实际例子如何?
1
2
3
4
5
6
7
8
9
10
11
12
13
|
def
has_permission
(
page
)
:
def
inner
(
username
)
:
if
username
==
'Admin'
:
return
"'{0}' does have access to {1}."
.
format
(
username
,
page
)
else
:
return
"'{0}' does NOT have access to {1}."
.
format
(
username
,
page
)
return
inner
current_user
=
has_permission
(
'Admin Area'
)
print
(
current_user
(
'Admin'
)
)
random_user
=
has_permission
(
'Admin Area'
)
print
(
current_user
(
'Not Admin'
)
)
|
这是一个简化过的函数,用来检查某个特定用户是否有权限访问特定的页面。你可以很容易的修改它从会话中抓取用户信息,查看他们是否具有访问当前路径的正确凭证。我们可以查询数据库来查看许可情况,然后根据是否具有正确的凭证来返回返回正确的视图,而不是检查user是否等于‘Admin’。
结论
闭包和工厂函数是内层函数最常见也最强大的用途。在大多数情况下,当你看到一个被装饰的函数,装饰器就是一个工程函数,它接受一个函数作为参数,然后返回一个新的函数,其闭包中包含了原函数。停一停。深呼吸。喝口咖啡。再读一遍。
换句话说,装饰器是一个语法糖,用来实现generate_power() 例子中提到的功能
我把这个例子留给你:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
def
generate_power
(
exponent
)
:
def
decorator
(
f
)
:
def
inner
(
*
args
)
:
result
=
f
(
*
args
)
return
exponent
*
*
result
return
inner
return
decorator
@
generate_power
(
2
)
def
raise_two
(
n
)
:
return
n
print
(
raise_two
(
7
)
)
@
generate_power
(
3
)
def
raise_three
(
n
)
:
return
n
print
(
raise_two
(
5
)
)
|
如果你的代码编辑器运行,请并排对比着查看generate_power(exponent)
和generate_power(number)
这两个函数来搞清楚我们所讨论的概念。(比如说Sublime Text有分栏浏览模式(译注:shift+alt+2))
如何你还没有编写这两个函数,打开编辑器并开始编写。对于新手程序员来讲,编码是一项亲手实践的活动,就像学习骑车,你必须去做才行——而且要独立完成。所以请回到手上的任务来。当你输入完成代码后,你可以清楚的看到它们产生了相同的结果,但是这之间是有不同的。对于那些从来没用过装饰器的人来说,注意到这些不同是理解它们的第一步——如果你要走这条路的话。