背景
最近笔者写Python代码比较多,同时又有多种编程语言的开发经验,现在的语言设计上大多趋同。当需要对数据集合操作时,非常喜欢java的stream声明式处理数据,萌生在Python中模仿Java的写法。虽然java的API易用性与Scala/Kotlin相比,还是有很大的差距,但与Python比起来,还是强不少。
我们先来看一下Java的玩法:
// 过滤Type是GROCERY,按Value倒排序,聚合Id,归并新的集合
List<Integer> tanscationsIds = transcations.parallelStream()
.filter(it -> it.getType() == Transcation.GROCERY)
.sorted(comparing(Transcation::getValue).resersed())
.map(Transcation::getId)
.collect(Collectors::toList());
函数式是一种声明式编程范式,上面的代码就像SQL语句一样,代码操作数据集合非常直观。笔者在去年写了一篇 飞哥讲代码16:函数式让数据处理更简洁 简单介绍了函数式在数据集合操作上的便利。
在数据分析领域,Python生态中有Pandas这类非常优秀的库,它对DataFrame(可以理解一张数据库表存储的数据集合)提供非常简单的API,支持对数据集的过滤、聚合、归并、填充与计算等,很方便地对数据进行清洗和加工。但它也并不是像Java的Stream一样操作,示例如下:
# 过滤graded是B并且loan_amnt大于5000,取出loan_amnt这一列,并对这一列的值倒排序
df[(df["grade"] == "B") & (df["loan_amnt"] > 5000)]["loan_amnt"].sort_values(ascending=False)
但Pandas并不是声明式编程,不会像Java的Stream一样分为中间操作与最终操作,当调用最终操作才执行计算。像上面的df[(df["grade"] == "B") & (df["loan_amnt"] > 5000)]
已经是执行过滤操作了。若只是对内存中的数据集合做一些操作,Pandas又过于厚重,杀鸡用牛刀了。
基础
Python的官方文档有一章节 函数式编程指引,从文档介绍来看,Python的函数式编程的内置能力还是非常基础。稍总结一下:
- 数据的生成:迭代器,生成器,列表推导式
- 数据的操作:
- 内置函数:提供一些高阶函数,如map,filter,zip, sorted,all,any
- itertools模块:提供一些计算,组合,过滤,分组等函数,如count,chain,filterfalse,takewhile,combinations,groupby
- operator模块:提供一些操作符的函数,如add(数学运算), not_(逻辑运算), and_(位运算), eq(比较), is_(确认对象)
- functools模块:常用的cmp_to_key,partial,reduce
- 函数:小函数和lambda表达式,不过lambda比较鸡肋
从上面可以看出,可能由于Python发展的历史原因,Python在数据集合操作上提供的对函数式编程并不是很系统化,有点七零八落的。在Python中,怎么样的写法才算是函数式编程,示例如下:
# 过程式
result = []
for x in range(5):
result.append(x**2)
# 函数式,列表推导式
result = [x**2 for x in range(5)]
# 函数式,内置函数+lambda表达式
result = map(lambda x: x**2, range(5))
再来一个稍复杂的例子:
from operator import add
from functools import reduce
# 计算下面字符中的数字的和
v = 'a b c 1 2 3 4 5'
reduce(add, map(int, filter(lambda x: x.isdigit(), v.split())))
# reduce(add, Iterator[int])可以使用sum
sum(map(int, filter(lambda x: x.isdigit(), v.split())))
在Python中函数已是一等公民了,函数可以作为变量,也可以是参数,函数内嵌套定义函数。Java的Stream是结合面向对象设计基础上提供pipeline执行流来支持函数式调用,Python虽然支持面向对象设计,但遗憾的是并没有像Stream这样的类型来简化函数式调用,而是纯函数的嵌套调用,在代码可读性上会差不少,尤其那么多括号匹配。
进阶
无副作用
函数式编程范式要求函数是无副作用的,它体现在对输入的数据本身无修改,对函数内部外部无状态修改。反例:
# 修改了输入
def reverse_data(data)
data.reverse()
return data
# 修改了外部状态
count = 0
def count_data(data):
for x in data:
if x > 3: count += 1
匿名函数
函数式编程,函数做为参数传入,有些时候,不需要显式地定义函数,直接传入匿名函数更方便,lambda表达式对匿名函数提供了有限支持。
从前面的例子中已多次出现它的身影:
- 关键字lambda表示匿名函数,冒号前面的x表示函数参数,冒号前面可以存在多个参数
- lambda只能有一个表达式,不用写return,返回值就是该表达式的结果
装饰器
装饰器(decorator)本质也是高阶函数,正如其名,通过对现有函数或对象进行装饰,增加其功能。比如我们需要对函数增加打印日志,可以定义一个log高阶函数:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print(f'call {func.__name__}()')
return func(*args, **kw)
return wrapper
@log
def test_func():
print('test_func body')
test_func() # 会先输出call test_func 再输出test_func body
当调用test_func时,不仅会运行test_func函数本身,还会调用log函数。把@log放到test_func函数的定义处,相当于执行了语句:test_func = log(test_func),当再调用test_func其实是调用log中的wrapper函数。
如果装饰器decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数:
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
为什么要使用functools.wraps对wrapper函数也装饰一下呢?因为decorator在实现的时候,被装饰后的函数其实已经是另外一个函数了,函数名等函数属性会发生改变。为了不影响,functools包中提供wraps的decorator来消除这样的副作用。写decorator的时候,最好在实现之前加上functools的wrap,保留原有函数的名称和函数属性。
偏函数
函数在执行时,要带上所有必要的参数进行调用。但是,有时参数可以在函数被调用之前提前获知。这种情况下,一个函数有一个或多个参数预先就能用上,以便函数能用更少的参数进行调用。示例:
# int()函数可以把字符串转换为整数
int('12345')
# int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换
int('12345', base=8)
from functools import partial
# 每次都传入int(x, base=2)非常麻烦,重新定义一个函数
bin2dec = partial(int, base=2)
bin2dec('1010101')
从例子中可以看出functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,原函数的各个参数依次作为新函数后续的参数,调用这个新函数会更简单。我们再简化前面的例子,稍会增加可读性:
from operator import add
from functools import reduce, partial
v = 'a b c 1 2 3 4 5'
filter_digit = partial(filter, lambda x: x.isdigit())
sum_int = partial(reduce, add)
to_int = partial(map, int)
sum_int(to_int(filter_digit(v.split())))
fn.py库
从前面的例子中,我们似乎了发现问题,Python对函数式支持并不是很现代化,而是很原始的方式,难以编写可读同时又可维护的函数式代码。fn.py库出现是为了简化python函数式编程而生,尽管它不可能解决函数式编程所有问题,还是给我们带来极大便利。
简化Lambda
fn.py受Scala的启发,提供了一个特别的_
对象简化Lambda语法:
list(map(lambda x: x*2, [1,2,3]))
# Scala写法: List(1,2,3).map(_*2)
from fn import _
# 可以像scala一样改进
list(map(_*2, [1,2,3]))
简化函数调用
fn.py提供一个类F
来简化偏函数以及函数嵌套调用,改写前面的例子:
from operator import add
from fn import F, _
from functools import reduce
v = 'a b c 1 2 3 4 5'
f = F(reduce, add) << F(map, int) << F(filter, _.call('isdigit'))
f(v.split())
# 也可以写成这样,每个>>与<<后面需要()包裹生成F对象,或者是函数或是一个callable,不需要写F(..)
f = F() >> _.call('split') >> (filter, _.call('isdigit')) >> (map, int) >> (reduce, add)
f(v)
注:简化Lambda语法当是函数调用时,不支持_.isdigit()
写法,它只支持类_Callbale
(_
即为fn.underscore._Callbale
对象)已实现的操作符(如+,> ,&等),及属性设置与获取,可点击参见源码。
其它
对于fn.py的使用,上面的讲解只例举一小部分,还支持:
- Persistent data structures,比如LinkedList,Stack,SkewHeap等
- Streams:惰性求值,数据生成
- recur:尾递归
- Itertools:统一python2与3的Itertools工具函数,以及新增一些常用函数,如head, tail, take, drop,flatten等
- currying:柯里化
- Option:减少if/else
其它的使用请参见在线文档。
PyFunctional库
fn.py让Python的函数组装调用便捷了,但对数据集的管道式操作支持不足。寻寻觅觅,在翻它的issue中,找到另一个库:PyFunctional。PyFunctional可以说是完全参照了Java的Pipeline链式调用来支持对数据集合操作,并且也是采用惰性求值的方式,把操作分为中间操作(Transformations)与最终操作(Actions)。
数据流管道
我们还是改写前面的例子:
from functional import seq
v = 'a b c 1 2 3 4 5'
seq(v.split())\
.filter(lambda x: x.isdigit())\
.map(lambda x: int(x))\
.reduce(lambda x, y: x + y)
# 或者
seq(v.split()).filter(lambda x: x.isdigit()).map(lambda x: int(x)).sum()
这种写法是不是熟悉的味道又回来了,seq是它核心的对象(相当于Java的Stream对象),所有要操作的数据先要转化为seq。不过每个回调的函数要使用lambda表达式还是有点辣眼,采用_
来简化lambda表达式已放在RoadMap中。
数据生成
有意思的是PyFunctional更进一步,数据生成支持文件或数据库对接,这有点Pandas的味道了,并且支持导成Pandas的Dataframe。
数据集合来源:
- seq.range(10)
- seq.open(filepath)
- seq.json(filepath), seq.jsonl(filepath)
- seq.csv(filepath), seq.csv_dict_reader(filepath)
- seq.sqlite3(filepath, ‘select * from data’)
结果数据集合导出:
- to_file(path)
- to_csv(path)
- to_json(path),to_jsonl(path)
- to_sqlite3(conn, tablename_or_query, *args, **kwargs)
- to_pandas(columns=None)
其它
对于PyFunctional的使用,上面的讲解只例举一小部分,尤其是它丰富的Transformations,Actions API,可以看出它已经完全能胜任数据集的过滤,分组,聚合,转换、计算等操作。API使用可以参考其官方文档。
结语
函数式让数据处理更简洁,Python内置的功能还是相对基础,并没有系统化支持,开源第三方库Fn.py与PyFunctional又在不同层次与不同方向支持函数式编程。本文不是第三方库API完整介绍,而是从实际工作中问题出发,本着学习与探索的精神,挖掘Python函数式编程不足与如何改进。