`if key in dict` vs.` try / except` - qual é o idioma mais legível?

93

Eu tenho uma pergunta sobre expressões idiomáticas e legibilidade, e parece haver um conflito de filosofias Python para este caso específico:

Quero construir o dicionário A a partir do dicionário B. Se uma chave específica não existir em B, não faça nada e continue.

Qual caminho é melhor?

try:
    A["blah"] = B["blah"]
except KeyError:
    pass

ou

if "blah" in B:
    A["blah"] = B["blah"]

"Faça e peça perdão" vs. "simplicidade e clareza".

Qual é melhor e por que?

LeeMobile
fonte
1
O segundo exemplo pode ser melhor escrito como if "blah" in B.keys(), ou if B.has_key("blah").
girasquid
2
se A.update(B)não funcionar para você?
SilentGhost
21
@Luke: has_keytornou-se obsoleto em favor de ine a verificação B.keys()muda uma operação O (1) para uma O (n).
kindall
4
@Luke: não, não é. .has_keyestá obsoleto e keyscria uma lista desnecessária em py2k e é redundante em py3k
SilentGhost
2
'construir' A, como em, A está vazio para começar? E queremos apenas algumas chaves? Use uma compreensão: A = dict((k, v) for (k, v) in B if we_want_to_include(k)).
Karl Knechtel

Respostas:

76

As exceções não são condicionais.

A versão condicional é mais clara. Isso é natural: é um controle de fluxo direto, que é para o que as condicionais são projetadas, não exceções.

A versão de exceção é usada principalmente como uma otimização ao fazer essas pesquisas em um loop: para alguns algoritmos, ela permite eliminar testes de loops internos. Não tem esse benefício aqui. Tem a pequena vantagem de evitar ter que dizer "blah"duas vezes, mas se você estiver fazendo muito disso, provavelmente deverá ter uma move_keyfunção auxiliar de qualquer maneira.

Em geral, eu recomendo fortemente manter a versão condicional por padrão, a menos que você tenha um motivo específico para não fazê-lo. As condicionais são a maneira óbvia de fazer isso, o que geralmente é uma forte recomendação para preferir uma solução a outra.

Glenn Maynard
fonte
3
Eu não concordo. Se você disser "faça X, e se isso não funcionar, faça Y". Principal razão contra a solução condicional aqui, você tem que escrever com "blah"mais frequência, o que leva a uma situação mais sujeita a erros.
glglgl
6
E, especialmente em Python, o EAFP é amplamente usado.
glglgl
8
Essa resposta estaria correta para qualquer linguagem que conheço, exceto Python.
Tomáš Zato - Reintegrar Monica
3
Se você estiver usando exceções como se fossem condicionais em Python, espero que ninguém mais tenha que ler.
Glenn Maynard
Então, qual é o veredicto final? :)
floatingpurr
60

Há também uma terceira maneira que evita exceções e pesquisa dupla, o que pode ser importante se a pesquisa for cara:

value = B.get("blah", None)
if value is not None: 
    A["blah"] = value

Caso você espere que o dicionário contenha Nonevalores, você pode usar algumas constantes mais esotéricas como NotImplemented, Ellipsisou fazer uma nova:

MyConst = object()
def update_key(A, B, key):
    value = B.get(key, MyConst)
    if value is not MyConst: 
        A[key] = value

De qualquer forma, usar update()é a opção mais legível para mim:

a.update((k, b[k]) for k in ("foo", "bar", "blah") if k in b)
lqc
fonte
14

Pelo que entendi, você deseja atualizar o dict A com pares de chave e valor do dict B

update é a melhor escolha.

A.update(B)

Exemplo:

>>> A = {'a':1, 'b': 2, 'c':3}
>>> B = {'d': 2, 'b':5, 'c': 4}
>>> A.update(B)
>>> A
{'a': 1, 'c': 4, 'b': 5, 'd': 2}
>>> 
pyfunc
fonte
"Se uma chave específica não existir em B" Desculpe, deveria ter sido mais claro, mas eu só quero copiar os valores se houver chaves específicas em B. Nem tudo em B.
LeeMobile
1
@LeeMobile -A.update({k: v for k, v in B.iteritems() if k in specificset})
Omnifarious
8

Citação direta do wiki de desempenho do Python:

Exceto pela primeira vez, cada vez que uma palavra é vista, o teste da instrução if falha. Se você estiver contando um grande número de palavras, muitas provavelmente ocorrerão várias vezes. Em uma situação em que a inicialização de um valor só ocorrerá uma vez e o aumento desse valor ocorrerá muitas vezes, é mais barato usar uma instrução try.

Portanto, parece que ambas as opções são viáveis ​​dependendo da situação. Para obter mais detalhes, você pode consultar este link: Try-except-performance

Sami Lehtinen
fonte
é uma leitura interessante, mas acho um tanto incompleta. O dict usado tem apenas 1 elemento e suspeito que dicts maiores terão um impacto significativo no desempenho
user2682863
3

Acho que a regra geral aqui é que A["blah"]normalmente existirá, se sim, tente - exceto que é bom, se não, useif "blah" in b:

Acho que "experimentar" sai barato na hora, mas "exceto" é mais caro.

Neil
fonte
10
Não aborde o código de uma perspectiva de otimização por padrão; abordá-lo de uma perspectiva de legibilidade e manutenção. A menos que o objetivo seja especificamente otimização, este é o critério errado (e se for otimização, a resposta é benchmarking, não adivinhação).
Glenn Maynard
Eu provavelmente deveria ter colocado o último ponto entre colchetes ou de alguma forma mais vago - meu ponto principal foi o primeiro e acho que tem a vantagem adicional do segundo.
Neil
3

Acho que o segundo exemplo é o que você deve escolher, a menos que este código faça sentido:

try:
    A["foo"] = B["foo"]
    A["bar"] = B["bar"]
    A["baz"] = B["baz"]
except KeyError:
    pass

Lembre-se de que o código será abortado assim que houver uma chave que não esteja em B. Se esse código fizer sentido, você deve usar o método de exceção; caso contrário, use o método de teste. Na minha opinião, por ser mais curto e expressar claramente a intenção, é muito mais fácil de ler do que o método de exceção.

Claro, as pessoas que dizem para você usar updateestão corretas. Se você estiver usando uma versão do Python que ofereça suporte a compreensão de dicionário, eu prefiro fortemente este código:

updateset = {'foo', 'bar', 'baz'}
A.update({k: B[k] for k in updateset if k in B})
Omniforme
fonte
"Lembre-se de que o código será abortado assim que houver uma chave que não esteja em B." - é por isso que é prática recomendada colocar apenas o mínimo absoluto no bloco try:, geralmente é uma única linha. O primeiro exemplo seria melhor como parte de um loop, comofor key in ["foo", "bar", "baz"]: try: A[key] = B[key]
Zim
2

A regra em outros idiomas é reservar exceções para condições excepcionais, ou seja, erros que não ocorrem no uso regular. Não sei como essa regra se aplica ao Python, já que StopIteration não deveria existir por essa regra.

Mark Ransom
fonte
Eu acho que isso se originou de linguagens onde o tratamento de exceções é caro e, portanto, pode ter um impacto significativo no desempenho. Nunca vi nenhuma justificativa ou raciocínio real por trás disso.
John La Rooy
@JohnLaRooy Não, o desempenho não é realmente o motivo. As exceções são um tipo de goto não local , que algumas pessoas consideram impedir a legibilidade do código. No entanto, o uso de exceções dessa forma é considerado idiomático em Python, portanto, o acima não se aplica.
Ian Goldby
retornos condicionais também são "goto não local" e muitas pessoas preferem esse estilo em vez de inspecionar sentinelas no final do bloco de código.
cowbert
1

Pessoalmente, inclino-me para o segundo método (mas usando has_key):

if B.has_key("blah"):
  A["blah"] = B["blah"]

Dessa forma, cada operação de atribuição tem apenas duas linhas (em vez de 4 com try / except), e quaisquer exceções que forem lançadas serão erros reais ou coisas que você perdeu (em vez de apenas tentar acessar as chaves que não existem) .

Acontece que (veja os comentários sobre sua pergunta) has_keyestá obsoleto - então acho que é melhor escrito como

if "blah" in B:
  A["blah"] = B["blah"]
girasquid
fonte
1

Começando Python 3.8, e a introdução de expressões de atribuição (PEP 572) ( :=operador), podemos capturar o valor da condição dictB.get('hello', None)em uma variável valuepara verificar se não é None(pois dict.get('hello', None)retorna o valor associado ou None) e então usá-lo dentro do corpo de a condição:

# dictB = {'hello': 5, 'world': 42}
# dictA = {}
if value := dictB.get('hello', None):
  dictA["hello"] = value
# dictA is now {'hello': 5}
Xavier Guihot
fonte
Isso falha se valor == 0
Eric
0

Embora a ênfase da resposta aceita no princípio "olhe antes de pular" possa se aplicar à maioria das linguagens, mais python pode ser a primeira abordagem, com base nos princípios python. Sem mencionar que é um estilo de codificação legítimo em python. O importante é certificar-se de que você está usando o bloco try except no contexto correto e de seguir as práticas recomendadas. Por exemplo. fazer muitas coisas em um bloco try, capturar uma exceção muito ampla ou, pior, a cláusula bare except etc.

Mais fácil pedir perdão do que permissão. (EAFP)

Veja a referência de documentos python aqui .

Além disso, este blog de Brett, um dos desenvolvedores principais, aborda a maior parte disso em breve.

Veja outra discussão do SO aqui :

AJ
fonte