函数是Python的头等对象。可以把函数分配给变量、存储在数据结构中、作为参数传递给其他函数,甚至作为其他函数的返回值。
深入掌握这些概念不仅有助于理解Python中像lambda和装饰器这样的高级特性,而且会让你接触函数式编程技术。
接下来的几页将通过一些示例帮助你对这些概念形成直观的理解。这些示例循序渐进,因此需要按顺序阅读,并不断在Python解释器会话中尝试。
理解这些概念可能需要比较长的时间。别担心,这完全正常,我也经历过。你开始可能会觉得毫无头绪,但学习到一定程度后就会豁然开朗。
本节会使用下面这个yell
函数来演示相关功能。这是个简单的示例,输出的内容很简单。
def yell(text):
return text.upper() + '!'
>>> yell('hello')
'HELLO!'
Python 函数特性 函数是对象
Python程序中的所有数据都是由对象或对象之间的关系来表示的。字符串、列表和模块等都是对象。Python中的函数也不例外,同样是对象。
由于yell
函数是Python中的一个对象,因此像任何其他对象一样,也可以将其分配给另一个变量:
>>> bark = yell
这一行没有调用函数,而是获取yell
引用的函数对象,再创建一个指向该对象的名称bark
。现在调用bark
就可以执行相同的底层函数对象:
>>> bark('woof')
'WOOF!'
函数对象及其名称是相互独立的实体,下面来验证一下。先删除该函数的原始名称(yell
),由于另一个名称(bark
)仍然指向底层函数,因此仍然可以通过bark
调用该函数:
>>> del yell
>>> yell('hello?')
NameError: "name 'yell' is not defined"
>>> bark('hey')
'HEY!'
顺便说一句,Python在创建函数时为每个函数附加一个用于调试的字符串标识符。使用__name__
属性可以访问这个内部标识符:从Python 3.3开始加入了作用相似的__qualname__
,用来返回限定名称(qualified name)字符串,以消除函数和类名的歧义(详见PEP 3155)。
>>> bark.__name__
'yell'
虽然函数的__name__
仍然是yell
,但已经无法用这个名称在代码中访问函数对象。名称标识符仅仅用来辅助调试,指向函数的变量和函数本身实际上是彼此独立的。
Python 函数特性 函数可存储在数据结构中
由于函数是头等对象,因此可以像其他对象一样存储在数据结构中。例如,可以将函数添加到列表中:
>>> funcs = [bark, str.lower, str.capitalize]
>>> funcs
[<function yell at 0x10ff96510>,
<method 'lower' of 'str' objects>,
<method 'capitalize' of 'str' objects>]
访问存储在列表中的函数对象与访问其他类型的对象一样:
>>> for f in funcs:
... print(f, f('hey there'))
<function yell at 0x10ff96510> 'HEY THERE!'
<method 'lower' of 'str' objects> 'hey there'
<method 'capitalize' of 'str' objects> 'Hey there'
存储在列表中的函数对象可以直接调用,无须事先为其分配一个变量。比如,在单个表达式中查找函数,然后立即调用这个“没有实体”的函数对象。
>>> funcs[0]('heyho')
'HEYHO!'
Python 函数特性 函数可传递给其他函数
由于函数是对象,因此可以将其作为参数传递给其他函数。下面这个greet
函数将另一个函数对象作为参数,用这个函数来格式化问候字符串,然后输出结果:
def greet(func):
greeting = func('Hi, I am a Python program')
print(greeting)
传递不同的函数会产生不同的结果,向greet
函数传递bark
函数会得到下面这个结果:
>>> greet(bark)
'HI, I AM A PYTHON PROGRAM!'
当然,还可以定义一个新的函数来产生不同形式的问候语。例如,如果不希望这个Python程序在问候时听起来像擎天柱那样声音浑厚,那么可以使用下面的whisper
函数:
def whisper(text):
return text.lower() + '...'
>>> greet(whisper)
'hi, i am a python program...'
将函数对象作为参数传递给其他函数的功能非常强大,可以用来将程序中的行为抽象出来并传递出去。在这个例子中,greet
函数保持不变,但传递不同的问候行为能得到不同的结果。
能接受其他函数作为参数的函数被称为高阶函数。高阶函数是函数式编程风格中必不可少的一部分。
Python中具有代表性的高阶函数是内置的map
函数。map
接受一个函数对象和一个可迭代对象,然后在可迭代对象中的每个元素上调用该函数来生成结果。
下面通过将bark
函数映射到多个问候语中来格式化字符串:
>>> list(map(bark, ['hello', 'hey', 'hi']))
['HELLO!', 'HEY!', 'HI!']
从上面可以看出,map
遍历整个列表并将bark
函数应用于每个元素。所以,现在得到一个新列表对象,其中包含修改后的问候语字符串。
Python 函数特性 函数可以嵌套
也许有点出人意料,不过Python允许在函数中定义函数,这通常被称为嵌套函数或内部函数。来看下面的例子:
def speak(text):
def whisper(t):
return t.lower() + '...'
return whisper(text)
>>> speak('Hello, World')
'hello, world...'
这里发生了什么?每次调用speak
时,都会定义一个新的内部函数whisper
并立即调用。从这里开始,我有点迷糊了,但总而言之还算相对简单。
但有个问题,whisper
只存在于speak
内部:
>>> whisper('Yo')
NameError:
"name 'whisper' is not defined"
>>> speak.whisper
AttributeError:
"'function' object has no attribute 'whisper'"
那怎么才能从speak
外部访问嵌套的whisper
函数呢?由于函数是对象,因此可以将内部函数返回给父函数的调用者。
例如,下面这个函数定义了两个内部函数。顶层函数根据传递进来的参数向调用者返回对应的内部函数:
def get_speak_func(volume):
def whisper(text):
return text.lower() + '...'
def yell(text):
return text.upper() + '!'
if volume > 0.5:
return yell
else:
return whisper
注意,get_speak_func
实际上不调用任何内部函数,只是根据volume
参数选择适当的内部函数,然后返回这个函数对象:
>>> get_speak_func(0.3)
<function get_speak_func.<locals>.whisper at 0x10ae18>
>>> get_speak_func(0.7)
<function get_speak_func.<locals>.yell at 0x1008c8>
返回的函数既可以直接调用,也可以先指定一个变量名称再使用:
>>> speak_func = get_speak_func(0.7)
>>> speak_func('Hello')
'HELLO!'
要深入领会一下这里的概念。这意味着函数不仅可以通过参数接受行为,还可以返回行为。很酷吧?
这些内容有点多。在继续写作之前,我要喝杯咖啡休息一下(建议你也休息一下)。
Python 函数特性 函数可捕捉局部状态
前面介绍了函数可以包含内部函数,甚至可以从父函数返回(默认情况下看不见的)内部函数。
现在做好准备,下面将进入函数式编程中较深的领域。(你刚刚休息了一会儿,对吧?)
内部函数不仅可以从父函数返回,还可以捕获并携带父函数的某些状态。这是什么意思呢?
下面对前面的get_speak_func
示例做些小改动来逐步说明这一点。新版在内部就会使用volume
和text
参数,因此返回的函数是可以直接调用的:
def get_speak_func(text, volume):
def whisper():
return text.lower() + '...'
def yell():
return text.upper() + '!'
if volume > 0.5:
return yell
else:
return whisper
>>> get_speak_func('Hello, World', 0.7)()
'HELLO, WORLD!'
仔细看看内部函数whisper
和yell
,注意其中并没有text
参数。但不知何故,内部函数仍然可以访问在父函数中定义的text
参数。它们似乎捕捉并“记住”了这个参数的值。
拥有这种行为的函数被称为词法闭包(lexical closure),简称闭包。闭包在程序流不在闭包范围内的情况下,也能记住封闭作用域(enclosing scope)中的值。
实际上,这意味着函数不仅可以返回行为,还可以预先配置这些行为。用另一个例子来演示一下:
def make_adder(n):
def add(x):
return x + n
return add
>>> plus_3 = make_adder(3)
>>> plus_5 = make_adder(5)
>>> plus_3(4)
7
>>> plus_5(4)
9
在这个例子中,make_adder
作为工厂函数来创建和配置各种adder函数。注意,这些adder函数仍然可以访问make_adder
函数中位于封闭作用域中的参数n
。
Python 函数特性 对象也可作为函数使用
虽然Python中的所有函数都是对象,但反之不成立。有些对象不是函数,但依然可以调用,因此在许多情况下可以将其当作函数来对待。
如果一个对象是可调用的,意味着可以使用圆括号函数调用语法,甚至可以传入调用参数。这些都由__call__
双下划线方法完成。下面这个类能够定义可调用对象:
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return self.n + x
>>> plus_3 = Adder(3)
>>> plus_3(4)
7
在幕后,像函数那样“调用”一个对象实例实际上是在尝试执行该对象的__call__
方法。
当然,并不是所有的对象都可以调用,因此Python内置了callable
函数,用于检查一个对象是否可以调用。
>>> callable(plus_3)
True
>>> callable(yell)
True
>>> callable('hello')
False
Python 函数特性 关键要点
-
Python中一切皆为对象,函数也不例外。可以将函数分配给变量或存储在数据结构中。作为头等对象,函数还可以被传递给其他函数或作为其他函数的返回值。
-
头等函数的特性可以用来抽象并传递程序中的行为。
-
函数可以嵌套,并且可以捕获并携带父函数的一些状态。具有这种行为的函数称为闭包。
-
对象可以被设置为可调用的,因此很多情况下可以将其作为函数对待。