Python 函数特性

函数是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示例做些小改动来逐步说明这一点。新版在内部就会使用volumetext参数,因此返回的函数是可以直接调用的:

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!'

仔细看看内部函数whisperyell,注意其中并没有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中一切皆为对象,函数也不例外。可以将函数分配给变量或存储在数据结构中。作为头等对象,函数还可以被传递给其他函数或作为其他函数的返回值。

  • 头等函数的特性可以用来抽象并传递程序中的行为。

  • 函数可以嵌套,并且可以捕获并携带父函数的一些状态。具有这种行为的函数称为闭包。

  • 对象可以被设置为可调用的,因此很多情况下可以将其作为函数对待。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程