O que faz com que [* a] seja alocado globalmente?

136

Aparentemente list(a), o macacão não é atribuído, o [x for x in a]macacão em alguns pontos e o [*a]macacão o tempo todo ?

Tamanhos até n = 100

Aqui estão os tamanhos n de 0 a 12 e os tamanhos resultantes em bytes para os três métodos:

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

Computado assim, reproduzível em repl.it , usando Python 3. 8 :

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

Então, como isso funciona? Como o [*a]globalocate? Na verdade, qual mecanismo ele usa para criar a lista de resultados a partir da entrada fornecida? Ele usa um iterador ae usa algo comolist.append ? Onde está o código fonte?

( Colab com dados e código que produziu as imagens.)

Aumentando o zoom para n menor:

Tamanhos até n = 40

Diminuindo o zoom para n maior:

Tamanhos até n = 1000

Stefan Pochmann
fonte
11
Fwiw, estendendo seus casos de teste, parece que a compreensão da lista se comporta como um loop e anexa cada item à lista, enquanto [*a]parece se comportar como o uso extendem uma lista vazia.
jdehesa 5/03
4
Pode ser útil examinar o código de bytes gerado para cada um. list(a)opera inteiramente em C; ele pode alocar o nó do buffer interno por nó à medida que itera a. [x for x in a]apenas usa LIST_APPENDmuito, de modo que segue o padrão normal de "alocar um pouco, realocar quando necessário" de uma lista normal. [*a]usa BUILD_LIST_UNPACK, o que ... Eu não sei o que isso faz, além de aparentemente
superalocar
2
Além disso, no Python 3.7, parece que list(a)e [*a]são idênticos, e a localização geral comparada a [x for x in a], então ... sys.getsizeofpode não ser a ferramenta certa a ser usada aqui.
chepner 5/03
7
@chepner Eu acho que sys.getsizeofé a ferramenta certa, apenas mostra que list(a)costumava ser usada em geral. Na verdade, o que há de novo no Python 3.8 menciona: "O construtor da lista não atribui [...] globalmente" .
Stefan Pochmann 5/03
5
@chepner: Foi um bug corrigido no 3.8 ; o construtor não deve alocar globalmente.
ShadowRanger 05/03

Respostas:

81

[*a] está fazendo internamente o equivalente C de :

  1. Faça um novo, vazio list
  2. Ligar newlist.extend(a)
  3. Retorna list.

Portanto, se você expandir seu teste para:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

Experimente online!

você verá os resultados getsizeof([*a])e l = []; l.extend(a); getsizeof(l)é o mesmo.

Geralmente é a coisa certa a fazer; quando extendvocê espera adicionar mais tarde e da mesma forma para descompactação generalizada, assume-se que várias coisas serão adicionadas uma após a outra. [*a]não é o caso normal; O Python assume que há vários itens ou iteráveis ​​sendo adicionados ao list( [*a, b, c, *d]), portanto, a alocação geral economiza trabalho no caso comum.

Por outro lado, um listconstruído a partir de uma iterável preset única (com list()) pode não crescer ou encolher durante o uso, e a localização geral é prematura até prova em contrário; Recentemente, o Python corrigiu um bug que fazia o construtor se localizar globalmente, mesmo para entradas com tamanho conhecido .

Quanto às listcompreensões, elas são efetivamente equivalentes a repetidasappend s ; portanto, você está vendo o resultado final do padrão de crescimento da alocação geral ao adicionar um elemento de cada vez.

Para ser claro, nada disso é uma garantia de idioma. É exatamente como o CPython o implementa. A especificação da linguagem Python geralmente não se preocupa com padrões de crescimento específicos em list(além de garantir se O(1) appende s amortizados popno final). Conforme observado nos comentários, a implementação específica muda novamente em 3.9; Embora isso não afete [*a], ele pode afetar outros casos em que o que costumava ser "constrói um tupleitem temporário de itens individuais e depois extendcom o tuple" agora se torna várias aplicações de LIST_APPEND, o que pode mudar quando a alocação geral ocorre e quais números entram no cálculo.

ShadowRanger
fonte
4
@ StefanPochmann: Eu tinha lido o código antes (e é por isso que eu já sabia disso). Este é o manipulador de código de bytesBUILD_LIST_UNPACK , ele usa _PyList_Extendcomo o equivalente C da chamada extend(apenas diretamente, e não pela pesquisa de método). Eles combinaram isso com os caminhos para a construção de um tuplecom desembalagem; tuples não atribuem um bom desempenho geral à construção fragmentada; portanto, sempre descompactam em um list(para se beneficiar da localização geral) e convertem-se tupleno final quando é o que foi solicitado.
ShadowRanger 5/03
4
Note-se que esta aparentemente muda em 3,9 , onde a construção é feito com bytecodes separados ( BUILD_LIST, LIST_EXTENDpara cada coisa a descompactar, LIST_APPENDpara itens individuais), em vez de carregar tudo na pilha antes de construir o todo listcom uma única instrução código de byte (que permite que o compilador para realizar otimizações que a instrução all-in-one não permitiu, como a implementação [*a, b, *c]como LIST_EXTEND, LIST_APPEND, LIST_EXTENDw / o a necessidade de envolver bem um um- tuplepara atender aos requisitos de BUILD_LIST_UNPACK).
ShadowRanger
18

Imagem completa do que acontece, aproveitando as outras respostas e comentários (especialmente a resposta do ShadowRanger , que também explica por que é assim).

Desmontar mostra que BUILD_LIST_UNPACKé usado:

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

Isso é tratado emceval.c que constrói uma lista vazia e estende-lo (com a):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend usa list_extend :

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

Que chama list_resizecom a soma dos tamanhos :

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

E isso totaliza da seguinte maneira:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Vamos verificar isso. Calcule o número esperado de pontos com a fórmula acima e calcule o tamanho esperado de bytes multiplicando-o por 8 (como estou usando Python de 64 bits aqui) e adicionando o tamanho de bytes de uma lista vazia (ou seja, a sobrecarga constante de um objeto de lista) :

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

Resultado:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

Corresponde a exceção n = 0, que list_extendna verdade é um atalho , e na verdade isso também corresponde:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {
Stefan Pochmann
fonte
8

Esses serão detalhes de implementação do interpretador CPython e, portanto, podem não ser consistentes com outros intérpretes.

Dito isto, você pode ver onde a compreensão e os list(a)comportamentos entram aqui:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

Especificamente para a compreensão:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Logo abaixo dessas linhas, existe o list_preallocate_exactque é usado ao chamar list(a).

Randy
fonte
11
[*a]não anexa elementos individuais, um de cada vez. Ele tem seu próprio bytecode dedicado, que faz a inserção em massa via extend.
ShadowRanger 05/03
Entendi - eu acho que não cavei o suficiente nisso. Removida a seção[*a]
Randy