Выражения генератора Python
У меня есть список словарей вроде:
lst = [{'a': 5}, {'b': 6}, {'c': 7}, {'d': 8}]
Я написал генераторное выражение вроде:
next((itm for itm in lst if itm['a']==5))
Теперь странная часть состоит в том, что хотя это работает для пары значений ключа 'a'
он выдает ошибку для всех остальных выражений в следующий раз.
Выражение:
next((itm for itm in lst if itm['b']==6))
Ошибка:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <genexpr>
KeyError: 'b'
Ответы
Ответ 1
Это не странно. Для каждого itm
в lst
. Сначала он будет оценивать предложение фильтра. Теперь, если предложение фильтра itm['b'] == 6
, оно попытается извлечь ключ 'b'
из этого словаря. Но поскольку первый словарь не имеет такого ключа, он будет вызывать ошибку.
Для первого примера фильтра это не проблема, так как первый словарь имеет ключ 'a'
. next(..)
интересует только первый элемент, излучаемый генератором. Поэтому он никогда не просит фильтровать больше элементов.
Здесь вы можете использовать .get(..)
, чтобы сделать поиск более надежным:
next((itm for itm in lst if itm.get('b',None)==6))
Если словарь не имеет такого ключа, часть .get(..)
вернет None
. И поскольку None
не равно 6, фильтр, таким образом, опустит первый словарь и продолжит поиск другого совпадения. Обратите внимание: если вы не укажете значение по умолчанию, значение None
является значением по умолчанию, поэтому эквивалентный оператор:
next((itm for itm in lst if itm.get('b')==6))
Мы также можем опустить скобку генератора: только если есть несколько аргументов, нам нужны эти дополнительные скобки:
next(itm for itm in lst if itm.get('b')==6)
Ответ 2
Взгляните на выражение вашего генератора отдельно:
(itm for itm in lst if itm['a']==5)
Это будет собирать все элементы в списке, где itm['a'] == 5
. Пока все хорошо.
Когда вы вызываете next()
на нем, вы указываете Python генерировать первый элемент из этого выражения генератора. Но только первый.
Итак, когда у вас есть условие itm['a'] == 5
, генератор возьмет первый элемент списка, {'a': 5}
и выполнит проверку на нем. Условие истинно, поэтому элемент генерируется выражением генератора и возвращается next()
.
Теперь, когда вы измените условие на itm['b'] == 6
, генератор снова возьмет первый элемент списка, {'a': 5}
и попытается получить элемент с ключом b
. Это не будет выполнено:
>>> itm = {'a': 5}
>>> itm['b']
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
itm['b']
KeyError: 'b'
У него даже нет возможности взглянуть на второй элемент, потому что он уже не работает, пытаясь взглянуть на первый элемент.
Чтобы решить эту проблему, вам нужно избегать использования выражения, которое может поднять KeyError
здесь. Вы можете использовать dict.get()
, чтобы попытаться получить значение без привлечения исключения:
>>> lst = [{'a': 5}, {'b': 6}, {'c': 7}, {'d': 8}]
>>> next((itm for itm in lst if itm.get('b') == 6))
{'b': 6}
Ответ 3
Очевидно, что itm['b']
поднимет значение KeyError
, если в словаре нет клавиши 'b'
. Один из способов - сделать
next((itm for itm in lst if 'b' in itm and itm['b']==6))
Если вы не ожидаете None
в любом из словарей, вы можете упростить его до
next((itm for itm in lst if itm.get('b')==6))
(это будет работать так же, как вы сравните с 6
, но это даст неверный результат, если вы сравните с None
)
или безопасно с помощью заполнитель
PLACEHOLDER = object()
next((itm for itm in lst if itm.get('b', PLACEHOLDER)==6))
Ответ 4
Действительно, ваша структура - это список словарей.
>>> lst = [{'a': 5}, {'b': 6}, {'c': 7}, {'d': 8}]
Чтобы лучше понять, что происходит с вашим первым состоянием, попробуйте следующее:
>>> 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'
Каждый раз, когда вы вызываете next
, вы обрабатываете следующий элемент и возвращаете элемент. Также...
next((itm for itm in lst if itm['a'] == 5))
Создает генератор, который не назначен какой-либо переменной, обрабатывает первый элемент в lst
, видит, что ключ 'a'
действительно существует и возвращает элемент. Затем генератор собирает мусор. Причина, по которой возникает ошибка, заключается в том, что первый элемент в lst
действительно содержит этот ключ.
Итак, если вы изменили ключ на то, что первый элемент не содержит, вы получите сообщение об ошибке:
>>> 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'
Решение
Хорошо, одно решение, как уже обсуждалось, - использовать функцию dict.get
. Здесь другая альтернатива, использующая 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)
Это выдает:
defaultdict(<function <lambda> at 0x10231ebf8>, {'b': 6})
defaultdict
вернет None
в случае отсутствия ключа.
Ответ 5
Возможно, вы можете попробовать следующее:
next(next((itm for val in itm.values() if val == 6) for itm in lst))
Это может быть немного сложно, он генерирует двухуровневый generator
, поэтому вам нужно два next
, чтобы получить результат.