前一部分讲了什么是函数式编程(Functional Programming),函数式编程特点和Python语言特性中的迭代器Iterator,列表生成式list comprehensive以及生成器Generator,可以参见此处

这一部分主要为一些与迭代器配合使用的Python内置函数,还有一些Python的标准库,以及lambda表达式。

Built-in functions

让我们更详细地看看经常与迭代器配合的内置函数:

  • map(f, iterA, iterB, ...)
  • filter(predicate, iter)
  • enumerate(iter, start=0)
  • sorted(iter, key=None, reverse=False)
  • any(iter)
  • all(iter)
  • zip(iterA, iterB, ...)

map

map(f, iterA, iterB, ...)在序列f(iterA[0], iterB[0], ...)f(iterA[1], iterB[1], ...)f(iterA[2], iterB[2], ...),…上返回一个迭代器。

1
2
>>> def upper(s):
... return s.upper()
1
2
3
4
>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

当然可以通过列表生成式达到同样的效果。

filter

filter(predicate, iter)返回满足一定条件的所有序列元素的迭代器,类似于通过列表生成式复制。一个predicate是一个函数,返回某个条件的真值。

1
2
>>> def is_even(x):
... return (x % 2) == 0
1
2
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

这也可以写成列表生成式:

1
2
>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]

enumerate

enumerate(iter, start=0)返回一个可迭代的2-tuples,容纳了count(从start开始)和iter中的每个元素。

1
2
3
4
5
>>> for item in enumerate(['subject', 'verb', 'object']):
... print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')

在循环列表并记录符合某些条件的索引时,通常使用enumerate()

1
2
3
4
f = open('data.txt', 'r')
for i, line in enumerate(f):
if line.strip() == '':
print('Blank line at line #%i' % i)

sorted

sorted(iterable, key=None, reverse=False)将迭代器的所有元素收集到list中,对list进行排序并返回排序的结果。keyreverse被传递给构造listsort()方法。

1
2
3
4
5
6
7
8
9
>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

有关排序的更详细讨论,请参阅Sorting HOW TO.

any 和 all

any(iter)all(iter)都会茶轴iterable中的真值。如果iterable中的存在任何一个元素为真,则any()返回True,如果所有元素均为真,则all()返回True

1
2
3
4
5
6
7
8
9
10
11
12
>>> any([0,1,0])
True
>>> any([0,0,0])
False
>>> any([1,1,1])
True
>>> all([0,1,0])
False
>>> all([0,0,0])
False
>>> all([1,1,1])
True

zip

zip(iterA, iterB, ...)从每个iterable中提取一个元素,并将它们返回到一个tuple中:

1
2
3
4
5
>>> for i in zip(['a', 'b', 'c'], (1, 2, 3)):
... print(i)
('a', 1)
('b', 2)
('c', 3)

tuple只有在被请求时才被构造和返回,即为惰性的,与迭代器,生成器类似。

如果iterable的长度不同,结果流的长度将与最短的iterable长度相同。

1
2
3
4
>>> for i in zip(['a', 'b', 'c'], (1, 2)):
... print(i)
('a', 1)
('b', 2)

itertools模块

itertools模块包含许多常用的迭代器以及用于组合多个迭代器的函数。

该模块的函数分为几个大类:

  • 用现有迭代器创建新迭代器的函数
  • 将迭代器的元素视为函数参数的函数
  • 选择迭代器的输出的函数
  • 分组迭代器的输出的函数

创建一个新的迭代器

itertools.count(start, step)返回一个具有均匀间隔的无限数据流的迭代器。start为起始,默认为1,step为步长,默认为1。通常用做参数传递给map()来生成连续的数据点。另外,可以使用zip()添加序列号。

1
2
3
4
5
6
itertools.count() =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

大致相当于:

1
2
3
4
5
6
7
def count(start=0, step=1):
# count(10) --> 10 11 12 13 14 ...
# count(2.5, 0.5) -> 2.5 3.0 3.5 ...
n = start
while True:
yield n
n += step

itertools.cycle(iter)保存提供的iterable的内容,并返回一个新的迭代器,新的迭代器将无限重复这些元素。

1
2
itertools.cycle([1,2,3,4,5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

类似于:

1
2
3
4
5
6
7
8
9
def cycle(iterable):
# cycle('ABCD') --> A B C D A B C D A B C D ...
saved = []
for element in iterable:
yield element
saved.append(element)
while saved:
for element in saved:
yield element

itertools.repeat(elem, [n])返回一个重复提供的elem n 次的迭代器,或者如果没有提供 n,则无限循环返回elem

1
2
3
4
itertools.repeat('abc') =>
abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
abc, abc, abc, abc, abc

大致相当于:

1
2
3
4
5
6
7
8
def repeat(object, times=None):
# repeat(10, 3) --> 10 10 10
if times is None:
while True:
yield object
else:
for i in range(times):
yield object

itertools.chain(iterA, iterB ,...)将任意数量的iterable作为输入,并返回一个迭代器,该迭代器先第一个iterable的所有元素,然后返回第二个iterable的所有元素,依此类推。

1
2
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3

大致相当于:

1
2
3
4
5
def chain(*iterables):
# chain('ABC', 'DEF') --> A B C D E F
for it in iterables:
for element in it:
yield element

itertools.islice(iter, [start], stop, [step])返回一个迭代器片段的数据流。

如果start为None,则迭代从0开始。如果stepNone,则step默认为1。使用单个stop参数,它将返回第一个stop元素。如果提供起始索引,则会获得stopstart元素,如果为step提供值,则会相应跳过元素。与Python的字符串和列表切片不同,startstopstep不能负值。

1
2
3
4
5
6
itertools.islice(range(10), 8) =>
0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
2, 4, 6

类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def islice(iterable, *args):
# islice('ABCDEFG', 2) --> A B
# islice('ABCDEFG', 2, 4) --> C D
# islice('ABCDEFG', 2, None) --> C D E F G
# islice('ABCDEFG', 0, None, 2) --> A C E G
s = slice(*args)
start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
it = iter(range(start, stop, step))
try:
nexti = next(it)
except StopIteration:
# Consume *iterable* up to the *start* position.
for i, element in zip(range(start), iterable):
pass
return
try:
for i, element in enumerate(iterable):
if i == nexti:
yield element
nexti = next(it)
except StopIteration:
# Consume to *stop*.
for i, element in zip(range(i + 1, stop), iterable):
pass

itertools.tee(iter, [n])复制一个迭代器,并返回n个独立的迭代器,它们都将返回源迭代器的内容。如果没有为n提供值,则默认值为2tee可能占据大量的内存(取决于需要存储多少临时数据)。一般来说,如果一个迭代器在另一个迭代器启动之前使用大部分或全部数据,则使用更快的list()而不是tee()

1
2
3
4
5
6
7
8
itertools.tee( itertools.count() ) =>
iterA, iterB

where iterA ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

and iterB ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

函数应用于元素

operator库包含一系列对应于python操作符的函数。例如operator.add(a, b) (a + b) , operator.ne(a, b) (a != b) 等。operator.attrgetter('id')返回一个获取id属性的函数,f = operator.attrgetter('name'),调用f(b)返回b.name; f = operator.attrgetter('name', 'date'),调用f(b)返回(b.name, b.date)

itertools.starmap(func,iter)假定这个iter将返回一个元组流,并使用这些元组作为参数调用func

1
2
3
4
5
itertools.starmap(os.path.join,
[('/bin', 'python'), ('/usr', 'bin', 'java'),
('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
/bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby

选择元素

还有另一组函数,根据predicate选择迭代器元素的子集。

itertools.filterfalse(predicate, iter)filter()相反,返回所有的那些predicate返回false的元素:

1
2
itertools.filterfalse(is_even, itertools.count()) =>
1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate, iter)只要predicate返回true,就返回元素。一旦predicate返回false,迭代器就会结束。

1
2
3
4
5
6
7
8
def less_than_10(x):
return x < 10

itertools.takewhile(less_than_10, itertools.count()) =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9

itertools.takewhile(is_even, itertools.count()) =>
0

itertools.dropwhile(predicate,iter)predicate返回true时丢弃元素,然后返回迭代结果的其余部分。

1
2
3
4
5
itertools.dropwhile(less_than_10, itertools.count()) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...

itertools.dropwhile(is_even, itertools.count()) =>
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

itertools.compress(data,selectors)接受两个迭代器,只返回data中与selectors相应元素为true的数据元素,当其中一个迭代器元素耗尽时停止:

1
2
itertools.compress([1,2,3,4,5], [True, True, False, False, True]) =>
1, 2, 5

组合排列

itertools.combinations(iterable,r)返回一个迭代器,给出iterable中包含的元素的所有可能的以r大小的tuple组合。

1
2
3
4
5
6
7
8
9
10
itertools.combinations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 3), (2, 4), (2, 5),
(3, 4), (3, 5),
(4, 5)

itertools.combinations([1, 2, 3, 4, 5], 3) =>
(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
(2, 3, 4), (2, 3, 5), (2, 4, 5),
(3, 4, 5)

每个tuple中的元素保持与iterable的顺序相同。 例如,在上面的示例中,数字1总是在2, 3, 4或5之前。 类似的函数itertools.permutations(iterable,r = None)删除了顺序这个约束,返回了长度为r的所有可能的排列:

1
2
3
4
5
6
7
8
9
10
11
itertools.permutations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 1), (2, 3), (2, 4), (2, 5),
(3, 1), (3, 2), (3, 4), (3, 5),
(4, 1), (4, 2), (4, 3), (4, 5),
(5, 1), (5, 2), (5, 3), (5, 4)

itertools.permutations([1, 2, 3, 4, 5]) =>
(1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
...
(5, 4, 3, 2, 1)

如果没有为r提供值,则使用iterable的长度。

这些函数按位置生成所有可能的组合,并且不要求iterable的内容是唯一的:

1
2
3
itertools.permutations('aba', 3) =>
('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

itertools.combinations_with_replacement(iterable,r)函数松弛了另一个的约束:元素可以在单个tuple中重复。 从概念上讲,为每个tuple的第一个位置选择一个元素,然后在选择第二个元素之前替换该元素。

1
2
3
4
5
6
itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
(2, 2), (2, 3), (2, 4), (2, 5),
(3, 3), (3, 4), (3, 5),
(4, 4), (4, 5),
(5, 5)

元素分组

这里讨论的itertools.groupby(iter,key_func = None)是最复杂的。 key_func(elem)是一个可以计算iterable返回的每个元素键值的函数。如果不提供key_func,则键值只是每个元素本身。

groupby()iterable中收集具有相同键值的所有连续元素,并返回一个包含键值的2-tuple的流和具有该键值的元素的迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
('Anchorage', 'AK'), ('Nome', 'AK'),
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
...
]

def get_state(city_state):
return city_state[1]

itertools.groupby(city_list, get_state) =>
('AL', iterator-1),
('AK', iterator-2),
('AZ', iterator-3), ...

where
iterator-1 =>
('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

groupby()假定iterable的内容已经根据键进行排序。 注意,返回的迭代器也使用iterable,因此在请求iterator-2及其相应的键之前,需要消耗iterator-1的结果。

functools模块

functools模块包含一些高阶函数。高阶函数将一个或多个函数作为输入并返回一个新函数。这个模块中最有用的工具是functools.partial()函数。

对于以函数式编写的程序,有时需要构造填充了一些参数的现有函数的变体。

考虑一个函数f(a,b,c), 你可能希望创建一个等于f(1,b,c)的新函数g(b,c)。这称为"partial function application"。

partial()的构造函数接受参数(function,arg1,arg2,...,kwarg1 = value1,kwarg2 = value2)。 生成的对象是可调用的,因此您可以调用它来调用已经填充过参数的函数。

例如:

1
2
3
4
5
6
7
8
9
import functools

def log(message, subsystem):
"""Write the contents of 'message' to the specified subsystem."""
print('%s: %s' % (subsystem, message))
...

server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

functools.reduce(func,iter,[initial_value])累加对所有iterable元素执行的操作,因此不能应用于无限的可迭代对象。

func必须是一个带有两个元素并返回单个值的函数。functools.reduce()获取迭代器返回的前两个元素A和B,并计算func(A,B)。然后它请求第三个元素C计算func(func(A,B),C),将此结果与返回的第四个元素组合,并继续直到迭代结束。如果iterable没有返回值,则引发TypeError异常。如果提供了初始值,则将其用作起点,即func(initial_value,A)是第一次计算。

1
2
3
4
5
6
7
8
9
10
11
>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1,2,3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1

如果将operator.add()functools.reduce()一起使用,则会累加iterable的所有元素。 更常见的则是用内置函数sum()来计算:

1
2
3
4
5
6
7
>>> import functools, operator
>>> functools.reduce(operator.add, [1,2,3,4], 0)
10
>>> sum([1,2,3,4])
10
>>> sum([])
0

虽然functools.reduce()还有很多用途,但是,编写for循环可以更清楚:

1
2
3
4
5
6
7
8
import functools
# Instead of:
product = functools.reduce(operator.mul, [1,2,3], 1)

# You can write:
product = 1
for i in [1,2,3]:
product *= i

另一个相关函数是itertools.accumulate(iterable,func = operator.add)。它执行相同的计算,但不是仅返回最终结果,accumulate()返回一个迭代器,它也产生每一步的结果:

1
2
3
4
5
itertools.accumulate([1,2,3,4,5]) =>
1, 3, 6, 10, 15

itertools.accumulate([1,2,3,4,5], operator.mul) =>
1, 2, 6, 24, 120

operator模块

之前提到过operator模块。它包含一系列与Python运算符相对应的函数。这些函数通常在函数式代码中很有用,因为它们可以避免编写执行单个操作的简单函数。

该模块中的一些函数有:

  • 数学运算符:add()sub()mul()floordiv()abs(), …
  • 逻辑运算符:not_()truth().
  • 位操作符:and_()or_()invert().
  • 比较:eq()ne()lt()le()gt(), 和 ge().
  • Object identity:is_()is_not().

完整列表,请参阅Python文档。

Lambda表达式

在编写函数式程序时,经常需要很少的函数作为predicate或以某种方式组合元素。

如果有内置的Python或适合的模块函数,则不需要定义新函数:

1
2
stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)

如果不存在,则需要编写它。一种方法是使用lambda语句。lambda接受许多参数和一个组合这些参数的表达式,并创建一个返回表达式值的匿名函数:

1
2
3
adder = lambda x, y: x+y

print_assign = lambda name, value: name + '=' + str(value)

等价于:

1
2
3
4
5
def adder(x, y):
return x + y

def print_assign(name, value):
return name + '=' + str(value)

哪种方案更可取? 这是一个风格问题,因人而异。

例如:

1
2
import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

需要时间来解开表达式来弄清楚发生了什么。使用简短的嵌套def语句可以使事情变得更好:

1
2
3
4
5
import functools
def combine(a, b):
return 0, a[1] + b[1]

total = functools.reduce(combine, items)[1]

使用for循环实现,则更加清晰:

1
2
3
total = 0
for a, b in items:
total += b

也可以使用sum()与生成器表达式:

1
total = sum(b for a,b in items)

实际上functools.reduce()的许多用法,使用for循环时都更清晰。

Fredrik Lundh曾经为重构lambda表达式提出了以下一套规则:

  • 写一个lambda函数。
  • 写评论,解释lambda的作用。
  • 研究评论一段时间,并想一个捕捉评论本质的名称。
  • 使用该名称将lambda转换为def语句。
  • 删除评论。

但终究使用与风格因人而异。

References

Python官方文档:Python HOWTOs