Python Pandas 数据框架,是传值还是传引用?

152
如果我将一个数据框传递给一个函数,并在函数内修改它,它是按值传递还是按引用传递?
我运行以下代码
a = pd.DataFrame({'a':[1,2], 'b':[3,4]})
def letgo(df):
    df = df.drop('b',axis=1)
letgo(a)

a 在函数调用后不会改变其值。这是否意味着它是按值传递?

我也尝试了以下内容

xx = np.array([[1,2], [3,4]])
def letgo2(x):
    x[1,1] = 100
def letgo3(x):
    x = np.array([[3,3],[3,3]])

原来letgo2()确实会改变xx,而letgo3()则不会。为什么会这样呢?


7个回答

176

简而言之,Python始终按值传递,但每个Python变量实际上都是指向某个对象的指针,因此有时它看起来像是按引用传递。

在Python中,每个对象都是可变或不可变的。例如,列表、字典、模块和Pandas数据帧是可变的,而整数、字符串和元组是不可变的。可变对象可以在内部更改(例如向列表添加元素),但不可变对象不能。

正如我在开头所说,您可以将每个Python变量视为指向对象的指针。当您将变量传递给函数时,函数内的变量(指针)始终是传入的变量(指针)的副本。因此,如果您将新内容分配给内部变量,您所做的只是更改局部变量以指向不同的对象。这不会改变指针所指向的原始对象,也不会使外部变量指向新对象。此时,外部变量仍然指向原始对象,但内部变量指向新对象。

如果要更改原始对象(仅限于可变数据类型),则必须执行一些更改对象的操作,而不是为局部变量分配完全新的值。这就是为什么letgo()letgo3()不会更改外部项,而letgo2()会更改它。

正如@ursan指出的那样,如果letgo()使用类似这样的东西,那么它将更改df指向的原始对象,这将更改通过全局变量a看到的值:

def letgo(df):
    df.drop('b', axis=1, inplace=True)

a = pd.DataFrame({'a':[1,2], 'b':[3,4]})
letgo(a)  # will alter a
在某些情况下,您可以完全清空原始变量并重新填充新数据,而无需进行直接赋值,例如这将更改v指向的原始对象,这将在以后使用v时更改所看到的数据。
def letgo3(x):
    x[:] = np.array([[3,3],[3,3]])

v = np.empty((2, 2))
letgo3(v)   # will alter v

请注意,我并没有直接将某个值分配给x;而是将某个值分配给x的整个内部范围。

如果您确实必须创建一个全新的对象,并使其在外部可见(这在pandas中有时是必需的),您有两个选择。"干净"的选择是只返回新对象,比如:

def letgo(df):
    df = df.drop('b',axis=1)
    return df

a = pd.DataFrame({'a':[1,2], 'b':[3,4]})
a = letgo(a)

另一种选择是在函数之外直接修改全局变量。这将更改a指向一个新对象,任何后续引用a的函数都将看到这个新对象:

def letgo():
    global a
    a = a.drop('b',axis=1)

a = pd.DataFrame({'a':[1,2], 'b':[3,4]})
letgo()   # will alter a!

直接修改全局变量通常是一个不好的主意,因为阅读你的代码的人会很难弄清楚 a 是如何被改变的。(我通常使用全局变量来共享多个函数中使用的参数,但我不允许它们修改这些全局变量。)


15

补充 @Mike Graham 的回答,他提供了一篇非常好的阅读材料:

在你的情况下,需要记住的是 名称 之间的区别。 adfxxx 都是 名称,但它们在不同的示例点引用相同或不同的

  • 在第一个示例中,letgo 重新绑定 df 到另一个值,因为 df.drop 返回一个新的 DataFrame,除非设置参数 inplace = True (参见文档)。这意味着名称 df (在 letgo 函数中的本地名称) 原本引用的值是 a 的值,现在它引用的是一个新值,即 df.drop 的返回值。值 a 引用的仍然存在,没有改变。

  • 在第二个示例中,letgo2 更改 x 的值,而不重新绑定它,这就是为什么 xxletgo2 修改的原因。与前一个示例不同,这里的本地名称 x 总是引用名称 xx 引用的值,并在原地 更改 该值,这就是为什么值 xx 引用的值已经更改的原因。

  • 在第三个例子中,letgo3 重新绑定 x 到一个新的 np.array 上。这导致了名字 xletgo3 中变成本地变量,并且之前引用的是 xx 的值,现在它指向另一个值,也就是新的 np.array。而变量 xx 引用的值并没有改变。


  • 13
    The question isn't PBV vs. PBR. These names only cause confusion in a language like Python; they were invented for languages that work like C or like Fortran (as the quintessential PBV and PBR languages). It is true, but not enlightening, that Python always passes by value. The question here is whether the value itself is mutated or whether you get a new value. Pandas usually errs on the side of the latter. http://nedbatchelder.com/text/names.html解释了Python的名称系统。

    1
    Python中传递和赋值的语义与Java完全相同,你所说的同样适用于Java。然而,在StackOverflow和互联网上,每当这个问题出现时,人们似乎会觉得强调Java始终是按值传递是“启迪人心的”。 - newacct

    7

    Python既不是传值也不是传引用,而是赋值传递。

    支持参考,Python FAQ: https://docs.python.org/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference

    简单来说:

    1. 如果你传递的是一个不可变对象,对它所做的修改不会改变调用者中的值 - 因为你重新绑定了名称到新对象。
    2. 如果你传递的是一个可变对象,在被调用的函数中所做的更改也会改变调用者中的值,只要你不重新绑定该名称到新对象。如果你重新分配变量,并创建一个新对象,那么这个更改和后续更改将不会在调用者中看到。

    所以,如果你传递了一个列表,并改变了它的第0个值,那么这个更改在被调用者和调用者中都可以看到。但如果你用一个新列表重新赋值这个列表,这个更改就会丢失。但是,如果你切片列表并用一个新列表替换它,那么这个更改在被调用者和调用者中都可以看到。

    例如:

    def change_it(list_):
        # This change would be seen in the caller if we left it alone
        list_[0] = 28
    
        # This change is also seen in the caller, and replaces the above
        # change
        list_[:] = [1, 2]
    
        # This change is not seen in the caller.
        # If this were pass by reference, this change too would be seen in
        # caller.
        list_ = [3, 4]
    
    thing = [10, 20]
    change_it(thing)
    # here, thing is [1, 2]
    

    如果您是C语言的粉丝,可以将其视为通过值传递指针 - 不是指向指针的指针,而是指向值的指针。
    希望对您有所帮助。

    必须喜欢 Stack Exchange - Dan

    2

    这里是有关 drop 的文档:

    返回已删除所请求轴标签的新对象。

    因此,将创建一个新的数据帧。原始数据帧未更改。

    但对于 Python 中的所有对象,数据帧通过引用传递给函数。


    但是我在函数内部将其分配给“df”,这不意味着引用值已更改为新对象吗? - nos
    在其他作用域中,将一个值赋给局部变量名不会改变其绑定的对象。 - Mike Graham

    2

    简短回答:

    • 按值传递: df2 = df.copy()
    • 按引用传递: df2 = df

    0

    你需要在函数开始时将 'a' 声明为全局变量,否则它将成为局部变量并不会改变主代码中的 'a'。


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