I recentemente comparou as velocidades de processamento de []
e list()
e ficou surpreso ao descobrir que []
corre mais de três vezes mais rápido do que list()
. Corri o mesmo teste com {}
e dict()
, e os resultados foram praticamente idênticos: []
e {}
tanto levou cerca 0.128sec / milhão de ciclos, enquanto que list()
e dict()
levou cerca de 0.428sec / milhão de ciclos cada.
Por que é isso? Fazer []
e {}
(e, provavelmente, ()
e ''
, também) imediatamente passar para trás cópias de alguns literal estoque vazio enquanto os seus homólogos explicitamente nomeados ( list()
, dict()
, tuple()
, str()
) ir totalmente sobre a criação de um objeto, ou não eles realmente têm elementos?
Não tenho idéia de como esses dois métodos diferem, mas eu adoraria descobrir. Não consegui encontrar uma resposta nos documentos ou no SO, e procurar colchetes vazios acabou sendo mais problemático do que eu esperava.
Obtive meus resultados de tempo ligando timeit.timeit("[]")
e timeit.timeit("list()")
, e timeit.timeit("{}")
e timeit.timeit("dict()")
, para comparar listas e dicionários, respectivamente. Estou executando o Python 2.7.9.
Eu descobri recentemente " Porque é que se True mais lento do que se 1? ", Que compara o desempenho de if True
para if 1
e parece tocar em um semelhante literal versus global de cenário; talvez valha a pena considerar também.
fonte
()
e''
são especiais, pois não são apenas vazios, são imutáveis e, como tal, é uma vitória fácil torná-los singletons; eles nem constroem novos objetos, apenas carregam o singleton para otuple
/ vaziostr
. Tecnicamente, é um detalhe de implementação, mas é difícil imaginar por que eles não armazenariam em cache o vaziotuple
/str
por motivos de desempenho. Portanto, sua intuição[]
e a{}
devolução de um literal de estoque estavam erradas, mas se aplica a()
e''
.{}
mais rápido que ligarset()
?Respostas:
Porque
[]
e{}
são sintaxe literal . O Python pode criar bytecode apenas para criar os objetos de lista ou dicionário:list()
edict()
são objetos separados. Seus nomes precisam ser resolvidos, a pilha precisa estar envolvida para enviar os argumentos, o quadro precisa ser armazenado para recuperar mais tarde e uma chamada deve ser feita. Tudo isso leva mais tempo.Para o caso vazio, isso significa que você tem pelo menos um
LOAD_NAME
(que precisa pesquisar no espaço de nomes global e no__builtin__
módulo ) seguido de umCALL_FUNCTION
, que deve preservar o quadro atual:Você pode cronometrar a pesquisa de nome separadamente com
timeit
:A discrepância de tempo provavelmente existe uma colisão de hash do dicionário. Subtraia esses horários dos horários para chamar esses objetos e compare o resultado com os horários para o uso de literais:
Portanto, ter que chamar o objeto leva um
1.00 - 0.31 - 0.30 == 0.39
segundo adicional a cada 10 milhões de chamadas.Você pode evitar o custo da pesquisa global usando o alias dos nomes globais como locais (usando uma
timeit
configuração, tudo que você vincula a um nome é local):mas você nunca pode superar esse
CALL_FUNCTION
custo.fonte
list()
requer uma pesquisa global e uma chamada de função, mas é[]
compilada em uma única instrução. Vejo:fonte
Porque
list
é uma função para converter digamos uma string em um objeto de lista, enquanto[]
é usada para criar uma lista fora do bastão. Tente isto (pode fazer mais sentido para você):Enquanto
Fornece uma lista real com o que você coloca nela.
fonte
[]
é mais rápido quelist()
, e não por que['wham bam']
é mais rápido quelist('wham bam')
.[]
/list()
é exatamente o mesmo que['wham']
/list('wham')
porque eles têm as mesmas diferenças de variáveis, assim como1000/10
é o mesmo que100/1
em matemática. Em teoria, você poderia tirarwham bam
o fato e o mesmo continuaria o mesmo, quelist()
tenta converter algo chamando o nome de uma função, enquanto[]
apenas converte a variável. As chamadas de função são diferentes sim, isso é apenas uma visão geral lógica do problema, por exemplo, um mapa de rede de uma empresa também é lógico de uma solução / problema. Vote como quiser.As respostas aqui são ótimas, objetivas e cobrem totalmente essa questão. Vou dar um passo adiante no código de bytes para os interessados. Estou usando o repo mais recente do CPython; versões mais antigas se comportam de maneira semelhante a esse respeito, mas pequenas alterações podem estar em vigor.
Aqui está um detalhamento da execução de cada um deles,
BUILD_LIST
para[]
eCALL_FUNCTION
paralist()
.A
BUILD_LIST
instrução:Você deve apenas ver o horror:
Terrivelmente complicado, eu sei. É assim que é simples:
PyList_New
(isso aloca principalmente a memória para um novo objeto de lista),oparg
sinalizando o número de argumentos na pilha. Direto ao ponto.if (list==NULL)
.PyList_SET_ITEM
(uma macro).Não é à toa que é rápido! É feito sob medida para criar novas listas, nada mais :-)
A
CALL_FUNCTION
instrução:Aqui está a primeira coisa que você vê quando espreita a manipulação de código
CALL_FUNCTION
:Parece bastante inofensivo, certo? Bem, infelizmente não, não
call_function
é um cara direto que chamará a função imediatamente, não pode. Em vez disso, ele pega o objeto da pilha, pega todos os argumentos da pilha e depois alterna com base no tipo do objeto; é um:PyCFunction_Type
? Não, élist
,list
não é do tipoPyCFunction
PyMethodType
? Não, veja anterior.PyFunctionType
? Nopee, veja anterior.Estamos chamando o
list
tipo, o argumento passado paracall_function
éPyList_Type
. Agora, o CPython precisa chamar uma função genérica para manipular qualquer objeto que possa ser chamado_PyObject_FastCallKeywords
, com mais chamadas de função.Essa função novamente verifica alguns tipos de função (que não consigo entender por que) e, depois de criar um dict para kwargs, se necessário , continua a chamar
_PyObject_FastCallDict
._PyObject_FastCallDict
finalmente nos leva a algum lugar! Depois de realizar ainda mais verificações que agarra atp_call
fenda dotype
dotype
que já passou em, ou seja, ele pegatype.tp_call
. Em seguida, ele cria uma tupla dos argumentos transmitidos_PyStack_AsTuple
e, finalmente, uma chamada pode finalmente ser feita !tp_call
, que corresponde aotype.__call__
controle e finalmente cria o objeto de lista. Ele chama as listas__new__
que correspondemPyType_GenericNew
e alocam memória para ele comPyType_GenericAlloc
: Esta é realmente a parte em que ele alcançaPyList_New
, finalmente . Todas as anteriores são necessárias para manipular objetos de maneira genérica.No final,
type_call
chamalist.__init__
e inicializa a lista com todos os argumentos disponíveis e, em seguida, retornamos do jeito que viemos. :-)Finalmente, lembre-se de
LOAD_NAME
que é outro cara que contribui aqui.É fácil ver que, ao lidar com nossa entrada, o Python geralmente precisa passar por obstáculos para realmente descobrir a
C
função apropriada para fazer o trabalho. Não tem a cortesia de chamá-lo imediatamente, porque é dinâmico, alguém pode mascararlist
( e o garoto faz muitas pessoas ) e outro caminho deve ser tomado.É aqui que
list()
perde muito: o Python explorador precisa fazer para descobrir o que diabos ele deve fazer.A sintaxe literal, por outro lado, significa exatamente uma coisa; não pode ser alterado e sempre se comporta de maneira pré-determinada.
Nota de rodapé: Todos os nomes de funções estão sujeitos a alterações de uma versão para outra. O ponto ainda permanece e provavelmente continuará em qualquer versão futura; é a pesquisa dinâmica que atrasa as coisas.
fonte
O maior motivo é que o Python trata
list()
exatamente como uma função definida pelo usuário, o que significa que você pode interceptá-lo alterando o nome de outra coisalist
e fazer algo diferente (como usar sua própria lista subclassificada ou talvez um deque).Ele cria imediatamente uma nova instância de uma lista interna com
[]
.Minha explicação procura dar-lhe a intuição para isso.
Explicação
[]
é comumente conhecido como sintaxe literal.Na gramática, isso é chamado de "exibição de lista". Dos documentos :
Em resumo, isso significa que um objeto interno do tipo
list
é criado.Não há como contornar isso - o que significa que o Python pode fazê-lo o mais rápido possível.
Por outro lado,
list()
pode ser interceptado a partir da criação de um built-inlist
usando o construtor da lista de built-in.Por exemplo, digamos que queremos que nossas listas sejam criadas ruidosamente:
Poderíamos então interceptar o nome
list
no escopo global no nível do módulo e, quando criamos umlist
, criamos nossa lista de subtipos:Da mesma forma, poderíamos removê-lo do espaço para nome global
e coloque-o no espaço para nome interno:
E agora:
E observe que a exibição da lista cria uma lista incondicionalmente:
Provavelmente, apenas fazemos isso temporariamente, então vamos desfazer nossas alterações - primeiro remova o novo
List
objeto dos componentes internos:Ah, não, perdemos a noção do original.
Não se preocupe, ainda podemos obter
list
- é o tipo de uma lista literal:Assim...
Como vimos - podemos sobrescrever
list
- mas não podemos interceptar a criação do tipo literal. Quando usamoslist
, temos que fazer as pesquisas para ver se há algo lá.Então, temos que ligar para qualquer coisa que possamos chamar. Da gramática:
Podemos ver que ele faz a mesma coisa com qualquer nome, não apenas na lista:
Pois
[]
não há chamada de função no nível de bytecode do Python:Ele simplesmente vai direto para a construção da lista sem nenhuma pesquisa ou chamada no nível do bytecode.
Conclusão
Demonstramos que
list
pode ser interceptado com o código do usuário usando as regras de escopo e quelist()
procura um chamador e depois o chama.Considerando que
[]
é uma exibição de lista, ou literal, e assim evita a pesquisa de nome e a chamada de função.fonte
list
e o compilador python não pode ter certeza se ele realmente retornará uma lista vazia.