我如何摆脱“循环”思想流派?


79

这是一个相当概念性的问题,但是我希望我能对此得到一些好的建议。我做的很多编程工作都是使用(NumPy)数组。我经常必须匹配大小不同的两个或多个数组中的项,而我要去的第一件事是一个for循环,甚至更糟的是嵌套的for循环。我想尽可能避免for循环,因为它们很慢(至少在Python中如此)。

我知道NumPy的很多功能都需要我研究一些预定义的命令,但是当您必须迭代某些内容时,您(作为经验丰富的程序员)是否会想到一个一般的思考过程?

所以我经常有这样的事情,这很糟糕,我想避免它:

small_array = np.array(["one", "two"])
big_array = np.array(["one", "two", "three", "one"])

for i in range(len(small_array)):
    for p in range(len(big_array)):
        if small_array[i] == big_array[p]:
            print "This item is matched: ", small_array[i]

我知道有多种方法可以实现这一目标,但是我对通用的思维方法(如果存在)感兴趣。


10
您正在寻找函数式编程:lambda表达式,高阶函数,生成表达式等。
Kilian Foth,2014年

42
I want to avoid for-loops as much as possible because they are slow (at least in Python).听起来您在这里解决了错误的问题。如果需要迭代某些事物,则需要迭代某些事物。无论您使用哪种Python构造,都会对性能产生类似的影响。如果您的代码很慢,那不是因为您有for循环。这是因为您在做不必要的工作,或者在Python方面可以在C方面完成。在您的示例中,您正在做额外的工作;您可以用一个循环而不是两个循环来完成。
2014年

24
@Doval不幸的是- 在NumPy中。对于实际的数组大小,执行元素逐加的Python for循环很容易比矢量化NumPy运算符(不仅用C编写,而且使用SSE指令和其他技巧)慢几倍(!)。

46
上面的一些评论似乎误解了这个问题。在NumPy中进行编程时,如果可以对计算进行矢量化,则将获得最佳结果—也就是说,用NumPy中的全数组操作替换Python中的显式循环。从概念上讲,这与Python中的普通编程有很大不同,并且需要花费一些时间来学习。因此,我认为OP寻求关于如何学习的建议是合理的。
加雷斯·里斯2014年

3
@PieterB:是的,是的。“向量化”与“选择最佳算法”不同。它们是提出有效实施方案的两个困难源,因此最好一次考虑一下。
加雷斯·里斯2014年

Answers:


89

当学习有效使用NumPy时,这是一个常见的概念难题。通常,Python中的数据处理最好用迭代器来表示,以保持较低的内存使用量,最大程度地提高与I / O系统并行的机会,并提供算法的重用和组合。

但是NumPy将所有事情都彻底搞清楚了:最好的方法是将算法表示为全数组操作序列,以最小化在慢速Python解释器中花费的时间,并最大化在快速编译的NumPy例程中花费的时间。

这是我采用的一般方法:

  1. 保留该函数的原始版本(您确信该函数是正确的),以便可以针对改进的版本对它的正确性和速度进行测试。

  2. 由内而外地工作:即从最里面的循环开始,看是否可以向量化;然后,完成此操作后,移出一个级别并继续。

  3. 花大量时间阅读NumPy文档。那里有很多功能和操作,而且它们的名称并不总是很漂亮,因此有必要了解它们。尤其是,如果您发现自己在想:“如果只有某项功能如此某某地完成”,那么花十分钟寻找它就值得了。通常在某个地方。

练习无可替代,因此,我将举例说明一些问题。每个问题的目标是重写函数,使其完全矢量化:也就是说,它由整个数组上的一系列NumPy操作组成,没有本地Python循环(没有forwhile语句,没有迭代器或理解)。

问题1

def sumproducts(x, y):
    """Return the sum of x[i] * y[j] for all pairs of indices i, j.

    >>> sumproducts(np.arange(3000), np.arange(3000))
    20236502250000

    """
    result = 0
    for i in range(len(x)):
        for j in range(len(y)):
            result += x[i] * y[j]
    return result

问题2

def countlower(x, y):
    """Return the number of pairs i, j such that x[i] < y[j].

    >>> countlower(np.arange(0, 200, 2), np.arange(40, 140))
    4500

    """
    result = 0
    for i in range(len(x)):
        for j in range(len(y)):
            if x[i] < y[j]:
                result += 1
    return result

问题3

def cleanup(x, missing=-1, value=0):
    """Return an array that's the same as x, except that where x ==
    missing, it has value instead.

    >>> cleanup(np.arange(-3, 3), value=10)
    ... # doctest: +NORMALIZE_WHITESPACE
    array([-3, -2, 10, 0, 1, 2])

    """
    result = []
    for i in range(len(x)):
        if x[i] == missing:
            result.append(value)
        else:
            result.append(x[i])
    return np.array(result)

扰流板如下。如果您在研究我的解决方案之前一意孤行,将会获得最好的结果!

答案1

np.sum(x)* np.sum(y)

答案2

np.sum(np.searchsorted(np.sort(x),y​​))

答案3

np.where(x ==缺失,值,x)


等等,最后一个答案是否有错字?还是NumPy修改了Python解释代码的方式?
Izkata 2014年

1
@Izkata本身并不会进行任何修改,但是定义了应用于数组的逻辑操作以返回布尔数组。
sapi 2014年

@sapi啊,我错过了doctest中发生的事情,以为那很简单list
Izkata 2014年

也许应该有一种嵌入APL的方法?

我爱你做功课。
Koray Tugay

8

为了使事情更快,您必须仔细阅读数据结构并使用适当的数据结构。

对于小数组和大数组的非平凡大小(假设small = 100元素和big = 10.000元素),一种方法是对小数组进行排序,然后遍历大数组并使用二进制搜索来找到匹配的元素在小阵列中。

这将使最大时间复杂度为O(N log N)(对于小的小型数组和非常大的大型数组,它更接近O(N)),其中嵌套循环解为O(N ^ 2)

然而。哪种数据结构最有效,很大程度上取决于实际问题。


-3

您可以使用字典来显着优化性能

这是另一个示例:

locations = {}
for i in range(len(airports)):
    locations[airports["abb"][i][1:-1]] = (airports["height"][i], airports["width"][i])

for i in range(len(uniqueData)):
    h, w = locations[uniqueData["dept_apt"][i]]
    uniqueData["dept_apt_height"][i] = h
    uniqueData["dept_apt_width"][i] = w

2
显然,这仍在使用“ for-loop”思想流派。
8bittree '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.