Pergunta

Eu tive um momento muito difícil com a compreensão da causa raiz de um problema em um algoritmo. Então, simplificando a etapa funções a passo eu descobri que a avaliação dos argumentos padrão em Python não se comporta como eu esperava.

O código é a seguinte:

class Node(object):
    def __init__(self, children = []):
        self.children = children

O problema é que cada instância de ações classe do nó o mesmo atributo children, se o atributo não é dado explicitamente, tais como:

>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176

Eu não entendo a lógica desta decisão design? Por que os designers Python decidir que argumentos padrão devem ser avaliadas em tempo de definição? Isto parece muito contra-intuitivo para mim.

Foi útil?

Solução

A alternativa seria bastante pesado - armazenar "valores de argumento padrão" no objeto de função como "thunks" do código a ser executado uma e outra vez cada vez que a função é chamada sem um valor especificado para que o argumento - e tornaria muito mais difícil de obter ligação antecipada (ligação em tempo de def), que é muitas vezes o que você quer. Por exemplo, em Python como ele existe:

def ack(m, n, _memo={}):
  key = m, n
  if key not in _memo:
    if m==0: v = n + 1
    elif n==0: v = ack(m-1, 1)
    else: v = ack(m-1, ack(m, n-1))
    _memo[key] = v
  return _memo[key]

... escrever uma função memoized como o acima é uma tarefa fundamental. Da mesma forma:

for i in range(len(buttons)):
  buttons[i].onclick(lambda i=i: say('button %s', i))

... o i=i simples, contando com o (tempo de definição) no início de ligação de valores arg padrão, é uma maneira trivialmente simples para obter ligação antecipada. Assim, a regra atual é simples, direto e permite que você faça tudo o que quiser de uma forma que é extremamente fácil de explicar e entender: se você quiser a ligação tardia do valor de uma expressão, avaliar essa expressão no corpo da função; se você quiser ligação antecipada, avaliá-lo como o valor padrão de uma arg.

A alternativa, forçando a ligação tardia para ambos situação, que não oferecem essa flexibilidade, e iria forçá-lo a ir através de aros (como envolvendo a função em uma fábrica de fechamento) cada vez que você precisava de ligação antecipada, como nos exemplos acima -. Ainda mais clichê-peso pesado obrigou o programador por essa decisão de projeto hipotético (além de os "invisíveis" de gerar e avaliar repetidamente thunks em todo o lugar)

Em outras palavras, "Deve haver um, e de preferência apenas um, maneira óbvia de fazer isso [1]": quando você quer a ligação tardia, já existe uma maneira perfeitamente óbvio para alcançá-lo (uma vez que todo o código da função só é executado em tempo de chamada, obviamente, tudo avaliado não é tardia); ter produtos de avaliação default-arg ligação início dá-lhe uma maneira óbvia para alcançar ligação antecipada, bem como (um plus -!) ao invés de dar DUAS maneiras óbvias para obter a ligação tardia e nenhuma maneira óbvia para obter ligação antecipada (a menos -!).

[1]: "Apesar de que maneira pode não ser óbvio à primeira vista, a menos que você seja holandês"

Outras dicas

A questão é esta.

É muito caro para avaliar uma função como um inicializador cada vez que a função é chamada .

  • 0 é um literal simples. Avaliá-lo uma vez, usá-lo para sempre.

  • int é uma função (como lista) que teria de ser avaliada cada vez que é exigido como um inicializador.

O [] construção é literal, como 0, isso significa que "este objeto exato".

O problema é que algumas pessoas esperam que a meios list como em "avaliar esta função para mim, por favor, para obter o objeto que é o inicializador".

Seria um peso esmagador para adicionar a instrução if necessário fazer essa avaliação o tempo todo. É melhor tomar todos os argumentos como literais e não fazer qualquer avaliação função adicional como parte de tentar fazer uma avaliação da função.

Além disso, mais fundamentalmente, é tecnicamente impossível para implementar padrões argumento como avaliações de função.

Considere, por um momento o horror recursiva deste tipo de circularidade. Digamos que em vez de valores padrão sendo literais, nós permitimos que eles sejam funções que são avaliados cada vez que os valores padrão de um parâmetro são necessários.

[Seria equivalente as obras maneira collections.defaultdict.]

def aFunc( a=another_func ):
    return a*2

def another_func( b=aFunc ):
    return b*3

O que é o valor de another_func()? Para obter o padrão para b, deve avaliar aFunc, o que requer um eval de another_func. Opa.

É claro que na sua situação é difícil de entender. Mas você deve ver, que a avaliação args padrão cada vez que colocaria um fardo pesado de tempo de execução no sistema.

Além disso, você deve saber, que em caso de tipos de recipiente pode ocorrer este problema - mas você pode contorná-la, fazendo a coisa explícita:

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children

A solução para este, discutido aqui (e muito sólida), é a seguinte:

class Node(object):
    def __init__(self, children = None):
        self.children = [] if children is None else children

Quanto ao porquê de olhar para uma resposta de von Löwis, mas é provável porque a definição da função torna um objeto de código devido à arquitetura de Python, e pode não haver uma facilidade para trabalhar com tipos de referência como esta em argumentos padrão.

Eu achava que isso era absurdo também, até que eu aprendi Python argumentos implementos padrão.

A função de um objeto. No tempo de carregamento, Python cria o objeto função, avalia os padrões na demonstração def, coloca-los em uma tupla, e acrescenta que tupla como um atributo da função chamada func_defaults. Então, quando uma função é chamada, se a chamada não fornecer um valor, Python agarra o valor padrão de func_defaults.

Por exemplo:

>>> class C():
        pass

>>> def f(x=C()):
        pass

>>> f.func_defaults
(<__main__.C instance at 0x0298D4B8>,)

Assim, todas as chamadas para f que não fornecem um argumento vai usar a mesma instância do C, porque esse é o valor padrão.

Quanto porque Python faz isso desta maneira: bem, isso tupla pode contêm funções que são chamados cada vez que era necessário um valor argumento padrão. Além do problema imediatamente óbvio de desempenho, você começar a entrar em um universo de casos especiais, como o armazenamento de valores literais em vez de funções para tipos não-mutáveis ??para evitar chamadas de funções desnecessárias. E é claro que existem implicações de desempenho abundância.

O comportamento real é realmente simples. E há uma solução trivial, no caso em que você deseja um valor padrão a ser produzido por uma chamada de função em tempo de execução:

def f(x = None):
   if x == None:
      x = g()

Isto vem de ênfase de python na sintaxe e execução simplicidade. uma declaração def ocorre em um determinado ponto durante a execução. Quando os alcances interpretador Python esse ponto, que avalia o código nessa linha, e, em seguida, cria um objeto de código a partir do corpo da função, que será executado mais tarde, quando você chamar a função.

É uma simples divisão entre a declaração função e corpo da função. A declaração é executado quando é atingido no código. O corpo é executado em tempo de chamada. Note-se que a declaração é executado cada vez que é atingido, assim você pode criar múltiplas funções por looping.

funcs = []
for x in xrange(5):
    def foo(x=x, lst=[]):
        lst.append(x)
        return lst
    funcs.append(foo)
for func in funcs:
    print "1: ", func()
    print "2: ", func()

Cinco funções separadas foram criadas, com uma lista separada criado cada vez que a declaração de função foi executado. Em cada ciclo através funcs, a mesma função é executado duas vezes em cada passagem por meio de, usando a mesma lista de cada vez. Isto dá os resultados:

1:  [0]
2:  [0, 0]
1:  [1]
2:  [1, 1]
1:  [2]
2:  [2, 2]
1:  [3]
2:  [3, 3]
1:  [4]
2:  [4, 4]

Outros deram-lhe a solução, de usar param = None, e atribuindo uma lista no corpo se o valor for Nenhum, que é totalmente python idiomática. É um pouco feio, mas a simplicidade é poderosa, ea solução não é muito doloroso.

Editado para acrescentar: Para mais discussão sobre este assunto, veja o artigo de effbot aqui: http: // effbot.org/zone/default-values.htm , ea referência da linguagem, aqui: http://docs.python.org/reference/compound_stmts.html#function

definições de funções Python são apenas um código, como todos os outros códigos; eles não são "mágica" da mesma forma que algumas línguas são. Por exemplo, em Java você pode referir-se "agora" a algo definido "depois":

public static void foo() { bar(); }
public static void main(String[] args) { foo(); }
public static void bar() {}

mas em Python

def foo(): bar()
foo()   # boom! "bar" has no binding yet
def bar(): pass
foo()   # ok

Assim, o argumento padrão é avaliada no momento em que essa linha de código é avaliado!

Porque se tivessem, então alguém postar uma pergunta perguntando por que ele não era o contrário :-P

Suponha agora que eles tinham. Como você implementar o comportamento atual, se necessário? É fácil criar novos objetos dentro de uma função, mas você não pode "incriado"-los (você pode excluí-los, mas não é o mesmo).

Vou fornecer uma opinião divergente, por addessing os principais argumentos em outras mensagens.

Avaliando argumentos padrão quando a função é executada seria ruim para o desempenho.

Eu acho isso difícil de acreditar. Se as atribuições argumento padrão como foo='some_string' realmente adicionar uma quantidade inaceitável de sobrecarga, tenho certeza de que seria possível identificar atribuições para literais imutáveis ??e precompute-los.

Se você quer uma atribuição padrão com um objeto mutável como foo = [], apenas foo = None uso, seguido por foo = foo or [] no corpo da função.

Embora isso possa ser problemático em casos individuais, como um padrão de design não é muito elegante. Acrescenta código clichê e obscurece valores de argumento padrão. Padrões como foo = foo or ... não funcionam se foo pode ser um objeto como uma matriz numpy com valor de verdade indefinido. E em situações onde None é um valor argumento significativo que pode ser passado intencionalmente, não pode ser usado como uma sentinela e esta solução torna-se realmente feio.

O comportamento atual é útil para padrão mutável objetos que deve ser compartilhada através de chamadas de função.

Eu ficaria feliz em ver evidências em contrário, mas na minha experiência este caso de uso é muito menos freqüente do que objetos mutáveis ??que deve ser criado de novo cada vez que a função é chamada. Para mim, isso também parece ser um caso de uso mais avançado, enquanto atribuições padrão acidentais com os recipientes vazios são uma pegadinha comum para novos programadores Python. Portanto, o princípio da menor surpresa sugere valores de argumento padrão devem ser avaliados quando a função é executada.

Além disso, parece-me que existe uma solução fácil para objetos mutáveis ??que devem ser compartilhados entre chamadas de função:. Inicializar-los fora da função

Então, eu diria que esta foi uma decisão de design ruim. Meu palpite é que ele foi escolhido porque a sua implementação é realmente mais simples e porque tem um caso de uso válido (embora limitado). Infelizmente, eu não acho que isso nunca vai mudar, uma vez que os principais desenvolvedores Python quer evitar uma repetição da quantidade de trás incompatibilidade que Python 3 introduzido.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top