`if key in dict` vs` try / except` - ¿cuál es el idioma más legible?

93

Tengo una pregunta sobre modismos y legibilidad, y parece haber un choque de filosofías de Python para este caso particular:

Quiero crear el diccionario A a partir del diccionario B. Si una clave específica no existe en B, no haga nada y continúe.

¿Cuál es mejor?

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

o

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

"Hacer y pedir perdón" versus "sencillez y explicidad".

¿Cuál es mejor y por qué?

LeeMobile
fuente
1
El segundo ejemplo podría escribirse mejor como if "blah" in B.keys(), o if B.has_key("blah").
girasquid
2
no A.update(B)no funcionar para usted?
SilentGhost
21
@Luke: has_keyse ha desaprobado a favor de iny la verificación B.keys()cambia una operación O (1) a una O (n).
algo así como el
4
@ Luke: no, no lo es. .has_keyestá en desuso y keyscrea una lista innecesaria en py2k, y es redundante en py3k
SilentGhost
2
'build' A, como en, A está vacío para empezar? ¿Y solo queremos ciertas llaves? Utilice una comprensión: A = dict((k, v) for (k, v) in B if we_want_to_include(k)).
Karl Knechtel

Respuestas:

75

Las excepciones no son condicionales.

La versión condicional es más clara. Eso es natural: este es un control de flujo sencillo, para lo que están diseñados los condicionales, no las excepciones.

La versión de excepción se utiliza principalmente como optimización cuando se realizan estas búsquedas en un bucle: para algunos algoritmos, permite eliminar las pruebas de los bucles internos. No tiene ese beneficio aquí. Tiene la pequeña ventaja de que evita tener que decir "blah"dos veces, pero si está haciendo muchas de estas, probablemente debería tener una move_keyfunción de ayuda de todos modos.

En general, recomiendo encarecidamente seguir con la versión condicional de forma predeterminada a menos que tenga una razón específica para no hacerlo. Los condicionales son la forma obvia de hacer esto, que suele ser una fuerte recomendación para preferir una solución sobre otra.

Glenn Maynard
fuente
3
No estoy de acuerdo Si dices "haz X, y si eso no funciona, haz Y". La razón principal en contra de la solución condicional aquí es que debe escribir con "blah"más frecuencia, lo que conduce a una situación más propensa a errores.
glglgl
6
Y, especialmente en Python, EAFP es muy utilizado.
glglgl
8
Esta respuesta sería correcta para cualquier idioma que conozca, excepto Python.
Tomáš Zato - Reincorpora a Monica
3
Si está usando excepciones como si fueran condicionales en Python, espero que nadie más tenga que leerlo.
Glenn Maynard
Entonces, ¿cuál es el veredicto final? :)
floatingpurr
61

También hay una tercera forma que evita tanto las excepciones como la doble búsqueda, que puede ser importante si la búsqueda es costosa:

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

En caso de que espere que el diccionario contenga Nonevalores, puede usar algunas constantes más esotéricas como NotImplemented, Ellipsiso hacer una nueva:

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

De todos modos, usar update()es la opción más legible para mí:

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

Por lo que tengo entendido, desea actualizar el dictado A con pares clave, valor del dictado B

update es una mejor opción.

A.update(B)

Ejemplo:

>>> 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
fuente
"Si una clave específica no existe en B" Lo siento, debería haber sido más claro, pero solo quiero copiar valores si existen claves específicas en B. No todo en B.
LeeMobile
1
@LeeMobile -A.update({k: v for k, v in B.iteritems() if k in specificset})
Omnifarious
8

Cita directa de la wiki de rendimiento de Python:

Excepto por la primera vez, cada vez que se ve una palabra, la prueba de la instrucción if falla. Si está contando una gran cantidad de palabras, es probable que muchas ocurran varias veces. En una situación en la que la inicialización de un valor solo ocurrirá una vez y el aumento de ese valor ocurrirá muchas veces, es más barato usar una instrucción try.

Entonces parece que ambas opciones son viables dependiendo de la situación. Para obtener más detalles, es posible que desee consultar este enlace: Try-except-performance

Sami Lehtinen
fuente
es una lectura interesante, pero creo que algo incompleta. El dict utiliza sólo tiene 1 elemento y sospecho que predice grandes tendrán un impacto significativo en el rendimiento
user2682863
3

Creo que la regla general aquí es que A["blah"]normalmente existirá, si es así, intente, excepto que es bueno si no, entonces useif "blah" in b:

Creo que "probar" es barato en el tiempo, pero "excepto" es más caro.

neil
fuente
10
No aborde el código desde una perspectiva de optimización de forma predeterminada; Abordarlo desde una perspectiva de legibilidad y mantenibilidad. A menos que el objetivo sea específicamente la optimización, este es el criterio incorrecto (y si se trata de optimización, la respuesta es realizar evaluaciones comparativas, no adivinar).
Glenn Maynard
Probablemente debería haber puesto el último punto entre corchetes o de alguna manera más vago: mi punto principal fue el primero y creo que tiene la ventaja adicional del segundo.
neil
3

Creo que el segundo ejemplo es lo que debería buscar a menos que este código tenga sentido:

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

Tenga en cuenta que el código se cancelará tan pronto como haya una clave que no esté en B . Si este código tiene sentido, entonces debe usar el método de excepción; de lo contrario, use el método de prueba. En mi opinión, debido a que es más corto y expresa claramente la intención, es mucho más fácil de leer que el método de excepción.

Por supuesto, las personas que le dicen que use updateson correctas. Si está utilizando una versión de Python que admita la comprensión del diccionario, preferiría mucho este código:

updateset = {'foo', 'bar', 'baz'}
A.update({k: B[k] for k in updateset if k in B})
De todo género
fuente
"Tenga en cuenta que el código se cancelará tan pronto como haya una clave que no esté en B". - esta es la razón por la que es una buena práctica poner solo el mínimo absoluto en el bloque try:, generalmente esta es una sola línea. El primer ejemplo sería mejor como parte de un bucle, comofor key in ["foo", "bar", "baz"]: try: A[key] = B[key]
Zim
2

La regla en otros idiomas es reservar excepciones para condiciones excepcionales, es decir, errores que no ocurren con el uso regular. No sé cómo se aplica esa regla a Python, ya que StopIteration no debería existir según esa regla.

Mark Ransom
fuente
Creo que esta castaña se originó en lenguajes donde el manejo de excepciones es costoso y, por lo tanto, puede tener un impacto significativo en el rendimiento. Nunca he visto ninguna justificación o razonamiento real detrás de esto.
John La Rooy
@JohnLaRooy No, el rendimiento no es realmente la razón. Las excepciones son una especie de goto no local , que algunas personas consideran que dificulta la legibilidad del código. Sin embargo, el uso de excepciones de esta manera se considera idiomático en Python, por lo que lo anterior no se aplica.
Ian Goldby
Los retornos condicionales también son "goto no locales" y mucha gente prefiere ese estilo en lugar de inspeccionar centinelas al final del bloque de código.
cowbert
1

Personalmente, me inclino por el segundo método (pero usando has_key):

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

De esa manera, cada operación de asignación consta de solo dos líneas (en lugar de 4 con try / except), y cualquier excepción que se produzca será un error real o cosas que se haya perdido (en lugar de simplemente intentar acceder a las claves que no están allí) .

Resulta que (vea los comentarios sobre su pregunta), has_keyestá en desuso, así que supongo que está mejor escrito como

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

A partir Python 3.8y la introducción de las expresiones de asignación (PEP 572) ( :=operador), podemos capturar el valor de la condición dictB.get('hello', None)en una variable valuepara comprobar si no lo es None(como dict.get('hello', None)devuelve el valor asociado o None) y luego usarlo dentro del cuerpo de la condición:

# dictB = {'hello': 5, 'world': 42}
# dictA = {}
if value := dictB.get('hello', None):
  dictA["hello"] = value
# dictA is now {'hello': 5}
Xavier Guihot
fuente
Esto falla si valor == 0
Eric
0

Aunque el énfasis de la respuesta aceptada en el principio de "mirar antes de saltar" podría aplicarse a la mayoría de los lenguajes, el primer enfoque podría ser más pitónico, basado en los principios de python. Sin mencionar que es un estilo de codificación legítimo en Python. Lo importante es asegurarse de que está utilizando el bloque try except en el contexto correcto y sigue las mejores prácticas. P.ej. haciendo demasiadas cosas en un bloque de prueba, detectando una excepción muy amplia, o peor, la cláusula de excepción, etc.

Es más fácil pedir perdón que permiso. (EAFP)

Vea la referencia de documentos de Python aquí .

Además, este blog de Brett, uno de los desarrolladores principales, aborda la mayor parte de esto en breve.

Vea otra discusión de SO aquí :

AJ
fuente