在Python中把一个函数作为一个参数传递给另一个函数

1,034 阅读14分钟

在我的 Python 入门培训中,我们学到的一个更令人毛骨悚然的事实是,你可以将函数传入其他函数中。 你可以将函数传入,因为在 Python 中,函数就是对象

在你使用Python的第一周,你可能不需要知道这个,但随着你对Python的深入了解,你会发现了解如何将一个函数传入另一个函数是非常方便的。

这是关于 "函数对象 "的各种属性的系列文章的第一部分,这篇文章的重点是一个新的 Python 程序员应该知道和理解的关于Python 函数的对象性质的内容。

函数可以被引用

如果你试图使用一个函数而不在它后面加上括号,Python 不会抱怨,但它也不会做任何有用的事情。

1
2
3
4
5
>>> def greet():
...     print("Hello world!")
...
>>> greet
<function greet at 0x7ff246c6d9d0>

这也适用于方法(方法是生活在对象上的函数)。

1
2
3
>>> numbers = [1, 2, 3]
>>> numbers.pop
<built-in method pop of list object at 0x7ff246c76a80>

Python 允许我们引用这些函数对象,就像我们引用一个字符串、一个数字或一个range 对象一样。

1
2
3
4
5
6
>>> "hello"
'hello'
>>> 2.5
2.5
>>> range(10)
range(0, 10)

由于我们可以像其他对象一样引用函数,我们可以把一个变量指向一个函数。

1
2
>>> numbers = [2, 1, 3, 4, 7, 11, 18, 29]
>>> gimme = numbers.pop

这个gimme 变量现在指向我们的numbers 列表中的pop 方法。因此,如果我们调用gimme ,它将做与调用numbers.pop 同样的事情。

1
2
3
4
5
6
7
8
9
10
>>> gimme()
29
>>> numbers
[2, 1, 3, 4, 7, 11, 18]
>>> gimme(0)
2
>>> numbers
[1, 3, 4, 7, 11, 18]
>>> gimme()
18

注意,我们并没有建立一个新的函数,我们只是把gimme 变量的名字指向了numbers.pop 函数。

1
2
3
4
>>> gimme
<built-in method pop of list object at 0x7ff246c76bc0>
>>> numbers.pop
<built-in method pop of list object at 0x7ff246c76bc0>

你甚至可以把函数存储在数据结构中,然后再引用它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> def square(n): return n**2
...
>>> def cube(n): return n**3
...
>>> operations = [square, cube]
>>> numbers = [2, 1, 3, 4, 7, 11, 18, 29]
>>> for i, n in enumerate(numbers):
...     action = operations[i % 2]
...     print(f"{action.__name__}({n}):", action(n))
...
square(2): 4
cube(1): 1
square(3): 9
cube(4): 64
square(7): 49
cube(11): 1331
square(18): 324
cube(29): 24389

取一个函数并给它另一个名字,或者把它存储在一个数据结构中,这不是很常见,但是 Python 允许我们做这些事情,因为函数可以被传递,就像其他对象一样

函数可以被传递到其它函数中

函数,像任何其它对象一样,可以作为参数传递给另一个函数。

例如,我们可以定义一个函数。

1
2
3
4
5
6
>>> def greet(name="world"):
...     """Greet a person (or the whole world by default)."""
...     print(f"Hello {name}!")
...
>>> greet("Trey")
Hello Trey!

然后把它传入内置的help 函数,看看它做了什么。

1
2
3
4
5
>>> help(greet)
Help on function greet in module __main__:

greet(name='world')
    Greet a person (or the whole world by default).

而我们可以将该函数传入其自身(是的,这很奇怪),在这里将其转换为一个字符串。

1
2
>>> greet(greet)
Hello <function greet at 0x7f93416be8b0>!

实际上,Python 中有相当多的内置函数是专门用来接受其他函数作为参数的。

内置的filter 函数接受两个东西作为参数:一个function 和一个iterable

1
2
3
4
5
6
>>> help(filter)

 |  filter(function or None, iterable) --> filter object
 |
 |  Return an iterator yielding those items of iterable for which function(item)
 |  is true. If function is None, return the items that are true.

给定的迭代器 (list, tuple, string, etc.) 被循环使用,给定的函数在迭代器中的每个项目上被调用:只要函数返回True (或另一个真值) ,该项目就被包含在filter 的输出中。

因此,如果我们传递给filter 一个is_odd 函数(当给定一个奇数时返回True )和一个数字列表,我们将得到我们给它的所有奇数。

1
2
3
4
5
6
7
>>> numbers = [2, 1, 3, 4, 7, 11, 18, 29]
>>> def is_odd(n): return n % 2 == 1
...
>>> filter(is_odd, numbers)
<filter object at 0x7ff246c8dc40>
>>> list(filter(is_odd, numbers))
[1, 3, 7, 11, 29]

filter 返回的对象是一个懒惰的迭代器,所以我们需要将其转换为list ,以便真正看到其输出。

由于函数可以传入函数,这也意味着函数可以接受另一个函数作为参数。filter 函数假定它的第一个参数是一个函数。你可以认为filter 函数与这个函数基本相同。

1
2
3
4
5
6
def filter(predicate, iterable):
    return (
        item
        for item in iterable
        if predicate(item)
    )

这个函数期望predicate 参数是一个函数(技术上可以是任何可调用的)。当我们调用该函数时(用predicate(item) ),我们向其传递一个参数,然后检查其返回值的真实性。

兰姆达函数就是这样的一个例子

lambda表达式是Python中用于创建匿名函数的一种特殊语法。 当你评估一个lambda表达式时,你得到的对象被称为lambda函数

1
2
3
4
5
>>> is_odd = lambda n: n % 2 == 1
>>> is_odd(3)
True
>>> is_odd(4)
False

lambda 函数和普通的 Python 函数几乎一样,但有一些注意事项。

与其他函数不同,lambda 函数没有名字 (它们的名字显示为<lambda>)。它们也不能有 docstrings,而且它们只能包含一个 Python 表达式。

1
2
3
4
5
6
>>> add = lambda x, y: x + y
>>> add(2, 3)
5
>>> add
<function <lambda> at 0x7ff244852f70>
>>> add.__doc__

你可以把 lambda 表达式看作是制作一个函数的捷径,它将评估一个单一的 Python 表达式并返回该表达式的结果。

所以定义一个 lambda 表达式实际上并不评估该表达式:它返回一个可以在以后评估该表达式的函数。

1
2
3
4
5
>>> greet = lambda name="world": print(f"Hello {name}")
>>> greet("Trey")
Hello Trey
>>> greet()
Hello world

我想指出的是,上面三个关于lambda 的例子都是不好的例子。如果你想让一个变量名指向一个以后可以使用的函数对象,你应该使用def 来定义一个函数:这是定义一个函数的通常方法。

1
2
3
4
5
6
>>> def is_odd(n): return n % 2 == 1
...
>>> def add(x, y): return x + y
...
>>> def greet(name="world"): print(f"Hello {name}")
...

Lambda表达式是为我们想定义一个函数并立即将其传入另一个函数的时候准备的。

例如,这里我们使用filter 来获取偶数,但我们使用的是lambda表达式,这样我们就不必在使用前定义一个is_even 函数。

1
2
3
4
>>> numbers
[2, 1, 3, 4, 7, 11, 18, 29]
>>> list(filter(lambda n: n % 2 == 0, numbers))
[2, 4, 18]

这是lambda表达式最恰当的用法:将一个函数传入另一个函数,同时在一行代码中定义该传入的函数。

正如我在《过度使用 lambda 表达式》中写到的,我并不喜欢 Python 的 lambda 表达式语法。 无论你是否喜欢这种语法,你应该知道这种语法只是创建函数的一个快捷方式。

每当你看到lambda 表达式时,请记住。

  1. 兰姆达表达式是一种特殊的语法,用于在一行代码中创建一个函数并将其传递给另一个函数
  2. lambda 函数就像所有其他的函数对象一样:两者都不比其他的更特殊,而且都可以被传递。

Python 中的所有函数都可以作为参数传递给另一个函数 (这正好是 lambda 函数的唯一目的)。

一个常见的例子:键函数

除了内置的filter 函数,你会在哪里看到一个函数被传递到另一个函数中呢? 在 Python 本身,你最常看到的地方可能是一个关键函数

一个常见的惯例是,接受一个待排序的可迭代的函数也接受一个名为key命名参数。这个key 参数应该是一个函数或另一个可调用的函数。

sorted,min, 和max函数都遵循这个惯例,接受一个key 函数。

1
2
3
4
5
6
7
8
9
>>> fruits = ['kumquat', 'Cherimoya', 'Loquat', 'longan', 'jujube']
>>> def normalize_case(s): return s.casefold()
...
>>> sorted(fruits, key=normalize_case)
['Cherimoya', 'jujube', 'kumquat', 'longan', 'Loquat']
>>> min(fruits, key=normalize_case)
'Cherimoya'
>>> max(fruits, key=normalize_case)
'Loquat'

该键函数为给定的迭代器中的每个值被调用,其返回值被用来对迭代器中的每个项目进行排序/排序。 你可以把这个键函数看成是为迭代器中的每个项目计算一个比较键

在上面的例子中,我们的比较键返回一个小写的字符串,所以每个字符串都被其小写的版本所比较(这导致了不区分大小写的排序)。

我们使用了一个normalize_case 函数来做这件事,但同样的事情也可以用str.casefold 来做。

1
2
3
>>> fruits = ['kumquat', 'Cherimoya', 'Loquat', 'longan', 'jujube']
>>> sorted(fruits, key=str.casefold)
['Cherimoya', 'jujube', 'kumquat', 'longan', 'Loquat']

注意:如果你不熟悉类的工作方式,这个str.casefold 技巧就有点奇怪了。 类存储了非绑定的方法,这些方法在调用时将接受该类的一个实例。 我们通常输入my_string.casefold() ,但str.casefold(my_string) 是 Python 将其翻译成的。这是另一个故事了。

在这里,我们要找到里面有最多字母的那个字符串。

1
2
>>> max(fruits, key=len)
'Cherimoya'

如果有多个最大值或最小值,则最早的一个获胜(这就是min/max 的工作原理)。

1
2
3
4
5
>>> fruits = ['kumquat', 'Cherimoya', 'Loquat', 'longan', 'jujube']
>>> min(fruits, key=len)
'Loquat'
>>> sorted(fruits, key=len)
['Loquat', 'longan', 'jujube', 'kumquat', 'Cherimoya']

这里有一个函数,它将返回一个包含给定字符串的长度和该字符串的大小写规范化版本的2项元组。

1
2
3
def length_and_alphabetical(string):
    """Return sort key: length first, then case-normalized string."""
    return (len(string), string.casefold())

我们可以把这个length_and_alphabetical 函数作为key 的参数传给sorted ,以便先按长度排序,然后再按大小写规范化的表示法排序。

1
2
3
4
>>> fruits = ['kumquat', 'Cherimoya', 'Loquat', 'longan', 'jujube']
>>> fruits_by_length = sorted(fruits, key=length_and_alphabetical)
>>> fruits_by_length
['jujube', 'longan', 'Loquat', 'kumquat', 'Cherimoya']

这依赖于Python的排序操作符进行深度比较的事实。

将一个函数作为参数传递的其他例子

sorted,min, 和max 所接受的key 参数只是将函数传入函数的一个常见例子。

还有两个接受函数的 Python 内置函数是mapfilter

我们已经看到,filter 将根据给定的函数的返回值来过滤我们的列表。

1
2
3
4
5
6
>>> numbers
[2, 1, 3, 4, 7, 11, 18, 29]
>>> def is_odd(n): return n % 2 == 1
...
>>> list(filter(is_odd, numbers))
[1, 3, 7, 11, 29]

map 函数将在给定的迭代器中的每个项目上调用给定的函数,并使用该函数调用的结果作为新的项目。

1
2
>>> list(map(is_odd, numbers))
[False, True, True, False, True, True, False, True]

例如这里我们要把数字转换为字符串,并对数字进行平方运算。

1
2
3
4
>>> list(map(str, numbers))
['2', '1', '3', '4', '7', '11', '18', '29']
>>> list(map(lambda n: n**2, numbers))
[4, 1, 9, 16, 49, 121, 324, 841]

注意:正如我在关于过度使用lambda的文章中指出的,我个人更喜欢使用生成器表达式而不是mapfilter 函数

map, 和filter 相似,还有来自itertools 模块的takewhiledropwhile。第一个函数与filter 相似,只是一旦它找到一个谓语函数为假的值就会停止。第二个函数的作用正好相反:它只包括谓语函数为假后的值。

1
2
3
4
5
6
7
8
>>> from itertools import takewhile, dropwhile
>>> colors = ['red', 'green', 'orange', 'purple', 'pink', 'blue']
>>> def short_length(word): return len(word) < 6
...
>>> list(takewhile(short_length, colors))
['red', 'green']
>>> list(dropwhile(short_length, colors))
['orange', 'purple', 'pink', 'blue']

还有functools.reduceitertools.accumulate,它们都在循环过程中调用一个双参数函数来累积数值。

1
2
3
4
5
6
7
8
9
>>> from functools import reduce
>>> from itertools import accumulate
>>> numbers = [2, 1, 3, 4, 7]
>>> def product(x, y): return x * y
...
>>> reduce(product, numbers)
168
>>> list(accumulate(numbers, product))
[2, 2, 6, 24, 168]

collections 模块中的defaultdict类是另一个例子。defaultdict 类创建了类似于字典的对象,当访问缺失的键时,绝不会引发KeyError ,而是会自动向字典中添加一个新的值。

1
2
3
4
5
6
>>> from collections import defaultdict
>>> counts = defaultdict(int)
>>> counts['jujubes']
0
>>> counts
defaultdict(<class 'int'>, {'jujubes': 0})

这个defaultdict 类接受一个可调用的(函数或类),每当访问一个缺失的键时,它将被调用以创建一个默认值。

上面的代码起作用了,因为int 在调用时没有参数,返回0

1
2
>>> int()
0

这里的缺省值是list ,在没有参数的情况下调用时返回一个新的列表。

1
2
3
4
5
>>> things_by_color = defaultdict(list)
>>> things_by_color['purple'].append('socks')
>>> things_by_color['purple'].append('shoes')
>>> things_by_color
defaultdict(<class 'list'>, {'purple': ['socks', 'shoes']})

functools 模块中的部分函数是另一个例子。partial 接受一个函数和任意数量的参数,并返回一个新的函数(严格地说,它返回一个可调用对象)。

这里有一个例子,partial 用来 "绑定 "sep 的关键字参数到print 的函数。

1
>>> print_each = partial(print, sep='\n')

现在返回的print_each 函数所做的事情与用sep='\n' 调用print 所做的事情相同。

1
2
3
4
5
6
7
8
9
10
>>> print(1, 2, 3)
1 2 3
>>> print(1, 2, 3, sep='\n')
1
2
3
>>> print_each(1, 2, 3)
1
2
3

你还会在第三方库中发现接受函数的函数,比如Djangonumpy。 任何时候你看到一个类或一个函数的文档说明它的一个参数应该是一个可调用的可调用的对象,这意味着 "你可以在这里传入一个函数"。

我跳过的一个话题:嵌套函数

Python 也支持嵌套函数 (在其他函数内部定义的函数)。 嵌套函数使用 Python 的装饰器语法。

我不打算在这篇文章中讨论嵌套函数,因为嵌套函数需要探索非局部变量闭包,以及其他Python的奇怪角落,当你刚开始把函数当作对象时,你不需要知道这些。

我计划以后写一篇关于这个主题的后续文章,并在此链接。 同时,如果你对 Python 中的嵌套函数感兴趣,搜索一下Python 中的高阶函数可能会有帮助。

将函数作为对象对待是正常的

Python 有第一类函数,这意味着。

  1. 你可以将函数赋值给变量
  2. 可以在列表、字典或其它数据结构中存储函数
  3. 可以将函数传给其它函数
  4. 可以编写返回函数的函数

把函数当作对象似乎很奇怪,但在 Python 中并不罕见。据我统计,大约有 15% 的 Python 内置函数是为了接受函数作为参数 (min,max,sorted,map,filter,iter,property,classmethod,staticmethod,callable)。

Python 的一级函数最重要的用途是。

  1. key 函数传递给内置的sorted,min, 和max 函数
  2. 将函数传入循环帮助器,如filteritertools.dropwhile
  3. 将一个 "默认值生成工厂函数 "传递给类,如defaultdict
  4. 通过将函数传入到 "部分评价 "函数中。functools.partial

这个话题比我在这里讨论的要深得多,但在你发现自己在写装饰器函数之前,你可能不需要进一步探索这个话题。