Python基本数据引用,相同引用的列表

7 投票
6 回答
4642 浏览
提问于 2025-04-16 09:11

假设我有两个列表:

>>> l1=[1,2,3,4]
>>> l2=[11,12,13,14]

我可以把这两个列表放进一个元组或者字典里,这样它们就都指向原来的列表:

>>> t=(l1,l2)
>>> d={'l1':l1, 'l2':l2}
>>> id(l1)==id(d['l1'])==id(t[0])
True
>>> l1 is d['l1'] is t[0]
True

因为它们是指向原列表的引用,所以如果我改变了 l1,那么在元组和字典中指向的数据也会相应改变:

>>> l1.append(5)
>>> l1
[1, 2, 3, 4, 5]
>>> t
([1, 2, 3, 4, 5], [11, 12, 13, 14])
>>> d
{'l2': [11, 12, 13, 14], 'l1': [1, 2, 3, 4, 5]}

比如如果我在字典 d 中添加了引用,或者在元组 t 中添加了可变引用:

>>> d['l1'].append(6)
>>> t[0].append(7)
>>> d
{'l2': [11, 12, 13, 14], 'l1': [1, 2, 3, 4, 5, 6, 7]}
>>> l1
[1, 2, 3, 4, 5, 6, 7]

如果我现在把 l1 设置为一个新的列表,原列表的引用计数就会减少:

>>> sys.getrefcount(l1)
4
>>> sys.getrefcount(t[0])
4
>>> l1=['new','list']
>>> l1 is d['l1'] is t[0]
False
>>> sys.getrefcount(l1)
2
>>> sys.getrefcount(t[0])
3

而且,添加或改变 l1 并不会影响 d['l1']t[0],因为它们现在指向的是一个新的引用。关于间接引用的概念在Python文档中有相当不错的介绍,但并不是完全的。

我的问题是:

  1. 一个可变对象总是是一个引用吗?你能总是假设修改它会修改原来的对象吗(除非你特意用 l2=l1[:] 这种方式复制)?

  2. 我能在Python中组装一个包含所有相同引用的列表吗?也就是说,有一个函数 f(l1),如果这些都指向同一个列表,它会返回 ['l1', 'd', 't'] 吗?

  3. 我假设只要有某个引用指向数据,它就会一直有效。

也就是说:

l=[1,2,3]         # create an object of three integers and create a ref to it
l2=l              # create a reference to the same object
l=[4,5,6]         # create a new object of 3 ints; the original now referenced 
                  # by l2 is unchanged and unmoved

6 个回答

0
1a. Is a mutable object always a reference?

可变对象和不可变对象之间没有区别。把变量名看作是引用对有C语言背景的人来说很有帮助(但这意味着它们可以被解引用,而实际上它们不能)。

1b. Can you always assume that modifying it modifies the original

请注意,这不是“原始对象”。它们是同一个对象。b = a 的意思是 b 和 a 现在指向的是同一个对象。

1c. (Unless you specifically make a copy with l2=l1[:] kind of idiom)?

没错,因为那样的话就不再是同一个对象了。(不过列表中的条目仍然会是和原始列表相同的对象)。

2. Can I assemble a list of all the same references in Python?

是的,可能是这样,但你永远都不会需要它,所以这样做就是浪费精力。:)

3. It is my assumption that no matter what, the data will remain valid so long as there is some reference to it.

是的,只要你有对一个对象的引用,它就不会被垃圾回收。(在这里使用“有效”这个词似乎不太准确,但我想这就是你的意思)。

3

在Python中,每个变量都是一个引用。

当你在处理列表时,可能只关注append()方法的结果,而忽略了Python数据结构的整体概念。列表还有其他方法,并且列表的构造方式也有其优缺点。可以把列表看作是对列表中其他对象的一个视图。它们并不“包含”任何东西,除了访问这些对象所需的规则和方法。

list.append(x) 这个方法实际上等同于l[len(l):]=[list]

所以:

>>> l1=range(3)
>>> l2=range(20,23)
>>> l3=range(30,33)
>>> l1[len(l1):]=[l2]    # equivalent to 'append' for subscriptable sequences 
>>> l1[len(l1):]=l3      # same as 'extend'
>>> l1
[0, 1, 2, [20, 21, 22], 30, 31, 32]
>>> len(l1)
7
>>> l1.index(30)
4
>>> l1.index(20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: list.index(x): x not in list
>>> 20 in l1
False
>>> 30 in l1
True

l1[len(l1):]=[l2]中把列表构造器放在l2周围,或者调用l.append(l2),你就创建了一个指向l2的引用。如果你改变l2,引用也会显示这个变化。列表中的长度是一个元素——指向被添加序列的引用。

如果没有构造器的快捷方式,比如l1[len(l1):]=l3,你就会复制序列中的每个元素。

如果你使用其他常见的列表方法,比如l.index(something)in,你不会在数据引用中找到元素。l.sort()也不会正确排序。这些操作是对对象的“浅”操作,而使用l1[len(l1):]=[l2]时,你实际上是在创建一个递归数据结构。

如果你使用l1[len(l1):]=l3,你就是在对l3中的元素进行真正的(浅)复制。

这些都是相当基础的Python用法,大多数情况下它们“做对了事情”。不过,有时你可能会得到意想不到的结果,比如:

>>> m=[[None]*2]*3
>>> m
[[None, None], [None, None], [None, None]]
>>> m[0][1]=33
>>> m
[[None, 33], [None, 33], [None, 33]]   # probably not what was intended...
>>> m[0] is m[1] is m[2]               # same object, that's why they all changed
True

一些Python新手尝试通过m=[[None]*2]*3来创建多维列表。第一次的序列复制按预期工作;它创建了2个None的副本。问题出在第二次:它创建了对第一个列表的三个引用。所以当你输入m[0][1]=33时,会修改m中列表里的列表,然后所有的引用都会显示这个变化。

对比一下:

>>> m=[[None]*2,[None]*2,[None]*2]
>>> m
[[None, None], [None, None], [None, None]]
>>> m[0][1]=33
>>> m
[[None, 33], [None, None], [None, None]]

你也可以使用嵌套列表推导来做到这一点,如下所示:

>>> m=[[ None for i  in range(2)] for j in range(3)]
>>> m
[[None, None], [None, None], [None, None]]
>>> m[0][1]=44
>>> m
[[None, 44], [None, None], [None, None]]
>>> m[0] is m[1] is m[2]                      # three different lists....
False

关于列表和引用,Fredrik Lundh有这篇文章,是个不错的入门。

至于你具体的问题:

1) 在Python中,一切都是指向对象的标签或引用。没有“原始对象”(这是C++的概念),也没有“引用”、指针或实际数据之间的区别(这是C/Perl的概念)。

2) Fredrik Lundh有一个很好的比喻,关于类似问题的引用

就像你想知道你在门廊上发现的那只猫的名字:猫(对象)本身无法告诉你它的名字,它也不在乎——所以唯一的办法就是问你所有的邻居(命名空间),看看它是不是他们的猫(对象)……

……如果你发现它有很多名字,或者根本没有名字,也不要感到惊讶!

你可以费点劲找到这个列表,但为什么呢?就像你给那只猫起个名字一样,随便叫它什么就行。

3) 这是真的。

6

1) 通过引用修改一个可变对象,总是会改变“原始”对象。老实说,这其实是对引用的误解。新的引用和其他任何引用一样,都是“原始”的。只要这两个名字指向同一个对象,通过任意一个名字修改这个对象,另一个名字访问时也能看到这些修改。

2) 这和你想要的并不完全一样。gc.get_referrers 会返回对这个对象的 所有 引用。

>>> l = [1, 2]
>>> d = {0: l}
>>> t = (l, )
>>> import gc
>>> import pprint
>>> pprint.pprint(gc.get_referrers(l))
[{'__builtins__': <module '__builtin__' (built-in)>,
  '__doc__': None,
  '__name__': '__main__',
  '__package__': None,
  'd': {0: [1, 2]},
  'gc': <module 'gc' (built-in)>,
  'l': [1, 2],
  'pprint': <module 'pprint' from '/usr/lib/python2.6/pprint.pyc'>,
  't': ([1, 2],)},   # This is globals()

 {0: [1, 2]},  # This is d
 ([1, 2],)]   # this is t

注意,l 所引用的实际对象并不在返回的列表中,因为它并不包含对自己的引用。而 globals() 被返回是因为它 确实 包含对原始列表的引用。

3) 如果你说的有效是指“不会被垃圾回收”,那么在没有极不可能的错误情况下,这个说法是正确的。要是垃圾回收器“偷走”了你的数据,那可真是太糟糕了。

撰写回答