Le découpage de tuple ne renvoie pas un nouvel objet par opposition au découpage de liste

12

En Python (2 et 3). Chaque fois que nous utilisons le découpage de liste, il renvoie un nouvel objet, par exemple:

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

Production

>>> 140344378384464
>>> 140344378387272

Si la même chose est répétée avec tuple, le même objet est retourné, par exemple:

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

Production

>>> 140344379214896
>>> 140344379214896

Ce serait formidable si quelqu'un pouvait faire la lumière sur la raison pour laquelle cela se produit, tout au long de mon expérience Python, j'avais l'impression que la tranche vide renvoie un nouvel objet.

Ma compréhension est qu'il retourne le même objet que les tuples sont immuables et il n'y a aucun intérêt d'en créer une nouvelle copie. Mais encore une fois, cela n'est mentionné nulle part dans les documents.

Vijay Jangir
la source
l2 = tuple(iter(l1))contourne l'optimisation
Chris_Rands
Vous avez remarqué que c-api aPyTuple_GetSlice été documenté de manière inexacte après avoir vu votre question. Les documents ont maintenant été corrigés (c'était le problème bpo38557 ).
wim

Réponses:

13

Les implémentations sont libres de renvoyer des instances identiques pour les types immuables (dans CPython, vous pouvez parfois voir des optimisations similaires pour les chaînes et les entiers). Étant donné que l'objet ne peut pas être modifié, il n'y a rien dans le code utilisateur qui doive se soucier s'il contient une instance unique ou simplement une autre référence à une instance existante.

Vous pouvez trouver le court-circuit dans le code C ici .

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 */
        }
    ...

Il s'agit d'un détail d'implémentation, notez que pypy ne fait pas de même.

wim
la source
Merci @wim. Cela a du sens maintenant. Juste une chose hors du sujet car je n'ai pas d'expérience en C. Que fait exactement a-> ob_item? J'ai essayé de le chercher. mais tout ce que je pouvais comprendre, c'est qu'il prend l'adresse de "a" et le fait avancer "ob_item". Ma compréhension était ob_item détient le nombre d'adresses de stockage qui fait "1" élément. #offTheTopic
Vijay Jangir
2
Il pourrait être utile de regarder le typedef pour tuple, ici . Il en a->ob_itemest de même (*a).ob_item, c'est-à-dire qu'il obtient le membre appelé ob_itemdu PyTupleObjectpointé par a, et le + ilow avance ensuite au début de la tranche.
wim
3

C'est un détail d'implémentation. Étant donné que les listes sont modifiables, vous l1[:] devez créer une copie, car vous ne vous attendez pas à ce que les modifications soient l2affectées l1.

Étant donné qu'un tuple est immuable , cependant, vous ne pouvez rien faire t2qui affecterait t1de manière visible, le compilateur est donc libre (mais pas obligatoire ) d'utiliser le même objet pour t1et t1[:].

chepner
la source
1

En Python 3. * my_list[:]est un sucre syntaxique pour type(my_list).__getitem__(mylist, slice_object)où: slice_objectest un objet tranche construit à partir my_listdes attributs (longueur) et de l'expression [:]. Les objets qui se comportent de cette façon sont appelés indexables dans le modèle de données Python voir ici . Pour les listes et les tuples __getitem__est une méthode intégrée.

En CPython, et pour les listes et les tuples, __getitem__est interprété par l'opération de bytecode BINARY_SUBSCRqui est implémentée pour les tuples ici et pour les listes ici .

Dans le cas de tuples, en parcourant le code, vous verrez que dans ce bloc de code , static PyObject* tuplesubscript(PyTupleObject* self, PyObject* item)renverra une référence à la même chose PyTupleObjectqu'il a obtenu comme argument d'entrée, si l'élément est de type PySliceet la tranche est évaluée pour le tuple entier.

    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. */
        ...
}

Vous examinez maintenant le code static PyObject * list_subscript(PyListObject* self, PyObject* item)et constatez par vous-même que quelle que soit la tranche, un nouvel objet liste est toujours renvoyé.

Fakher Mokadem
la source
1
Notez que ceci est différent en 2.7 , où une start:stoptranche sur le type intégré, y compris tup[:], ne passe pas BINARY_SUBSCR. Le découpage étendu start:stop:steppasse par l'abonnement, cependant.
wim
D'accord, merci de mettre à jour pour spécifier la version de python.
Fakher Mokadem
0

Je n'en suis pas sûr, mais il semble que Python vous fournisse un nouveau pointeur vers le même objet pour éviter la copie car les tuples sont identiques (et puisque l'objet est un tuple, il est immuable).

michotross
la source