Fatiar com tupla não retornando um novo objeto, em vez de fatiar com lista

12

Em Python (2 e 3). Sempre que usamos o fatiamento de lista, ele retorna um novo objeto, por exemplo:

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

Resultado

>>> 140344378384464
>>> 140344378387272

Se a mesma coisa é repetida com tupla, o mesmo objeto é retornado, por exemplo:

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

Resultado

>>> 140344379214896
>>> 140344379214896

Seria ótimo se alguém pudesse esclarecer por que isso está acontecendo. Ao longo da minha experiência com o Python, fiquei com a impressão de que a fatia vazia retorna um novo objeto.

Meu entendimento é que ele está retornando o mesmo objeto que as tuplas são imutáveis ​​e não há sentido em criar uma nova cópia dele. Mas, novamente, isso não é mencionado nos documentos em nenhum lugar.

Vijay Jangir
fonte
l2 = tuple(iter(l1))ignora a otimização
Chris_Rands 22/10/19
Observei que a c-api forPyTuple_GetSlice foi documentada incorretamente depois de ver sua pergunta. Os documentos foram corrigidos (foi o problema bpo38557 ).
Wim

Respostas:

13

As implementações são livres para retornar instâncias idênticas para tipos imutáveis (no CPython, às vezes você pode ver otimizações semelhantes para seqüências de caracteres e números inteiros). Como o objeto não pode ser alterado, não há nada no código do usuário que precise se preocupar se ele possui uma instância exclusiva ou apenas outra referência a uma instância existente.

Você pode encontrar o curto-circuito no código C aqui .

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 é um detalhe de implementação, observe que o pypy não faz o mesmo.

wim
fonte
Obrigado @wim. Isso faz sentido agora. Apenas uma coisa fora do tópico, pois não tenho experiência em C. O que exatamente a-> ob_item faz? Eu tentei procurar por isso. mas tudo que eu pude entender é que pega o endereço de "a" e o move "ob_item" para frente. Meu entendimento era ob_item contém o número de endereço de armazenamento que faz o item "1". #offTheTopic
Vijay Jangir
2
Pode ser útil examinar o typedef para tupla, aqui . Assim a->ob_itemé como (*a).ob_item, isto é, obtém o membro chamado ob_itemdo PyTupleObjectque a está apontando, e o + ilow avança para o início da fatia.
quer
3

É um detalhe de implementação. Como as listas são mutáveis, é l1[:] necessário criar uma cópia, porque você não espera que as alterações l2sejam afetadas l1.

Como uma tupla é imutável , porém, não há nada que você possa fazer t2que possa afetar de t1maneira visível, portanto o compilador é livre (mas não obrigatório ) para usar o mesmo objeto para t1e t1[:].

chepner
fonte
1

No Python 3. * my_list[:]é o açúcar sintático para type(my_list).__getitem__(mylist, slice_object)where: slice_objecté um objeto de fatia criado a partir my_listdos atributos (comprimento) e da expressão [:]. Objetos que se comportam dessa maneira são chamados de subscritos no modelo de dados Python, veja aqui . Para listas e tuplas __getitem__é um método interno.

No CPython, e para listas e tuplas, __getitem__é interpretado pela operação de bytecode, BINARY_SUBSCRque é implementada para tuplas aqui e para listas aqui .

No caso de tuplas, percorrendo o código, você verá que, neste bloco de código , static PyObject* tuplesubscript(PyTupleObject* self, PyObject* item)retornará uma referência à mesma PyTupleObjectque obteve como argumento de entrada, se o item for do tipo PySlicee a fatia for avaliada para toda a tupla.

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

Agora você examina o código static PyObject * list_subscript(PyListObject* self, PyObject* item)e vê por si mesmo que, independentemente da fatia, um novo objeto de lista é sempre retornado.

Fakher Mokadem
fonte
11
Observe que isso é diferente no 2.7 , onde uma start:stopfatia no tipo embutido, incluindo tup[:], não passa BINARY_SUBSCR. A fatia estendida start:stop:steppassa por assinatura, no entanto.
quer
Ok, obrigado será atualizado para especificar a versão python.
Fakher Mokadem 25/10/19
0

Não tenho certeza disso, mas parece que o Python fornece um novo ponteiro para o mesmo objeto para evitar a cópia, pois as tuplas são idênticas (e como o objeto é uma tupla, é imutável).

michotross
fonte