Python自学手册
在Python入门,有一个令人颇为惊讶的知识点:你可以将函数传入其他函数。之所以能这样传递函数,是因为在Python中,函数本身就是对象。
刚使用Python的第一周,你可能不需要了解这一点,但随着对Python的深入学习,你会发现理解如何将函数传入其他函数会带来极大的便利。
本文是我计划撰写的“函数对象各类特性”系列文章的第一篇。本文将聚焦于Python新手应当了解并掌握的核心内容——Python函数的对象本质。
函数可以被引用
如果你尝试使用一个函数却不在其后添加括号,Python不会报错,但也不会执行任何有用的操作:
python>>> def greet():... print("Hello world!")...>>> greet<function greet at 0x7ff246c6d9d0> |
这种特性同样适用于方法(方法是绑定在对象上的函数):
python>>> numbers = [1, 2, 3]>>> numbers.pop<built-in method pop of list object at 0x7ff246c76a80> |
Python允许我们引用这些函数对象,就像我们引用字符串、数字或range对象一样:
python>>> "hello"'hello'>>> 2.52.5>>> range(10)range(0, 10) |
既然我们可以像引用其他对象一样引用函数,我们也可以将一个变量指向一个函数:
python>>> numbers = [2, 1, 3, 4, 7, 11, 18, 29]>>> gimme = numbers.pop |
现在,gimme变量就指向了numbers列表的pop方法。因此,当我们调用gimme时,它的作用与调用numbers.pop完全相同:
python>>> 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函数:
python>>> gimme<built-in method pop of list object at 0x7ff246c76bc0>>>> numbers.pop<built-in method pop of list object at 0x7ff246c76bc0> |
你甚至可以将函数存储在数据结构中,之后再进行引用:
python>>> 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): 4cube(1): 1square(3): 9cube(4): 64square(7): 49cube(11): 1331square(18): 324cube(29): 24389 |
虽然给函数重命名或将其存储在数据结构中的做法并不常见,但Python允许我们这样做——因为函数和其他任何对象一样,都是可以被传递的。
函数可以传入其他函数
函数和其他任何对象一样,可以作为参数传入另一个函数。
例如,我们可以定义一个函数:
python>>> def greet(name="world"):... """Greet a person (or the whole world by default)."""... print(f"Hello {name}!")...>>> greet("Trey")Hello Trey! |
然后将它传入内置的help函数,查看其功能:
python>>> help(greet)Help on function greet in module __main__:greet(name='world')Greet a person (or the whole world by default). |
我们甚至可以将函数传入它自身(没错,这看起来有些怪异),此时函数会被转换为字符串:
python>>> greet(greet)Hello <function greet at 0x7f93416be8b0>! |
实际上,Python中有不少内置函数专门用于接收其他函数作为参数。
内置的filter函数接收两个参数:一个函数和一个可迭代对象。
python>>> 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. |
函数会遍历传入的可迭代对象(列表、元组、字符串等),并对该可迭代对象中的每个元素调用传入的函数:每当函数返回True(或其他真值)时,该元素就会被纳入filter的输出结果中。
因此,如果我们给filter传入一个is_odd函数(传入奇数时返回True)和一个数字列表,我们将得到列表中所有的奇数。
python>>> 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返回的是一个惰性迭代器,因此我们需要将其转换为列表才能看到实际的输出结果。
既然函数可以传入其他函数,这也意味着函数可以接收另一个函数作为参数。filter函数默认其第一个参数是一个函数。你可以将filter函数理解为与以下函数几乎完全相同:
pythondef filter(predicate, iterable):return (itemfor item in iterableif predicate(item)) |
这个函数要求predicate参数是一个函数(严格来说,它可以是任何可调用对象)。当我们调用该函数时(即predicate(item)),会向其传入一个参数,然后检查其返回值的真值性。
Lambda函数是典型示例
Lambda表达式是Python中用于创建匿名函数的特殊语法。当你执行一个Lambda表达式时,得到的对象被称为Lambda函数。
python>>> is_odd = lambda n: n % 2 == 1>>> is_odd(3)True>>> is_odd(4)False |
Lambda函数与普通的Python函数基本一致,但有几点限制。
与其他函数不同,Lambda函数没有名称(它们的名称显示为<lambda>)。它们也不能有文档字符串,并且只能包含一个Python表达式。
python>>> add = lambda x, y: x + y>>> add(2, 3)5>>> add<function <lambda> at 0x7ff244852f70>>>> add.__doc__ |
你可以将Lambda表达式理解为一种快捷方式,用于创建一个仅执行单个Python表达式并返回该表达式结果的函数。
因此,定义一个Lambda表达式并不会实际执行该表达式:它会返回一个可以在后续执行该表达式的函数。
python>>> greet = lambda name="world": print(f"Hello {name}")>>> greet("Trey")Hello Trey>>> greet()Hello world |
需要说明的是,以上三个Lambda示例都不是理想的用法。如果你希望一个变量名指向一个后续可以使用的函数对象,你应该使用def来定义函数:这是定义函数的常规方式。
python>>> 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函数:
python>>> 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表达式时,请记住:
•Lambda表达式是一种特殊语法,用于在一行代码中创建函数并将其传入另一个函数;
•Lambda函数与所有其他函数对象一样:两者并无高低之分,都可以被传递;
•Python中的所有函数都可以作为参数传入另一个函数(而这恰好是Lambda函数的唯一用途)。
常见示例:键函数(key functions)
除了内置的filter函数,你还会在哪些地方看到函数被传入其他函数呢?在Python本身中,最常见的场景可能就是键函数了。
对于接收待排序/待排序可迭代对象的函数来说,通常会有一个约定:它们还会接收一个名为key的命名参数。这个key参数应当是一个函数或其他可调用对象。
sorted、min和max函数都遵循这一约定,支持接收键函数:
python>>> 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来实现:
python>>> fruits = ['kumquat', 'Cherimoya', 'Loquat', 'longan', 'jujube']>>> sorted(fruits, key=str.casefold)['Cherimoya', 'jujube', 'kumquat', 'longan', 'Loquat'] |
注:如果你不熟悉类的工作原理,str.casefold这种用法可能会有些奇怪。类会存储未绑定方法,这些方法在被调用时会接收该类的一个实例作为参数。我们通常会写my_string.casefold(),但Python会将其转换为str.casefold(my_string)。这部分内容我们留到以后再探讨。
以下示例用于查找字符数最多的字符串:
python>>> max(fruits, key=len)'Cherimoya' |
如果存在多个最大值或最小值,那么最先出现的那个会被选中(这是min/max函数的工作原理):
python>>> fruits = ['kumquat', 'Cherimoya', 'Loquat', 'longan', 'jujube']>>> min(fruits, key=len)'Loquat'>>> sorted(fruits, key=len)['Loquat', 'longan', 'jujube', 'kumquat', 'Cherimoya'] |
以下是一个函数,它会返回一个包含两个元素的元组,其中包含传入字符串的长度和其大小写归一化后的版本:
pythondef length_and_alphabetical(string):"""Return sort key: length first, then case-normalized string."""return (len(string), string.casefold()) |
我们可以将这个length_and_alphabetical函数作为key参数传入sorted,从而先按字符串长度排序,再按其大小写归一化后的形式排序:
python>>> 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还有两个内置函数也接收函数作为参数,它们是map和filter。
我们已经了解到,filter会根据传入函数的返回值对列表进行筛选。
python>>> 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函数会对传入的可迭代对象中的每个元素调用传入的函数,并将函数调用的结果作为新元素:
python>>> list(map(is_odd, numbers))[False, True, True, False, True, True, False, True] |
例如,我们可以将数字转换为字符串,也可以对数字求平方:
python>>> 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的文章中提到的,我个人更喜欢使用生成器表达式,而不是map和filter函数。
与map和filter类似,itertools模块中还有takewhile和dropwhile函数。第一个函数(takewhile)与filter类似,但一旦找到使谓词函数返回False的元素就会停止。第二个函数(dropwhile)则相反:它只包含谓词函数返回False之后的元素。
python>>> 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.reduce和itertools.accumulate函数,它们都会调用一个双参数函数,在遍历过程中累积计算值:
python>>> 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异常,而是会自动向字典中添加一个新值。
python>>> from collections import defaultdict>>> counts = defaultdict(int)>>> counts['jujubes']0>>> countsdefaultdict(<class 'int'>, {'jujubes': 0}) |
这个defaultdict类接收一个可调用对象(函数或类),当访问不存在的键时,会调用该对象来生成一个默认值。
上面的代码之所以能运行,是因为调用无参的int()会返回0:
以下示例中,默认值是list,调用无参的list()会返回一个新的空列表:
python>>> things_by_color = defaultdict(list)>>> things_by_color['purple'].append('socks')>>> things_by_color['purple'].append('shoes')>>> things_by_colordefaultdict(<class 'list'>, {'purple': ['socks', 'shoes']}) |
functools模块中的partial函数是另一个示例。partial接收一个函数和任意数量的参数,并返回一个新函数(严格来说,它返回一个可调用对象)。
以下示例使用partial为print函数“绑定”sep关键字参数:
python>>> print_each = partial(print, sep='\n') |
此时返回的print_each函数,其作用与调用print时指定sep='\n'完全相同:
python>>> print(1, 2, 3)1 2 3>>> print(1, 2, 3, sep='\n')123>>> print_each(1, 2, 3)123 |
你也会在第三方库(如Django和numpy)中发现“接收函数作为参数”的函数。每当你看到某个类或函数的文档说明其某个参数应为可调用对象(callable)时,这就意味着“你可以在这里传入一个函数”。
暂不展开的话题:嵌套函数
Python也支持嵌套函数(在其他函数内部定义的函数)。嵌套函数是Python装饰器语法的核心。
本文不会讨论嵌套函数,因为嵌套函数需要深入探讨非局部变量、闭包以及Python的其他一些进阶特性,而刚接触“将函数视为对象”的新手暂时不需要了解这些内容。
我计划后续撰写一篇关于这个话题的后续文章,并在此处添加链接。在此期间,如果你对Python中的嵌套函数感兴趣,可以搜索“Python高阶函数(higher order functions)”相关内容。
将函数视为对象是常规操作
Python支持一等函数,这意味着:
•你可以将函数赋值给变量;
•你可以将函数存储在列表、字典或其他数据结构中;
•你可以将函数传入其他函数;
•你可以编写返回函数的函数。
将函数视为对象可能看起来有些怪异,但在Python中这并不是什么不寻常的事情。据我统计,大约15%的Python内置函数是用于接收函数作为参数的(如min、max、sorted、map、filter、iter、property、classmethod、staticmethod、callable)。
Python一等函数最核心的用途包括:
•为内置函数sorted、min和max传入键函数;
•为filter和itertools.dropwhile等遍历工具传入函数;
•为defaultdict等类传入“生成默认值的工厂函数”;
•通过将函数传入functools.partial,实现函数的“部分求值”。
这个主题的深度远不止我在这里所探讨的这些,但除非你实际需要编写装饰器函数,否则你大概率没必要深入研究这个主题。