python函数式编程?

news/发布时间2024/5/15 22:46:16

概述¶

本章介绍函数式编程的基本概念。.

编程语言支持通过以下几种方式来解构具体问题:

  • 大多数的编程语言都是 过程式 的,所谓程序就是一连串告诉计算机怎样处理程序输入的指令。C、Pascal 甚至 Unix shells 都是过程式语言。

  • 在 声明式 语言中,你编写一个用来描述待解决问题的说明,并且这个语言的具体实现会指明怎样高效的进行计算。 SQL 可能是你最熟悉的声明式语言了。 一个 SQL 查询语句描述了你想要检索的数据集,并且 SQL 引擎会决定是扫描整张表还是使用索引,应该先执行哪些子句等等。

  • 面向对象 程序会操作一组对象。 对象拥有内部状态,并能够以某种方式支持请求和修改这个内部状态的方法。Smalltalk 和 Java 都是面向对象的语言。 C++ 和 Python 支持面向对象编程,但并不强制使用面向对象特性。

  • 函数式 编程则将一个问题分解成一系列函数。 理想情况下,函数只接受输入并输出结果,对一个给定的输入也不会有影响输出的内部状态。 著名的函数式语言有 ML 家族(Standard ML,Ocaml 以及其他变种)和 Haskell。

一些语言的设计者选择强调一种特定的编程方式。 这通常会让以不同的方式来编写程序变得困难。其他多范式语言则支持几种不同的编程方式。Lisp,C++ 和 Python 都是多范式语言;使用这些语言,你可以编写主要为过程式,面向对象或者函数式的程序和函数库。在大型程序中,不同的部分可能会采用不同的方式编写;比如 GUI 可能是面向对象的而处理逻辑则是过程式或者函数式。

在函数式程序里,输入会流经一系列函数。每个函数接受输入并输出结果。函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。消除副作用意味着不能使用随程序运行而更新的数据结构;每个函数的输出必须只依赖于输入。

有些语言对纯洁性要求非常严格,甚至没有诸如 a=3 或 c = a + b 之类的赋值语句,但很难避免所有的副作用,如打印到屏幕上或写到磁盘文件之类的副作用。另一个例子是调用 print() 或 time.sleep() 函数,它们都没有返回一个有用的值。这两个函数被调用只是为了它们的副作用,即向屏幕发送一些文本或暂停执行一秒钟。

函数式风格的 Python 程序并不会极端到消除所有 I/O 或者赋值的程度;相反,他们会提供像函数式一样的接口,但会在内部使用非函数式的特性。比如,函数的实现仍然会使用局部变量,但不会修改全局变量或者有其他副作用。

函数式编程可以被认为是面向对象编程的对立面。对象就像是颗小胶囊,包裹着内部状态和随之而来的能让你修改这个内部状态的一组调用方法,以及由正确的状态变化所构成的程序。函数式编程希望尽可能地消除状态变化,只和流经函数的数据打交道。在 Python 里你可以把两种编程方式结合起来,在你的应用(电子邮件信息,事务处理)中编写接受和返回对象实例的函数。

函数式设计在工作中看起来是个奇怪的约束。为什么你要消除对象和副作用呢?不过函数式风格有其理论和实践上的优点:

  • 形式证明。

  • 模块化。

  • 组合性。

  • 易于调试和测试。

形式证明

一个理论上的优点是,构造数学证明来说明函数式程序是正确的相对更容易些。

很长时间,研究者们对寻找证明程序正确的数学方法都很感兴趣。这和通过大量输入来测试,并得出程序的输出基本正确,或者阅读一个程序的源代码然后得出代码看起来没问题不同;相反,这里的目标是一个严格的证明,证明程序对所有可能的输入都能给出正确的结果。

证明程序正确性所用到的技术是写出 不变量,也就是对于输入数据和程序中的变量永远为真的特性。然后对每行代码,你说明这行代码执行前的不变量 X 和 Y 以及执行后稍有不同的不变量 X' 和 Y' 为真。如此一直到程序结束,这时候在程序的输出上,不变量应该会与期望的状态一致。

函数式编程之所以要消除赋值,是因为赋值在这个技术中难以处理;赋值可能会破坏赋值前为真的不变量,却并不产生任何可以传递下去的新的不变量。

不幸的是,证明程序的正确性很大程度上是经验性质的,而且和 Python 软件无关。即使是微不足道的程序都需要几页长的证明;一个中等复杂的程序的正确性证明会非常庞大,而且,极少甚至没有你日常所使用的程序(Python 解释器,XML 解析器,浏览器)的正确性能够被证明。即使你写出或者生成一个证明,验证证明也会是一个问题;里面可能出了差错,而你错误地相信你证明了程序的正确性。

模块化

函数式编程的一个更实用的优点是,它强制你把问题分解成小的方面。因此程序会更加模块化。相对于一个进行了复杂变换的大型函数,一个小的函数更明确,更易于编写, 也更易于阅读和检查错误。

易于调试和测试

测试和调试函数式程序相对来说更容易。

调试很简单是因为函数通常都很小而且清晰明确。当程序无法工作的时候,每个函数都是一个可以检查数据是否正确的接入点。你可以通过查看中间输入和输出迅速找到出错的函数。

测试更容易是因为每个函数都是单元测试的潜在目标。在执行测试前,函数并不依赖于需要重现的系统状态;相反,你只需要给出正确的输入,然后检查输出是否和期望的结果一致。

组合性

当你编写函数式风格的程序时,你会写出很多带有不同输入和输出的函数。其中一些不可避免地会局限于特定的应用,但其他的却可以广泛的用在程序中。举例来说,一个接受文件夹目录返回所有文件夹中的 XML 文件的函数; 或是一个接受文件名,然后返回文件内容的函数,都可以应用在很多不同的场合。

久而久之你会形成一个个人工具库。通常你可以重新组织已有的函数来组成新的程序,然后为当前的工作写一些特殊的函数。

迭代器

我会从 Python 的一个语言特性, 编写函数式风格程序的重要基石开始说起:迭代器。

迭代器是一个表示数据流的对象;这个对象每次只返回一个元素。Python 迭代器必须支持 __next__() 方法;这个方法不接受参数,并总是返回数据流中的下一个元素。如果数据流中没有元素,__next__() 会抛出 StopIteration 异常。迭代器未必是有限的;完全有理由构造一个输出无限数据流的迭代器。

内置的 iter() 函数接受任意对象并试图返回一个迭代器来输出对象的内容或元素,并会在对象不支持迭代的时候抛出 TypeError 异常。Python 有几种内置数据类型支持迭代,最常见的就是列表和字典。如果一个对象能生成迭代器,那么它就会被称作 iterable。

你可以手动试验迭代器的接口。

>>>

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it  
<...iterator object at ...>
>>> it.__next__()  # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):File "<stdin>", line 1, in <module>
StopIteration
>>>

Python 有不少要求使用可迭代的对象的地方,其中最重要的就是 for 表达式。在表达式 for X in Y,Y 要么自身是一个迭代器,要么能够由 iter() 创建一个迭代器。以下两种表达是等价的:

for i in iter(obj):print(i)for i in obj:print(i)

可以用 list() 或 tuple() 这样的构造函数把迭代器具体化成列表或元组:

>>>

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

序列的解压操作也支持迭代器:如果你知道一个迭代器能够返回 N 个元素,你可以把他们解压到有 N 个元素的元组:

>>>

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

像 max() 和 min() 这样的内置函数可以接受单个迭代器参数,然后返回其中最大或者最小的元素。 "in" 和 "not in" 操作也支持迭代器:如果能够在迭代器 iterator 返回的数据流中找到 X 的话,则 X in iterator 为真。很显然,如果迭代器是无限的,这么做你就会遇到问题;max() 和 min() 永远也不会返回;如果元素 X 也不出现在数据流中, "in" 和 "not in" 操作同样也永远不会返回。

注意你只能在迭代器中顺序前进;没有获取前一个元素的方法,除非重置迭代器,或者重新复制一份。迭代器对象可以提供这些额外的功能,但迭代器协议只明确了 __next__() 方法。函数可能因此而耗尽迭代器的输出,如果你要对同样的数据流做不同的操作,你必须重新创建一个迭代器。

支持迭代器的数据类型

我们已经知道列表和元组支持迭代器。实际上,Python 中的任何序列类型,比如字符串,都自动支持创建迭代器。

对字典调用 iter() 会返回一个遍历字典的键的迭代器:

>>>

>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
...      'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
...     print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

注意从 Python 3.7 开始,字典的遍历顺序一定和输入顺序一样。先前的版本并没有明确这一点,所以不同的实现可能不一致。

对字典使用 iter() 总是会遍历键,但字典也有返回其他迭代器的方法。如果你只遍历值或者键/值对,你可以明确地调用 values() 或 items() 方法得到合适的迭代器。

dict() 构造函数可以接受一个迭代器,然后返回一个有限的 (key, value) 元组的数据流:

>>>

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

文件也可以通过调用 readline() 来遍历,直到穷尽文件中所有的行。这意味着你可以像这样读取文件中的每一行:

for line in file:# do something for each line...

集合可以从可遍历的对象获取内容,也可以让你遍历集合的元素:

>>>

>>> S = {2, 3, 5, 7, 11, 13}
>>> for i in S:
...     print(i)
2
3
5
7
11
13

生成器表达式和列表推导式

迭代器的输出有两个很常见的使用方式,1) 对每一个元素执行操作,2) 选择一个符合条件的元素子集。比如,给定一个字符串列表,你可能想去掉每个字符串尾部的空白字符,或是选出所有包含给定子串的字符串。

列表推导式和生成器表达式 (简写: "listcomp" 和 "genexp") 让这些操作更加简明,这个形式借鉴自函数式编程语言 Haskell (Haskell Language)。 你可以用以下代码去掉一个字符串流中的所有空白符:

>>>

>>> line_list = ['  line 1\n', 'line 2  \n', ' \n', '']>>> # Generator expression -- returns iterator
>>> stripped_iter = (line.strip() for line in line_list)>>> # List comprehension -- returns list
>>> stripped_list = [line.strip() for line in line_list]

你可以加上条件语句 "if" 来选取特定的元素:

>>>

>>> stripped_list = [line.strip() for line in line_list
...                  if line != ""]

通过列表推导式,你会获得一个 Python 列表;stripped_list 就是一个包含所有结果行的列表,并不是迭代器。 生成器表达式会返回一个迭代器,它在必要的时候计算结果,避免一次性生成所有的值。 这意味着,如果迭代器返回一个无限数据流或者大量的数据,列表推导式就不太好用了。 这种情况下生成器表达式会更受青睐。

生成器表达式两边使用圆括号 ("()") ,而列表推导式则使用方括号 ("[]")。生成器表达式的形式为:

( expression for expr in sequence1if condition1for expr2 in sequence2if condition2for expr3 in sequence3...if condition3for exprN in sequenceNif conditionN )

再次说明,列表推导式只有两边的括号不一样(方括号而不是圆括号)。

这些生成用于输出的元素会成为 expression 的后继值。其中 if 语句是可选的;如果给定的话 expression 只会在符合条件时计算并加入到结果中。

生成器表达式总是写在圆括号里面,不过也可以算上调用函数时用的括号。如果你想即时创建一个传递给函数的迭代器,可以这么写:

obj_total = sum(obj.count for obj in list_all_objects())

其中 for...in 语句包含了将要遍历的序列。这些序列并不必须同样长,因为它们会从左往右开始遍历,而 不是 同时执行。对每个 sequence1 中的元素,sequence2 会从头开始遍历。sequence3 会对每个 sequence1 和 sequence2 的元素对开始遍历。

换句话说,列表推导式器是和下面的 Python 代码等价:

for expr1 in sequence1:if not (condition1):continue   # Skip this elementfor expr2 in sequence2:if not (condition2):continue   # Skip this element...for exprN in sequenceN:if not (conditionN):continue   # Skip this element# Output the value of# the expression.

这说明,如果有多个 for...in 语句而没有 if 语句,输出结果的长度就是所有序列长度的乘积。如果你的两个列表长度为3,那么输出的列表长度就是9:

>>>

>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]  
[('a', 1), ('a', 2), ('a', 3),('b', 1), ('b', 2), ('b', 3),('c', 1), ('c', 2), ('c', 3)]

为了不让 Python 语法变得含糊,如果 expression 会生成元组,那这个元组必须要用括号括起来。下面第一个列表推导式语法错误,第二个则是正确的:

# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]

生成器

生成器是一类用来简化编写迭代器工作的特殊函数。普通的函数计算并返回一个值,而生成器返回一个能返回数据流的迭代器。

毫无疑问,你已经对如何在 Python 和 C 中调用普通函数很熟悉了,这时候函数会获得一个创建局部变量的私有命名空间。当函数到达 return 表达式时,局部变量会被销毁然后把返回给调用者。之后调用同样的函数时会创建一个新的私有命名空间和一组全新的局部变量。但是,如果在退出一个函数时不扔掉局部变量会如何呢?如果稍后你能够从退出函数的地方重新恢复又如何呢?这就是生成器所提供的;他们可以被看成可恢复的函数。

这里有简单的生成器函数示例:

>>>

>>> def generate_ints(N):
...    for i in range(N):
...        yield i

任何包含了 yield 关键字的函数都是生成器函数;Python 的 bytecode 编译器会在编译的时候检测到并因此而特殊处理。

当你调用一个生成器函数,它并不会返回单独的值,而是返回一个支持生成器协议的生成器对象。当执行 yield 表达式时,生成器会输出 i 的值,就像 return 表达式一样。yield 和 return 最大的区别在于,到达 yield 的时候生成器的执行状态会挂起并保留局部变量。在下一次调用生成器 __next__() 方法的时候,函数会恢复执行。

这里有一个 generate_ints() 生成器的示例:

>>>

>>> gen = generate_ints(3)
>>> gen  
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):File "stdin", line 1, in <module>File "stdin", line 2, in generate_ints
StopIteration

同样,你可以写出 for i in generate_ints(5),或者 a, b, c = generate_ints(3)

在生成器函数里面,return value 会触发从 __next__() 方法抛出 StopIteration(value) 异常。一旦抛出这个异常,或者函数结束,处理数据的过程就会停止,生成器也不会再生成新的值。

你可以手动编写自己的类来达到生成器的效果,把生成器的所有局部变量作为实例的成员变量存储起来。比如,可以这么返回一个整数列表:把 self.count 设为0,然后通过 count`()。然而,对于一个中等复杂程度的生成器,写出一个相应的类可能会相当繁杂。

包含在 Python 库中的测试套件 Lib/test/test_generators.py 里有很多非常有趣的例子。这里是一个用生成器实现树的递归中序遍历示例。:

# A recursive generator that generates Tree leaves in in-order.
def inorder(t):if t:for x in inorder(t.left):yield xyield t.labelfor x in inorder(t.right):yield x

另外两个 test_generators.py 中的例子给出了 N 皇后问题(在 NxN 的棋盘上放置 N 个皇后,任何一个都不能吃掉另一个),以及马的遍历路线(在NxN 的棋盘上给马找出一条不重复的走过所有格子的路线)的解。

向生成器传递值

在 Python 2.4 及之前的版本中,生成器只产生输出。一旦调用生成器的代码创建一个迭代器,就没有办法在函数恢复执行的时候向它传递新的信息。你可以设法实现这个功能,让生成器引用一个全局变量或者一个调用者可以修改的可变对象,但是这些方法都很繁杂。

在 Python 2.5 里有一个简单的将值传递给生成器的方法。yield 变成了一个表达式,返回一个可以赋给变量或执行操作的值:

val = (yield i)

我建议你在处理 yield 表达式返回值的时候, 总是 两边写上括号,就像上面的例子一样。括号并不总是必须的,但是比起记住什么时候需要括号,写出来会更容易一点。

(PEP 342 解释了具体的规则,也就是 yield 表达式必须括起来,除非是出现在最顶级的赋值表达式的右边。这意味着你可以写 val = yield i,但是必须在操作的时候加上括号,就像 val = (yield i) + 12

可以调用 send(value)() <generator.send> 方法向生成器发送值。这个方法会恢复执行生成器的代码,然后 yield 表达式返回特定的值。如果调用普通的 __next__`方法,``yield`() 会返回 None.

这里有一个简单的每次加1的计数器,并允许改变内部计数器的值。

def counter(maximum):i = 0while i < maximum:val = (yield i)# If value provided, change counterif val is not None:i = valelse:i += 1

这是改变计数器的一个示例

>>>

>>> it = counter(10)  
>>> next(it)  
0
>>> next(it)  
1
>>> it.send(8)  
8
>>> next(it)  
9
>>> next(it)  
Traceback (most recent call last):File "t.py", line 15, in <module>it.next()
StopIteration

因为 yield 很多时候会返回 None,所以你应该总是检查这个情况。不要在表达式中使用 yield 的值,除非你确定 send() 是唯一的用来恢复你的生成器函数的方法。

除了 send() 之外,生成器还有两个其他的方法:

  • throw(value) 用于在生成器内部抛出异常;这个异常会在生成器暂停执行的时候由 yield 表达式抛出。

  • generator.close() 会在生成器内部抛出 GeneratorExit 异常来结束迭代。当接收到这个异常时,生成器的代码会抛出 GeneratorExit 或者 StopIteration;捕捉这个异常作其他处理是非法的,并会出发 RuntimeError。close() 也会在 Python 垃圾回收器回收生成器的时候调用。

    如果你要在 GeneratorExit 发生的时候清理代码,我建议使用 try: ... finally: 组合来代替 GeneratorExit。

这些改变的累积效应是,让生成器从单向的信息生产者变成了既是生产者,又是消费者。

生成器也可以成为 协程 ,一种更广义的子过程形式。子过程可以从一个地方进入,然后从另一个地方退出(从函数的顶端进入,从 return 语句退出),而协程可以进入,退出,然后在很多不同的地方恢复(yield 语句)。

内置函数

我们可以看看迭代器常常用到的函数的更多细节。

Python 内置的两个函数 map() 和 filter() 复制了生成器表达式的两个特性:

map(f, iterA, iterB, ...) 返回一个遍历序列的迭代器

f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....

>>>

>>> def upper(s):
...     return s.upper()

>>>

>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

你当然也可以用列表推导式达到同样的效果。

filter(predicate, iter) 返回一个遍历序列中满足指定条件的元素的迭代器,和列表推导式的功能相似。 predicate (谓词)是一个在特定条件下返回真值的函数;要使用函数 filter(),谓词函数必须只能接受一个参数。

>>>

>>> def is_even(x):
...     return (x % 2) == 0

>>>

>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

这也可以写成列表推导式:

>>>

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

enumerate(iter, start=0) 计数可迭代对象中的元素,然后返回包含每个计数(从 start 开始)和元素两个值的元组。:

>>>

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

enumerate() 常常用于遍历列表并记录达到特定条件时的下标:

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

sorted(iterable, key=None, reverse=False) 会将 iterable 中的元素收集到一个列表中,然后排序并返回结果。其中 key 和 reverse 参数会传递给所创建列表的 sort() 方法。:

>>>

>>> 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]

(对排序更详细的讨论可参见 排序的技巧。)

内置函数 any(iter) 和 all(iter) 会查看一个可迭代对象内容的逻辑值。any() 在可迭代对象中任意一个元素为真时返回 True,而 all() 在所有元素为真时返回 True:

>>>

>>> 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(iterA, iterB, ...) 从每个可迭代对象中选取单个元素组成列表并返回:

zip(['a', 'b', 'c'], (1, 2, 3)) =>('a', 1), ('b', 2), ('c', 3)

它并不会在内存创建一个列表并因此在返回前而耗尽输入的迭代器;相反,只有在被请求的时候元组才会创建并返回。(这种行为的技术术语叫惰性计算,参见 lazy evaluation.)

这个迭代器设计用于长度相同的可迭代对象。如果可迭代对象的长度不一致,返回的数据流的长度会和最短的可迭代对象相同

zip(['a', 'b'], (1, 2, 3)) =>('a', 1), ('b', 2)

然而,你应该避免这种情况,因为所有从更长的迭代器中取出的元素都会被丢弃。这意味着之后你也无法冒着跳过被丢弃元素的风险来继续使用这个迭代器。

itertools 模块

itertools 模块包含很多常用的迭代器以及用于组合多个迭代器的函数。 本节会用一些小例子来介绍这个模块的内容。

这个模块里的函数大致可以分为几类:

  • 从已有的迭代器创建新的迭代器的函数。

  • 接受迭代器元素作为参数的函数。

  • 选取部分迭代器输出的函数。

  • 给迭代器输出分组的函数。

创建新的迭代器

itertools.count(start, step) 返回一个等分的无限数据流。初始值默认为0,间隔默认为1,你也选择可以指定初始值和间隔:

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, ...

itertools.cycle(iter) 保存一份所提供的可迭代对象的副本,并返回一个能产生整个可迭代对象序列的新迭代器。新迭代器会无限重复这些元素。:

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

itertools.repeat(elem, [n]) 返回 n 次所提供的元素,当 n 不存在时,返回无数次所提供的元素。

itertools.repeat('abc') =>abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>abc, abc, abc, abc, abc

itertools.chain(iterA, iterB, ...) 接受任意数量的可迭代对象作为输入,首先返回第一个迭代器的所有元素,然后是第二个的所有元素,如此一直进行下去,直到消耗掉所有输入的可迭代对象。

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

itertools.islice(iter, [start], stop, [step]) 返回一个所输入的迭代器切片的数据流。如果只单独给定 stop 参数的话,它会返回从起始算起 stop 个数量的元素。如果你提供了起始下标 start,你会得到 stop-start 个元素;如果你给定了 step 参数,数据流会跳过相应的元素。和 Python 里的字符串和列表切片不同,你不能在 startstop 或者 step 这些参数中使用负数。:

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

itertools.tee(iter, [n]) 可以复制一个迭代器;它返回 n 个能够返回源迭代器内容的独立迭代器。如果你不提供参数 n,默认值为 2。复制迭代器需要保存源迭代器的一部分内容,因此在源迭代器比较大的时候会显著地占用内存;同时,在所有新迭代器中,有一个迭代器会比其他迭代器占用更多的内存。

itertools.tee( itertools.count() ) =>iterA, iterBwhere 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) (把两个数加起来),operator.ne(a, b) (和 a != b 相同),以及 operator.attrgetter('id') (返回获取 .id 属性的可调用对象)。

itertools.starmap(func, iter) 假定可迭代对象能够返回一个元组的流,并且利用这些元组作为参数来调用 func:

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

选择元素

另外一系列函数根据谓词选取一个迭代器中元素的子集。

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

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

itertools.takewhile(predicate, iter) 返回一直让 predicate 返回 true 的元素。一旦 predicate 返回 false,迭代器就会发出终止结果的信号。:

def less_than_10(x):return x < 10itertools.takewhile(less_than_10, itertools.count()) =>0, 1, 2, 3, 4, 5, 6, 7, 8, 9itertools.takewhile(is_even, itertools.count()) =>0

itertools.dropwhile(predicate, iter) 在 predicate 返回 true 的时候丢弃元素,并且返回可迭代对象的剩余结果。:

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 中使相应地 selector 中的元素为真的元素;它会在任一个迭代器耗尽的时候停止:

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

组合函数

itertools.combinations(iterable, r) 返回一个迭代器,它能给出输入迭代器中所包含的元素的所有可能的 r 元元组的组合。:

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)

每个元组中的元素保持着 可迭代对象 返回他们的顺序。例如,在上面的例子中数字 1 总是会在 2, 3, 4 或 5 前面。一个类似的函数,itertools.permutations(iterable, r=None),取消了保持顺序的限制,返回所有可能的长度为 r 的排列:

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 参数的值,它会使用可迭代对象的长度,也就是说会排列所有的元素。

注意这些函数会输出所有可能的位置组合,并不要求 可迭代对象 的内容不重复:

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

同一个元组 ('a', 'a', 'b') 出现了两次,但是两个 'a' 字符来自不同的位置。

itertools.combinations_with_replacement(iterable, r) 函数放松了一个不同的限制:元组中的元素可以重复。从概念讲,为每个元组第一个位置选取一个元素,然后在选择第二个元素前替换掉它。:

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) 是一个可以对迭代器返回的每个元素计算键值的函数。 如果你不提供这个键值函数,它就会简化成每个元素自身。

groupby() 从所依据的可迭代对象中连续地收集具有相同值的元素,然后返回一个长度为2的元组的数据流, 每个元组包含键值以及对应这个键值的元素所组成的迭代器。

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() 假定了所依据的可迭代对象的内容已经根据键值排序。注意,返回的迭代器也会使用所依据的可迭代对象,所以在请求迭代器 2和相应的键之前你必须先消耗迭代器 1 的结果。

functools 模块

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

对于用函数式风格编写的程序,有时你会希望通过给定部分参数,将已有的函数构变形称新的函数。考虑一个 Python 函数 f(a, b, c);你希望创建一个和 f(1, b, c) 等价的新函数 g(b, c);也就是说你给定了 f() 的一个参数的值。这就是所谓的“部分函数应用”。

partial() 接受参数 (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2)。它会返回一个可调用的对象,所以你能够直接调用这个结果以使用给定参数的 function

这里有一个很小但很现实的例子:

import functoolsdef 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]) 持续地在可迭代对象的所有元素上执行操作,因此它不能够用在无限的可迭代对象上。func 必须是一个接受两个元素并返回一个值的函数。functools.reduce() 接受迭代器返回的前两个元素 A 和 B 并计算 func(A, B) 。然后它会请求第三个元素,C,计算 func(func(A, B), C),然后把这个结果再和第四个元素组合并返回,如此继续下去直到消耗整个可迭代对象。如果输入的可迭代对象完全不返回任何值,TypeError 异常就会抛出。如果提供了初值(initial value),它会被用作起始值,也就是先计算 func(initial_value, A) 。:

>>>

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

如果你在 functools.reduce() 中使用 operator.add(),你就会把可迭代对象中的所有元素加起来.这种情况非常常见, 所以 Python 有一个特殊的内置函数 sum():

>>>

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

不过, 对于很多使用 functools.reduce() 的情形, 使用明显的 for 循环会更清晰:

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() 不是仅仅返回最终结果,而是返回一个会产生每个部分结果的迭代器:

itertools.accumulate([1, 2, 3, 4, 5]) =>1, 3, 6, 10, 15itertools.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()

  • 确认对象: is_()is_not()

全部函数列表可以参考 operator 模块的文档。

小函数和 lambda 表达式

编写函数式风格程序时,你会经常需要很小的函数,作为谓词函数或者以某种方式来组合元素。

如果合适的 Python 内置的或者其他模块中的函数,你就一点也不需要定义新的函数:

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

如果不存在你需要的函数,你就必须自己编写。一个编写小函数的方式是使用 lambda 表达式。lambda 接受一组参数以及组合这些参数的表达式,它会创建一个返回表达式值的匿名函数:

adder = lambda x, y: x+yprint_assign = lambda name, value: name + '=' + str(value)

另一种替代方案就是通常的使用 def 语句来定义函数:

def adder(x, y):return x + ydef print_assign(name, value):return name + '=' + str(value)

哪一种更受青睐呢?这是一个风格问题;我通常的做法是避免使用 lambda

我这么偏好的一个原因是,lambda 能够定义的函数非常受限。函数的结果必须能够作为单独的表达式来计算,这意味着你不能使用多路 if... elif... else 比较,或者 try... except 语句。如果你尝试在 lambda 语句中做太多事情,你最终会把表达式过于复杂以至于难以阅读。你能快速的说出下面的代码做了什么事情吗?:

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

你可以弄明白,不过要花上时间来理清表达式来搞清楚发生了什么。使用一个简短的嵌套的 def 语句可以让情况变得更好:

import functools
def combine(a, b):return 0, a[1] + b[1]total = functools.reduce(combine, items)[1]

如果我仅仅使用一个 for 循环会更好:

total = 0
for a, b in items:total += b

或者使用内置的 sum() 和一个生成器表达式:

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

许多使用 functools.reduce() 的情形可以更清晰地写成 for 循环的形式。

Fredrik Lundh 曾经建议以下一组规则来重构 lambda 的使用:

  1. 写一个 lambda 函数。

  2. 写一句注释来说明这个 lambda 究竟干了什么。

  3. 研究一会这个注释,然后想出一个抓住注释本质的名字。

  4. 用这个名字,把这个 lambda 改写成 def 语句。

  5. 把注释去掉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.bcls.cn/PPHy/5611.shtml

如若内容造成侵权/违法违规/事实不符,请联系编程老四网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

node.js使用multer在vue中实现图片上传

效果演示 点击上传选择要上传的图片。 上传成功会加载图片的缩略图。 此时&#xff0c;图片以保存在后端的静态目录中。 设计思路 vue中使用input标签上传图片&#xff0c;绑定change事件&#xff0c;事件负责把图片发送给后端&#xff0c;后端通过multer模块处理前端传来的…

SpringBoot使用classfinal-maven-plugin插件加密Jar包

jar包加密 1、在启动类的pom.xml中加入classfinal-maven-plugin插件 <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><…

10分钟快速开始SkyWalking结合Springboot项目

10分钟快速开始SkyWalking结合Springboot项目 实习期间&#xff0c;公司让我去学习一下链路追踪如何集成到Springboot项目中。 为此有两个方案&#xff1a; 1.opentelementryjaegerprometheus opentelementry 收集器收集线上的metrics和traces&#xff0c;然后发送给jaeger和p…

css transform 会影响position 定位

比如通过以下代码.实现导航条上的每个li栏目,以不同的时间间隔,从上向下移动进来并显示 .my-navbar ul li {position: relative;opacity: 0;transform: translateY(-30px);transition: transform .6s cubic-bezier(.165,.84,.44,1),opacity .6s cubic-bezier(.165,.84,.44,1);…

第12章-生成树协议

1. STP产生背景 1.1 桥接网络&#xff1a;网桥 1.2 交换机网络 1.3 解决方案 2. STP生成树协议 2.1 概念 2.2 BPDU(Bridge Protocol Data Unit) 2.3 选举机制&#xff08;网桥&#xff1a;一进一出&#xff09; 2.4 例题 2.5 端口状态 2.6 STP计时器 2.7 STP的问题 …

计算机网络实验八 利用 Java /C++开发网络聊天应用程序

一、实验目的和要求 1)基本掌握利用 Java 开发环境调试应用程序的方法。 2)理解基于套接字开发网络应用程序的过程,深入理解客户/服务器方式工作原理。 3)掌握基于Java和C++开发网络通信程序的方法。 二、实验环境 1)运行 Windows 2008 Server/XP/7 操作系统的 PC 2 台…

git commit 后,本地远端都没有记录,消失不见

今天git commit 之后发现远端没有记录&#xff0c;本地没有最新代码记录 git commit 后&#xff0c;提交记录会消失不见的原因可能是&#xff1a; git只git commit了&#xff0c;没有push到远程分支&#xff0c;切换到其他分支时丢失。而且看不到提交记录&#xff0c;和找不到…

【前端素材】推荐优质后台管理系统Upcube平台模板(附源码)

一、需求分析 后台管理系统在多个层次上提供了丰富的功能和细致的管理手段&#xff0c;帮助管理员轻松管理和控制系统的各个方面。其灵活性和可扩展性使得后台管理系统成为各种网站、应用程序和系统不可或缺的管理工具。 当我们从多个层次来详细分析后台管理系统时&#xff0…

一文读懂什么是HTTPS检查

我们在什么情况下需要做HTTPS检查&#xff1f; 做HTTPS检查是为了确保通过HTTPS协议传输的数据的安全性&#xff0c;防止数据在传输过程中被窃取或篡改&#xff0c;保护用户隐私和企业数据安全。 HTTPS检查是指通过使用与网络连接上的在途攻击相同的技术来检查加密的Web流量的…

【Android】View 与 ViewGroup

View 是 Android 所有控件的基类&#xff0c;我们平常所用的 TextView 和 ImageView 都是继承自 View 的&#xff0c;源码如下&#xff1a; public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {... }public class ImageView extends View {.…

抖音视频评论抓取软件|视频批量下载

抖音视频评论采集软件是一款基于C#开发的高效、便捷的工具&#xff0c;旨在为用户提供全面的数据采集和分析服务。该软件不仅支持通过关键词进行搜索抓取&#xff0c;还能够通过分享链接进行单个视频的抓取和下载&#xff0c;让用户轻松获取抖音视频评论数据。 &#x1f50d; …

python 循环语句 while 循环

while循环 Python 编程中 while 语句用于循环执行程序&#xff0c;即在某条件下&#xff0c;循环执行某段程序&#xff0c;以处理需要重复处理的相同任务。其基本形式为&#xff1a; while 判断条件(condition)&#xff1a; 执行语句(statements)…… 执行语句可以是单个语句…

[计算机网络]--IP协议

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、IP协议…

追觅科技社招校招北森在线测评考情分析、真题题库、通关答案、答题技巧

追觅科技社招校招北森在线测评考情分析、真题题库、通关答案、答题技巧 追觅是一家专注智能生活家电的全球化科技公司&#xff0c;于2017年创立。它起源于清华大学校内规模最大的科技平台“天空工场”。早在2015年&#xff0c;以“天空工场”成员为核心的追觅初创团队&#x…

Jmeter接口性能测试工具

1、mac上安装 Apache JMeter - Download Apache JMeter 打开文件夹中/bin目录&#xff0c;sh jmeter 即可打开。 2、配置测试计划 3、添加测试Thread group 一个group用来控制Jmeter并发时产生线程的数量&#xff0c;在它的下一级菜单下只有一个组件&#xff08;线程组&…

【数据结构】队列OJ题《用队列实现栈》(题库+解析+代码)

1.前言 通过前面队列的实现和详解大家对队列应该有一定熟悉了&#xff0c;现在上强度开始做题吧 队列详解&#xff1a;http://t.csdnimg.cn/dvTsW 2.OJ题目训练225. 用队列实现栈 题目分析 请你仅使用两个队列实现一个后入先出&#xff08;LIFO&#xff09;的栈&#xff0…

模板方法模式:封装不变步骤,引导扩展实现灵活多态的算法流程

文章目录 一、引言二、应用场景与技术背景三、模式定义与实现四、实例详解五、优缺点分析总结 一、引言 ​ 模板方法模式&#xff08;Template Method Pattern&#xff09;是一种行为型设计模式&#xff0c;它在父类中定义一个操作中的算法骨架&#xff0c;并允许子类为其中的…

抖店的运营难不难?2024商家开通抖店,需要搞清楚这三件事!

我是王路飞。 抖店的运营难不难呢&#xff1f; 根据我做抖店四年多的经验来看&#xff08;2020年开始入局抖店&#xff09;&#xff0c;抖店的运营并不难。 毕竟我们做抖店&#xff0c;并不是为了把某些类目产品做到行业top级别&#xff0c;我们只是用无货源的思维去放大平台…

.NET高级面试指南专题十一【 设计模式介绍,为什么要用设计模式】

设计模式是软件工程中常用的解决特定问题的通用设计方法。它们提供了经过验证的解决方案&#xff0c;可用于解决在软件开发过程中经常遇到的一些常见问题。设计模式不是一种具体的编程语言特性或语法&#xff0c;而是一种通用的设计思想或模板&#xff0c;可以帮助开发人员设计…

【openGL教程08】基于C++的着色器(02)

LearnOpenGL - Shaders 一、说明 着色器是openGL渲染的重要内容&#xff0c;客户如果想自我实现渲染灵活性&#xff0c;可以用着色器进行编程&#xff0c;这种程序小脚本被传送到GPU的显卡内部&#xff0c;起到动态灵活的着色作用。 二、着色器简述 正如“Hello Triangle”一章…
推荐文章