Использование try vs if в Python

151

Есть ли смысл решить, какую из конструкций tryили ifиспользовать при тестировании переменной на наличие значения?

Например, есть функция, которая либо возвращает список, либо не возвращает значение. Я хочу проверить результат перед его обработкой. Что из перечисленного было бы предпочтительнее и почему?

result = function();
if (result):
    for r in result:
        #process items

или

result = function();
try:
    for r in result:
        #process items
except TypeError:
    pass;

Связанное обсуждение:

Проверка существования члена в Python

артданил
источник
Имейте в виду, что если ваша строфа #process items может вызвать ошибку TypeError, вам нужно обернуть другой блок try: except:. В этом конкретном примере я бы просто использовал if:
richo

Ответы:

251

Вы часто слышите, что Python поощряет стиль EAFP («проще попросить прощения, чем разрешения»), а не стиль LBYL («посмотрите, прежде чем прыгнуть»). Для меня это вопрос эффективности и удобочитаемости.

В вашем примере (скажем, что вместо возврата списка или пустой строки функция должна была вернуть список или None), если вы ожидаете, что 99% времени на resultсамом деле будет содержать что-то итеративное, я бы использовал этот try/exceptподход. Будет быстрее, если исключения действительно будут исключительными. Если resultэто Noneболее 50% случаев, то, ifвероятно, лучше использовать.

Чтобы подтвердить это с помощью нескольких измерений:

>>> import timeit
>>> timeit.timeit(setup="a=1;b=1", stmt="a/b") # no error checking
0.06379691968322732
>>> timeit.timeit(setup="a=1;b=1", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.0829463709378615
>>> timeit.timeit(setup="a=1;b=0", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.5070195056614466
>>> timeit.timeit(setup="a=1;b=1", stmt="if b!=0:\n a/b")
0.11940114974277094
>>> timeit.timeit(setup="a=1;b=0", stmt="if b!=0:\n a/b")
0.051202772912802175

Итак, в то время как ifутверждение всегда стоит вам денег, установить try/exceptблок почти бесплатно . Но когда это Exceptionпроисходит на самом деле, цена намного выше.

Мораль:

  • Совершенно нормально (и "питонически") использовать try/exceptдля управления потоком,
  • но это имеет смысл больше всего, когда Exceptions на самом деле исключительны.

Из документов Python:

EAFP

Проще просить прощения, чем разрешения. Этот общий стиль кодирования Python предполагает наличие действительных ключей или атрибутов и перехватывает исключения, если предположение оказывается ложным. Для этого чистого и быстрого стиля характерно наличие множества tryи exceptвысказываний. Эта техника контрастирует со стилем LBYL, общим для многих других языков, таких как C.

Тим Пицкер
источник
1
Спасибо. Я вижу, что в этом случае обоснованием может быть ожидание результата.
artdanil 02
6
.... и это одна (из многих) причин, почему так сложно выполнить настоящую оптимизирующую JIT для Python. Как доказывает недавно выпущенная бета-версия LuaJIT 2, динамические языки могут быть действительно очень быстрыми; но это сильно зависит от исходного языкового дизайна и стиля, который он поощряет. (кстати, из-за языкового дизайна даже самые лучшие JIT JavaScirpt не могут сравниться с LuaJIT 1, не говоря уже о 2)
Хавьер
1
@ 2rs2ts: Я сам делал подобные тайминги. В Python 3 это try/exceptбыло на 25% быстрее, чем if key in d:в случаях, когда ключ был в словаре. Это было намного медленнее, когда ключа не было в словаре, как ожидалось и соответствовало этому ответу.
Tim Pietzcker 06
5
Однако я знаю, что это старый ответ: использование таких операторов, как 1/1with timeit, не лучший выбор, потому что они будут оптимизированы (см. dis.dis('1/1')И обратите внимание, что деления не происходит).
Андреа Корбеллини,
1
Это также зависит от операции, которую вы хотите провести. Одно деление выполняется быстро, а обнаружение ошибки обходится дорого. Но даже в тех случаях, когда стоимость обработки исключений приемлема, внимательно подумайте о том, что вы просите компьютер сделать. Если сама операция стоит очень дорого, независимо от ее результатов, и есть дешевый способ узнать, возможен ли успех: используйте его. Пример: не копируйте половину файла только для того, чтобы понять, что места недостаточно.
Bachsau
15

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

for r in function():
    # process items
Брэндон
источник
2
Я полностью согласен с вами в этом вопросе; однако функция не моя, и я просто использую ее.
artdanil 02
2
@artdanil: Итак, вы можете обернуть эту функцию в одну, которую вы пишете, которая работает как та, о которой думает Брэндон Корфман.
quamrana 02
24
что вызывает вопрос: должна ли оболочка использовать if или попытку обработать не повторяющийся случай?
jcdyer 03
@quamrana, что я и пытаюсь сделать. Итак, как указал @jcd, вопрос в том, как я должен обрабатывать ситуацию в функции-оболочке.
artdanil 03
12

Не обращайте внимания на мое решение, если предоставленный мной код на первый взгляд неочевиден и вам необходимо прочитать объяснение после примера кода.

Могу ли я предположить, что «значение не возвращено» означает, что возвращаемое значение - None? Если да, или если «нет значения» имеет логическое значение False, вы можете сделать следующее, поскольку ваш код по существу обрабатывает «нет значения» как «не повторять»:

for r in function() or ():
    # process items

Если function()возвращает что-то, что не соответствует истине, вы перебираете пустой кортеж, то есть не выполняете никаких итераций. По сути, это LBYL.

цот
источник
4

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

Дэйв Кирби
источник
4

Что из перечисленного было бы предпочтительнее и почему?

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

(Я согласен с Брэндоном Корфманом: возврат None вместо «нет элементов» вместо пустого списка нарушен. Это неприятная привычка программистов Java, которая не должна встречаться в Python или Java.)

бобинс
источник
4

В общем, у меня сложилось впечатление, что исключения следует делать на исключительные обстоятельства. Если resultожидается, что он никогда не будет пустым (но может быть, если, например, произошел сбой диска и т. Д.), Второй подход имеет смысл. Если, с другой стороны, пустое значение resultвполне разумно при нормальных условиях, проверка его с помощью ifоператора имеет больше смысла.

Я имел в виду (более распространенный) сценарий:

# keep access counts for different files
file_counts={}
...
# got a filename somehow
if filename not in file_counts:
    file_counts[filename]=0
file_counts[filename]+=1

вместо эквивалента:

...
try:
    file_counts[filename]+=1
except KeyError:
    file_counts[filename]=1
Манагу
источник
Это пример различия подходов, упомянутых Тимом Пицкером: первый - LBYL, второй
EAFP
Просто небольшое примечание, которое ++не работает в python, используйте += 1вместо этого.
tgray 02
3

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

result = function();
try:
    it = iter(result)
except TypeError:
    pass
else:
    for r in it:
        #process items

Как видите, довольно некрасиво. Я не предлагаю этого, но для полноты картины следует упомянуть об этом.

AFoglia
источник
На мой взгляд, преднамеренное появление ошибок типа всегда является плохим стилем программирования. Разработчик должен знать, чего ожидать, и быть готовым обрабатывать эти значения без исключений. Каждый раз, когда операция выполняла то, что должна была сделать (с точки зрения программиста) и ожидаемые результаты представлены списком или None, всегда проверяйте результат с помощью is Noneили is not None. С другой стороны, создавать исключения для получения законных результатов - тоже плохой стиль. Исключения бывают неожиданными. Пример: str.find()возвращает -1, если ничего не найдено, потому что сам поиск завершился без ошибок.
Bachsau
1

Что касается производительности, использование блока try для кода, который обычно не вызывает исключений, быстрее, чем использование оператора if каждый раз. Итак, решение зависит от вероятности исключительных случаев.

серый
источник
-5

Как правило, вы никогда не должны использовать try / catch или какие-либо средства обработки исключений для управления потоком. Несмотря на то, что скрытая итерация контролируется путем создания StopIterationисключений, вы все равно должны предпочесть свой первый фрагмент кода второму.

Ilowe
источник
7
Это верно для Java. Python довольно сильно поддерживает EAFP.
fengb 02
3
Это все еще странная практика. По моему опыту, мозги большинства программистов запрограммированы так, чтобы пропускать EAFP. В конечном итоге они задаются вопросом, почему был выбран определенный путь. При этом есть время и место, чтобы нарушить каждое правило.
ilowe 02