8.3 函数式,函数的,函数化

Python包括很多针对函数式编程的工具。这些内置的函数涵盖了以下这些基本部分。

 
  • map(function, iterable)iterable中的每一个元素应用function,并在Python 2中返回一个列表,或者在Python 3中返回可迭代的map对象。

Python 3中map的用法

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

>>> map(lambda x: x + "bzz!", ["I think", "I'm good"])
<map object at 0x7fe7101abdd0>
>>> list(map(lambda x: x + "bzz!", ["I think", "I'm good"]))
['I thinkbzz!', "I'm goodbzz!"]
 
  • filter(function or None, iterable)iterable中的元素应用function对返回结果进行过滤,并在Python 2中返回一个列表,或者在Python 3中返回可迭代的filter对象,如示例8.2所示。

示例 8.2 Python 3 filter的用法

>>> filter(lambda x: x.startswith("I "), ["I think", "I'm good"])
<filter object at 0x0d636dd0>
>>> list(filter(lambda x: x.startswith("I "), ["I think", "I'm good"]))
['I think']

 提示

可以使用生成器和列表解析实现与filter或者map等价的函数。
使用列表解析实现map
>> (x + "bzz!" for x in ["I think", "I'm good"])
<generator object <genexpr> at 0x0d697dc0>
>> [x + "bzz!" for x in ["I think", "I'm good"]]
['I thinkbzz!', "I'm goodbzz!"]
使用列表解析实现filter
>> (x for x in ["I think", "I'm good"] if x.startswith("I "))
<generator object <genexpr> at 0x0d697dc0>
>> [x for x in ["I think", "I'm good"] if x.startswith("I ")]
['I think']
在Python 2中这样使用生成器会返回一个可迭代的对象而不是列表,就像Python 3中的mapfilter函数。
 
  • enumerate(iterable[, start])返回一个可迭代的enumerate对象,它生成一个元组序列,每个元组包括一个整型索引(如果提供了的话,则从start开始)和iterable中对应的元素。当需要参考数组的索引编写代码时这是很有用的。例如,不使用下面的写法:
  i = 0
  while i < len(mylist):
  print("Item %d: %s" % (i, mylist[i]))
  i += 1

可以用这样的写法:

  for i, item in enumerate(mylist):
  print("Item %d: %s" % (i, item))
 
  • sorted(iterable, key=None, reverse=False)返回iterable的一个已排序版本。通过参数key可以提供一个返回要排序的值的函数。
  • any(iterable)``和``all(iterable)都返回一个依赖于iterable返回的值的布尔值。下面这两个函数是等价对:
  def all(iterable):
  for x in iterable:
    if not x:
      return False
  return True
  def any(iterable):
  for x in iterable:
    if x:
      return True
  return False

这两个函数对于判断这个可迭代对象中的任何或所有值是否满足给定的条件非常有用:

  mylist = [0, 1, 3, -1]
  if all(map(lambda x: x > 0, mylist)):
  print("All items are greater than 0")
  if any(map(lambda x: x > 0, mylist)):
  print("At least one item is greater than 0")
 
  • zip(iter1 [,iter2 [...]])接收多个序列并将它们组合成元组。它在将一组键和一组值组合成字典时很有用。像上面描述的其他函数一样,它在Python 2中返回一个列表,在Python 3中返回一个可迭代的对象。
  >>> keys = ["foobar", "barzz", "ba!"]
  >>> map(len, keys)
  <map object at 0x7fc1686100d0>
  >>> zip(keys, map(len, keys))
  <zip object at 0x7fc16860d440>
  >>> list(zip(keys, map(len, keys)))
  [('foobar', 6), ('barzz', 5), ('ba!', 3)]
  >>> dict(zip(keys, map(len, keys)))
  {'foobar': 6, 'barzz': 5, 'ba!': 3}

到这里你可能已经注意到Python 2和Python 3的返回类型的不同。在Python 2中大多数Python内置的纯函数会返回列表而不是可迭代的对象,这使得它们的内存利用没有Python 3.x中那么高效。如果正计划使用这些函数编写代码,记住在Python 3中才能最大的发挥它们的作用。如果你仍然在使用Python 2,也不用气馁,标准库中的itertools模块提供了许多这些函数的迭代器版本(itertools.izipitertoolz.imapitertools.ifilter等)。

然而,在上面这个列表中仍然缺少一个重要的工具。处理列表时,一个常见的任务就是在从列表中找出第一个满足条件的元素。这通常可以用函数这么实现:

def first_positive_number(numbers):
  for n in numbers:
    if n > 0:
      return n

也可以写一个函数式风格的:

def first(predicate, items):
  for item in items:
    if predicate(item):
      return item
first(lambda x: x > 0, [-1, 0, 1, 2])

或者更精确一点儿:

# Less efficient
list(filter(lambda x: x > 0, [-1, 0, 1, 2]))[0] ①
# Efficient but for Python 3
next(filter(lambda x: x > 0, [-1, 0, 1, 2]))
# Efficient but for Python 2
next(itertools.ifilter(lambda x: x > 0, [-1, 0, 1, 2]))

① 注意,如果没有元素满足条件,可能会抛出IndexError,促使list(filter())返回空列表。

为了避免在每一个程序中都写同样的函数,可以包含这个小巧且使用的Python包firsthttps://pypi.python.org/pypi/first)。

示例 8.3 使用first

>>> from first import first
>>> first([0, False, None, [], (), 42])
42
>>> first([-1, 0, 1, 2])
-1
>>> first([-1, 0, 1, 2], key=lambda x: x > 0)
1

参数key可以用来指定一个函数,接收每个元素作为参数并返回布尔值指示该元素是否满足条件。

你可能已经注意到,在本章相当一部分示例中我们使用了lambda表达式。lambda最早在Python中引入实际是为了给函数式编程函数提供便利,如map()filter(),否则每次想要检查不同的条件时将需要重写一个完整的新函数:

import operator
from first import first

def greater_than_zero(number):
  return number > 0

first([-1, 0, 1, 2], key=greater_than_zero)

这段代码和前面的例子功能相同,但更加难以处理:如果想要获得序列中第一个大于42的数,则需要定义一个合适的函数而不是以内联(in-line)的方式在first中调用。

尽管lambda在帮助我们避免此类问题时是有用的,但它仍然是有问题的。首先也最明显的,如果需要超过一行代码则不能通过lambda传入key函数。在这种场景下,则又需要返回繁复的模式,为每个需要的key编写一个新的函数定义。我们真的需要这样做吗?

functools.partial是以更为灵活的方案替换lambda的第一步。它允许通过一种反转的方式创建一个包装器函数:它修改收到的参数而不是修改函数的行为。

from functools import partial
from first import first

def greater_than(number, min=0):
  return number > min

first([-1, 0, 1, 2], key=partial(greater_than, min=42))

默认情况下,新的greater_than函数功能和之前的greater_than_zero函数类似,但是可以指定要比较的值。在这个例子中,我们向functools.partial传入我们的函数和期望的最小值min,就可以得到一个min设定为42的新函数。换句话说,可以写一个函数并利用functools.partial根据需要针对给定的某个场景进行自定义。

在这个例子中,尽管需求严格限定但还是需要两行代码。例子中需要做的只是比较两个数,假如Python中有内置的这种比较函数呢?事实证明,operator模块就是我们要找的。

import operator
from functools import partial
from first import first

first([-1, 0, 1, 2], key=partial(operator.le, 0))

这里我们看到,functools.partial也支持位置参数。在这个例子中,operator.le(a, b)接收两个数并返回第一个数是否小于等于第二个数。这里向functools.partial传入的0会被赋值给a,向由functools.partial返回的函数传递的参数会被赋值给b。所以它运行起来和最初的例子完全一样,不需要使用lambda或其他额外的函数。

 注意

functools.partial在替换lambda时是很有用的,而且通常被认为是更好的选择。在Python语言中lambda被看做是一种非常规方式,因为它将函数体限定为一行表达式2。另一方面,functools.partial可以围绕原函数进行很好的封装。

Python标准库中的itertools模块也提供了一组非常有用的函数,也很有必要记住。因为尽管Python本身提供了这些函数,但我还是看到很多程序员试图实现自己的版本。

 
  • chain(*iterables)依次迭代多个iterables但并不会构造包含所有元素的中间列表。
  • c``ombinations(iterable, r)从给定的iterable中生成所有长度为r的组合。
  • compress(data, selectors)data应用来自selectors的布尔掩码并从data中返回selectors中对应为真的元素。
  • count(start, step)创建一个无限的值的序列,从start开始,步长为step
  • cycle(iterable)重复的遍历iterable中的值。
  • dropwhile(predicate, iterable)过滤iterable中的元素,丢弃符合predicate描述的那些元素。
  • groupby(iterable, keyfunc)根据keyfunc函数返回的结果对元素进行分组并返回一个迭代器。
  • permutations(iterable[, r])返回iterabler个元素的所有组合。
  • product(*iterables)返回iterables的笛卡儿积的可迭代对象,但不使用嵌套的for循环。
  • takewhile(predicate, iterable)返回满足predicate条件的iterable中的元素。

这些函数在和operator模块组合在一起时特别有用。当一起使用时,itertoolsoperator能够覆盖通常程序员依赖lambda表达式的大部分场景,如示例8.4所示。

示例 8.4 结合itertools.groupby使用operator模块

>>> import itertools
>>> a = [{'foo': 'bar'}, {'foo': 'bar', 'x': 42}, {'foo': 'baz', 'y': 43}]
>>> import operator
>>> list(itertools.groupby(a, operator.itemgetter('foo')))
[('bar', <itertools._grouper object at 0xb000d0>), ('baz', <itertools.
_grouper object at 0xb00110>)]
>>> [(key, list(group)) for key, group in list(itertools.groupby(a, operator.
itemgetter('foo')))]
[('bar', []), ('baz', [{'y': 43, 'foo': 'baz'}])]

在这个例子中,也可以写成lambda x: x['foo'],但使用operator可以完全避免使用lambda


1技术上可以,但那不是期望的方式。
2曾经计划在 Python 3中移除,但是最终没有。