Python - 'если foo в dict' против 'try: dict [foo]'

24

Полагаю, это не столько вопрос о характере типизации утки, сколько о том, как остаться питоническим.

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

if myKey in dict:
    do_some_work(dict[myKey])
else:
    pass

И, конечно, Е. Олде подход «прощение против разрешения».

try:
    do_some_work(dict[myKey])
except KeyError:
    pass

Будучи подмастерьем Python, я чувствую, что вижу, что последний предпочел много, что кажется странным, потому что в Python документы try/exceptsкажутся предпочтительными, когда есть реальная ошибка, а не ... отсутствие успеха?

Если случайный dict не имеет ключа в myDict, и известно, что он не всегда будет иметь этот ключ, попытка / исключает контекстуально вводит в заблуждение? Это не ошибка программирования, это просто факт данных - у этого слова просто не было этого конкретного ключа.

Это кажется особенно важным, когда вы смотрите на синтаксис try / exception / else, который выглядит очень полезным, когда нужно убедиться, что попытка не перехватывает слишком много ошибок. Вы можете сделать что-то вроде:

try:
    foo += bar
except TypeError:
    pass
else:
    return some_more_work(foo)

Не приведет ли это к проглатыванию всевозможных странных ошибок, которые, вероятно, являются результатом какого-то плохого кода? Приведенный выше код может просто помешать вам увидеть, что вы пытаетесь добавить, 2 + {}и вы, возможно, никогда не поймете, что какая-то часть вашего кода пошла ужасно неправильно. Я не предлагаю проверять все типы, поэтому это Python, а не JavaScript - но опять же с контекстом try / Кроме того, похоже, что он должен поймать программу, делающую то, что он не должен делать, вместо этого дать ему возможность продолжить.

Я понимаю, что приведенный выше пример является спорным аргументом и на самом деле намеренно плох. Но, учитывая питоническое убеждение, better to ask forgiveness than permissionя не могу не чувствовать, что возникает вопрос о том, где грань в песке на самом деле находится между правильным применением if / else против try / исключения, в частности, когда вы знаете, чего ожидать от данные, с которыми вы работаете.

Я даже не говорю о проблемах со скоростью или о лучших практиках, я просто немного сбит с толку воспринимаемой диаграммой Венна случаев, когда кажется, что это может пойти в любом случае, но люди ошибаются на стороне попытки / кроме потому что «кто-то где-то сказал, что это Pythonic». Я сделал неправильные выводы о применении этого синтаксиса?


источник
Я согласен с тем, что тенденция к попыткам - исключая риск скрытия ошибок. Мое мнение таково, что вы, как правило, должны избегать попытки, за исключением условий, которые вы ожидаете увидеть (конечно, некоторые исключения относятся к этому правилу).
Гордон Бин

Ответы:

26

Вместо этого используйте метод get :

some_dict.get(the_key, default_value)

... где default_valueвозвращаемое значение, если the_keyне в some_dict. Если вы пропустите default_value, Noneвозвращается, если ключ отсутствует.

В целом, в Python люди предпочитают попробовать / кроме проверки чего-либо сначала - см. Запись EAFP в глоссарии . Обратите внимание, что многие функции «проверки на членство» используют исключения за кулисами.

detly
источник
Разве это не та же самая проблема, хотя? Если мы пытаемся работать с dictи выполняем работу, основываясь на том, что найдено, то кажется, что if myDict.get(a_key) is not None: do_work(myDict[a_key])это более многословно и еще один пример неявного просмотра перед прыжком - плюс это немного менее читабельное ИМХО. Возможно, я не понимаю dict.get(a_key, a_default)в правильном контексте, но кажется, что это предшествует написанию «not-a-switch-Statement if / elses» или магических значений. Мне нравится то, что делает этот метод, но я рассмотрю его глубже.
1
@Stick - вы делаете:, data = myDict.get(a_key, default)и либо do_workбудете делать правильные вещи, когда дано default(например. None), Или вы делаете if data: do_work(data). В любом случае нужен только один поиск. (Это может быть if data is not None: ..., в любом случае, это все еще только один поиск, и тогда ваша собственная логика может взять верх.)
ловко
1
Поэтому я решил, что dict.get () в значительной степени сэкономил мне массу усилий в моем текущем проекте. Это уменьшило мой счетчик строк, убрало много ужасно выглядящего try/except/elseмусора и только в целом улучшило ИМХО читаемость моего кода. Я усвоил ценный урок, который слышал раньше, но сумел забыть принять близко к сердцу: «Если вы чувствуете, что пишете слишком много кода, остановитесь и посмотрите на возможности языка». Благодарность!!
@ Стик - рад это слышать :) Мне нравится этот совет, я никогда не слышал его раньше.
детально
1
Технически, какой из них самый быстрый?
Оливье Понс
4

Отсутствующий ключ - это правило или исключение ?

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

Еще одна вещь в пользу условия - это предложение else - гораздо легче понять, когда любой блок, если он выполнен, учитывает, что даже один и тот же класс исключения может быть сгенерирован из нескольких операторов в блоке try.

Код в условии кроме не должен быть частью нормального недостатка (например, если ключа нет, добавьте его) - он должен обрабатывать ошибку.

Евгений
источник
4
«С прагматической точки зрения бросать / ловить намного медленнее, чем проверять, находится ли ключ в таблице» - нет, это не так. Во всяком случае, не обязательно. В последний раз я проверял, что наиболее распространенные реализации Python делают try/ catchверсию быстрее.
детально
2
Бросок / улов в Python не так уж и важен с точки зрения производительности, поскольку он часто используется для управления потоком в Python. Кроме того, в некоторых ситуациях существует вероятность, что этот dict может быть изменен между тестом и доступом.
whatsisname
@whatsisname - я подумал добавить это к своему ответу, но если у вас есть такие гоночные опасности, у вас есть гораздо большие проблемы, чем у EAFP против LBYL: P
детально
1

В духе того, чтобы быть "питоническим" и, в частности, печатать на утке - попытка / исключение выглядит почти хрупкой для меня.

Все, что вам действительно нужно, это что-то с доступом, похожим на диктовку, но __getitem__его можно изменить, чтобы сделать что-то не совсем так, как вы ожидаете. Например, используя defaultdictиз стандартной библиотеки:

In [1]: from collections import defaultdict

In [2]: foo = defaultdict(int)

In [3]: foo['a'] = 1

In [4]: foo['b']  # Someone did access checking with a try/except exactly as you have in the question
Out[4]: 0

In [5]: 'a' in foo
Out[5]: True

In [6]: 'b' in foo  # We never set it, but now it exists!
Out[6]: True

In [7]: 'c' in foo
Out[7]: False

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

К сожалению, несколько месяцев спустя, эти дополнительные ключи фактически стали причиной проблем и трудных для отслеживания ошибок, потому что 0стали действительным значением. И иногда мы использовали бы,in чтобы проверить, существует ли ключ, заставляя код делать разные вещи, когда он должен был быть идентичным.

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

Используйте первую версию if myKey in mydict.


Также обратите внимание, что речь идет именно о примере в вопросе. Кредо Python «Лучше просить прощения, чем разрешения» в значительной степени предназначено для обеспечения того, чтобы вы действительно действовали правильно, когда вы не получаете того, что хотите. Например, проверка, существует ли файл, и затем попытка чтения из него - по крайней мере, 3 вещи, которые я могу придумать, могут пойти не так:

  • Условием гонки является то, что файл мог быть удален / перемещен / и т. Д.
  • У вас нет прав на чтение файла
  • Файл был поврежден

Первый, на который вы можете попытаться «попросить разрешения», но по характеру условий гонки это не всегда будет работать; второй слишком легко забыть проверить; и третий не может быть проверен, не пытаясь прочитать файл. В любом случае, что вы делаете после провала почти наверняка будет то же самое, так что в этом примере, что это лучше «просить прощения», используя попробовать / за исключением.

Izkata
источник