Python基本数据引用,相同引用的列表
假设我有两个列表:
>>> 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文档中有相当不错的介绍,但并不是完全的。
我的问题是:
一个可变对象总是是一个引用吗?你能总是假设修改它会修改原来的对象吗(除非你特意用
l2=l1[:]
这种方式复制)?我能在Python中组装一个包含所有相同引用的列表吗?也就是说,有一个函数
f(l1)
,如果这些都指向同一个列表,它会返回['l1', 'd', 't']
吗?我假设只要有某个引用指向数据,它就会一直有效。
也就是说:
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 个回答
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.
是的,只要你有对一个对象的引用,它就不会被垃圾回收。(在这里使用“有效”这个词似乎不太准确,但我想这就是你的意思)。
在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) 这是真的。
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) 如果你说的有效是指“不会被垃圾回收”,那么在没有极不可能的错误情况下,这个说法是正确的。要是垃圾回收器“偷走”了你的数据,那可真是太糟糕了。