ʻIf key in dict` vs.` try / except` - какая идиома более читабельна?

95

У меня вопрос об идиомах и удобочитаемости, и, похоже, в этом конкретном случае происходит столкновение философий Python:

Я хочу построить словарь A из словаря B. Если определенный ключ не существует в B, ничего не делайте и продолжайте.

Какой способ лучше?

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

или

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

«Сделай и попроси прощения» vs. «простота и ясность».

Что лучше и почему?

LeeMobile
источник
1
Второй пример лучше было бы записать как if "blah" in B.keys(), или if B.has_key("blah").
girasquid
2
у вас A.update(B)не работает?
SilentGhost
21
@Luke: has_keyустарел, поэтому inпроверка B.keys()меняет операцию O (1) на операцию O (n).
kindall
4
@ Люк: это не так. .has_keyустарел и keysсоздает ненужный список в py2k и является избыточным в py3k
SilentGhost
2
'build' A, например, A пусто для начала? И нам нужны только определенные ключи? Используйте понимание: A = dict((k, v) for (k, v) in B if we_want_to_include(k)).
Карл Кнехтель

Ответы:

77

Исключения не являются условными.

Условная версия более понятна. Это естественно: это прямое управление потоком, для которого и предназначены условные выражения, а не исключения.

Версия исключения в основном используется для оптимизации при выполнении этих поисков в цикле: для некоторых алгоритмов она позволяет исключить тесты из внутренних циклов. Здесь нет такой выгоды. У него есть небольшое преимущество в том, что он позволяет избежать повторения "blah"дважды, но если вы делаете много из них, вам, вероятно, в move_keyлюбом случае следует иметь вспомогательную функцию.

В общем, я настоятельно рекомендую по умолчанию придерживаться условной версии, если у вас нет особых причин не делать этого. Условные предложения - очевидный способ сделать это, что обычно является сильной рекомендацией предпочесть одно решение другому.

Гленн Мейнард
источник
3
Я не согласен. Если вы говорите «сделайте X, а если это не сработает, сделайте Y». Основная причина против условного решения здесь "blah"заключается в том , что вам нужно писать чаще, что приводит к более подверженной ошибкам ситуации.
glglgl
6
И, особенно в Python, очень широко используется EAFP.
glglgl
8
Этот ответ будет правильным для любого языка, который я знаю, кроме Python.
Tomáš Zato - Reinstate Monica
3
Если вы используете исключения как условные выражения в Python, я надеюсь, что это никому не нужно читать.
Гленн Мейнард
Итак, каков окончательный вердикт? :)
floatingpurr
62

Существует также третий способ, позволяющий избежать как исключений, так и двойного поиска, что может быть важно, если поиск стоит дорого:

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

Если вы ожидаете, что словарь будет содержать Noneзначения, вы можете использовать еще несколько эзотерических констант, например NotImplemented, Ellipsisили создать новую:

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

Во всяком случае, update()для меня наиболее читабельным вариантом является использование :

a.update((k, b[k]) for k in ("foo", "bar", "blah") if k in b)
lqc
источник
14

Насколько я понимаю, вы хотите обновить dict A парами ключей и значений из dict B

update лучший выбор.

A.update(B)

Пример:

>>> 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
источник
«Если определенный ключ не существует в B» Извините, это должно было быть более ясным, но я хочу копировать только значения, если существуют определенные ключи в B. Не все в Б.
LeeMobile
1
@LeeMobile -A.update({k: v for k, v in B.iteritems() if k in specificset})
Omnifarious
8

Прямая цитата из вики по производительности Python:

За исключением первого раза, каждый раз, когда встречается слово, проверка оператора if не выполняется. Если вы считаете большое количество слов, многие, вероятно, будут встречаться несколько раз. В ситуации, когда инициализация значения происходит только один раз, а увеличение этого значения будет происходить во много раз, дешевле использовать оператор try.

Таким образом, кажется, что оба варианта жизнеспособны в зависимости от ситуации. Для получения дополнительной информации вы можете проверить эту ссылку: Попробуйте, кроме производительности.

Сами Лехтинен
источник
это интересное чтение, но я думаю, что оно несколько неполное. Используемый dict имеет только 1 элемент, и я подозреваю, что более крупные dicts окажут значительное влияние на производительность
user2682863
3

Я думаю, что здесь A["blah"]обычно существует общее правило , если так, попробуйте-except, хорошо, если нет, тогда используйтеif "blah" in b:

Я думаю, что «попробовать» дешево по времени, но «кроме» дороже.

Нил
источник
10
По умолчанию не подходите к коду с точки зрения оптимизации; подходить к нему с точки зрения удобочитаемости и ремонтопригодности. Если целью не является конкретная оптимизация, это неправильный критерий (и если это оптимизация, ответ - сравнительный анализ, а не предположение).
Гленн Мейнард,
Мне, наверное, следовало заключить последний пункт в скобки или как-то еще расплывчатее - моя основная мысль была первой, и я думаю, что у нее есть дополнительное преимущество перед вторым.
Нил
3

Я думаю, что вам следует использовать второй пример, если этот код не имеет смысла:

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

Имейте в виду, что код будет прерван, как только ключ отсутствует B. Если этот код имеет смысл, вам следует использовать метод исключения, в противном случае используйте метод тестирования. На мой взгляд, поскольку он короче и ясно выражает намерение, его намного легче читать, чем метод исключения.

Конечно, люди, которые говорят вам использовать update, правы. Если вы используете версию Python, которая поддерживает понимание словаря, я бы сильно предпочел этот код:

updateset = {'foo', 'bar', 'baz'}
A.update({k: B[k] for k in updateset if k in B})
Всевозможный
источник
«Имейте в виду, что код будет прерван, как только появится ключ, которого нет в B.» - вот почему лучше всего помещать в блок try: только абсолютный минимум, обычно это одна строка. Первый пример лучше использовать как часть цикла, напримерfor key in ["foo", "bar", "baz"]: try: A[key] = B[key]
Зим
2

Правило на других языках - резервировать исключения для исключительных условий, то есть ошибок, которые не возникают при регулярном использовании. Не знаю, как это правило применимо к Python, так как StopIteration не должно существовать по этому правилу.

Марк Рэнсом
источник
Я думаю, что этот каштан возник из языков, где обработка исключений стоит дорого и поэтому может существенно повлиять на производительность. Я никогда не видел реальных оправданий или доводов в пользу этого.
Джон Ла Рой,
@JohnLaRooy Нет, дело не в производительности. Исключения - это своего рода нелокальный переход , который, по мнению некоторых, мешает читаемости кода. Однако использование исключений таким образом считается в Python идиоматическим, поэтому вышесказанное не применяется.
Ян Голдби
условные возвраты также являются «нелокальным переходом», и многие люди предпочитают этот стиль вместо проверки контрольных точек в конце блока кода.
Cowbert
1

Лично я склоняюсь ко второму способу (но использующему has_key):

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

Таким образом, каждая операция присваивания состоит только из двух строк (вместо 4 с помощью try / except), и любые возникающие исключения будут реальными ошибками или вещами, которые вы пропустили (вместо того, чтобы просто пытаться получить доступ к клавишам, которых нет) .

Как оказалось (см. Комментарии к вашему вопросу), has_keyон устарел, поэтому я думаю, что его лучше записать как

if "blah" in B:
  A["blah"] = B["blah"]
Жираскид
источник
1

Начиная Python 3.8с введения выражений присваивания (PEP 572) ( :=оператор), мы можем зафиксировать значение условия dictB.get('hello', None)в переменной value, чтобы проверить, не является ли оно неверным None(поскольку dict.get('hello', None)возвращает либо связанное значение, либо None), а затем использовать его в теле состояние:

# dictB = {'hello': 5, 'world': 42}
# dictA = {}
if value := dictB.get('hello', None):
  dictA["hello"] = value
# dictA is now {'hello': 5}
Ксавье Гихо
источник
1
Это не удается, если value == 0
Эрик
0

Хотя в принятом ответе акцент на принципе «посмотрите, прежде чем прыгать» может применяться к большинству языков, более питонический может быть первым подходом, основанным на принципах питона. Не говоря уже о том, что это законный стиль кодирования на Python. Важно убедиться, что вы используете блок try except в правильном контексте и следуете лучшим практикам. Например. делать слишком много вещей в блоке try, перехватывать очень широкое исключение или, что еще хуже, просто исключить предложение и т. д.

Проще просить прощения, чем разрешения. (EAFP)

См. Ссылку на документы Python здесь .

Кроме того, этот блог Бретта, одного из основных разработчиков, вкратце касается большей части этого.

Смотрите еще одно обсуждение SO здесь :

AJ
источник