Qual é a maneira pythônica de evitar parâmetros padrão que são listas vazias?

117

Às vezes, parece natural ter um parâmetro padrão que é uma lista vazia. Ainda assim, o Python apresenta um comportamento inesperado nessas situações .

Se, por exemplo, tenho uma função:

def my_func(working_list = []):
    working_list.append("a")
    print(working_list)

A primeira vez que for chamado de padrão funcionará, mas as chamadas posteriores atualizarão a lista existente (com um "a" em cada chamada) e imprimirão a versão atualizada.

Então, qual é a forma pítônica de obter o comportamento que desejo (uma lista nova em cada ligação)?

John Mulder
fonte
17
se alguém estiver interessado em saber por que isso acontece, confira effbot.org/zone/default-values.htm
Ryan Haining
O mesmo comportamento acontece para conjuntos, embora você precise de um exemplo um pouco mais complicado para que ele apareça como um bug.
abeboparebop
À medida que os links morrem, deixe-me apontar explicitamente que esse é o comportamento desejado. As variáveis ​​padrão são avaliadas na definição da função (o que acontece na primeira vez que é chamada), e NÃO cada vez que a função é chamada. Conseqüentemente, se você alterar um argumento padrão mutável, qualquer chamada de função subsequente poderá usar apenas o objeto mutado.
Moritz

Respostas:

150
def my_func(working_list=None):
    if working_list is None: 
        working_list = []

    working_list.append("a")
    print(working_list)

Os documentos dizem que você deve usar Nonecomo padrão e testar explicitamente no corpo da função.

HenryR
fonte
1
É melhor dizer: if working_list == None: ou if working_list: ??
John Mulder
2
Esta é a forma preferida de fazer em python, mesmo que eu não goste, pois é feia. Eu diria que a prática recomendada seria "se working_list for None".
e-satis
21
A forma preferida neste exemplo é dizer: se working_list for None. O chamador pode ter usado um objeto semelhante a uma lista vazia com um anexo personalizado.
tzot
5
Mohit Ranka: cuidado para que não working_list seja True se seu comprimento for 0. Isso leva a um comportamento inconsistente: se a função receber uma lista com algum elemento nela, seu chamador terá sua lista atualizada, e se a lista estiver vazia, ele não será tocado.
Vincent
1
@PatrickT A ferramenta certa depende do caso - uma função varargs é muito diferente daquela que leva um argumento de lista (opcional). Situações em que você teria que escolher entre eles aparecem com menos frequência do que você imagina. Varargs é ótimo quando a contagem de argumentos muda, mas é corrigido quando o código é ESCRITO. Como seu exemplo. Se for variável de tempo de execução, ou você quiser chamar f()uma lista, terá que chamar o f(*l)que é bruto. Pior, implementar mate(['larch', 'finch', 'robin'], ['bumble', 'honey', 'queen'])seria SUCK w / varargs. Muito melhor se for def mate(birds=[], bees=[]):.
FeRD
26

As respostas existentes já forneceram as soluções diretas solicitadas. No entanto, como essa é uma armadilha muito comum para novos programadores de Python, vale a pena adicionar a explicação de por que o python se comporta dessa maneira, que está bem resumida no " Guia do Hitchhikers para Python " como " Argumentos padrão mutáveis ": http: // docs .python-guide.org / en / latest / writing / gotchas /

Citação: " Os argumentos padrão do Python são avaliados uma vez quando a função é definida, não toda vez que a função é chamada (como em Ruby, digamos). Isso significa que se você usar um argumento padrão mutável e alterá-lo, você terá modificou esse objeto para todas as chamadas futuras para a função também "

Código de amostra para implementá-lo:

def foo(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to
Zhenhua
fonte
13

Não que isso importe neste caso, mas você pode usar a identidade do objeto para testar nenhum:

if working_list is None: working_list = []

Você também pode tirar proveito de como o operador booleano ou é definido em python:

working_list = working_list or []

No entanto, isso irá se comportar de forma inesperada se o chamador fornecer uma lista vazia (que conta como falsa) como working_list e esperar que sua função modifique a lista fornecida por ele.

Bendin
fonte
10

Se a intenção da função é modificar o parâmetro passado comoworking_list , consulte a resposta de HenryR (= None, verifique se há None dentro).

Mas se você não pretendia transformar o argumento, apenas use-o como ponto de partida para uma lista, você pode simplesmente copiá-lo:

def myFunc(starting_list = []):
    starting_list = list(starting_list)
    starting_list.append("a")
    print starting_list

(ou, neste caso simples, print starting_list + ["a"]mas acho que foi apenas um exemplo de brinquedo)

Em geral, modificar seus argumentos é um estilo ruim em Python. As únicas funções que podem sofrer mutação em um objeto são os métodos do objeto. É ainda mais raro transformar um argumento opcional - um efeito colateral que acontece apenas em algumas chamadas é realmente a melhor interface?

  • Se você fizer isso a partir do hábito C de "argumentos de saída", isso é completamente desnecessário - você sempre pode retornar vários valores como uma tupla.

  • Se você fizer isso para construir com eficiência uma longa lista de resultados sem construir listas intermediárias, considere escrevê-la como um gerador e usá-la result_list.extend(myFunc())ao chamá-la. Dessa forma, suas convenções de chamada permanecem muito claras.

Um padrão em que a mutação de um arg opcional é frequentemente feita é um arg "memo" oculto em funções recursivas:

def depth_first_walk_graph(graph, node, _visited=None):
    if _visited is None:
        _visited = set()  # create memo once in top-level call

    if node in _visited:
        return
    _visited.add(node)
    for neighbour in graph[node]:
        depth_first_walk_graph(graph, neighbour, _visited)
Beni Cherniavsky-Paskin
fonte
3

Posso estar fora do assunto, mas lembre-se de que se você quiser apenas passar um número variável de argumentos, a forma pythônica é passar uma tupla *argsou um dicionário **kargs. Eles são opcionais e são melhores do que a sintaxemyFunc([1, 2, 3]) .

Se você quiser passar uma tupla:

def myFunc(arg1, *args):
  print args
  w = []
  w += args
  print w
>>>myFunc(1, 2, 3, 4, 5, 6, 7)
(2, 3, 4, 5, 6, 7)
[2, 3, 4, 5, 6, 7]

Se você quiser passar um dicionário:

def myFunc(arg1, **kargs):
   print kargs
>>>myFunc(1, option1=2, option2=3)
{'option2' : 2, 'option1' : 3}
Mapad
fonte
0

Já foram fornecidas respostas boas e corretas. Eu só queria dar outra sintaxe para escrever o que você deseja fazer, o que acho mais bonito quando você, por exemplo, deseja criar uma classe com listas vazias padrão:

class Node(object):
    def __init__(self, _id, val, parents=None, children=None):
        self.id = _id
        self.val = val
        self.parents = parents if parents is not None else []
        self.children = children if children is not None else []

Este snippet usa a sintaxe do operador if else. Eu gosto especialmente porque é uma linha simples sem dois pontos, etc. envolvida e quase se parece com uma frase normal em inglês. :)

No seu caso, você poderia escrever

def myFunc(working_list=None):
    working_list = [] if working_list is None else working_list
    working_list.append("a")
    print working_list
drssdinblck
fonte
-3

Eu fiz o curso de extensão UCSC Python for programmer

O que é verdadeiro para: def Fn (dados = []):

a) é uma boa ideia para que suas listas de dados comecem vazias a cada chamada.

b) é uma boa ideia para que todas as chamadas para a função que não fornecem nenhum argumento na chamada obtenham a lista vazia como dados.

c) é uma ideia razoável, desde que seus dados sejam uma lista de strings.

d) é uma má ideia porque o padrão [] acumulará dados e o padrão [] será alterado com as chamadas subsequentes.

Responda:

d) é uma má ideia porque o padrão [] acumulará dados e o padrão [] será alterado com as chamadas subsequentes.

Peter Chen
fonte