Почему в Python проще попросить прощения, чем получить разрешение?

Почему «Легче попросить прощения, чем получить разрешение» (EAFP) считается хорошей практикой в ​​Python? Как у новичка в программировании, у меня сложилось впечатление, что использование многих подпрограмм try...except скорее приведет к раздуванию и менее читаемому коду, чем использование других проверок.

В чем преимущество подхода EAFP?

NB: Я знаю, что здесь есть похожие вопросы, но они в основном относятся к какому-то конкретному примеру, а меня больше интересует философия, стоящая за принципом.


person n1000    schedule 02.10.2015    source источник
comment
Утверждения не предназначены для потока кода.   -  person Ignacio Vazquez-Abrams    schedule 02.10.2015
comment
@ IgnacioVazquez-Abrams Не могли бы вы уточнить? Я так понимаю они альтернатива try...except? Должен ли я изменить вопрос?   -  person n1000    schedule 02.10.2015
comment
Предполагается, что они должны использоваться для того, чтобы ваша программа не делала глупостей в грушевидных ситуациях, а не для проверки того, что кто-то передал целое число вместо строки.   -  person Ignacio Vazquez-Abrams    schedule 02.10.2015
comment
ОК, я удалил assert. Видимо, я не использовал их должным образом в прошлом...   -  person n1000    schedule 02.10.2015


Ответы (4)


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

Причина, по которой Python является EAFP, заключается в том, что, в отличие от других языков (например, Java), в Python перехват исключений является относительно недорогой операцией, и поэтому вам рекомендуется ее использовать.

Пример для EAFP:

try:
    snake = zoo['snake']
except KeyError as e:
    print "There's no snake in the zoo"
    snake = None

Пример для LBYL:

if 'snake' in zoo:
    snake = zoo['snake']
else:
    snake = None
person Nir Alfasi    schedule 02.10.2015
comment
Обратите внимание, что второй должен вместо этого использовать уникальный объект-дозорный, иначе он завершится ошибкой при любом ложном значении. - person Ignacio Vazquez-Abrams; 02.10.2015
comment
@IgnacioVazquez-Abrams, вы правы, но в этом примере я могу с уверенностью предположить, что False, 0 или любое другое ложное значение не будет представлять змею в зоопарке :) - person Nir Alfasi; 02.10.2015
comment
Если вы используете dict.get, вам следует просто передать None в качестве значения по умолчанию: zoo.get('snake', None). Это не очень хороший пример для LBYL из-за использования прощающего dict.get. - person poke; 02.10.2015
comment
@poke правда, его можно сократить до: snake = zoo.get('snake', None), но смысл был в том, чтобы показать пример LBYL, а не как его обойти :) - person Nir Alfasi; 02.10.2015
comment
Затем сделайте правильную часть «поиска» и проверьте, существует ли ключ в словаре, не получая из него значение и не проверяя истинность этого значения. - person poke; 02.10.2015
comment
Причина, по которой Python является EAFP, заключается в том, что в отличие от других языков (например, Java) в Python перехват исключений является недорогой операцией: ну, это просто неправильно. Настройка блока try/except обходится дешево, а перехват исключения — нет. Попробуйте этот код, чтобы проверить его gist.github.com/RealBigB/38f4579c543261f1400f - здесь (python 2.7.x) я получаю 1.3156080246 для eafp и 0.368113994598 для lbyl. - person bruno desthuilliers; 02.10.2015
comment
@brunodesthuilliers. Считаете ли вы пример, который выдает миллион исключений и перехватывает их, хорошим примером, который можно использовать в качестве модели для обычной программы? :) - person Nir Alfasi; 02.10.2015
comment
@brunodesthuilliers Как я пытался объяснить в своем ответе, перехват исключений в Python дешевле по сравнению с другими языками, но, конечно, это не бесплатно. Исключения по-прежнему предназначены для исключительных случаев, в данном случае, если вы ожидаете, что ключ будет существовать большую часть времени. Тогда дополнительная проверка для LBYL будет немного дороже. - person poke; 02.10.2015
comment
@brunodesthuilliers возьмем ваш пример: (1,3156080246-0,368113994598)/1000000 = 0,00000094749403, что является средней стоимостью отлова одного исключения в приведенном вами примере. Это меньше микросекунды, или 947 наносекунд, если быть более точным... - person Nir Alfasi; 02.10.2015
comment
@alfasin wrt/ код - это всего лишь ваш собственный пример, и утверждение, что в Python перехват исключений является недорогой операцией, просто вводит в заблуждение. Это может быть дешевле, чем в некоторых других языках, но все же намного дороже, чем поиск по ключу. - person bruno desthuilliers; 02.10.2015
comment
@brunodesthuilliers, вы вырываете мои слова из контекста, это недорого по сравнению с другими языками, и поэтому его рекомендуется использовать. Я не говорил, что это «бесплатно». - person Nir Alfasi; 02.10.2015
comment
@brunodeshuilliers Но это это недорогая операция. Это не значит, что вы можете сравнить его с чем-то еще более дешевым и сказать, что это неправильно. Это все равно, что сказать, что print дорого, потому что не печатать намного дешевле. Попробуйте запустить тот же пример, например. Java, и вы можете понять, почему исключения здесь недороги. - person poke; 02.10.2015
comment
Однажды мне пришлось исправлять чужой код для довольно сложной ежедневной вычислительной задачи, которая отнимала слишком много времени. Он использовал EAFP повсюду. Простое изменение этого на LBYL почти вдвое сократило время выполнения. В приведенном выше примере версия EAFP всего в 3,5 раза медленнее, да, конечно, это недорого. И это будет мой последний комментарий по этому поводу. - person bruno desthuilliers; 02.10.2015
comment
@brunodesthuilliers, это в 3,5 раза медленнее, что делает его медленнее на 1 секунду на миллион выполнений. Когда вы улучшали его код, вы, должно быть, делали другие вещи, о которых даже не подозреваете... - person Nir Alfasi; 02.10.2015

Здесь вы смешиваете две вещи: утверждения и логику на основе EAFP.

Утверждения используются для проверки контракта функций, то есть его пред- и постусловий, а иногда и его инвариантов. Они гарантируют, что функция будет использоваться так, как она должна использоваться. Однако они не предназначены для потока кода, поскольку полностью прерывают выполнение при ошибке. Типичным примером является проверка None аргументов в вызовах функций.

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

Теперь EAFP — это нечто другое. Он используется в потоке управления, или, скорее, он позволяет избежать дополнительного потока управления в пользу того, чтобы ожидать, что все будет правильно, и вместо этого перехватывать исключения, если они неверны. Типичным примером, показывающим разницу, является доступ к ключу в словаре:

# LBYL
if key in dic:
    print(dic[key])
else:
    handleError()

# EAFP
try:
    print(dic[key])
except KeyError:
    handleError()

Теперь это выглядит очень похоже, хотя вы должны иметь в виду, что решение LBYL проверяет словарь дважды. Как и в случае со всем кодом, перехватывающим исключения, вы должны делать это только в том случае, если отсутствие ключа является исключительным случаем. Поэтому, если обычно предоставленный ключ не содержится в словаре, то это EAFP, и вы должны просто получить к нему прямой доступ. Если вы не ожидаете, что ключ будет присутствовать в словаре, то вам, вероятно, следует сначала проверить его существование (хотя исключения в Python дешевле, они все же не бесплатны, поэтому оставьте их для исключительных случаев).

Преимущество EAFP здесь также заключается в том, что глубже в логике вашей библиотеки или приложения, где key приходит сверху, вы можете просто предположить, что здесь был передан действительный ключ. Таким образом, вам не нужно перехватывать здесь исключения, а просто позвольте им подняться до более высокой точки в вашем коде, где вы затем сможете обработать ошибку. Это позволяет вам иметь низкоуровневые функции, полностью свободные от таких проверок.

person poke    schedule 02.10.2015
comment
Спасибо. Большой +1 за иллюстрацию и объяснение различных преимуществ подхода EAFP в Python. - person n1000; 02.10.2015

Хороший вопрос! В StackOverflow очень мало вопросов о «философии, лежащей в основе принципа».

Что касается определения EAFP в глоссарии Python, я бы даже пошел дальше. и говорят, что его упоминание об «исключениях кэширования, если предположение оказывается ложным» в этом контексте несколько вводит в заблуждение. Потому что, скажем прямо, следующий 2-й фрагмент кода НЕ выглядит более «чистым и быстрым» (термин, используемый в вышеупомянутом определении). Неудивительно, что ОП задал этот вопрос.

# LBYL
if key in dic:
    print(dic[key])
else:
    handleError()

# EAFP
try:
    print(dic[key])
except KeyError:
    handleError()

Я бы сказал, что настоящий момент, когда EAFP сияет, заключается в том, что вы вообще НЕ пишете try ... except ..., по крайней мере, не в большей части своей базовой кодовой базы. Потому что, первое правило обработки исключений: не делать обработка исключений. Имея это в виду, теперь давайте перепишем второй фрагмент следующим образом:

# Real EAFP
print(dic[key])

Теперь, разве это не настоящий подход EAFP, чистый и быстрый?

person RayLuo    schedule 23.04.2019

Я расширим ответ от @RayLuo.

Проблема с LBYL в том, что он вообще не работает. Если у вас нет однопоточного приложения, всегда возможно состояние гонки:

# LBYL
if key in dic:
    # RACE CONDITION
    print(dic[key])
else:
    handleError()

# EAFP
try:
    print(dic[key])
except KeyError:
    handleError()

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

Это означает, что правильный способ реализации LBYL:

if key in dic:
    try:
        print(dic[key])
    except KeyError:
        handleError()
else:
    handleError()

Обратите внимание, что предложение try / except точно такое же, как и в подходе EAFP.

Поскольку вы должны обрабатывать исключение в стиле EAFP даже при использовании подхода LBYL, вы могли бы также использовать подход EAFP в первую очередь.

Единственное обстоятельство, при котором я бы использовал проверку if, — это если последующее действие (в данном случае print) очень дорогое / требует много времени для инициирования. Это было бы редкостью и не было бы причиной каждый раз использовать проверку if.

Итог: LBYL в общем случае не работает, но EAFP работает. Хорошие разработчики сосредотачиваются на общих шаблонах решений, которые они могут уверенно использовать в широком диапазоне задач. Научитесь использовать EAFP последовательно.

person Chris Johnson    schedule 01.03.2021