为什么重复列表不是引用而是复制?

2024-04-25 20:19:49 发布

您现在位置:Python中文网/ 问答频道 /正文

>>> L = [4, 5, 6]
>>> X = L * 4                    
>>> X
[4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6]
>>> L[1] = 0  

为什么

>>> X
[4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6]

而不是

>>> X
[4, 0, 6, 4, 0, 6, 4, 0, 6, 4, 0, 6]

什么?你知道吗

L * 4被求值时,调用什么函数或方法?你知道吗

  • L.copy()
  • list(L)
  • copy.copy(L)
  • 或者别的什么?你知道吗

谢谢。你知道吗


Tags: 方法函数listcopy
3条回答

调用L * 4时,调用的引擎盖下的机器类似于以下Python代码:

>>> L = [4, 5, 6]
>>> N = 4
>>> new = [x for _ in range(N) for x in L]  # i.e. L * N
>>> # Alternatively...
>>> new = []
>>> for _ in range(N):
...     for x in L:
...         new.append(x)
...

这意味着每个元素都是对原始元素的引用,但会创建一个新的列表对象。它和L.copy()list(L)、或copy.copy(L)都不一样。你知道吗

让我们看看这是如何在引擎盖下实现的。下面是遇到*运算符时在CPython中发生的情况:

    TARGET(BINARY_MULTIPLY) {
        PyObject *right = POP();
        PyObject *left = TOP();
        PyObject *res = PyNumber_Multiply(left, right);
        Py_DECREF(left);
        Py_DECREF(right);
        SET_TOP(res);
        if (res == NULL)
            goto error;
        DISPATCH();
    }

PyNumber_Multiply函数中,如果数字乘法失败,它会假定其中一个对象是序列,因此(经过一段间接寻址之后)最终调用^{}(这是一种过于简单的操作,有关详细信息,请参阅第一条注释)。这是密码

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);  // <== Make a new list of the expanded size
    if (np == NULL)
        return NULL;

    items = np->ob_item;
    if (Py_SIZE(a) == 1) {  // <== Optimization for L * 1
        elem = a->ob_item[0];
        for (i = 0; i < n; i++) {
            items[i] = elem;
            Py_INCREF(elem);
        }
        return (PyObject *) np;
    }
    p = np->ob_item;
    items = a->ob_item;
    for (i = 0; i < n; i++) {  // <== Reference the original elements in the new list in the appropriate locations.
        for (j = 0; j < Py_SIZE(a); j++) {
            *p = items[j];
            Py_INCREF(*p);
            p++;
        }
    }
    return (PyObject *) np;
}

如您所见,它首先分配一个新列表,然后将列表中的每个对象的引用放到新列表中,而不是简单地创建几个原始对象的引用。因此,列表中的每个元素都是对原始元素的引用,但列表本身并不是对原始元素的引用。你知道吗

它不复制引用。请参见:

In [14]: L = [4, 5, 6]

In [15]: fourL = 4*L

In [16]: [hex(id(i)) for i in L]
Out[16]: ['0x1002148d0', '0x1002148f0', '0x100214910']

In [17]: [hex(id(i)) for i in fourL]
Out[17]:
['0x1002148d0',
 '0x1002148f0',
 '0x100214910',
 '0x1002148d0',
 '0x1002148f0',
 '0x100214910',
 '0x1002148d0',
 '0x1002148f0',
 '0x100214910',
 '0x1002148d0',
 '0x1002148f0',
 '0x100214910']

没有修改X的原因是

X = L * 4

语法糖是用来

X = L.__mul__(4)

__mul__函数是Python在使用*操作符时在后台调用的函数。它返回一个新对象:

>>> l = [1, 2]
>>> id(l.__mul__(4)) == id(l)
False
>>> 

因此X保存对L.__mul__(4)返回的新对象的引用,而不是对L正在引用的对象的引用。基本上,XL引用完全不同的列表对象,因此更改一个变量引用不会影响另一个变量引用

请注意,虽然Python不会复制L所持有的引用,但它会创建对L引用的列表对象中的每个元素的引用的副本。这就是为什么子列表相乘会导致意外行为:

>>> l = [[0]*2]*2
>>> l
[[0, 0], [0, 0]]
>>> l[0][0] = 1
>>> l
[[1, 0], [1, 0]]
>>>

相关问题 更多 >