Argumento padrão mutável do Python: Por quê?


20

Eu sei que argumentos padrão são criados no momento da inicialização da função e nem sempre que a função é chamada. Veja o seguinte código:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

O que não entendo é por que isso foi implementado dessa maneira. Quais benefícios esse comportamento oferece em relação a uma inicialização a cada chamada?


Isso já é discutido em detalhes surpreendentes no Stack Overflow: stackoverflow.com/q/1132941/5419599
Wildcard

Respostas:


14

Eu acho que o motivo é a simplicidade da implementação. Deixe-me elaborar.

O valor padrão da função é uma expressão que você precisa avaliar. No seu caso, é uma expressão simples que não depende do fechamento, mas pode ser algo que contém variáveis ​​livres - def ook(item, lst = something.defaultList()). Se você deseja projetar o Python, terá uma escolha - você o avalia uma vez quando a função é definida ou toda vez que a função é chamada. O Python escolhe a primeira (ao contrário do Ruby, que acompanha a segunda opção).

Existem alguns benefícios para isso.

Primeiro, você obtém um aumento na velocidade e na memória. Na maioria dos casos, você terá argumentos padrão imutáveis ​​e o Python poderá construí-los apenas uma vez, em vez de em todas as chamadas de função. Isso economiza (alguns) memória e tempo. Obviamente, isso não funciona muito bem com valores mutáveis, mas você sabe como dar a volta.

Outro benefício é a simplicidade. É muito fácil entender como a expressão é avaliada - ela usa o escopo lexical quando a função é definida. Se eles seguissem o outro caminho, o escopo lexical poderia mudar entre a definição e a invocação e dificultar um pouco a depuração. O Python ajuda bastante a ser extremamente direto nesses casos.


3
Ponto interessante - embora seja geralmente o princípio da menor surpresa com o Python. Algumas coisas são simples no sentido formal de complexidade do modelo, mas não óbvias e surpreendentes, e acho que isso conta.
Steve314

1
A questão da menor surpresa aqui é a seguinte: se você tiver a semântica de avaliação em todas as chamadas, poderá obter um erro se o fechamento mudar entre duas chamadas de função (o que é bem possível). Isso pode ser mais surpreendente do que saber que é avaliado uma vez. Claro, pode-se argumentar que, quando você vem de outros idiomas, exceto semântica avaliar-em-cada-call e que é a surpresa, mas você pode ver como ele vai nos dois sentidos :)
Stefan Kanev

Bom ponto sobre o escopo
0xc0de 26/07/12

Eu acho que o escopo é realmente o pouco mais importante. Como você não está restrito a constantes como padrão, pode ser necessário variáveis ​​que não estão no escopo no site de chamada.
Mark Ransom

5

Uma maneira de colocar isso é que o parâmetro lst.append(item) não muda lst. lstainda faz referência à mesma lista. Só que o conteúdo dessa lista foi alterado.

Basicamente, o Python não possui (que eu me lembre) nenhuma variável constante ou imutável - mas possui alguns tipos constantes e imutáveis. Você não pode modificar um valor inteiro, apenas pode substituí-lo. Mas você pode modificar o conteúdo de uma lista sem substituí-lo.

Como um número inteiro, você não pode modificar uma referência, apenas pode substituí-la. Mas você pode modificar o conteúdo do objeto que está sendo referenciado.

Quanto à criação do objeto padrão uma vez, imagino que seja principalmente uma otimização, para economizar nas despesas de criação de objetos e coleta de lixo.


+1 Exatamente. É importante entender a camada de indireção - que uma variável não é um valor; em vez disso, faz referência a um valor. Para alterar uma variável, o valor pode ser trocado , ou mutação (se é mutável).
Joonas Pulakka

Quando confrontado com algo complicado envolvendo variáveis ​​em python, acho útil considerar "=" como o "operador de ligação de nome"; o nome sempre é recuperado, independentemente de o que estamos vinculando seja novo (objeto novo ou instância do tipo imutável) ou não.
StarWeaver 28/08/14

4

Quais benefícios esse comportamento oferece em relação a uma inicialização a cada chamada?

Permite selecionar o comportamento desejado, como demonstrado no seu exemplo. Portanto, se você deseja que o argumento padrão seja imutável, use um valor imutável , como Noneou 1. Se você deseja tornar o argumento padrão mutável, use algo mutável, como []. É apenas flexibilidade, embora seja certo que pode morder se você não souber.


2

Eu acho que a resposta real é: Python foi escrito como uma linguagem processual e só adotou aspectos funcionais após o fato. O que você procura é que a padronização dos parâmetros seja feita como um fechamento, e os fechamentos no Python são realmente apenas pela metade. Para evidência desta tentativa:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

o que dá [2, 2, 2]onde você esperaria um verdadeiro fechamento para produzir [0, 1, 2].

Há muitas coisas que eu gostaria que o Python tivesse a capacidade de agrupar os parâmetros padrão nos fechamentos. Por exemplo:

def foo(a, b=a.b):
    ...

Seria extremamente útil, mas "a" não está no escopo no momento da definição da função, portanto você não pode fazer isso e, em vez disso, precisa fazer o desajeitado:

def foo(a, b=None):
    if b is None:
        b = a.b

O que é quase a mesma coisa ... quase.



1

Um grande benefício é a memorização. Este é um exemplo padrão:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

e para comparação:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Medições de tempo no ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s

0

Isso acontece porque a compilação no Python é executada executando o código descritivo.

Se alguém dissesse

def f(x = {}):
    ....

seria bem claro que você queria uma nova matriz a cada vez.

Mas e se eu disser:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Aqui, acho que quero criar coisas em várias listas e ter um único exemplo global quando não especificar uma lista.

Mas como o compilador adivinharia isso? Então, por que tentar? Poderíamos confiar se esse nome foi ou não, e isso pode ajudar às vezes, mas na verdade seria apenas um palpite. Ao mesmo tempo, há uma boa razão para não tentar - consistência.

Como é, Python apenas executa o código. A variável list_of_all já está atribuída a um objeto, de modo que o objeto é passado por referência no código que padroniza x da mesma maneira que uma chamada para qualquer função obteria uma referência a um objeto local chamado aqui.

Se quiséssemos distinguir o caso não nomeado do caso nomeado, isso envolveria o código na compilação executando a atribuição de uma maneira significativamente diferente da executada no tempo de execução. Portanto, não fazemos o caso especial.


-5

Isso acontece porque as funções no Python são objetos de primeira classe :

Os valores padrão dos parâmetros são avaliados 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 .

Continua explicando que editar o valor do parâmetro modifica o valor padrão para chamadas subseqüentes e que uma solução simples de usar None como padrão, com um teste explícito no corpo da função, é tudo o que é necessário para garantir nenhuma surpresa.

O que significa que def foo(l=[])se torna uma instância dessa função quando chamada e é reutilizada para outras chamadas. Pense nos parâmetros de função como se separando dos atributos de um objeto.

Os profissionais podem incluir alavancar isso para que as classes tenham variáveis ​​estáticas do tipo C. Portanto, é melhor declarar os valores padrão None e inicializá-los conforme necessário:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

rendimentos:

[5] [5] [5] [5]

em vez do inesperado:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]


5
Não. Você pode definir funções (de primeira classe ou não) de maneira diferente para avaliar a expressão de argumento padrão novamente para cada chamada. E tudo depois disso, ou seja, aproximadamente 90% da resposta, está completamente fora de questão. -1

1
Então, compartilhe conosco esse conhecimento de como definir funções para avaliar o argumento padrão de cada chamada. Gostaria de saber de uma maneira mais simples do que o Python Docs recomenda.
Invertido

2
Em um nível de design de linguagem, quero dizer. A definição da linguagem Python atualmente afirma que os argumentos padrão são tratados da maneira que são; poderia igualmente afirmar que os argumentos padrão são tratados de alguma outra maneira. Como você está respondendo "é assim que as coisas são" à pergunta "por que as coisas são do jeito que são"?

O Python poderia ter implementado parâmetros padrão semelhantes à maneira como o Coffeescript faz isso. Ele inseria o bytecode para verificar se havia parâmetros ausentes e, se eles estivessem ausentes, avaliaria a expressão.
Winston Ewert
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.