化于无形的 lambda 语法

针对数据集合的每个成员进行计算是很常见的任务,用循环语句当然能实现,但比较麻烦,算个简单的求和都要写很多句代码。

编程语言经常把这些运算封装成函数,比如 Python 的 sum 函数,求订单价格总和是这样写的:

total_price = orders['price'].sum()

SQL 也可以写成:

select sum(price) total_price from orders

SPL 当然也没问题:

total_price = orders.sum(price)

看起来都很简洁。

任务当然不会总是这么简单,看一个更复杂的例子:对员工计算标签列,薪酬在 5000 以上的经理标签为 yes,其他员工为 no。

这个计算可以用一个不太复杂的表达式来描述,但不能在循环外部事先计算出结果,而要在循环中针对每个集合成员计算。这时候,可以把这个表达式定义成函数,再把这个函数作为参数传递给循环计算的函数,Python 可以这么写:

def calc_flag(row):
return 'yes' if row['position'] == 'manager' and row['salary'] > 5000 else 'no'

employee['flag']=employee.apply(calc_flag, axis=1)

这段代码先定义了一个函数 calc_flag,对传入的记录 row 计算表达式。

apply 函数以 calc_flag 为参数,在循环中将集合的当前成员(记录)传递给 calc_flag 计算,并用结果组成新序列。

显然,每次都预先定义成一个函数实在是麻烦,特别是对这么一个简单表达式就能搞定的任务。于是业界发明了 lambda 语法,可以在参数中定义函数,代码简洁很多:

employee['flag'] = employee.apply(lambda row: 'yes' if row['position'] == 'manager' and row['salary'] > 5000 else 'no', axis=1)

apply 函数的参数中用 lambda 关键字定义了一个匿名函数,传入参数是记录 row。在循环过程中,apply 函数将每个成员(记录)传给 lambda 函数,计算得到新的序列。

Python 的这种写法是显式的 lambda 语法,有 lambda 关键字,要定义参数,还要写函数体。

SQL 又是如何处理这种问题的呢?

SELECT *,
    CASE WHEN position = 'manager' AND salary > 5000 THEN 'yes' ELSE 'no' END AS flag
FROM employee;

没有 lambda,似乎更简单了。

其实,CASE WHEN 表达式还是相当于定义了一个函数。这还是把表达式定义的函数当成循环运算的参数,本质上仍是 lambda 语法。只是 SQL 更简洁,已经看不出 lambda 语法的形式了。

SQL 专业面向结构化数据计算,仅支持二维数据表这一种集合,lambda 函数传入参数只能是记录。SQL 就不需要像 Python 那样显式定义一个 row 参数,而可以直接访问字段,这会更便捷。大多数情况下,lambda 函数中可以直接使用字段名,只有存在同名字段时才需要冠以表名(或表别名)以示区分。这样,表名和记录参数都省了,SQL 就把 lambda 函数写成了简单表达式,将 lambda 语法化于无形了。

esProc SPL 继承了 SQL 这些优点,同样把 lambda 化于无形:

employee.derive(if(position == "manager" && salary > 5000, "yes","no"))

数据集合并不只有数据表这一种。SQL 不支持其他形式的集合,处理起来会麻烦了。对于单值成员组成的集合,SQL 还可以用只有一个字段的数据表来对付。比如求一组数值的平方和,SQL 这样写:

create table numbers as
    select value as n
    from (select 4.3 as value union all select 2.3 union all select 6.5 union all select 44.1) t;
select sum(n*n) from numbers;

这组数值还要额外起一个字段名和表名,有点啰嗦了。

Python 支持单值组成的集合,可以用 lambda 语法写出这样的运算:

numbers=pd.Series([4.3,2.3,6.5,44.1])
result=numbers.apply(lambda x: x * x).sum()

SPL 也有单值集合:

numbers=[4.3,2.3,6.5,44.1]
numbers.sum(~*~)

这里,sum 函数对集合循环计算的时候,将当前成员 ~ 传递给 lambda 函数求平方,再由 sum 函数求和。~ 就相当于前面 Python 代码中的 x。

在循环中,lambda 函数几乎总是用到集合的当前成员,SPL 把这个参数固化为 ~ 符号,这样就省去了参数的定义,从而把 lambda 写成简单表达式,继续保持将 lambda 化于无形的优点。

不过,对于这种相对简单的情况,Python 更提倡对位集合运算,可以避免使用 lambda 语法:

numbers=pd.Series([4.3,2.3,6.5,44.1])
result = (numbers * numbers).sum()

这显得更简洁。

前面那个员工标签的例子也可以写出来:

employee['flag'] = np.where((employee['position'] == 'manager') & (employee['salary'] > 5000), 'yes', 'no')

当表达式较复杂的时候,看着就不如 lamdba 语法简洁了。

SPL 也支持对位集合运算的书写形式:

(numbers ** numbers).sum()

看起来和 Python 差不多,但不如化于无形的 lambda 语法简单了。

SPL 还支持集合的集合。实际上,只要是集合,SPL 就都可以使用化于无形的 lambda 语法。比如求员工超过 10 个的部门有哪些员工:

employee.group(department).select(~.len()>10)

group 函数按部门分组后得到一个大集合,其成员是同一部门员工组成的子集合。表达式 ~.len()>10 是一个 lambda 函数,其中的 ~ 是集合的当前成员,也就是分组子集。

select 函数对大集合循环计算时,将当前成员(子集)传递给 lambda 函数,判断子集长度是否大于 10,再由 select 函数保留或舍弃这个子集。这是很自然的解题思路。

Python 一定程度也可以表示集合的集合,可以写出类似代码:

result=employee.groupby('department').filter(lambda x: len(x) >10)

对于集合的集合,就不能再使用对位运算了,采用 lambda 语法是 Python 最简单的写法,换其它方法,思路和代码都会变得更复杂。

SQL 不能描述集合的集合,对于这个问题要换种思路去实现(麻烦很多),lamdba 语法对这个问题已经无能为力了。

SPL 不仅仅是针对结构化数据计算的,但由于结构化数据过于常见,SPL 和 SQL 一样专门做了语法简化。再看一下前面计算员工标签列的例子,其实 SPL 引用字段的完整写法应该是 ~.position、~.salary,而 SPL 也提供了直接访问字段的便捷机制,就能把 lambda 函数写的和 SQL 一样简洁:

if(position == "manager" && salary > 5000, "yes","no")

Python 没做这种简化,只能写成下面这样,会导致这种最常见的情况写起来比较啰嗦。

lambda row: 'yes' if row['position'] == 'manager' and row['salary'] > 5000 else 'no'

除了当前成员之外,针对有序集合的循环计算经常还会用到成员的序号。比如:要取出一组数值中,第偶数个成员。简单思路是循环计算这个集合,如果成员的序号能被 2 整除就保留,否则就舍弃。

Python 只能给 lambda 函数传入当前成员这一个参数,必须改造这个集合,给每个成员附加上序号,才能实现这个思路:

result = filter(lambda x: x[0] % 2 == 1, enumerate(number))
even_index_members = [x[1] for x in result]

enumerate 函数将 number 的每个成员都变成数组,数组的第 0 个成员是序号,第 1 个成员是原来的数值。这样,lambda 函数才能用 x[0] 取得成员序号。过滤后还要把原来的数值拆出来,这个过程很绕,代码也繁琐。

SQL 基于无序集合,成员序号没有意义,这个问题又得绕路实现。

SPL 用 #表示当前成员的序号,用上述简单思路写出的代码非常简洁:

number.select(# % 2 ==0)

#和 ~ 一样,也是 SPL 循环函数中 lambda 函数的传入参数。

小结一下:

SQL 把二维数据表运算的 lambda 语法化于无形,用于描述常规结构化数据集合运算还是比较方便简捷的,但不能支持结构化数据以外的集合。Python 支持各种形式的集合,但支持 lambda 语法的函数不全,lambda 函数代码写起来也有些啰嗦。SPL 继承了 SQL 的所有优势,且对各种形式的集合都可以使用化于无形的 lambda 语法。SPL lambda 语法也多了 ~、# 等符号,是三者中最强的。