【问题标题】:Python: Why are int & list function parameters differently treated?Python:为什么 int 和 list 函数参数的处理方式不同?
【发布时间】:2015-01-04 01:10:10
【问题描述】:

我们都知道全局变量不好的教条。当我开始学习 python 时,我读到传递给函数的参数在函数中被视为局部变量。这似乎至少是事实的一半:

def f(i):
    print("Calling f(i)...")
    print("id(i): {}\n".format(id(i)))
    print("Inside f(): i += 1")
    i += 1
    print("id(i): {}".format(id(i)))
    return

i = 1
print("\nBefore function call...")
print("id(i): {}\n".format(id(i)))
f(i)

计算结果为:

Before function call...
id(i): 507107200

Calling f(i)...
id(i): 507107200

Inside f(): i += 1
id(i): 507107232

正如我现在所读到的,Python 中函数的调用机制是“通过对象引用调用”。这意味着参数最初是通过它的对象引用传递的,但是如果在函数内部对其进行了修改,则会创建一个new对象变量。避免函数无意修改全局变量的设计对我来说似乎是合理的。

但是如果我们将列表作为参数传递会发生什么?

def g(l):
    print("Calling f(l)...")
    print("id(l): {}\n".format(id(l)))
    print("Inside f(): l[0] += 1")
    l[0] += 1
    print("id(l): {}".format(id(l)))
    return

l = [1, 2, 3]
print("\nBefore function call...")
print("id(l): {}\n".format(id(l)))
g(l)

这会导致:

Before function call...
id(l): 120724616

Calling f(l)...
id(l): 120724616

Inside f(): l[0] += 1
id(l): 120724616

正如我们所见,对象引用保持不变!所以我们处理一个全局变量,不是吗?

我知道我们可以通过将列表的副本传递给函数来轻松克服这个问题:

g(l[:])

但我的问题是:在 Python 中实现两种不同的函数参数行为的原因是什么?如果我们打算操作一个全局变量,我们也可以像对待整数一样使用“全局”关键字来表示列表,不是吗?这种行为如何与python“显式胜于隐式”的禅相一致?

【问题讨论】:

标签: python function parameters globals


【解决方案1】:

Python 有两种类型的对象——可变的和不可变的。大多数内置类型,如 int、string 或 float,都是不可变的。这意味着他们无法改变。 list、dict 或 array 等类型是可变的,这意味着它们的状态可以更改。几乎所有用户定义的对象也是可变的。

当您执行i += 1 时,您为i 分配了一个新值,即i + 1。这不会以任何方式改变 i,它只是说它应该忘记 i 并将其替换为 i + 1 的值。然后i 被一个全新的对象取代。 但是当您在列表中执行i[0] += 1 时,您对列表说应该用i[0] + 1 替换元素0。这意味着 id(i[0]) 将被新对象更改,并且列表 i 的状态将更改,但它的身份保持不变 - 它是同一个对象,只是更改了。

请注意,在 Python 中,字符串并非如此,因为它们是不可变的,更改一个元素将复制具有更新值的字符串并创建新对象。

【讨论】:

  • x += y 确实分配给了x,但如果它适当地定义了__iadd__,它也可能改变先前引用的对象x。例如,列表就是这样做的。
  • "Python 有两种类型的对象——可变的和不可变的。"语言中没有“可变”或“不可变”的概念。 “可变”只是程序员对当一个方法有一个 API 供您修改其内容时的非正式描述。 “可变”和“不可变”在语言语义上没有区别。
【解决方案2】:

为什么 int 和 list 函数参数的处理方式不同?

他们不是。无论类型如何,所有参数都被同等对待。

您在这两种情况下看到了不同的行为因为您对l 做了不同的事情

首先,让我们将+= 简化为=+:在第一种情况下是l = l + 1,在第二种情况下是l[0] = l[0] + 1。 (+=并不总是等于赋值和+;它取决于左侧对象的运行时类,可以覆盖它;但这里,对于ints,它相当于一个赋值和+。)另外,赋值的右侧只是读取内容,并不有趣,所以我们暂时忽略它;所以你有:

l = something (in the first case)
l[0] = something (in the second case)

第二个是“分配给一个元素”,实际上是调用. __setitem__()方法的语法糖:

l.__setitem__(0, something)

所以现在你可以看到两者之间的区别了——

  • 在第一种情况下,您将分配给变量l。 Python 是按值传递的,因此这对外部代码没有影响。分配给变量只是使它指向一个新对象;它对它曾经指向的对象没有影响。如果您在第二种情况下为 l 分配了一些东西,它也不会对原始对象产生任何影响。
  • 在第二种情况下,您正在调用l 指向的对象上的方法。此方法恰好是列表上的变异方法,因此修改了列表对象的内容,原始列表对象是一个指向该方法的指针。确实,int(在第一种情况下是 l 的运行时类)恰好没有发生变异的方法,但这不是重点。

如果您在两种情况下都对l 做了同样的事情(如果可能的话),那么您可以期待相同的语义。

【讨论】:

    【解决方案3】:

    这在很多语言中都很常见(例如 Ruby)。

    变量本身的作用域是函数。但该变量只是一个指向内存中某处浮动对象的指针——那个对象是可以更改的。

    【讨论】:

      【解决方案4】:

      在 Python 中,一切都是对象,因此一切都由引用表示。 Python 中变量最值得注意的是它们包含对对象的引用,而不是对象本身。现在,当参数传递给函数时,它们是通过引用传递的。因此,在函数范围内,每个参数都分配给参数的引用,然后在函数内部被视为局部变量。当您为参数分配新值时,您正在更改它所引用的对象,因此您有一个新对象并且对它的任何更改(即使它是可变对象)都不会在函数范围之外看到问题,并且与传递的参数无关。也就是说,当您不为参数分配新引用时,它会保持参数的引用,并且对它的任何更改(当且仅当它是可变的)都将在函数范围之外看到。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-11-10
        • 1970-01-01
        • 1970-01-01
        • 2016-03-03
        • 2011-02-14
        • 2017-11-12
        • 2022-07-20
        • 1970-01-01
        相关资源
        最近更新 更多