Python为什么不是传值或传引用? - mathspp


本文解释了为什么 Python 不使用传值系统,也不使用传引用。
当你在 Python 中调用一个函数并给它一些参数时......它们是按值传递的吗?不!引用?不!他们是通过分配赋值assignment传递的。
许多传统的编程语言在向函数传递参数时采用以下两种模型之一:

  • 一些语言使用传值模型;和
  • 大多数其他人使用传递引用模型。

话虽如此,了解 Python 使用的模型很重要,因为这会影响您的代码的行为方式。
在这个 Pydon't 中,您将:
  • 看到 Python 不使用传递值也不使用传递引用模型;
  • 了解 Python 使用传递赋值模型;
  • 了解内置函数id;
  • 更好地理解 Python 对象模型;
  • 意识到每个对象都有 3 个非常重要的属性来定义它;
  • 了解可变对象和不可变对象之间的区别;
  • 了解浅拷贝和深拷贝的区别;和
  • 了解如何使用该模块copy进行两种类型的对象复制。

 
Python 是按值传递的吗?
在传值模型中,当您使用一组参数调用函数时,数据会被复制到函数中。这意味着您可以随心所欲地修改参数,并且您将无法在函数之外更改程序的状态。这不是 Python 所做的,Python 不使用值传递模型。
查看下面的代码片段,它可能看起来像 Python 使用按值传递:
def foo(x):
    x = 4

a = 3
foo(a)
print(a)
# 3

这看起来像传值模型,因为我们给了它 3,然后将其更改为 4,但是更改没有反映在外部(结果a仍然是 3)。
但是,实际上,Python 并没有将数据复制到函数中。
为了证明这一点,我将向您展示一个不同的函数:

def clearly_not_pass_by_value(my_list):
    my_list[0] = 42

l = [1, 2, 3]
clearly_not_pass_by_value(l)
print(l)
# [42, 2, 3]

正如我们所见,l列表的值,在函数clearly_not_pass_by_value调用以后发生了变化。因此,Python 不使用值传递模型。
 
Python 是按引用传递的吗?
在真正的引用传递模型中,被调用函数可以访问被调用者的变量!有时,它看起来像是 Python 所做的,但 Python 不使用传递引用模型。
我会尽力解释为什么这不是 Python 所做的:

def not_pass_by_reference(my_list):
    my_list = [42, 73, 0]

l = [1, 2, 3]
not_pass_by_reference(l)
print(l)
# [1, 2, 3]

如果 Python 使用传递引用模型,该函数将会完全改变l 函数外部的值,但正如我们所见,事实并非如此。
 
Python对象模型
要真正理解 Python 在调用函数时的行为方式,最好先了解 Python 对象是什么,以及如何表征它们。
在 Python 中,一切都是对象,每个对象都有三个特点:

  • 它的身份(唯一标识对象的整数,就像社会安全号码标识人一样);
  • 类型(标识您可以对对象执行的操作);和
  • 对象的内容。

这是一个对象及其三个特征:

>>> id(obj)
2698212637504       # the identity身份 of `obj`
>>> type(obj)
<class 'list'>      # the type类型 of `obj`
>>> obj
[1, 2, 3]           # the contents内容 of `obj`

对象的可变性取决于它的类型。换句话说,可变性是类型的特征,而不是特定对象的特征!
但是,对象可变究竟意味着什么呢?或者一个对象是不可变的?
回想一下,对象的特征在于其身份、类型和内容。如果您可以更改其对象的内容而不更改其标识和类型,则类型是可变的。
列表是可变数据类型的一个很好的例子。为什么?因为列表是容器:您可以将内容放入列表中,也可以从相同列表中删除内容。
在下面,您可以看到列表的内容如何在obj我们进行方法调用时发生变化,但列表的标识保持不变:

>>> obj = []
>>> id(obj)
2287844221184
>>> obj.append(0); obj.extend([1, 2, 3]); obj
[42, 0, 1, 2, 3]
>>> id(obj)
2287844221184
>>> obj.pop(0); obj.pop(0); obj.pop(); obj
42
0
3
[1, 2]
>>> id(obj)
2287844221184

然而,当处理不可变对象时,情况就完全不同了。如果我们查一下英语词典,我们会得到“immutable”的定义:immutable——随时间不变或无法改变。
不可变对象的内容永远不会改变。以字符串为例:
obj = "Hello, world!"
字符串是本次讨论的一个很好的例子,因为有时它们看起来是可变的。但他们不是!
一个对象是不可变的一个很好的指标是它的所有方法都返回一些东西,例如如果.append方法在列表上使用,则不会获得返回值。另一方面,无论您在字符串上使用什么方法,结果都会返回给您:

>>> [].append(0)    # No return.
>>> obj.upper()     # A string is returned.
'HELLO, WORLD!"

请注意如何obj没有自动更新为"HELLO, WORLD!". 相反,新字符串已创建并返回给您。
字符串不可变这一事实的另一个重要提示是您不能为其分配索引:
>>> obj[0]
'H'
>>> obj[0] = "h"
Traceback (most recent call last):
  File
"<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

这表明,当一个字符串被创建时,它保持不变。它可用于构建其他字符串,但字符串本身总是如此不变。
int、float、bool、str、tuple和complex是最常见的不可变对象类型; list、set和dict是最常见的可变对象类型。
 
变量名作为标签
另一个需要理解的重要事情是变量名与对象本身几乎没有关系。
实际上,名称obj只是我决定附加到标识为 2698212637504、具有列表类型和内容 1、2、3 的对象的标签。
就像我obj给那个对象贴上标签一样,我可以给它贴上更多的名字:
>>> foo = bar = baz = obj
同样,这些名称只是标签。我决定贴在同一个物体上的标签。我们怎么知道它是同一个对象?好吧,他们所有的“社会安全号码”(id)都匹配,所以他们必须是同一个对象:

>>> id(foo)
2698212637504
>>> id(bar)
2698212637504
>>> id(baz)
2698212637504
>>> id(obj)
2698212637504

因此,我们得出结论,foo、bar、baz、 和obj是所有引用同一个对象的变量名。
 
操作符is
它检查两个对象是否相同。
要使两个对象相同,它们必须具有相同的标识:

>>> foo is obj
True
>>> bar is foo
True
>>> obj is foo
True

作为昵称的分配
分配变量就像给某人一个新的昵称。我的中学朋友叫我“罗杰”。我的大学朋友叫我“Girão”。我不熟悉的人用我的名字称呼我——“罗德里戈”。然而,不管他们叫我什么,我还是我,对吧?
如果有一天我决定改变我的发型,每个人都会看到新发型,不管他们叫我什么!
以类似的方式,如果我修改对象的内容,我可以使用我喜欢的任何昵称来查看这些更改发生的情况。例如,我们可以更改我们一直在玩的列表的中间元素:

>>> foo[1] = 42
>>> bar
[1, 42, 3]
>>> baz
[1, 42, 3]
>>> obj
[1, 42, 3]

我们使用昵称foo来修改中间元素,但该更改也可以从所有其他昵称中看到。
为什么?
因为它们都指向同一个列表对象。
 
Python 是按分配赋值传递的
完成所有这些之后,我们现在准备了解 Python 如何将参数传递给函数。
当我们调用一个函数时,函数的每个参数都被分配给传入的对象。本质上,现在每个参数都成为了传入对象的新昵称。

  • 不可变参数

如果我们传入不可变的参数,那么我们就无法修改参数本身。毕竟,这就是不可变的意思:“不会改变”。
这就是为什么它看起来像 Python 使用按值传递模型的原因。因为我们可以让参数保存其他东西的唯一方法是将它分配给一个完全不同的东西。当我们这样做时,我们对不同的对象重复使用相同的昵称:
def foo(bar):
    bar = 3
    return bar

foo(5)

在上面的例子中,当我们使用参数5调用foo时,就好像我们在函数的开头bar = 5做的一样。
紧接着,我们有了bar = 3. 这意味着“取昵称“bar”并将其指向整数3“。Python 并不关心bar,因为昵称(作为变量名)已经被使用了。它现在指向那个3!

  • 可变参数

另一方面,可以更改可变参数。我们可以修改它们的内部内容。可变对象的一个​​主要例子是列表:它的元素可以改变(长度也可以)。
这就是为什么它看起来像 Python 使用传递引用模型的原因。然而,当我们改变一个对象的内容时,我们并没有改变这个对象本身的身份。同样,当你改变你的发型和你的衣服,你的社会安全号码并没有改变:
>>> l = [42, 73, 0]
>>> id(l)
3098903876352
>>> l[0] = -1
>>> l.append(37)
>>> id(l)
3098903876352