python dictionary generator generator-expression

Generador de expresiones de Python



dictionary generator (5)

Tengo una lista de diccionarios como los siguientes:

lst = [{''a'': 5}, {''b'': 6}, {''c'': 7}, {''d'': 8}]

Escribí una expresión generadora como:

next((itm for itm in lst if itm[''a'']==5))

Ahora, la parte extraña es que, aunque esto funciona para el par de valores clave de ''a'' se produce un error para todas las demás expresiones la próxima vez. Expresión:

next((itm for itm in lst if itm[''b'']==6))

Error:

Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <genexpr> KeyError: ''b''


De hecho, su estructura es una lista de diccionarios .

>>> lst = [{''a'': 5}, {''b'': 6}, {''c'': 7}, {''d'': 8}]

Para tener una mejor idea de lo que está sucediendo con su primera condición, intente esto:

>>> gen = (itm for itm in lst if itm[''a''] == 5) >>> next(gen) {''a'': 5} >>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <genexpr> KeyError: ''a''

Cada vez que llama a next , procesa el siguiente elemento y devuelve un elemento. También...

next((itm for itm in lst if itm[''a''] == 5))

Crea un generador que no se asigna a ninguna variable, procesa el primer elemento en el lst , ve que la clave ''a'' existe y devuelve el elemento. El generador es entonces recogido de basura. La razón por la que no se produce un error es porque el primer elemento en lst sí contiene esta clave.

Por lo tanto, si cambió la clave para que sea algo que el primer elemento no contiene, obtendrá el error que vio:

>>> gen = (itm for itm in lst if itm[''b''] == 6) >>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <genexpr> KeyError: ''b''

La solución

Bueno, una solución como ya se ha explicado es usar la función dict.get . Aquí hay otra alternativa usando defaultdict :

from collections import defaultdict from functools import partial f = partial(defaultdict, lambda: None) lst = [{''a'': 5}, {''b'': 6}, {''c'': 7}, {''d'': 8}] lst = [f(itm) for itm in lst] # create a list of default dicts for i in (itm for itm in lst if itm[''b''] == 6): print(i)

Esto imprime:

defaultdict(<function <lambda> at 0x10231ebf8>, {''b'': 6})

El valor defaultdict devolverá None en caso de que la clave no esté presente.


Eche un vistazo a la expresión de su generador por separado:

(itm for itm in lst if itm[''a'']==5)

Esto recopilará todos los elementos de la lista donde es itm[''a''] == 5 . Hasta ahora tan bueno.

Cuando llama a next() en él, le dice a Python que genere el primer elemento de esa expresión del generador. Pero sólo el primero.

Entonces, cuando tenga la condición itm[''a''] == 5 , el generador tomará el primer elemento de la lista, {''a'': 5} y realizará la verificación. La condición es verdadera, por lo que ese elemento es generado por la expresión del generador y devuelto por next() .

Ahora, cuando cambia la condición a itm[''b''] == 6 , el generador nuevamente tomará el primer elemento de la lista, {''a'': 5} , e intentará obtener el elemento con la tecla b . Esto fallará:

>>> itm = {''a'': 5} >>> itm[''b''] Traceback (most recent call last): File "<pyshell#1>", line 1, in <module> itm[''b''] KeyError: ''b''

Ni siquiera tiene la oportunidad de mirar el segundo elemento porque ya falla al intentar mirar el primer elemento.

Para resolver esto, debe evitar el uso de una expresión que pueda generar un KeyError aquí. Podría usar dict.get() para intentar recuperar el valor sin generar una excepción:

>>> lst = [{''a'': 5}, {''b'': 6}, {''c'': 7}, {''d'': 8}] >>> next((itm for itm in lst if itm.get(''b'') == 6)) {''b'': 6}


Eso no es raro. Para cada itm en el lst . Primero se evaluará la cláusula de filtro . Ahora, si la cláusula de filtro es itm[''b''] == 6 , intentará obtener la clave ''b'' de ese diccionario. Pero como el primer diccionario no tiene esa clave, generará un error.

Para el primer ejemplo de filtro, eso no es un problema, ya que el primer diccionario tiene una tecla ''a'' . El next(..) solo está interesado en el primer elemento emitido por el generador. Por eso nunca pide filtrar más elementos.

Puede usar .get(..) aquí para hacer que la búsqueda sea más segura:

next((itm for itm in lst if itm.get(''b'',None)==6))

En caso de que el diccionario no tenga dicha clave, la parte .get(..) devolverá None . Y dado que None no es igual a 6, el filtro omitirá el primer diccionario y buscará otra coincidencia. Tenga en cuenta que si no especifica un valor predeterminado , None es el valor predeterminado, por lo que una declaración equivalente es:

next((itm for itm in lst if itm.get(''b'')==6))

También podemos omitir el paréntesis del generador: solo si hay varios argumentos, necesitamos estos paréntesis adicionales:

next(itm for itm in lst if itm.get(''b'')==6)


Obviamente, itm[''b''] generará un KeyError si no hay una clave ''b'' en un diccionario. Una forma sería hacer

next((itm for itm in lst if ''b'' in itm and itm[''b'']==6))

Si no espera None en ninguno de los diccionarios, puede simplificarlo para

next((itm for itm in lst if itm.get(''b'')==6))

(Esto funcionará igual si se compara con 6 , pero daría un resultado incorrecto si lo comparara con None )

o de forma segura con un marcador de posición

PLACEHOLDER = object() next((itm for itm in lst if itm.get(''b'', PLACEHOLDER)==6))


Tal vez puedas probar esto:

next(next((itm for val in itm.values() if val == 6) for itm in lst))

Esto puede ser un poco complicado, ya que genera un generador de dos niveles, por lo que necesita dos más para obtener el resultado.