原文:《Python 风格的函数式》,公众号 BOTManJL~
Readability counts. —— The Zen of Python, by Tim Peters (
import this
)
现代编程语言之间 常常相互借鉴(例如 几乎所有语言都支持了 lambda 表达式/匿名函数/闭包),所以许多人会说:
学什么编程语言都一样,会一种就行。
但我 不赞同 这个观点 —— 我认为:用不同的语言写代码,就应该 “入乡随俗”,多领会各种语言的 设计的艺术。
三人行,必有我师焉;择其善者而从之,其不善者而改之。——《论语‧述而》
Python 为了提高 可读性 (readability),提供了很多 语法糖 (syntactic sugar),开创了别具一格的 Python 风格 (Pythonic) 的 函数式编程 (functional programming)。
本文提到的所有概念 均可参考文中的 链接,实例代码包括了一些常见的 Tricks。
# 什么是 Pythonic (TL;DR)
# 迭代器
# 高阶函数
# 生成器
# 推导式
# 最后聊聊 Python 语言
什么是 Pythonic (TL;DR)
举个例子,实现一个简单的需求:
- 按行打印 当前脚本 内容
- 去掉每行 右侧空字符
- 过滤所有 空行(去掉右侧空字符后)
- 打印时加上 行号(不包括空行)
学习 Python 前,凭感觉会这么写:
file = open(__file__)
try:
index = 1
while True:
line = file.readline()
if not line:
break
strip_line = line.rstrip()
if len(strip_line) != 0:
print('{:2}: {}'.format(index, strip_line))
index += 1
finally:
file.close()
学习 Python 后,只需要 3 行代码:
with open(__file__) as file:
for index, line in enumerate(filter(len, map(str.rstrip, file)), 1):
print(f'{index}: {line}')
- 异常安全 (exception safe) 的打开/关闭文件
- 前:将
close()
写在 finally 语句 内,避免异常时泄露 - 后:使用 with 语句(类似 C++ 的 资源获取即初始化 (Resource Acquisition Is Initialization, RAII) 思想)
- 前:将
- 迭代 (iterate) 读取脚本 文件的每一行
- 前:使用
while
循环调用readline()
函数,直到读到None
时结束 - 后:使用
for
循环遍历 迭代器 (iterator) 获取结果
- 前:使用
- 去掉空字符、过滤空行
- 前:使用临时变量存储每行
rstrip()
的结果,使用if
判断len()
是否为空 - 后:使用高阶函数
map()
/filter()
消除循环和临时变量(参考《高阶函数:消除循环和临时变量》)
- 前:使用临时变量存储每行
- 记录行号
- 前:使用自增的临时变量存储(上述代码如果少一个缩进,结果就会不一样)
- 后:使用 enumerate() 函数 从
1
开始生成下标,并通过 可迭代解包 (iterable unpacking) 直接得到index
和line
变量
- 格式化输出
- 前:使用常规的 format() 函数
- 后:使用特有的 f-string (formatted string literal) 化简
所以,什么是 Pythonic —— 用代码描述 做什么 (what-to-do),而不是 怎么做 (how-to-do) —— 提升可读性。
迭代器
什么是迭代器(iterator)—— 用于遍历容器中元素的对象,需要支持next() 函数(即 对象实现 __next__() 方法):
- 如果仍有元素,返回容器中的下一个元素
- 如果迭代结束,抛出
StopIteration 异常
Python 中的迭代器,类似于C++ 的输入迭代器(input iterator)—— 每次只能读取一个元素,不能跨越,也不能后退,更不能随机访问。
例如,迭代器常用于 for
循环:
for i in [1, 2, 3]:
print(i) # use(i)
等效实现为 while
循环:
it = iter([1, 2, 3])
while True:
try:
i = next(it)
print(i) # use(i)
except StopIteration:
break
Python 提出了可迭代(iterable)的概念,支持从 容器或迭代器 通过iter() 函数返回迭代器(即 容器或迭代器实现 __iter__() 方法,而迭代器返回self
)。
高阶函数
普通迭代器 只能遍历容器的 已有元素;但在多数情况下,需要在迭代过程中,修改 原始元素 并构造出 新的元素。
在命令式编程中,常用for
循环遍历已有元素,并用 临时变量 存储修改后的结果;而函数式编程中,常用高阶函数(higher-order function)消除循环和临时变量(具体方法参考《高阶函数:消除循环和临时变量》):
map(str.upper, ['aaa', 'bbb'])
# ['AAA', 'BBB']
filter(lambda x: x % 2, range(10))
# [1, 3, 5, 7, 9]
reduce(lambda d, s: dict(d, **{s: s.upper()}), ['aaa', 'bbb'], {})
# {'aaa': 'AAA', 'bbb': 'BBB'} (Trick: construct dict)
注:
- 上述代码仅用于 Python 2(原因见下文)
- Python 3 移除了 reduce 内置函数,替换为functools.reduce()
生成器
在 Python 2 中,内置的高阶函数map()
/filter()
以及zip()
/range()
会直接返回list 类型的结果。对于使用者来说,局限性非常大。
一方面,无用的计算 会带来 额外的开销:
- 例如,设计一个读取数据库的函数(表中有 1,000,000 行数据),通过
return cursor.fetchall()
一次性返回所有数据 - 如果使用者 并不需要 所有数据,从数据库中读取了 不需要的部分,造成浪费
- 如果使用者 需要修改 部分数据,那么使用者还需要遍历 整个列表,非常耗时
def get_data():
# ...
return cursor.fetchall()
data = get_data()
# <list of 1,000,000 rows>
另一方面,不支持 表示 无穷的 (potential infinite) 数据结构:
- 例如,表示一个从 0 到
sys.maxint
的范围 - Python 2 中
range(sys.maxint)
会返回一个 很长的列表,占用大量内存 - Python 3 移除了 sys.maxint,并支持无限大数值,而构造出一个 无限长的范围(列表),将无法存储在内存
range(sys.maxint)
# MemoryError
在函数式编程中,常用 惰性求值 (lazy evaluation) 的方法解决上述问题。
Python 使用生成器(generator)实现惰性求值 —— 带有yield 表达式的函数,对外支持和迭代器相同的next()
接口(对使用者透明),按需 生成并返回 结果:
- 对于读取数据库的函数,可以将
return
改为yield
,通过yield cursor.fetchone()
逐个返回结果 - 而使用者可以通过 和迭代器相同的方式(例如
for
循环),按需 使用或修改 数据
def get_data():
# ...
yield cursor.fetchone()
for row in get_data():
print(row)
- Python 2 额外支持了
itertools.imap()
/itertools.ifilter()
/itertools.izip()
/xrange()
用于替换内置函数:返回迭代器(生成器),而不是列表 - Python 3 直接修改了
map()
/filter()
/zip()
/range()
等内置函数:返回迭代器(生成器),而不是列表(取代了 Python 2 的itertools.i*()
/xrange()
函数)
range(sys.maxsize)
# range(0, 9223372036854775807)
list(range(sys.maxsize))
# [0, 1, 2, ...] (MemoryError)
zip(*[[1, 2], [3, 4], [5, 6]])
# <zip object at 0x000001F9BCD2AB88>
list(zip(*[[1, 2], [3, 4], [5, 6]]))
# [(1, 3, 5), (2, 4, 6)] (Trick: matrix transpose)
- Python 还提供了
itertools.count()
/itertools.cycle()
/itertools.repeat()
无穷迭代器 (infinite iterator)(生成器),用于表示无穷的数据结构
dict(zip(itertools.count(), ['a', 'b', 'c']))
# {0: 'a', 1: 'b', 2: 'c'} (Trick: enumerate)
list(itertools.repeat('{}', 3))
# ['{}', '{}', '{}'] (Trick: sequence repetition)
# ['{}'] * 3 == ['{}', '{}', '{}']
# '{}' * 3 == '{}{}{}'
推导式
Python 为了化简 map()
/filter()
的高阶函数写法,提供了 类似 Haskell 的 列表/字典/集合 推导式 (list/dict/set comprehensions) 和 生成器表达式 (generator expressions) 语法:
(s.upper() for s in ['aaa', 'bbb'])
# <generator object <genexpr> at 0x0000029CA8E65938>
[s.upper() for s in ['aaa', 'bbb']]
# ['AAA', 'BBB']
[x for x in range(10) if x % 2]
# [1, 3, 5, 7, 9]
{s: s.upper() for s in ['aaa', 'bbb']}
# {'aaa': 'AAA', 'bbb': 'BBB'}
{s.upper() for s in ['aaa', 'bbb']}
# {'AAA', 'BBB'}
- 一方面,可以使用 生成器表达式 快速构造
map()
/filter()
等效的迭代器(生成器) - 另一方面,可以使用 推导式 直接构造出 列表/字典/集合 对象
上边简单的例子看不出 推导式相对于高阶函数 的 优势,所以下边举一个求 0-100 之间所有 毕达哥拉斯三元组 的例子。
用 命令式编程 的直观方法是使用 三层 for 循环 实现:
ret = []
for x in range(1, 100):
for y in range(1, 100):
for z in range(1, 100):
if x < y and x ** 2 + y ** 2 == z ** 2:
ret.append((x, y, z))
# [(3, 4, 5), (5, 12, 13), ... (65, 72, 97)]
可以用 笛卡尔积 (cartesian product) itertools.product()
化简三层循环:
ret = []
for x, y, z in itertools.product(range(1, 100), repeat=3):
if x < y and x ** 2 + y ** 2 == z ** 2:
ret.append((x, y, z))
然而,基于笛卡尔积的遍历会 存在冗余,可以根据三元组定义,优化 遍历顺序、迭代下标、判断条件:
ret = []
for z in range(1, 100):
for x in range(1, z + 1):
for y in range(x, z + 1):
if x ** 2 + y ** 2 == z ** 2:
ret.append((x, y, z))
进一步用 函数式编程 的高阶函数 map()
/filter()
消除循环和临时变量:
- 通过
itertools.chain.from_iterable()
链接迭代器列表,实现flatmap()
,将 二维数组 展开为 一维数组 - 通过嵌套闭包,传递上一轮迭代的元素
def flatmap(*args):
return itertools.chain.from_iterable(map(*args))
list(filter(
lambda t: t[0] ** 2 + t[1] ** 2 == t[2] ** 2,
flatmap(lambda z:
flatmap(lambda x:
map(lambda y:
(x, y, z),
range(x, z + 1)),
range(1, z + 1)),
range(1, 100))))
最后用嵌套列表推导式(nested list comprehensions)实现优雅(elegant)的函数式代码:
- 没有 心智负担 (cognitive load),不需要思考用
map()
还是flatmap()
- 在保证 高效(惰性求值)的情况下,可读性 最佳
[(x, y, z) for z in range(1, 100)
for x in range(1, z + 1)
for y in range(x, z + 1)
if x ** 2 + y ** 2 == z ** 2]
延伸阅读:
- Python List Comprehensions: Explained Visually —— 可视化解释:如何把 高阶函数 转换为 列表推导式
- Overusing list comprehensions and generator expressions in Python —— 如何正确使用 列表推导式
- "Modern" C++ Lamentations —— 使用 C++ 20 range 实现上述代码的问题(编译慢、运行慢、心智负担)
- The Surprising Limitations of C++ Ranges Beyond Trivial Cases —— 如果你觉得高阶函数例子中,为了 make it right 而引入的flatmap()
很复杂;读完这篇文章,你会发现 C++ 里 make it compile 的方式更复杂
最后聊聊 Python 语言
首先,虽然 Python 的可读性不错,但对 其他语言用户 的可写性不好(仁者见仁,智者见智):
len(LIST)
而不是LIST.length()
(参考:Why does Python use methods for some functionality (e.g. list.index()) but functions for other (e.g. len(list))? | Design and History FAQ)STR.join(LIST)
而不是LIST.join(STR)
(但LIST.split(STR)
却是有的,参考:Why is join() a string method instead of a list or tuple method? | Design and History FAQ)COND ? EXPR1 : EXPR2
三元运算符 写为EXPR1 if COND else EXPR
(参考:Is there an equivalent of C’s “?:” ternary operator? | Programming FAQ)
其次,作为一个 非脚本语言用户,离开了 编译器的检查 和 IDE 强大的 智能提示,感觉自己不会写代码了:
- Python 是 运行时强类型 语言(参考:Strong versus Weak Typing (A Conversation with Guido van Rossum))
- 只有在 运行时,才能发现函数的参数(个数/类型)错误
- 另外,Python 2 的函数不能 指定参数/返回值的类型,IDE 的 智能提示 经常失效
Life is short, you need Python. —— Bruce Eckel
最后,Python 的 核心语言 (core language) 还算 比较简单(反例:C++),很多概念都是 良好定义 (well-defined) 的 —— 只要理解基本原理,就能 快速上手(写本文时,我的 Python 代码量未超过 1,000 行)。
如果有什么问题,欢迎交流。
Delivered under MIT License © 2019, BOT Man