最近正在研究python3与python2的特性区别,也搜了一些资料,不过好像大家都没有提到python3中函数式编程部分与python2的区别。正好官方文档里面有个howto就说了这一节,稍微看了看,总结了一下python3中FP的特点。(其实就是翻译嘛:))

HOWTO Functional Programming

迭代器(Iterators) 与 数据结构

这部分相比2.x没有变化,值得注意的是iterator不一定是有限的,也可能会一直next下去。

生成器推导式与列表推导式

这部分与2.x也没有变化,()包裹的是生成器推导式,而[]包裹的是列表推导式。

以前我一直觉得推导式与map没什么区别,基本map能做到的推导式也可以。不过看了文档里的一个例子发现貌似推导式还能实现更多的一些功能。

1
2
3
4
5
6
>>> 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)]

求seq1与seq2之间的笛卡尔积,如果用map的话同时处理多个列表实质是处理zip之后的结果,要实现上面的功能写起来应该要复杂得多。

另外,生成器推导式有一个特殊的地方,就是写在函数调用里的话不需要再加一层括号,只需要外层的括号即可。

1
2
>>> ''.join(str(x) for x in range(10))
'0123456789'

关于这个问题的讨论Python generator expression parentheses oddity

生成器(Generator)

仍然没有变化,这部分就不深入讨论了,yield与send结合使用可以在python中简单实现coroutines。

内建函数

这部分开始有了一些变化。

reduce函数被移除,放到了functools模块中。

所有的内建函数(除了sorted,any,all)都返回的是迭代器,文档给出的几个函数map,filter,enumerate,zip。而在2.x中,map和zip返回的都是列表,需要借助itertools中的imap与izip,在3.x中imap与izip被移除。普通的切片操作返回的仍是列表,itertools.islice提供迭代版切片操作。

itertools模块与functools模块

除了上面提到的一些改动之外,基本与2.x没有多大区别。itertools中新提供了一个accumulate,具体来说它完成的工作与ruduce相同,但是把reduce时每次计算的结果都记录了,以一个迭代器的形式返回。在计算前缀和这种类型的结果时很有用。

functools中比较有用的就是partial了,这部分不是新的。当然还有functools.reduce,这里真的很想吐槽一下这单独把reduce放到这里是个什么意思……

而且文档中还处处透着一股“还不如写for循环”的气息……

EX: 关于闭包的碎碎念

这部分不是文档中的,是我之前看网上的一些资料自己写的,其实说白了也就是一个nonlocal关键字的事。

2.x中闭包的缺陷

Python毕竟不是为了FP而生的语言,虽然其也支持一些FP的特性,不过确实有些地方的机制比较蛋疼。在2.x中,闭包就一直是一大硬伤,例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
def counter():
num = 0
def add_one():
num += 1
print(num)
return add_one

con = counter()
con()
con()
con()

这段代码在2.x中运行会报错: UnboundLocalError: local variable ‘num’ referenced before assignment.

要解释这个问题,还得从python自身的变量作用域机制说起,接下来我尽可能清楚的表述一下…….

作用域概念与global关键字

2.x变量只有全局作用域和局部作用域,这里是对num同时进行了assignm与reference两个操作,即“引用这个变量来对变量进行赋值”,这样就会发生UnboundLocalError。如果仅仅是“对这个变量进行赋值”,则会在局部作用域创建一个新的变量。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
num = 0

def set_one():
num = 1

def add_one():
num += 1

def set_global_to_one():
global num
num = 1

def add_global_one():
global num
num += 1

def print_num():
print(num)

set_one()
print_num()
set_global_to_one()
print_num()
add_global_one()
print_num()
add_one()
print_num()

上面程序的运行结果是这样的

1
2
3
4
5
6
7
8
9
0
1
2
Traceback (most recent call last):
File "global-test.py", line 23, in <module>
add_one()
File "global-test.py", line 7, in add_one
num += 1
UnboundLocalError: local variable 'num' referenced before assignment

global关键字可以声明全局作用域的变量,如果没有global,在set_one中num=1就是创建了一个局部变量num,跟全局变量的num没关系,所以print的结果仍然是0.

print_num引用了num的值,如果只是引用的话,Python会按照从内层到外层的顺序逐个寻找作用域中的变量,这里找到的是全局变量num。

下面两个加了global的关键字函数,运行逻辑没有问题。而最后一个add_one函数报错了,原因是“引用num的值来给num赋值”,但是num这时候还没有定义,所以就报错了。同样是对num的操作,为什么set_one与print_num没有报错而add_one却报错了,原因就在这里。

总结一下就是:函数内引用全局变量不需要global,函数内修改全局变量要加global。

2.x中的解决方案

这样来看上面的闭包,我们似乎就无法对num这个变量进行赋值了,因为它位于counter的作用域中,使用global也没法获取。解决这个问题的方法,似乎只能定义一个全局变量然后同时声明global,但这又违背了闭包的初衷,打破了封闭作用域。

不过2.x中还是有解决办法的,就是使用一个容器对象

1
2
3
4
5
6
7
8
9
10
11
def counter():
num = []
def add_one():
num.append('tick')
print(len(num))
return add_one

con = counter()
con()
con()
con()

这样就可以实现相同的功能,因为add_one里面没有对num进行assign的操作,只是调用了一个成员函数append,对象本身并没有改变,这也就不会报错了。

3.x中的nonlocal关键字

3.x中新增了nonlocal关键字,用于声明非全局的外层变量,其实与global的作用是一样的,不过是作用于外层作用域罢了,它的原理和用法也跟global是相同的。

这样上面的例子,只需要加一个nonlocal声明就可以了

1
2
3
4
5
6
7
8
9
10
11
12
def counter():
num = 0
def add_one():
nonlocal num
num += 1
print(num)
return add_one

con = counter()
con()
con()
con()

总结

看来python3也不是完全一无是处,还是有一些改进的地方的,虽然从2到3会有一个从不适应到适应的过程,不过最终大家还是得接受新的事物。现在很多项目也开始注重2.x与3.x的代码兼容性,逐渐养成编写compatible代码的习惯,最终完全过渡到3.x,也是很有必要的。不过官方文档中对于编写compatible代码也有一条指示“Only worry about supporting Python 2.7”,这让我想到Django的向下兼容也只兼容到Python2.7,这是因为2.6中没有字典推导式的写法。所以个人认为2.6及以下版本,如果不是专业需要的话还是升级一下比较好……

参考资料

Python2 HOWTO-Functional Programming
Python3 HOWTO-Functional Programming
itertools module
functools module
PEP 3104 — Access to Names in Outer Scopes