据我所知,Python 中的变量只是指针。
基于此规则,我可以假设此代码段的结果:
i = 5
j = i
j = 3
print(i)
将是3
。
但我得到了一个意想不到的结果,它是5
。
此外,我的 Python 书确实涵盖了这个例子:
i = [1,2,3]
j = i
i[0] = 5
print(j)
结果将是[5,2,3]
。
我理解错了什么?
据我所知,Python 中的变量只是指针。
基于此规则,我可以假设此代码段的结果:
i = 5
j = i
j = 3
print(i)
将是3
。
但我得到了一个意想不到的结果,它是5
。
此外,我的 Python 书确实涵盖了这个例子:
i = [1,2,3]
j = i
i[0] = 5
print(j)
结果将是[5,2,3]
。
我理解错了什么?
我们称它们为参考。他们像这样工作
i = 5 # create int(5) instance, bind it to i
j = i # bind j to the same int as i
j = 3 # create int(3) instance, bind it to j
print i # i still bound to the int(5), j bound to the int(3)
小整数被实习,但这对这个解释并不重要
i = [1,2,3] # create the list instance, and bind it to i
j = i # bind j to the same list as i
i[0] = 5 # change the first item of i
print j # j is still bound to the same list as i
变量不是指针。当您分配给一个变量时,您将名称绑定到一个对象。从那时起,您可以通过使用名称来引用对象,直到该名称被重新绑定。
在您的第一个示例中,名称i
绑定到 value 5
。将不同的值绑定到名称j
上没有任何影响i
,因此当您稍后打印该值时,i
该值的值仍然是5
.
在第二个示例中,您将i
和绑定j
到同一个列表对象。当您修改列表的内容时,无论您使用哪个名称来引用列表,您都可以看到更改。
请注意,如果您说“两个列表都已更改”,那将是不正确的。只有一个列表,但它有两个引用它的名称 (i
和)。j
相关文档
从文档:
名称指的是对象。名称由名称绑定操作引入。程序文本中每次出现的名称指的是在包含使用的最内层功能块中建立的该名称的绑定。
当你这样做
i = 5
j = i
这和这样做是一样的:
i = 5
j = 5
j
不指向i
,并且在分配之后,j
不知道i
存在。 j
简单地绑定到i
分配时指向的任何内容。
如果你在同一行做作业,它看起来像这样:
i = j = 5
结果将完全相同。
因此,后来做
i = 3
不会改变j
指向的东西——你可以交换它——j = 3
不会改变i
指向的东西。
所以当你这样做时:
i = [1,2,3]
j = i
这和这样做是一样的:
i = j = [1,2,3]
所以i
两者j
都指向同一个列表。然后你的例子改变了列表:
i[0] = 5
Python 列表是可变对象,因此当您从一个引用更改列表并从另一个引用查看它时,您会看到相同的结果,因为它是同一个列表。
TLDR:Python名称的工作方式类似于具有自动取消/引用的指针,但不允许显式指针操作。其他目标表示间接,其行为类似于指针。
Python 语言规范没有定义名称等实际上是什么,只定义了它们的行为方式。但是,可以使用指针来解释该行为。
CPython 实现在底层使用类型指针PyObject*
。因此,可以将名称语义转换为指针操作。关键是将名称与实际对象分开。
示例 Python 代码包括名称 ( i
) 和对象 ( 5
)。
i = 5 # name `i` refers to object `5`
j = i # ???
j = 3 # name `j` refers to object `3`
这可以粗略地翻译成具有单独名称和对象的 C 代码。
int three=3, five=5; // objects
int *i, *j; // names
i = &five; // name `i` refers to position of object `5`
j = i; // name `j` refers to referent of `i`
j = &three; // name `j` refers to position of object `3`
重要的部分是“名称作为指针”不存储对象!我们没有定义*i = five
,但是i = &five
。名称和对象彼此独立存在。
名称仅指向内存中的现有对象。
从名称分配到名称时,不交换任何对象!当我们定义j = i
时,这等价于j = &five
。既不i
也不j
与对方相连。
+- name i -+ -\
\
--> + <five> -+
/ | 5 |
+- name j -+ -/ +----------+
因此,更改一个名称的目标不会影响另一个名称。它只更新该特定名称所指向的内容。
Python 还有其他种类的名称元素:属性引用 ( i.j
)、订阅 ( i[j]
) 和切片 ( i[:j]
)。与直接引用对象的名称不同,所有三个都间接引用对象的元素。
示例代码包括名称 ( i
) 和订阅 ( i[0]
)。
i = [1,2,3] # name `i` refers to object `[1, 2, 3]`
j = i # name `j` refers to referent of `i`
i[0] = 5 # ???
CPython 在底层list
使用 CPyObject*
指针数组。这可以再次粗略地转换为具有单独名称和对象的 C 代码。
typedef struct{
int *elements[3];
} list; // length 3 `list` type
int one = 1, two = 2, three = 3, five = 5;
list values = {&one, &two, &three}; // objects
list *i, *j; // names
i = &values; // name `i` refers to object `[1, 2, 3]`
j = i; // name `j` refers to referent of `i`
i->elements[0] = &five; // leading element of `i` refers to object `5`
重要的是我们没有更改任何名称!我们确实改变i->elements[0]
了,我们的名字都指向的对象的元素。
现有复合对象的值可能会更改。
通过名称更改对象的值时,名称不会更改。两者i
并且j
仍然引用同一个对象,我们可以更改其值。
+- name i -+ -\
\
--> + <values> -+
/ | elements | --> [1, 2, 3]
+- name j -+ -/ +-----------+
中间对象的行为类似于指针,因为我们可以直接更改它所指向的内容并从多个名称中引用它。
它们不完全是指针。它们是对对象的引用。对象可以是可变的,也可以是不可变的。不可变对象在修改时会被复制。可变对象就地更改。整数是一个不可变对象,您可以通过 i 和 j 变量引用它。列表是一个可变对象。
在你的第一个例子中
i = 5
# The label i now references 5
j = i
# The label j now references what i references
j = 3
# The label j now references 3
print i
# i still references 5
在您的第二个示例中:
i = [1, 2, 3]
# 'i' references a list object (a mutable object)
j = i
# 'j' now references the same object as 'i' (they reference the same mutable object)
i[0] = 5
# Sets first element of references object to 5
print j
# Prints the list object that 'j' references. It's the same one as 'i'.
赋值不会修改对象;它所做的只是改变变量指向的位置。改变一个变量点的位置不会改变另一个变量点的位置。
您可能正在考虑列表和字典是可变类型的事实。有一些操作符可以就地修改实际对象,如果您使用其中之一,您将看到指向同一对象的所有变量的变化:
x = []
y = x
x.append(1)
# x and y both are now [1]
但是赋值仍然只是移动指针:
x = [2]
# x now points to new list [2]; y still points to old list [1]
与字典和列表不同,数字是不可变的。如果你这样做x = 3; x += 2
了,你并没有将数字 3 转换为数字 5。您只是将变量x
指向 5 。3 仍然没有改变,任何指向它的变量仍然会将 3 视为它们的值。
(在实际实现中,数字可能根本不是引用类型;更有可能的是变量实际上直接包含值的表示,而不是指向它。但是该实现细节不会改变涉及不可变类型的语义.)
当您设置j=3
标签j
不再适用(指向)时i
,它开始指向整数3
。名称i
仍然是指您最初设置的值,5
.
'=' 符号左侧的任何变量都分配有 '=' 右侧的值
i = 5
j = i
--- j 有 5 个
j = 3
--- j 有 3(覆盖 5 的值)但关于 i 没有任何改变
print(i)
-- 所以这会打印 5
在 Python 中,一切都是对象,包括返回的内存片段本身。这意味着,当创建新的内存块时(无论您创建了什么:int、str、自定义对象等),您都有一个新的内存对象。在您的情况下,这是对 3 的分配,它创建一个新的(内存)对象,因此具有一个新地址。
如果你运行以下命令,你会很容易明白我的意思。
i = 5
j = i
print("id of j: {}", id(j))
j = 3
print("id of j: {}", id(j))
IMO,内存方面,这是 C 和 Python 之间的关键理解/区别。在 C/C++ 中,您将返回一个内存指针(当然,如果您使用指针语法)而不是内存对象,这为您在更改引用地址方面提供了更大的灵活性。