El corte de tupla no devuelve un nuevo objeto en lugar del corte de lista

12

En Python (2 y 3). Cada vez que usamos el corte de lista, devuelve un nuevo objeto, por ejemplo:

l1 = [1,2,3,4]
print(id(l1))
l2 = l1[:]
print(id(l2))

Salida

>>> 140344378384464
>>> 140344378387272

Si se repite lo mismo con tupla, se devuelve el mismo objeto, por ejemplo:

t1 = (1,2,3,4)
t2 = t1[:]
print(id(t1))
print(id(t2))

Salida

>>> 140344379214896
>>> 140344379214896

Sería genial si alguien puede arrojar algo de luz sobre por qué está sucediendo esto, a lo largo de mi experiencia en Python tuve la impresión de que el segmento vacío devuelve un nuevo objeto.

Tengo entendido que está devolviendo el mismo objeto ya que las tuplas son inmutables y no tiene sentido crear una nueva copia. Pero, de nuevo, no se menciona en los documentos en ninguna parte.

Vijay Jangir
fuente
l2 = tuple(iter(l1))omite la optimización
Chris_Rands
Noté que la c-apiPyTuple_GetSlice se documentó de manera incorrecta después de ver su pregunta. Los documentos ahora se han solucionado (este era el problema bpo38557 ).
wim

Respuestas:

13

Las implementaciones son libres de devolver instancias idénticas para tipos inmutables (en CPython, a veces puede ver optimizaciones similares para cadenas y enteros). Como el objeto no se puede cambiar, no hay nada en el código de usuario que deba preocuparse si contiene una instancia única o simplemente otra referencia a una instancia existente.

Puede encontrar el cortocircuito en el código C aquí .

static PyObject*
tuplesubscript(PyTupleObject* self, PyObject* item)
{
    ... /* note: irrelevant parts snipped out */
    if (start == 0 && step == 1 &&
                 slicelength == PyTuple_GET_SIZE(self) &&
                 PyTuple_CheckExact(self)) {
            Py_INCREF(self);          /* <--- increase reference count */
            return (PyObject *)self;  /* <--- return another pointer to same */
        }
    ...

Este es un detalle de implementación, tenga en cuenta que pypy no hace lo mismo.

wim
fuente
Gracias @wim. Esto tiene sentido ahora. Solo una cosa fuera del tema, ya que no tengo experiencia en C. ¿Qué hace exactamente a-> ob_item? Intenté buscarlo. pero todo lo que pude entender es que toma la dirección de "a" y la mueve "ob_item" hacia adelante. Entiendo que ob_item contiene el número de direcciones de almacenamiento que conforman el elemento "1". #offTheTopic
Vijay Jangir
2
Puede ser útil mirar el typedef para tupla, aquí . Así a->ob_itemes como (*a).ob_item, es decir, obtiene el miembro llamado ob_itemdesde el PyTupleObjectque está apuntando a, y el + ilow luego avanza al comienzo del segmento.
wim
3

Es un detalle de implementación. Debido a que las listas son mutables, l1[:] debe crear una copia, ya que no esperaría que los cambios l2afecten l1.

Sin embargo, dado que una tupla es inmutable , no hay nada que pueda hacer t2que afecte t1de manera visible, por lo que el compilador es libre (pero no obligatorio ) de usar el mismo objeto para t1y t1[:].

chepner
fuente
1

En Python 3. * my_list[:]es el azúcar sintáctico para type(my_list).__getitem__(mylist, slice_object)donde: slice_objectes un objeto de corte construido a partir my_listde los atributos (longitud) y la expresión [:]. Los objetos que se comportan de esta manera se denominan subscriptables en el modelo de datos de Python, ver aquí . Para listas y tuplas __getitem__es un método incorporado.

En CPython, y para listas y tuplas, __getitem__se interpreta mediante la operación de código de bytes BINARY_SUBSCRque se implementa para las tuplas aquí y para las listas aquí .

En el caso de las tuplas, al recorrer el código, verá que en este bloque de código , static PyObject* tuplesubscript(PyTupleObject* self, PyObject* item)devolverá una referencia al mismo PyTupleObjectque obtuvo como argumento de entrada, si el elemento es de tipo PySlicey el segmento se evalúa como la tupla completa.

    static PyObject*
    tuplesubscript(PyTupleObject* self, PyObject* item)
    {
        /* checks if item is an index */ 
        if (PyIndex_Check(item)) { 
            ...
        }
        /* else it is a slice */ 
        else if (PySlice_Check(item)) { 
            ...
        /* unpacks the slice into start, stop and step */ 
        if (PySlice_Unpack(item, &start, &stop, &step) < 0) { 
            return NULL;
        }
       ...
        }
        /* if we start at 0, step by 1 and end by the end of the tuple then !! look down */
        else if (start == 0 && step == 1 &&
                 slicelength == PyTuple_GET_SIZE(self) && 
                 PyTuple_CheckExact(self)) {
            Py_INCREF(self); /* increase the reference count for the tuple */
            return (PyObject *)self; /* and return a reference to the same tuple. */
        ...
}

Ahora examina el código static PyObject * list_subscript(PyListObject* self, PyObject* item)y comprueba por sí mismo que, sea cual sea el segmento, siempre se devuelve un nuevo objeto de lista.

Fakher Mokadem
fuente
1
Tenga en cuenta que esto es diferente en 2.7 , donde un start:stopsegmento en el tipo incorporado tup[:], incluido , no pasa BINARY_SUBSCR. Sin start:stop:stepembargo, el corte extendido pasa por suscripción.
wim
Bien, gracias se actualizará para especificar la versión de Python.
Fakher Mokadem
0

No estoy seguro de esto, pero parece que Python le proporciona un nuevo puntero al mismo objeto para evitar la copia, ya que las tuplas son idénticas (y dado que el objeto es una tupla, es inmutable).

michotross
fuente