"Menos espanto" e o argumento padrão mutável


2594

Qualquer pessoa que mexa no Python por tempo suficiente foi mordida (ou rasgada em pedaços) pelo seguinte problema:

def foo(a=[]):
    a.append(5)
    return a

Novatos Python seria de esperar esta função para retornar sempre uma lista com apenas um elemento: [5]. O resultado é muito diferente e muito surpreendente (para um iniciante):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Um gerente meu teve seu primeiro encontro com esse recurso e o chamou de "uma falha dramática no design" da linguagem. Respondi que o comportamento tinha uma explicação subjacente, e é realmente muito intrigante e inesperado se você não entende os aspectos internos. No entanto, não fui capaz de responder (a mim mesmo) à seguinte pergunta: qual é o motivo para vincular o argumento padrão na definição da função e não na execução da função? Duvido que o comportamento experiente tenha um uso prático (quem realmente usou variáveis ​​estáticas em C, sem erros de criação?)

Editar :

Baczek fez um exemplo interessante. Juntamente com a maioria dos seus comentários e os de Utaal em particular, eu elaborei ainda mais:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Para mim, parece que a decisão de design foi relativa a onde colocar o escopo dos parâmetros: dentro da função ou "junto" com ela?

Fazer a ligação dentro da função significaria que xestá efetivamente ligado ao padrão especificado quando a função é chamada, não definida, algo que apresentaria uma falha profunda: a deflinha seria "híbrida" no sentido de que parte da ligação (de o objeto da função) aconteceria na definição e parte (atribuição de parâmetros padrão) no tempo de chamada da função.

O comportamento real é mais consistente: tudo nessa linha é avaliado quando essa linha é executada, ou seja, na definição da função.



4
Não duvido que argumentos mutáveis ​​violem o princípio de menor espanto para uma pessoa comum, e vi iniciantes chegando lá, substituindo heroicamente as listas de discussão por tuplas. No entanto, argumentos mutáveis ​​ainda estão alinhados com o Python Zen (Pep 20) e se enquadram na cláusula "óbvio para holandês" (entendida / explorada por programadores de núcleo python). A solução recomendada com a sequência de documentos é a melhor, mas a resistência a essas sequências e a qualquer documento (escrito) não é tão incomum hoje em dia. Pessoalmente, eu preferiria um decorador (digamos @fixed_defaults).
Serge

5
Meu argumento quando me deparo com isso é: "Por que você precisa criar uma função que retorne um mutável que opcionalmente pode ser um mutável que você passaria para a função? Ou ele altera um mutável ou cria um novo. Por que você precisa fazer as duas coisas com uma função? E por que o intérprete deve ser reescrito para permitir que você faça isso sem adicionar três linhas ao seu código? " Porque estamos falando de reescrever a maneira como o intérprete lida com definições e evocações de funções aqui. Isso é muito o que fazer para um caso de uso quase necessário.
27675 Alan

12
"Os iniciantes em Python esperam que essa função retorne sempre uma lista com apenas um elemento:. [5]" Eu sou um novato em Python e não esperaria isso, porque obviamente foo([1])retornarei [1, 5], não [5]. O que você quis dizer é que um novato espera que a função chamada sem parâmetro sempre retorne [5].
symplectomorphic

2
Esta pergunta é "Por que isso [da maneira errada] foi implementado?" Não pergunta "Qual é o caminho certo?" , abordado por [ Por que usar arg = None corrige o problema de argumento padrão mutável do Python? ] * ( stackoverflow.com/questions/10676729/… ). Os novos usuários quase sempre estão menos interessados ​​no primeiro e muito mais no último, então, às vezes, é um link / dupe muito útil para citar.
SMCI

Respostas:


1612

Na verdade, isso não é uma falha de design e não é por causa de componentes internos ou desempenho.
Ele vem simplesmente do fato de que as funções no Python são objetos de primeira classe, e não apenas um pedaço de código.

Assim que você pensa dessa maneira, faz completamente sentido: uma função é um objeto que está sendo avaliado em sua definição; os parâmetros padrão são como "dados do membro" e, portanto, seu estado pode mudar de uma chamada para outra - exatamente como em qualquer outro objeto.

De qualquer forma, o Effbot tem uma explicação muito boa dos motivos desse comportamento nos Valores padrão dos parâmetros no Python .
Achei isso muito claro e sugiro realmente lê-lo para um melhor conhecimento de como os objetos funcionais funcionam.


80
Para quem lê a resposta acima, recomendo vivamente que leia o artigo vinculado do Effbot. Assim como todas as outras informações úteis, é muito útil saber a parte de como esse recurso de linguagem pode ser usado para armazenar em cache / memoisation de resultado!
Cam Jackson

85
Mesmo se for um objeto de primeira classe, ainda é possível visualizar um design em que o código para cada valor padrão seja armazenado junto com o objeto e reavaliado sempre que a função for chamada. Não estou dizendo que seria melhor, apenas que as funções de objetos de primeira classe não a excluem totalmente.
gerrit

312
Desculpe, mas qualquer coisa considerada "O maior WTF do Python" é definitivamente uma falha de design . Essa é uma fonte de bugs para todos em algum momento, porque ninguém espera esse comportamento a princípio - o que significa que ele não deveria ter sido projetado dessa maneira para começar. Eu não me importo com o que eles tiveram que pular, eles deveriam ter projetado o Python para que os argumentos padrão não sejam estáticos.
BlueRaja - Danny Pflughoeft

192
Seja ou não uma falha de design, sua resposta parece implicar que esse comportamento é de alguma forma necessário, natural e óbvio, uma vez que funções são objetos de primeira classe e isso simplesmente não é o caso. Python tem encerramentos. Se você substituir o argumento padrão por uma atribuição na primeira linha da função, ele avaliará a expressão a cada chamada (potencialmente usando nomes declarados em um escopo). Não há nenhuma razão para que não seja possível ou razoável ter argumentos padrão avaliados cada vez que a função é chamada exatamente da mesma maneira.
Mark Amery

24
O design não segue diretamente de functions are objects. No seu paradigma, a proposta seria implementar os valores padrão das funções como propriedades e não como atributos.
Bukzor

273

Suponha que você tenha o seguinte código

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Quando vejo a declaração de comer, a coisa menos surpreendente é pensar que, se o primeiro parâmetro não for fornecido, será igual à tupla ("apples", "bananas", "loganberries")

No entanto, suposto mais tarde no código, eu faço algo como

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

se os parâmetros padrão fossem vinculados à execução da função, e não à declaração da função, eu ficaria surpreso (de uma maneira muito ruim) ao descobrir que os frutos haviam sido alterados. Isso seria mais surpreendente do que descobrir que sua foofunção acima estava alterando a lista.

O verdadeiro problema está nas variáveis ​​mutáveis, e todos os idiomas têm esse problema até certo ponto. Aqui está uma pergunta: suponha que em Java eu ​​tenha o seguinte código:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Agora, meu mapa usa o valor da StringBufferchave quando foi colocada no mapa ou armazena a chave por referência? De qualquer maneira, alguém está surpreso; ou a pessoa que tentou tirar o objeto do Mapuso de um valor idêntico àquele em que o colocou, ou a pessoa que parece não conseguir recuperar o objeto, mesmo que a chave usada seja literalmente o mesmo objeto que foi usado para colocá-lo no mapa (é por isso que o Python não permite que seus tipos de dados internos mutáveis ​​sejam usados ​​como chaves de dicionário).

Seu exemplo é um bom exemplo de caso em que os novatos em Python serão surpreendidos e mordidos. Mas eu argumentaria que, se "consertássemos" isso, isso criaria apenas uma situação diferente em que eles seriam mordidos, e essa seria ainda menos intuitiva. Além disso, esse é sempre o caso quando se lida com variáveis ​​mutáveis; você sempre encontra casos em que alguém pode esperar intuitivamente um ou o comportamento oposto, dependendo do código que está escrevendo.

Pessoalmente, gosto da abordagem atual do Python: os argumentos da função padrão são avaliados quando a função é definida e esse objeto é sempre o padrão. Suponho que eles poderiam usar um caso especial usando uma lista vazia, mas esse tipo de caixa especial causaria ainda mais espanto, sem mencionar que é incompatível com versões anteriores.


30
Eu acho que é uma questão de debate. Você está atuando em uma variável global. Qualquer avaliação realizada em qualquer lugar do seu código envolvendo sua variável global agora (corretamente) se referirá a ("mirtilos", "mangas"). o parâmetro padrão pode ser como qualquer outro caso.
27513 Stefano Borini

47
Na verdade, acho que não concordo com o seu primeiro exemplo. Não tenho certeza se gosto da ideia de modificar um inicializador como esse em primeiro lugar, mas se o fizesse, esperaria que ele se comportasse exatamente como você descreve - alterando o valor padrão para ("blueberries", "mangos").
11459 Ben Blank

12
O parâmetro padrão é como qualquer outro caso. O que é inesperado é que o parâmetro é uma variável global e não local. Por sua vez, é porque o código é executado na definição da função, não na chamada. Depois de conseguir isso, e o mesmo vale para as aulas, fica perfeitamente claro.
Lennart Regebro 15/07/2009

17
Acho o exemplo mais enganoso do que brilhante. Se some_random_function()acrescenta a fruitsvez de atribuir a ele, o comportamento de eat() vai mudar. Tanto para o design maravilhoso atual. Se você usar um argumento padrão que é referenciado em outro lugar e depois modificar a referência de fora da função, estará solicitando problemas. O verdadeiro WTF é quando as pessoas definem um novo argumento padrão (uma lista literal ou uma chamada para um construtor) e ainda recebem pouco.
9114 alexis

13
Você acabou de declarar globale reatribuir explicitamente a tupla - não há absolutamente nada de surpreendente se eatfuncionar de maneira diferente depois disso.
user3467349

241

A parte relevante da documentação :

Os valores padrão dos parâmetros são avaliados da esquerda para a direita quando a definição da função é executada. Isso significa que a expressão é avaliada uma vez, quando a função é definida, e que o mesmo valor "pré-calculado" é usado para cada chamada. Isso é especialmente importante para entender quando um parâmetro padrão é um objeto mutável, como uma lista ou um dicionário: se a função modificar o objeto (por exemplo, anexando um item a uma lista), o valor padrão será efetivamente modificado. Isso geralmente não é o que se pretendia. Uma maneira de contornar isso é usar Nonecomo padrão e testá-lo explicitamente no corpo da função, por exemplo:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
As frases "geralmente não é o que se pretendia" e "uma maneira de contornar isso" cheiram como se estivessem documentando uma falha de design.
bukzor

4
@ Matthew: Estou bem ciente, mas não vale a pena a armadilha. Geralmente, você vê guias e estilos de linha sinalizar incondicionalmente os valores padrão mutáveis ​​como incorretos por esse motivo. A maneira explícita de fazer a mesma coisa é inserir um atributo na função ( function.data = []) ou, melhor ainda, criar um objeto.
bukzor

6
@bukzor: As armadilhas precisam ser anotadas e documentadas, e é por isso que esta pergunta é boa e recebeu tantos votos positivos. Ao mesmo tempo, as armadilhas não precisam necessariamente ser removidas. Quantos iniciantes em Python passaram uma lista para uma função que a modificou e ficaram chocados ao ver as alterações aparecerem na variável original? No entanto, tipos de objetos mutáveis ​​são maravilhosos quando você entende como usá-los. Eu acho que tudo se resume a opinião sobre essa armadilha em particular.
Matthew

33
A frase "geralmente não é o que se pretendia" significa "não o que o programador realmente queria que acontecesse", não "não o que o Python deveria fazer".
precisa

4
@holdenweb Uau, estou muito atrasado para a festa. Dado o contexto, o bukzor está completamente certo: eles estão documentando comportamentos / conseqüências que não foram "planejados" quando decidiram que a linguagem deveria executar a definição da função. Como é uma consequência não intencional de sua escolha de design, é uma falha de design. Se não fosse uma falha de design, não haveria necessidade de oferecer "uma maneira de contornar isso".
code_dredd

118

Não sei nada sobre o funcionamento interno do intérprete Python (e também não sou especialista em compiladores e intérpretes); portanto, não me culpe se propuser algo insensível ou impossível.

Desde que os objetos python sejam mutáveis , acho que isso deve ser levado em consideração ao projetar os argumentos padrão. Quando você instancia uma lista:

a = []

você espera obter uma nova lista referenciada por a.

Por que o a=[]in

def x(a=[]):

instanciar uma nova lista na definição de função e não na invocação? É como se você estivesse perguntando "se o usuário não fornecer o argumento, instancie uma nova lista e use-a como se tivesse sido produzida pelo chamador". Eu acho que isso é ambíguo:

def x(a=datetime.datetime.now()):

usuário, você deseja ausar como padrão a data e hora correspondentes ao definir ou executar x? Nesse caso, como no anterior, manterei o mesmo comportamento como se o argumento padrão "assignment" fosse a primeira instrução da função ( datetime.now()chamada na chamada de função). Por outro lado, se o usuário quisesse o mapeamento de tempo de definição, ele poderia escrever:

b = datetime.datetime.now()
def x(a=b):

Eu sei, eu sei: isso é um fechamento. Como alternativa, o Python pode fornecer uma palavra-chave para forçar a ligação no tempo de definição:

def x(static a=b):

11
Você poderia fazer: def x (a = None): E então, se um for Nenhum, definir a = datetime.datetime.now ()
Anon

20
Obrigado por isso. Eu realmente não conseguia entender por que isso me irrita sem fim. Você fez isso lindamente com um mínimo de confusão e confusão. Como alguém que vem da programação de sistemas em C ++ e, às vezes, ingenuamente "traduzindo" os recursos da linguagem, esse falso amigo me chutou no meio do nada, como os atributos de classe. Entendo por que as coisas são assim, mas não posso deixar de detestá-las, não importa o quão positivo possa vir disso. Pelo menos é tão contrária à minha experiência, que eu provavelmente vou (espero) nunca se esqueça ele ...
AndreasT

5
@ Andreas, depois de usar o Python por tempo suficiente, você começa a ver como é lógico para o Python interpretar as coisas como atributos de classe da maneira que faz - é apenas por causa das peculiaridades e limitações específicas de linguagens como C ++ (e Java e C # ...) que faz sentido que o conteúdo do class {}bloco seja interpretado como pertencente às instâncias :) Mas quando as classes são objetos de primeira classe, obviamente o natural é que seu conteúdo (na memória) reflita seu conteúdo (em código).
22711 Karl Knechtel

6
Estrutura normativa não é peculiar nem limitação no meu livro. Eu sei que pode ser desajeitado e feio, mas você pode chamá-lo de "definição" de algo. As linguagens dinâmicas me parecem um pouco anarquistas: é claro que todo mundo é livre, mas você precisa de estrutura para que alguém esvazie o lixo e pavimente a estrada. Acho que sou velho ... :)
AndreasT

4
A definição da função é executada no tempo de carregamento do módulo. O corpo da função é executado no momento da chamada da função. O argumento padrão faz parte da definição da função, não do corpo da função. (Fica mais complicado para funções aninhadas).
Lutz Prechelt

84

Bem, a razão é simplesmente que as ligações são feitas quando o código é executado, e a definição da função é executada, bem ... quando as funções são definidas.

Compare isto:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Esse código sofre exatamente a mesma ocorrência inesperada. bananas é um atributo de classe e, portanto, quando você adiciona itens, é adicionado a todas as instâncias dessa classe. O motivo é exatamente o mesmo.

É apenas "Como funciona", e fazê-lo funcionar de maneira diferente no caso da função provavelmente seria complicado e, no caso da classe, provavelmente impossível, ou pelo menos diminua bastante a instanciação do objeto, pois você teria que manter o código da classe por perto e execute-o quando objetos forem criados.

Sim, é inesperado. Mas quando o centavo cai, ele se encaixa perfeitamente na maneira como o Python funciona em geral. Na verdade, é uma boa ajuda para o ensino e, depois de entender por que isso acontece, você entenderá o python muito melhor.

Dito isto, ele deve aparecer com destaque em qualquer bom tutorial sobre Python. Porque, como você mencionou, todo mundo corre para esse problema mais cedo ou mais tarde.


Como você define um atributo de classe que é diferente para cada instância de uma classe?
Kieveli

19
Se for diferente para cada instância, não é um atributo de classe. Atributos de classe são atributos na CLASS. Daí o nome. Portanto, eles são iguais para todas as instâncias.
Lennart Regebro 15/07/2009

1
Como você define um atributo em uma classe diferente para cada instância de uma classe? (Redefinido para aqueles que não conseguiram determinar que uma pessoa não familiarizada com as convenções de nomenclatura do Python possa estar perguntando sobre variáveis ​​normais de membros de uma classe).
Kieveli 16/07/2009

@Kievieli: Você está falando sobre variáveis ​​de membros normais de uma classe. :-) Você define os atributos da instância dizendo self.attribute = value em qualquer método. Por exemplo __init __ ().
Lennart Regebro

@ Kieveli: Duas respostas: você não pode, porque qualquer coisa que você definir em um nível de classe será um atributo de classe e qualquer instância que acessar esse atributo acessará o mesmo atributo de classe; você pode, / classificar de /, usando propertys - que na verdade são funções no nível de classe que agem como atributos normais, mas salvam o atributo na instância em vez da classe (usando self.attribute = valuecomo Lennart disse).
Ethan Furman

66

Por que você não introspectiva?

Estou realmente surpreso que ninguém tenha realizado a introspecção perspicaz oferecida pelo Python ( 2e 3aplique) em chamadas.

Dada uma pequena função simples funcdefinida como:

>>> def func(a = []):
...    a.append(5)

Quando o Python o encontra, a primeira coisa que ele faz é compilá-lo para criar um codeobjeto para esta função. Enquanto essa etapa de compilação é concluída, o Python avalia * e, em seguida, armazena os argumentos padrão (uma lista vazia []aqui) no próprio objeto de função . Como resposta principal mencionada: a lista aagora pode ser considerada um membro da função func.

Então, vamos fazer uma introspecção, um antes e um depois para examinar como a lista é expandida dentro do objeto de função. Estou usando Python 3.xpara isso, para o Python 2 o mesmo se aplica (use __defaults__ou func_defaultsno Python 2; sim, dois nomes para a mesma coisa).

Função Antes da Execução:

>>> def func(a = []):
...     a.append(5)
...     

Depois que o Python executar essa definição, ele pegará todos os parâmetros padrão especificados ( a = []aqui) e os apertará no __defaults__atributo do objeto de função (seção relevante: Chamadas):

>>> func.__defaults__
([],)

Ok, uma lista vazia como entrada única __defaults__, exatamente como esperado.

Função após a execução:

Vamos agora executar esta função:

>>> func()

Agora, vamos vê-los __defaults__novamente:

>>> func.__defaults__
([5],)

Atônito? O valor dentro do objeto muda! As chamadas consecutivas à função agora serão simplesmente anexadas ao listobjeto incorporado :

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Então, aí está, a razão pela qual essa 'falha' acontece é porque os argumentos padrão fazem parte do objeto de função. Não há nada de estranho acontecendo aqui, é tudo um pouco surpreendente.

A solução comum para combater isso é usar Nonecomo padrão e, em seguida, inicializar no corpo da função:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Como o corpo da função é executado novamente a cada vez, você sempre obtém uma nova lista vazia se nenhum argumento foi passado a.


Para verificar se a lista __defaults__é a mesma usada na função, funcbasta alterar sua função para retornar ida lista ausada dentro do corpo da função. Em seguida, compare-o com a lista em __defaults__(posição [0]em __defaults__) e você verá como eles realmente se referem à mesma instância da lista:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Tudo com o poder da introspecção!


* Para verificar se o Python avalia os argumentos padrão durante a compilação da função, tente executar o seguinte:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

como você notará, input()é chamado antes do processo de criação da função e vinculá-la ao nome bar.


1
É id(...)necessário para essa última verificação ou o isoperador responderia à mesma pergunta?
Das

1
O @ das-g isfuncionaria perfeitamente, usei id(val)porque acho que pode ser mais intuitivo.
Dimitris Fasarakis Hilliard

Usar Nonecomo padrão limita severamente a utilidade da __defaults__introspecção, então não acho que funcione bem como uma defesa de ter o __defaults__trabalho da maneira que funciona. A avaliação preguiçosa faria mais para manter os padrões de função úteis dos dois lados.
Brilliand

58

Eu costumava pensar que criar os objetos em tempo de execução seria a melhor abordagem. Estou menos certo agora, já que você perde alguns recursos úteis, embora possa valer a pena, independentemente simplesmente para evitar confusão de novatos. As desvantagens de fazer isso são:

1. Desempenho

def foo(arg=something_expensive_to_compute())):
    ...

Se a avaliação do tempo de chamada for usada, a função cara será chamada toda vez que sua função for usada sem argumento. Você pagaria um preço caro em cada chamada ou precisaria armazenar em cache manualmente o valor externamente, poluindo seu espaço para nome e adicionando verbosidade.

2. Forçando parâmetros vinculados

Um truque útil é vincular parâmetros de um lambda à ligação atual de uma variável quando o lambda é criado. Por exemplo:

funcs = [ lambda i=i: i for i in range(10)]

Isso retorna uma lista de funções que retornam 0,1,2,3 ... respectivamente. Se o comportamento for alterado, eles serão vinculados iao valor de tempo de chamada de i, para que você obtenha uma lista de funções que todas retornaram 9.

A única maneira de implementar isso de outra forma seria criar um fechamento adicional com o i bound, ou seja:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspecção

Considere o código:

def foo(a='test', b=100, c=[]):
   print a,b,c

Podemos obter informações sobre os argumentos e padrões usando o comando inspect módulo, que

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Esta informação é muito útil para coisas como geração de documentos, metaprogramação, decoradores etc.

Agora, suponha que o comportamento dos padrões possa ser alterado para que isso seja equivalente a:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

No entanto, perdemos a capacidade de introspecção e ver quais são os argumentos padrão . Como os objetos não foram construídos, nunca podemos alcançá-los sem realmente chamar a função. O melhor que podemos fazer é armazenar o código-fonte e retorná-lo como uma string.


1
você também poderia obter introspecção se, para cada uma delas, houvesse uma função para criar o argumento padrão em vez de um valor. o módulo de inspeção chamará essa função.
Yairchu

@ SilentGhost: Estou falando sobre se o comportamento foi alterado para recriá-lo - criar uma vez é o comportamento atual e por que existe o problema padrão mutável.
187 Brian Brian

1
@ yairchu: Isso pressupõe que a construção seja segura (ou seja, sem efeitos colaterais). A introspecção dos argumentos não deve fazer nada, mas avaliar o código arbitrário pode acabar tendo um efeito.
187 Brian Brian

1
Um design de linguagem diferente geralmente significa apenas escrever coisas de maneira diferente. Seu primeiro exemplo pode ser facilmente escrito como: _expensive = caro (); def foo (arg = _expensive), se você especificamente não deseja que seja reavaliado.
Glenn Maynard

@ Glenn - é o que eu estava me referindo com "armazenar em cache a variável externamente" - é um pouco mais detalhada, e você acaba com variáveis ​​extras em seu espaço para nome.
Brian

55

5 pontos em defesa do Python

  1. Simplicidade : o comportamento é simples no seguinte sentido: A maioria das pessoas cai nessa armadilha apenas uma vez, e não várias.

  2. Consistência : Python sempre passa objetos, não nomes. O parâmetro padrão é, obviamente, parte do cabeçalho da função (não o corpo da função). Portanto, ele deve ser avaliado no tempo de carregamento do módulo (e somente no tempo de carregamento do módulo, a menos que esteja aninhado), não no tempo de chamada da função.

  3. Utilidade : Como Frederik Lundh aponta em sua explicação sobre "Valores padrão de parâmetros em Python" , o comportamento atual pode ser bastante útil para programação avançada. (Use com moderação.)

  4. Documentação suficiente : Na documentação mais básica do Python, o tutorial, o problema é anunciado em voz alta como um "Aviso importante" na primeira subseção da Seção "Mais sobre a definição de funções" . O aviso ainda usa negrito, que raramente é aplicado fora dos títulos. RTFM: Leia o manual fino.

  5. Meta-aprendizagem : cair na armadilha é realmente um momento muito útil (pelo menos se você é um aprendiz reflexivo), porque posteriormente você entenderá melhor o ponto "Consistência" acima e isso lhe ensinará muito sobre Python.


18
Levei um ano para descobrir que esse comportamento estava atrapalhando meu código na produção, e acabei removendo um recurso completo até que encontrei essa falha de design por acaso. Estou usando o Django. Como o ambiente de armazenamento temporário não tinha muitas solicitações, esse bug nunca teve nenhum impacto no controle de qualidade. Quando fomos ao ar e recebemos muitos pedidos simultâneos - algumas funções utilitárias começaram a sobrescrever os parâmetros uns dos outros! Fazendo falhas de segurança, bugs e o que não.
oriadam 5/09/15

7
@oriadam, sem ofensas, mas gostaria de saber como você aprendeu Python sem se deparar com isso antes. Estou apenas aprendendo Python agora e essa possível armadilha é mencionada no tutorial oficial do Python, ao lado da primeira menção dos argumentos padrão. (Como mencionado no ponto 4 desta resposta.) Suponho que a moral é - de maneira bastante antipática - ler os documentos oficiais da linguagem que você usa para criar software de produção.
Wildcard

Além disso, seria surpreendente (para mim) se uma função de complexidade desconhecida fosse chamada além da chamada de função que estou fazendo.
Vatine 2/09

52

Esse comportamento é fácil de explicar por:

  1. A declaração da função (classe etc.) é executada apenas uma vez, criando todos os objetos de valor padrão
  2. tudo é passado por referência

Assim:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a não muda - toda chamada de atribuição cria um novo objeto int - novo objeto é impresso
  2. b não muda - a nova matriz é criada a partir do valor padrão e impressa
  3. c alterações - a operação é realizada no mesmo objeto - e é impressa

(Na verdade, add é um mau exemplo, mas inteiros sendo imutável ainda é o meu ponto principal.)
Anon

Percebi isso para meu desgosto depois de verificar para ver que, com b definido como [], b .__ add __ ([1]) retorna [1], mas também deixa b imóvel [], mesmo que as listas sejam mutáveis. Foi mal.
Anon

@ ANon: existe __iadd__, mas não funciona com int. Claro. :-)
Veky

35

O que você está perguntando é por que isso:

def func(a=[], b = 2):
    pass

não é internamente equivalente a isso:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

exceto no caso de chamar explicitamente func (None, None), que ignoraremos.

Em outras palavras, em vez de avaliar os parâmetros padrão, por que não armazenar cada um deles e avaliá-los quando a função é chamada?

Provavelmente, uma resposta está aí - transformaria efetivamente todas as funções com parâmetros padrão em um fechamento. Mesmo que tudo esteja escondido no intérprete e não em um fechamento completo, os dados precisam ser armazenados em algum lugar. Seria mais lento e consumiria mais memória.


6
Não precisaria ser um fechamento - uma maneira melhor de pensar nisso seria simplesmente tornar o bytecode criando padrões a primeira linha de código - afinal, você está compilando o corpo nesse ponto de qualquer maneira - não há diferença real entre o código nos argumentos e código no corpo.
Brian

10
É verdade, mas isso ainda tornaria o Python mais lento, e seria realmente bastante surpreendente, a menos que você faça o mesmo para as definições de classe, o que tornaria estupidamente lento, pois você teria que executar novamente toda a definição de classe sempre que instanciar um classe. Como mencionado, a correção seria mais surpreendente que o problema.
Lennart Regebro 16/07/2009

Concordou com Lennart. Como Guido gosta de dizer, para cada recurso de idioma ou biblioteca padrão, há alguém por aí usando.
Jason Baker

6
Mudá-lo agora seria insanidade - estamos apenas explorando por que é assim. Se fizesse uma avaliação padrão tardia, não seria necessariamente surpreendente. Definitivamente, é verdade que essa diferença de análise central teria efeitos amplos e provavelmente obscuros no idioma como um todo.
Glenn Maynard

35

1) O chamado problema do "Argumento padrão mutável" é, em geral, um exemplo especial que demonstra que:
"Todas as funções com esse problema também sofrem de um problema de efeito colateral semelhante no parâmetro atual ",
que é contra as regras da programação funcional, geralmente indesejável e deve ser consertado juntos.

Exemplo:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Solução : a cópia de
uma solução absolutamente seguro é copyou deepcopyo objeto de entrada em primeiro lugar e, em seguida, fazer o que quer com a cópia.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Muitos tipos mutáveis ​​incorporados têm um método de cópia como some_dict.copy()ou some_set.copy()ou podem ser copiados facilmente como somelist[:]ou list(some_list). Cada objeto também pode ser copiado por copy.copy(any_object)ou mais minuciosamente copy.deepcopy()(o último é útil se o objeto mutável for composto de objetos mutáveis). Alguns objetos são baseados fundamentalmente em efeitos colaterais como o objeto "arquivo" e não podem ser significativamente reproduzidos por cópia. copiando

Problema de exemplo para uma pergunta SO semelhante

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

Ele não deve ser salvo em nenhum atributo público de uma instância retornada por esta função. (Supondo que atributos privados de instância não devam ser modificados de fora desta classe ou subclasses por convenção. Ou seja, _var1é um atributo privado)

Conclusão:
Os objetos dos parâmetros de entrada não devem ser modificados no local (mutados) nem devem ser vinculados a um objeto retornado pela função. (Se preferirmos a programação sem efeitos colaterais, o que é altamente recomendado. Consulte o Wiki sobre "efeito colateral" (os dois primeiros parágrafos são relevantes nesse contexto.).)

2)
Somente se o efeito colateral no parâmetro real for necessário, mas indesejável no parâmetro padrão, a solução útil serádef ...(var1=None): if var1 is None: var1 = [] Mais.

3) Em alguns casos, o comportamento mutável dos parâmetros padrão é útil .


5
Espero que você esteja ciente de que o Python não é uma linguagem de programação funcional.
Veky

6
Sim, o Python é uma linguagem multiparagigm com alguns recursos funcionais. ("Não faça todo problema parecer um prego só porque você tem um martelo.") Muitos deles estão nas melhores práticas de Python. O Python possui uma interessante programação funcional do HOWTO Outros recursos são fechamentos e currying, não mencionados aqui.
Hynekcer

1
Eu também acrescentaria, nesta fase tardia, que a semântica de atribuição do Python foi projetada explicitamente para evitar a cópia de dados quando necessário, de modo que a criação de cópias (e especialmente de cópias profundas) afetará negativamente o tempo de execução e o uso de memória. Portanto, eles devem ser usados ​​apenas quando necessário, mas os recém-chegados costumam ter dificuldade em entender quando isso acontece.
holdenweb

1
@holdenweb eu concordo. Uma cópia temporária é a maneira mais comum e, às vezes, a única maneira possível de proteger os dados mutáveis ​​originais de uma função estranha que os modifica potencialmente. Felizmente, uma função que modifica os dados de maneira irracional é considerada um bug e, portanto, incomum.
hynekcer

Eu concordo com esta resposta. E não entendo por que a def f( a = None )construção é recomendada quando você realmente quer dizer outra coisa. A cópia está correta, porque você não deve alterar argumentos. E quando o faz if a is None: a = [1, 2, 3], você copia a lista de qualquer maneira.
Koddo

30

Na verdade, isso não tem nada a ver com valores padrão, exceto que muitas vezes surge como um comportamento inesperado quando você escreve funções com valores padrão mutáveis.

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

Nenhum valor padrão à vista neste código, mas você obtém exatamente o mesmo problema.

O problema é que fooestá modificando uma variável mutável transmitida pelo chamador, quando o chamador não espera isso. Código como este seria bom se a função fosse chamada algo comoappend_5 ; o chamador chamaria a função para modificar o valor que eles transmitem e o comportamento seria esperado. Porém, é improvável que uma função desse tipo aceite um argumento padrão e provavelmente não retorne a lista (já que o chamador já tem uma referência a essa lista; a que acabou de passar).

Seu original foo, com um argumento padrão, não deve ser modificadoa se foi explicitamente passado ou obteve o valor padrão. Seu código deve deixar argumentos mutáveis ​​em paz, a menos que fique claro no contexto / nome / documentação que os argumentos devem ser modificados. Usar valores mutáveis ​​passados ​​como argumentos como temporários locais é uma péssima idéia, se estamos em Python ou não e se há argumentos padrão envolvidos ou não.

Se você precisar manipular destrutivamente um temporário local durante o cálculo de alguma coisa e precisar iniciar sua manipulação a partir de um valor de argumento, faça uma cópia.


7
Embora relacionado, acho que esse é um comportamento distinto (como esperamos appendmudar a"no local"). Que um mutável padrão não seja re-instanciado em cada chamada é o bit "inesperado" ... pelo menos para mim. :)
Andy Hayden

2
@AndyHayden se a função é esperada para modificar o argumento, por que não faria sentido ter um padrão?
Mark Ransom

@ MarkRansom o único exemplo em que consigo pensar é cache={}. No entanto, suspeito que esse "menor espanto" seja quando você não espera (ou deseja) a função que está chamando para alterar o argumento.
Andy Hayden

1
@AndyHayden Deixei minha própria resposta aqui com uma expansão desse sentimento. Diz-me o que pensas. Eu poderia adicionar o seu exemplo cache={}para completar.
Mark Ransom

1
@AndyHayden O ponto da minha resposta é que, se você ficar surpreso ao alterar acidentalmente o valor padrão de um argumento, terá outro bug, que é o código que pode alterar acidentalmente o valor de um chamador quando o padrão não foi usado. E observe que usar Nonee atribuir o padrão real se o argumento for None não resolve esse problema (considero um anti-padrão por esse motivo). Se você corrigir o outro bug, evitando alterar os valores dos argumentos, com ou sem padrão, você nunca notará ou se importará com esse comportamento "surpreendente".
Ben

27

Tópico já ocupado, mas pelo que li aqui, o seguinte me ajudou a perceber como está funcionando internamente:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

2
na verdade, isso pode ser um pouco confuso para os novatos, pois a = a + [1]sobrecargas a... considere alterá-lo para b = a + [1] ; print id(b)e adicionar uma linha a.append(2). Isso tornará mais óbvio que +em duas listas sempre cria uma nova lista (atribuída a b), enquanto uma modificação aainda pode ter a mesma id(a).
precisa saber é o seguinte

25

É uma otimização de desempenho. Como resultado dessa funcionalidade, quais dessas duas chamadas de função você acha mais rápidas?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Vou te dar uma dica. Aqui está a desmontagem (consulte http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Duvido que o comportamento experiente tenha um uso prático (quem realmente usou variáveis ​​estáticas em C, sem erros de reprodução?)

Como você pode ver, não é um benefício de desempenho ao utilizar argumentos padrão imutáveis. Isso pode fazer a diferença se for uma função chamada com frequência ou se o argumento padrão demorar muito para ser construído. Além disso, lembre-se de que Python não é C. Em C, você tem constantes praticamente livres. No Python, você não tem esse benefício.


24

Python: o argumento padrão mutável

Os argumentos padrão são avaliados no momento em que a função é compilada em um objeto de função. Quando usado pela função, várias vezes por essa função, eles são e permanecem o mesmo objeto.

Quando são mutáveis, quando são mutados (por exemplo, adicionando um elemento a ele), eles permanecem mutados em chamadas consecutivas.

Eles permanecem mutantes porque são o mesmo objeto todas as vezes.

Código equivalente:

Como a lista está vinculada à função quando o objeto da função é compilado e instanciado, isso:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

é quase exatamente equivalente a isso:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Demonstração

Aqui está uma demonstração - você pode verificar se eles são o mesmo objeto cada vez que são referenciados por

  • vendo que a lista é criada antes que a função termine de compilar em um objeto de função,
  • observando que o ID é o mesmo sempre que a lista é referenciada,
  • observando que a lista permanece alterada quando a função que a utiliza é chamada pela segunda vez,
  • observando a ordem em que a saída é impressa a partir da fonte (que eu convenientemente numerei para você):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

e executando-o com python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Isso viola o princípio de "Mínimo espanto"?

Essa ordem de execução é frequentemente confusa para novos usuários do Python. Se você entende o modelo de execução Python, torna-se bastante esperado.

A instrução usual para novos usuários de Python:

Mas é por isso que a instrução usual para novos usuários é criar seus argumentos padrão como este:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Isso usa o singleton None como um objeto sentinela para informar à função se obtivemos ou não um argumento diferente do padrão. Se não obtivermos nenhum argumento, na verdade, queremos usar uma nova lista vazia,[] , como padrão.

Como a seção do tutorial sobre fluxo de controle diz:

Se você não deseja que o padrão seja compartilhado entre as chamadas subseqüentes, você pode escrever a função assim:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

24

A resposta mais curta provavelmente seria "definição é execução", portanto, todo o argumento não faz sentido estrito. Como um exemplo mais artificial, você pode citar isto:

def a(): return []

def b(x=a()):
    print x

Espero que seja suficiente mostrar que não executar as expressões de argumento padrão no momento da execução da definstrução não é fácil ou não faz sentido, ou ambas.

Eu concordo que é uma pegadinha quando você tenta usar construtores padrão, no entanto.


20

Uma solução simples usando None

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

19

Esse comportamento não é surpreendente se você levar o seguinte em consideração:

  1. O comportamento dos atributos de classe somente leitura nas tentativas de atribuição e que
  2. Funções são objetos (explicados bem na resposta aceita).

O papel de (2) foi abordado extensivamente neste segmento. (1) é provavelmente o fator causador de espanto, pois esse comportamento não é "intuitivo" quando proveniente de outras línguas.

(1) é descrito no tutorial do Python sobre classes . Em uma tentativa de atribuir um valor a um atributo de classe somente leitura:

... todas as variáveis ​​encontradas fora do escopo mais interno são somente leitura ( uma tentativa de gravar em uma variável criará simplesmente uma nova variável local no escopo mais interno, mantendo inalterada a variável externa com nome idêntico ).

Olhe para o exemplo original e considere os pontos acima:

def foo(a=[]):
    a.append(5)
    return a

Aqui fooestá um objeto e aé um atributo de foo(disponível em foo.func_defs[0]). Como aé uma lista, aé mutável e, portanto, é um atributo de leitura / gravação defoo . É inicializado na lista vazia conforme especificado pela assinatura quando a função é instanciada e está disponível para leitura e gravação enquanto o objeto da função existir.

Chamar foosem substituir um padrão usa o valor desse padrão foo.func_defs. Nesse caso, foo.func_defs[0]é usado para ao escopo de código do objeto de função. Alterações na aalteração foo.func_defs[0], que fazem parte dofoo objeto e persistem entre a execução do código em foo.

Agora, compare isso com o exemplo da documentação sobre como emular o comportamento padrão do argumento de outras linguagens , de modo que os padrões de assinatura da função sejam usados ​​toda vez que a função for executada:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Levando em consideração (1) e (2) , pode-se perceber por que isso realiza o comportamento desejado:

  • Quando o fooobjeto da função é instanciado, foo.func_defs[0]é definido como None, um objeto imutável.
  • Quando a função é executada com padrões (sem parâmetro especificado Lna chamada de função), foo.func_defs[0]( None) fica disponível no escopo local como L.
  • Em cima L = [], a atribuição não pode ter êxito foo.func_defs[0], porque esse atributo é somente leitura.
  • Por (1) , uma nova variável local também chamada Lé criada no escopo local e usada para o restante da chamada de função. foo.func_defs[0]assim permanece inalterado para futuras invocações de foo.

19

Vou demonstrar uma estrutura alternativa para passar um valor de lista padrão para uma função (funciona igualmente bem com dicionários).

Como outros comentaram extensivamente, o parâmetro de lista é vinculado à função quando é definido, ao contrário de quando é executado. Como listas e dicionários são mutáveis, qualquer alteração nesse parâmetro afetará outras chamadas para esta função. Como resultado, as chamadas subseqüentes à função receberão essa lista compartilhada que pode ter sido alterada por outras chamadas para a função. Pior ainda, dois parâmetros estão usando o parâmetro compartilhado dessa função ao mesmo tempo, alheios às alterações feitas pela outra.

Método errado (provavelmente ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Você pode verificar se eles são um e o mesmo objeto usando id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Por "Python Efetivo: 59 maneiras específicas de escrever um Python melhor", de Brett Slatkin, item 20: Use Nonee Docstrings para especificar argumentos padrão dinâmicos (p. 48)

A convenção para alcançar o resultado desejado no Python é fornecer um valor padrão Nonee documentar o comportamento real na documentação.

Essa implementação garante que cada chamada para a função receba a lista padrão ou a lista passada para a função.

Método preferido :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

Pode haver casos de uso legítimos para o 'Método Errado', pelo qual o programador pretendia que o parâmetro da lista padrão fosse compartilhado, mas essa é provavelmente a exceção que a regra.


17

As soluções aqui são:

  1. Use Nonecomo seu valor padrão (ou não object) e ative-o para criar seus valores em tempo de execução; ou
  2. Use a lambdacomo seu parâmetro padrão e chame-o dentro de um bloco try para obter o valor padrão (esse é o tipo de coisa para a qual a abstração lambda é).

A segunda opção é boa porque os usuários da função podem transmitir uma chamada, que pode já existir (como a type)


16

Quando fazemos isso:

def foo(a=[]):
    ...

... atribuímos o argumento aa uma lista sem nome , se o chamador não passar o valor de a.

Para simplificar as coisas para esta discussão, vamos nomear temporariamente a lista sem nome. Que tal pavlo?

def foo(a=pavlo):
   ...

A qualquer momento, se o chamador não nos disser o que aé, reutilizamos pavlo.

Se pavloé mutável (modificável), e fooacaba modificando-o, um efeito que notamos na próxima vez fooé chamado sem especificar a.

Então é isso que você vê (lembre-se, pavloé inicializado para []):

 >>> foo()
 [5]

Agora pavloé [5].

A chamada foo()novamente modifica pavlonovamente:

>>> foo()
[5, 5]

A especificação de aquando a chamada foo()garante pavlonão é tocada.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Então, pavloainda é [5, 5].

>>> foo()
[5, 5, 5]

16

Às vezes, exploro esse comportamento como uma alternativa ao seguinte padrão:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Se singletonfor usado apenas por use_singleton, eu gosto do seguinte padrão como substituição:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Eu usei isso para instanciar classes de clientes que acessam recursos externos e também para criar dictos ou listas para memorização.

Como não acho que esse padrão seja bem conhecido, faço um breve comentário para evitar futuros mal-entendidos.


2
Prefiro adicionar um decorador para memorização e colocar o cache de memorização no próprio objeto de função.
Stefano Borini 06/02

Este exemplo não substitui o padrão mais complexo que você mostra, porque você chama _make_singletonno tempo de def no exemplo de argumento padrão, mas no tempo de chamada no exemplo global. Uma substituição verdadeira usaria algum tipo de caixa mutável para o valor padrão do argumento, mas a adição do argumento cria uma oportunidade para passar valores alternativos.
Yann Vernier

15

Você pode contornar isso substituindo o objeto (e, portanto, o empate pelo escopo):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

Feio, mas funciona.


3
Essa é uma boa solução nos casos em que você está usando um software de geração automática de documentação para documentar os tipos de argumentos esperados pela função. Colocar a = None e depois definir a como [] se a for None não ajuda o leitor a entender rapidamente o que é esperado.
Michael Scott Cuthbert

Ideia legal: religar esse nome garante que ele nunca possa ser modificado. Eu realmente gosto daquilo.
holdenweb

Esta é exatamente a maneira de fazer isso. O Python não faz uma cópia do parâmetro, portanto, cabe a você fazer a cópia explicitamente. Depois de ter uma cópia, você poderá modificar como quiser, sem efeitos colaterais inesperados.
Mark Ransom

13

Pode ser verdade que:

  1. Alguém está usando todos os recursos de idioma / biblioteca e
  2. Mudar o comportamento aqui seria desaconselhável, mas

é totalmente consistente manter os dois recursos acima e ainda fazer outro ponto:

  1. É um recurso confuso e é lamentável em Python.

As outras respostas, ou pelo menos algumas delas, fazem pontos 1 e 2, mas não 3, ou fazem o ponto 3 e minimizam os pontos 1 e 2. Mas todos os três são verdadeiros.

Pode ser verdade que trocar de cavalo no meio do caminho aqui exigiria uma quebra significativa, e que poderia haver mais problemas criados ao alterar o Python para lidar intuitivamente com o fragmento de abertura de Stefano. E pode ser verdade que alguém que conhecia bem o interior do Python pudesse explicar um campo minado de consequências. Contudo,

O comportamento existente não é pitonico e o Python é bem-sucedido porque muito pouco sobre a linguagem viola o princípio de menos espanto em qualquer lugar próximoisso mal. É um problema real, se seria sensato ou não arrancá-lo. É uma falha de design. Se você entende a linguagem muito melhor tentando rastrear o comportamento, posso dizer que o C ++ faz tudo isso e muito mais; você aprende muito navegando, por exemplo, erros sutis de ponteiro. Mas isso não é Pythonic: as pessoas que se preocupam com o Python o suficiente para perseverar diante desse comportamento são atraídas pela linguagem porque o Python tem muito menos surpresas do que outra linguagem. Dabblers e curiosos se tornam Pythonistas quando ficam surpresos com o pouco tempo necessário para que algo funcione - não por causa de um design fl - quero dizer, quebra-cabeça lógico oculto - que contraria as intuições de programadores que são atraídos por Python porque simplesmente funciona .


6
-1 Embora seja uma perspectiva defensável, essa não é uma resposta, e eu discordo dela. Muitas exceções especiais geram seus próprios casos de canto.
Marcin

3
Então, é "incrivelmente ignorante" dizer que, em Python, faria mais sentido que um argumento padrão de [] permanecesse [] toda vez que a função for chamada?
Christos Hayward

3
E é ignorante considerar como um idioma infeliz a configuração de um argumento padrão como Nenhum e, no corpo do corpo da configuração da função, se argumento == Nenhum: argumento = []? É ignorante considerar esse idioma lamentável com a frequência que as pessoas desejam o que um recém-chegado ingênuo esperaria: se você atribuir f (argumento = []), o argumento será automaticamente padronizado para um valor de []?
precisa

3
Mas em Python, parte do espírito da linguagem é que você não precisa fazer muitos mergulhos profundos; array.sort () funciona e funciona independentemente de quão pouco você entenda sobre classificação, big-O e constantes. A beleza do Python no mecanismo de classificação de matrizes, para dar um dos inúmeros exemplos, é que você não precisa se aprofundar nos aspectos internos. E para dizer de outra maneira, a beleza do Python é que normalmente não é necessário mergulhar profundamente na implementação para obter algo que simplesmente funcione. E há uma solução alternativa (... se argumento == Nenhum: argumento = []), FAIL.
Christos Hayward

3
Como autônoma, a instrução x=[]significa "criar um objeto de lista vazio e vincular o nome 'x' a ele". Portanto, def f(x=[])também é criada uma lista vazia. Nem sempre é vinculado a x, então, ao contrário, é vinculado ao substituto padrão. Mais tarde, quando f () é chamado, o padrão é eliminado e vinculado a x. Como a própria lista vazia foi esquivada, essa mesma lista é a única coisa disponível para vincular a x, independentemente de algo ter sido preso nela ou não. Como poderia ser de outra maneira?
Jerry B

10

Esta não é uma falha de design . Quem tropeça nisso está fazendo algo errado.

Existem 3 casos em que você pode encontrar esse problema:

  1. Você pretende modificar o argumento como um efeito colateral da função. Nesse caso, nunca faz sentido ter um argumento padrão. A única exceção é quando você está abusando da lista de argumentos para ter atributos de função, por exemplo cache={}, e não se espera que você chame a função com um argumento real.
  2. Você pretende deixar o argumento não modificado, mas acidentalmente o modificou. Isso é um bug, conserte.
  3. Você pretende modificar o argumento para uso dentro da função, mas não esperava que a modificação fosse visível fora da função. Nesse caso, você precisa fazer uma cópia do argumento, se foi o padrão ou não! O Python não é uma linguagem de chamada por valor, portanto, não cria a cópia para você, você precisa ser explícito.

O exemplo na pergunta pode ser da categoria 1 ou 3. É estranho que ambos modifiquem a lista passada e a retornem; você deve escolher um ou outro.


"Fazendo algo errado" é o diagnóstico. Dito isto, acho que há momentos em que nenhum padrão é útil, mas geralmente você não deseja modificar se for aprovado um mutável nesse caso (2). O cache={}padrão é realmente uma solução apenas para entrevistas, no código real que você provavelmente deseja @lru_cache!
Andy Hayden

9

Esse "bug" me deu muitas horas extras de trabalho! Mas estou começando a ver um uso potencial dele (mas eu gostaria que ainda estivesse no momento da execução)

Vou dar o que vejo como um exemplo útil.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

imprime o seguinte

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Apenas mude a função para ser:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Eu acho que a resposta para essa pergunta está em como o python passa dados para o parâmetro (passa por valor ou por referência), não por mutabilidade ou como o python lida com a instrução "def".

Uma breve introdução. Primeiro, existem dois tipos de dados em python, um é o tipo de dados elementar simples, como números, e outro é objetos. Segundo, ao passar dados para parâmetros, o python passa o tipo de dados elementar por valor, ou seja, faz uma cópia local do valor para uma variável local, mas passa o objeto por referência, ou seja, ponteiros para o objeto.

Admitindo os dois pontos acima, vamos explicar o que aconteceu com o código python. É apenas por passar por referência para objetos, mas não tem nada a ver com mutável / imutável, ou sem dúvida o fato de que a instrução "def" é executada apenas uma vez quando é definida.

[] é um objeto, então python passa a referência de [] para a, ou seja, aé apenas um ponteiro para [] que fica na memória como um objeto. Há apenas uma cópia de [] com, no entanto, muitas referências a ele. Para o primeiro foo (), a lista [] é alterada para 1 pelo método append. Mas observe que há apenas uma cópia do objeto de lista e esse objeto agora se torna 1 . Ao executar o segundo foo (), o que a página da web do effbot diz (os itens não são mais avaliados) está errado. aé avaliado como o objeto de lista, embora agora o conteúdo do objeto seja 1 . Este é o efeito de passar por referência! O resultado de foo (3) pode ser facilmente derivado da mesma maneira.

Para validar ainda mais minha resposta, vamos dar uma olhada em dois códigos adicionais.

====== No. 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]é um objeto, assim é None(o primeiro é mutável enquanto o segundo é imutável. Mas a mutabilidade não tem nada a ver com a questão). Nenhum está em algum lugar no espaço, mas sabemos que está lá e há apenas uma cópia de Nenhum lá. Portanto, toda vez que foo é chamado, os itens são avaliados (em oposição a alguma resposta que é avaliada apenas uma vez) como Nenhum, para ser claro, a referência (ou o endereço) de Nenhum. Em seguida, no foo, o item é alterado para [], ou seja, aponta para outro objeto que possui um endereço diferente.

====== No. 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

A invocação de foo (1) faz com que os itens aponte para um objeto de lista [] com um endereço, por exemplo, 11111111. o conteúdo da lista é alterado para 1 na função foo na sequela, mas o endereço não é alterado, ainda 11111111 Então foo (2, []) está chegando. Embora o [] in foo (2, []) tenha o mesmo conteúdo que o parâmetro padrão [] ao chamar foo (1), o endereço deles é diferente! Desde que nós fornecemos o parâmetro explicitamente, itemstem que levar o endereço deste novo[] , digamos 2222222, e devolvê-lo depois de fazer algumas alterações. Agora foo (3) é executado. desde apenasxé fornecido, os itens precisam assumir seu valor padrão novamente. Qual é o valor padrão? Ele é definido ao definir a função foo: o objeto de lista localizado em 11111111. Portanto, os itens são avaliados como o endereço 11111111 com um elemento 1. A lista localizada em 2222222 também contém um elemento 2, mas não é apontado por nenhum item Mais. Conseqüentemente, um apêndice 3 fará items[1,3].

Pelas explicações acima, podemos ver que a página da web do effbot recomendada na resposta aceita falhou em fornecer uma resposta relevante a esta pergunta. Além do mais, acho que um ponto na página do effbot está errado. Eu acho que o código sobre o UI.Button está correto:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Cada botão pode conter uma função distinta de retorno de chamada que exibirá valores diferentes de i. Eu posso fornecer um exemplo para mostrar isso:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Se executarmos x[7](), obteremos 7 conforme o esperado e x[9]()forneceremos 9, outro valor de i.


5
Seu último ponto está errado. Experimente e você verá que x[7]()é 9.
Duncan

2
"python passa o tipo de dado elementar por valor, ou seja, faz uma cópia local do valor para uma variável local" está completamente incorreto. Estou surpreso que alguém possa obviamente conhecer muito bem o Python, mas tenha um entendimento tão horrível dos fundamentos. :-(
Veky

6

TLDR: os padrões de tempo de definição são consistentes e estritamente mais expressivos.


A definição de uma função afeta dois escopos: o escopo de definição que contém a função e o escopo de execução contido na função. Embora seja bastante claro como os blocos são mapeados para os escopos, a questão é onde def <name>(<args=defaults>):pertence:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

A def nameparte deve ser avaliada no escopo definido - nameafinal, queremos estar disponíveis lá. Avaliar a função apenas dentro de si mesma a tornaria inacessível.

Como parameteré um nome constante, podemos "avaliá-lo" ao mesmo tempo que def name. Isso também tem a vantagem de produzir a função com uma assinatura conhecida como name(parameter=...):, em vez de vazia name(...):.

Agora, quando avaliar default?

A consistência já diz "na definição": todo o resto def <name>(<args=defaults>):é melhor avaliado também na definição. Atrasar partes dela seria a escolha surpreendente.

As duas opções também não são equivalentes: Se defaultfor avaliada no momento da definição, ainda poderá afetar o tempo de execução. Se defaultfor avaliado no tempo de execução, não poderá afetar o tempo de definição. Escolher "na definição" permite expressar os dois casos, enquanto escolher "na execução" pode expressar apenas um:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

"A consistência já diz" na definição ": todo o resto def <name>(<args=defaults>):é melhor avaliado também na definição." Eu não acho que a conclusão se segue da premissa. Só porque duas coisas estão na mesma linha não significa que elas devam ser avaliadas no mesmo escopo. defaulté uma coisa diferente do resto da linha: é uma expressão. Avaliar uma expressão é um processo muito diferente da definição de uma função.
LarsH 23/09/19

As definições da função @LarsH são avaliadas em Python. Se isso é de uma declaração ( def) ou expressão ( lambda) não altera que a criação de uma função significa avaliação - especialmente de sua assinatura. E os padrões fazem parte da assinatura de uma função. Isso não defaults médios têm de ser avaliados imediatamente - dicas tipo não pode, por exemplo. Mas certamente sugere que deveriam, a menos que haja uma boa razão para não fazê-lo.
MisterMiyagi 23/09/19

OK, criar uma função significa avaliação em algum sentido, mas obviamente não no sentido de que toda expressão dentro dela é avaliada no momento da definição. A maioria não é. Não está claro para mim em que sentido a assinatura é especialmente "avaliada" no momento da definição, assim como o corpo da função é "avaliado" (analisado em uma representação adequada); enquanto expressões no corpo da função claramente não são avaliadas no sentido completo. Desse ponto de vista, a consistência diria que as expressões na assinatura também não deveriam ser avaliadas "totalmente".
LarsH 24/09/19

Não quero dizer que você esteja errado, apenas que sua conclusão não decorre apenas da consistência.
LarsH 24/09/19

Os padrões @LarsH não fazem parte do corpo, nem estou afirmando que a consistência é o único critério. Você pode fazer uma sugestão de como esclarecer a resposta?
MisterMiyagi

3

Todas as outras respostas explicam por que esse é realmente um comportamento agradável e desejado ou por que você não deveria estar precisando disso. O meu é para aqueles teimosos que querem exercer seu direito de dobrar o idioma à sua vontade, e não o contrário.

"Corrigiremos" esse comportamento com um decorador que copiará o valor padrão em vez de reutilizar a mesma instância para cada argumento posicional deixado em seu valor padrão.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Agora vamos redefinir nossa função usando este decorador:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

Isso é particularmente interessante para funções que recebem vários argumentos. Comparar:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

com

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

É importante observar que a solução acima é interrompida se você tentar usar a palavra-chave args, assim:

foo(a=[4])

O decorador pode ser ajustado para permitir isso, mas deixamos isso como um exercício para o leitor;)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.