Python Numpy和内存效率(按引用传递 vs. 按值传递)

17

最近我在做it技术方面的工作时,越来越多地使用python代替c/c++,因为用它可以将我的编码时间缩短几倍。但是当我处理大量数据时,我的python程序运行速度开始变得比c慢很多。我想知道这是否是因为我没有有效地使用大对象/数组。是否有一份全面的指南,介绍numpy/python如何处理内存?什么情况下是按引用传递,什么情况下是按值传递,什么类型是可变的,什么类型是不可变的,以及什么情况下会进行复制操作等信息。


10
“几倍因子”是我与非技术人员谈论为何应该转用Python的新技术术语。 - BlackVegetable
4
这篇帖子包含大量与此问题相关的数据...... - jdero
1
@BlackVegetable 正确。忽略jdero所说的。虽然原始类型(仅限原始类型)真正按值传递,但几乎不可检测,您可以将其视为优化。 - user395760
1
假设您不知道某个东西是否可变,但您想出了办法来确定它。那么您要如何进行变异呢?如果您通过找到特定接口来确定它是可变的,那么这实际上并不是在询问可变性 - 这是一种鸭子类型检查。如果您所学的只是它是可变的,那么您仍然不知道该怎么做。 - user2357112
1
一个合理的概述是Python C API参考或者使用C扩展Python。如果你要编写Numpy,你可以使用这个参考作为起点。有一些具体的例子,从C的角度来看,是按引用调用还是按值调用。绝大多数情况(从C的角度来看)都是按引用调用。从Python的角度来看——这并不重要。 - dawg
显示剩余6条评论
2个回答

15

在Python(和大多数主流语言)中,对象作为引用传递。

以NumPy为例,通过索引现有数组创建的“新”数组仅是原始数组的视图。例如:

import numpy as np

>>> vec_1 = np.array([range(10)])
>>> vec_1
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> vec_2 = vec_1[3:] # let vec_2 be vec_1 from the third element untill the end
>>> vec_2
array([3, 4, 5, 6, 7, 8, 9])
>>> vec_2[3] = 10000
array([3, 4, 5, 10000, 7, 8, 9])
>>> vec_1
array([0, 1, 2, 3, 4, 5, 10000, 7, 8, 9])

Numpy有一个方便的方法来帮助您解决问题,称为may_share_memory(obj1, obj2)。所以:

>>> np.may_share_memory(vec_1, vec_2)
True

请小心,因为该方法有可能返回错误的结果(尽管我从未看到过)。

在2013年的SciPy会议上,有一个关于numpy的教程 (http://conference.scipy.org/scipy2013/tutorial_detail.php?id=100)。最后,讲师谈论了一下numpy如何处理内存。建议大家观看。

通常情况下,对象通常不会默认按值传递。即使是封装在另一个对象中的对象也是如此。这里有一个列表演示的例子:

Class SomeClass():

    def __init__(a_list):
        self.inside_list = a_list

    def get_list(self):
        return self.inside_list

>>> original_list = range(5)
>>> original_list
[0,1,2,3,4]
>>> my_object = SomeClass(original_list)
>>> output_list = my_object.get_list()
>>> output_list
[0,1,2,3,4]
>>> output_list[4] = 10000
>>> output_list
[0,1,2,3,10000]
>>> my_object.original_list
[0,1,2,3,10000]
>>> original_list
[0,1,2,3,10000]

有点毛骨悚然,对吗? 使用赋值符号(“=”),或在函数末尾返回一个对象,你将始终创建指向该对象或其部分的指针。只有在显式复制对象时,如使用类似于some_dict.copy或array[:]的复制方法时,才会复制对象。对象只有在你明确要求复制时才会被复制。例如:

>>> original_list = range(5)
>>> original_list
[0,1,2,3,4]
>>> my_object = SomeClass(original_list[:])
>>> output_list = my_object.get_list()
>>> output_list
[0,1,2,3,4]
>>> output_list[4] = 10000
>>> output_list
[0,1,2,3,10000]
>>> my_object.original_list
[0,1,2,3,10000]
>>> original_list
[0,1,2,3,4]

理解了吗?

我认为在你最后的例子中,my_object.original_list 应该改为 my_object.get_list()。另外,在你的第一个例子中,当给它们赋值时,你可能想说明一下 vec2[:] 的行为与 vec2 相比如何。 - kon psych
我认为[:]仅适用于列表的复制。如果原始列表是一个数组,它将不会被复制。 - R Zhang

2

所以我将引用EOL的话,因为我认为他的回答非常相关:

3)最后一点与问题标题有关:“按值传递”和“按引用传递”不是Python中相关的概念。相反,相关的概念是“可变对象”和“不可变对象”。列表是可变的,而数字则不是,这解释了您观察到的情况。此外,您的Person1和bar1对象是可变的(这就是为什么可以更改人的年龄)。您可以在文本教程和视频教程中找到有关这些概念的更多信息。维基百科也提供了一些(更技术性的)信息。一个示例说明了可变和不可变之间行为差异的区别- EOL的回答

总的来说,我发现Numpy/Scipy遵循这些规则;更重要的是,它们在文档中明确告诉您正在发生什么。

例如,np.random.shuffle 要求输入一个数组并返回 None,而 np.random.permutation 返回一个数组。在这里,您可以清楚地看到哪个返回值,哪个不返回值。
类似地,数组具有传递引用语义,在一般情况下我发现 Numpy/Scipy 非常高效。
我认为可以公正地说,如果使用 pass-by-reference 更快,他们就会使用它。只要按照文档中的方式使用函数,您不应该在速度方面遇到重大问题。
你是在问特定的类型吗?

谢谢你的回答。不,我并没有考虑特定的类型;我更想知道关于计算效率的一般最佳编码风格。我认为这在Python中可能不存在,除了相信numpy/scipy方法已经被优化过了。 - DilithiumMatrix
总的来说,Python 是为了方便而不是速度而设计的 : )。但是,您可以使用 C 编写想要实现快速运行的部分,并在 Python 中调用它们,或者像您所说的那样使用 numpy/scipy。此外,针对您特定的构建编译 numpy/scipy 可以帮助进一步优化它们! - Eiyrioü von Kauyf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接