手记

诡异的 list comprehension 展开

问题描述

存在一些文件名类似 2016-07-16_161.res 2016-07-18_161.res 的文件,文件内容如下:

less 2016-07-16_161.res
1.34.0.68
1.34.1.37
1.34.1.121
1.34.5.87
1.34.5.182
1.34.6.72
1.34.6.245
1.34.9.149
1.34.11.74
1.34.13.161
...

希望通过 Spark 统计出存活超过 n 天的 IP,并可以同时看到该 IP 分别在哪天存活。

因此需要先将这些文件转换成 Pair RDD,其中的每一项以 IP 为 key,以日期为 value。

编写 Spark 脚本如下:

rdds = [sc.textFile(f).map(lambda x: (x, f.split('_')[0])) for f in glob('*.res')]

预期得到结果:

rdds[0].first()
(u'210.240.117.126', '2016-07-16')

rdds[1].first()
(u'210.240.117.126', '2016-07-18')

但实际得到的结果为:

rdds[0].first()
(u'210.240.117.126', '2016-07-18')

rdds[1].first()
(u'210.240.117.126', '2016-07-18')

即所有的 RDD 中每一项的 value 都为同一值。

问题定位

看到这个现象基本把可能出问题的点锁定在了 map(lambda x: (x, f.split('_')[0])) 附近。

查找了 pyspark 中 map 函数的实现,也没发现有什么不妥的地方。rdd.py 中 RDD::map() 的实现如下:

    def map(self, f, preservesPartitioning=False):
        """
        Return a new RDD by applying a function to each element of this RDD.
        >>> rdd = sc.parallelize(["b", "a", "c"])
        >>> sorted(rdd.map(lambda x: (x, 1)).collect())
        [('a', 1), ('b', 1), ('c', 1)]
        """
        def func(_, iterator):
            return map(f, iterator)        return self.mapPartitionsWithIndex(func, preservesPartitioning)

另一个值得怀疑的点就是 lambda 的行为是不是真的符合预期,但是一直只是怀疑,没有找到有效的方法来验证自己的猜测。直到看了 @张通 转发的 《Python 中的 lambda 和「真正的」lambda 有什么区别?》[1],才确认并搞清楚问题发生的根本原因。

问题发生的根本原因在于 Python 中的 lambda 在实现上存在缺陷[2],导致 Spark 脚本中传入 map 函数的 lambda 表达式共享了同一个变量 f,从而导致了上述问题的发生。

举一反三

目前大概可以确定,Python 实现的 lambda 表达式中的变量可能并不像正常的函数那样具有独立的作用域。

以下述代码为例:

test = [lambda x: x+i for i in range(10)]print test[0](1)print test[9](1)

这段代码并不会如预期的输出 110,而是会输出 1010。使用 dis 模块分析列表中的任意一个 lambda 表达式得到如下结果。

In [3]: dis.dis(test[0])  1           0 LOAD_FAST                0 (x)              3 LOAD_GLOBAL              0 (i)              6 BINARY_ADD              7 RETURN_VALUE

从上述 Python bytecode 中可以看出 lambda 表达式中的变量 i 的确没有一个独立的作用域,而是使用了相对全局的作用域,而此时该作用域中的变量 i 已经变成了 9,因此得到了上述结果。

更近一步,list comprehension 中的变量的作用域又是怎样的呢?是仅仅作用于 list comprehension 内部,还是也会影响到外部呢?

实测代码如下:

In [13]: p = 100In [14]: a = [p for p in range(10)]

In [15]: print a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [16]: print p9

可见 list comprehension 中的变量也会对其外的变量产生影响,即 list comprehension 中的变量也不具有独立的作用域。所以,虽然 list comprehension 具有执行效率高和可读性强等优点,在实际的编码中也需要多注意这些副作用,防止被坑。

问题解决

下面两个方法均可解决该问题:

def gen_rdd(f):
    return sc.textFile(f).map(lambda x: (x, f.split('_')[0]))
    
rdds = [gen_rdd(f) for f in glob('*.res')]
rdds = map(    lambda f: sc.textFile(f).map(lambda x: (x, f.split('_')[0])), glob('*.res')
)

推荐使用第二种方式。



作者:DragonKid
链接:https://www.jianshu.com/p/17926082e1a3

0人推荐
随时随地看视频
慕课网APP