"Просите прощения, а не разрешения" - объясните

Я не прошу личных "религиозных" мнений об этой философии, а немного более технических.

Я понимаю, что эта фраза является одним из нескольких тестов лакмусовой бумажки, чтобы узнать, является ли ваш код "pythonic". Но для меня pythonic означает чистый, простой и интуитивно понятный, не загруженный обработчиками исключений для плохого кодирования.

Итак, практический пример. Я определяю класс:

class foo(object):
    bar = None

    def __init__(self):
        # a million lines of code
        self.bar = "Spike is my favorite vampire."
        # a million more lines of code

Теперь, исходя из процедурного фона, в другой функции я хочу сделать это:

if foo.bar:
    # do stuff

Я получаю исключение атрибута, если я был нетерпелив и не делал начального foo = None. Итак, "просить прощения не разрешение" предполагает, что я должен сделать это вместо этого?

try:
    if foo.bar:
        # do stuff
except:
    # this runs because my other code was sloppy?

Почему мне было бы лучше добавить дополнительную логику в блок try, чтобы я мог оставить определение класса более неоднозначным? Почему бы не определить все изначально, прежде чем явно предоставить разрешение?

(Не бить меня о том, как использовать try/except blocks... Я использую их везде. Я просто не думаю, что правильно использовать их, чтобы поймать собственные ошибки, потому что я не был программистом.

Или... я полностью неправильно понимаю мантру "Спроси мужа"?

Ответ 1

Классическим примером "попросить прощения без разрешения" является доступ к значениям из dict, которые могут не существовать. Например:.

names = { 'joe': 'Joe Nathan', 'jo': 'Jo Mama', 'joy': 'Joy Full' }
name = 'hikaru'

try:
    print names[name]
except KeyError:
    print "Sorry, don't know this '{}' person".format(name)

Здесь указано исключение (KeyError), поэтому вы не просите прощения за каждую ошибку, которая может произойти, но только тот, который, естественно, произойдёт. Для сравнения, подход "спросить разрешения первый" может выглядеть так:

if name in names:
    print names[name]
else:
    print "Sorry, don't know this '{}' person".format(name)

или

real_name = names.get(name, None)
if real_name:
    print real_name
else:
    print "Sorry, don't know this '{}' person".format(name)

Такие примеры "просить прощения" часто слишком просты. ИМО не кристально ясно, что блоки try/except по своей сути лучше, чем if/else. Реальная ценность намного яснее при выполнении операций, которые могут потерпеть неудачу различными способами - например, синтаксический анализ; используя eval(); доступ к операционной системе, промежуточное ПО, база данных или сетевые ресурсы; или выполнения сложной математики. Когда существует несколько возможных режимов отказа, быть готовым получить прощение является чрезвычайно ценным.

Другие примечания о примерах кода:

Вам не нужно загружать try/except блоки вокруг использования каждой переменной. Это было бы ужасно. И вам не нужно устанавливать self.bar в свой __init__(), поскольку он установлен в вашем определении class выше. Обычно он определяется либо в классе (если данные могут быть разделены между всеми экземплярами класса), либо в __init__() (если это данные экземпляра, характерные для каждого экземпляра).

Значение None не undefined, или ошибка, кстати. Это конкретное и законное значение, означающее none, nil, null или nothing. Многие языки имеют такие значения, поэтому программисты не "перегружают" 0, -1, '' (пустая строка) или аналогичные полезные значения.

Ответ 2

"Просить прощения, а не разрешения" выступает против двух стилей программирования. "Запросить разрешение" выглядит следующим образом:

if can_do_operation():
    perform_operation()
else:
    handle_error_case()

"Просить прощения" выглядит следующим образом:

try:
    perform_operation()
except Unable_to_perform:
    handle_error_case()

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

Есть две основные причины, по которым лучше попросить прощения:

  • В мире concurrrent (в многопоточной программе, или если операция включает объекты, которые являются внешними по отношению к программе, такие как файлы, другие процессы, сетевые ресурсы и т.д.), ситуация может измениться между временем выполнения can_do_operation() и время запуска perform_operation(). Поэтому вам все равно придется обрабатывать ошибку.
  • Вам необходимо использовать правильные критерии для запроса разрешения. Если вы ошибетесь, вы либо не сможете выполнить операцию, которую вы могли бы выполнить, либо произошла ошибка, потому что вы не сможете выполнить операцию в конце концов. Например, если вы проверяете, существует ли файл перед его открытием, возможно, что файл существует, но вы не можете его открыть, потому что у вас нет разрешения. И наоборот, возможно, файл создается, когда вы его открываете (например, потому что это происходит через сетевое соединение, которое появляется только тогда, когда вы действительно открываете файл, а не когда вы только ткните, чтобы увидеть, есть ли он там).

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

Когда вы пишете foo.bar, небытие bar обычно не считается сбоем объекта foo. Обычно это ошибка программиста: попытка использовать объект таким образом, чтобы он не был предназначен. Последствием ошибки программиста в Python является необработанное исключение (если вам повезло: конечно, некоторые ошибки программиста не могут быть обнаружены автоматически). Поэтому, если bar является необязательной частью объекта, обычным способом справиться с этим является наличие поля bar, которое инициализируется на None, и устанавливает другое значение, если присутствует дополнительная часть. Чтобы проверить, присутствует ли bar, напишите

if foo.bar != None:
     handle_optional_part(foo.bar)
else:
     default_handling()

Вы можете сокращать if foo.bar != None: до if foo.bar: только в том случае, если bar всегда будет истинным, если интерпретируется как логическое - если bar может быть 0, [], {} или любым другим объектом, который имеет ложное значение истины, вам нужно != None. Это также понятно, если вы тестируете дополнительную часть (в отличие от тестирования между True и False).

На этом этапе вы можете спросить: почему бы не опустить инициализацию bar, когда она там отсутствует, и проверить ее наличие с помощью hasattr или уловить ее с помощью обработчика AttributeError? Поскольку ваш код имеет смысл только в двух случаях:

  • объект не имеет поля bar;
  • объект имеет поле bar, что означает, что вы думаете, что оно означает.

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

Ответ 3

Вы правы - цель try и except не должна охватывать ваше неаккуратное кодирование. Это просто приводит к более неряшливому кодированию.

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

Ответ 4

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

Тем не менее, try/except часто используется, когда существуют "лучшие" альтернативы. Например:

# Do this    
value = my_dict.get("key", None)

# instead of this
try:
  value = my_dict["key"]
except KeyError:
  value = None

Что касается вашего примера, используйте if hasattr(foo, "bar"), если у вас нет контроля над foo, и вам нужно проверить соответствие вашим ожиданиям, иначе просто используйте foo.bar, и пусть полученная ошибка станет вашим руководством по идентификации и исправлению неаккуратных код.

Ответ 5

В контексте Python "Просить прощения без разрешения" подразумевается стиль программирования, в котором вы не проверяете, что все должно быть так, как вы ожидаете, но вы обрабатываете ошибки, которые возникают, если они не являются. Классическим примером является не проверка того, что словарь содержит данный ключ, как в:

d = {}
k = "k"
if k in d.keys():
  print d[k]
else:
  print "key \"" + k + "\" missing"

Но скорее обработать результирующую ошибку, если отсутствует ключ:

d = {}
k = "k"
try:
  print d[k]
except KeyError:
  print "key \"" + k + "\" missing"

Однако не следует заменять каждый if в коде кодом try/except; что сделает ваш код явно беспорядочным. Вместо этого вы должны ловить ошибки, когда вы действительно можете что-то сделать с ними. В идеале это уменьшило бы общую обработку ошибок в вашем коде, что сделало бы его фактическую цель более очевидной.

Ответ 6

Запрашивать прощение не означает, что это означает упрощение кода. Это означало, что код должен быть написан так, когда есть разумное ожидание того, что .bar может вызывать AttributeError.

 try:
     print foo.bar
 except AttributeError as e
     print "No Foo!" 

Ваш код появляется как для разрешения, так и для прощения:)

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

Ответ 7

Здесь есть много хороших ответов, я просто подумал, что добавлю то, о чем я не упоминал до сих пор.

Часто прошение о прощении вместо разрешения имеет улучшения производительности.

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

Обычно случаи сбоев встречаются редко, а это значит, что если вы только просите разрешения, вам вряд ли придется делать какие-либо дополнительные операции. Да, когда он терпит неудачу, он генерирует исключение, а также выполняет дополнительную операцию, но исключения в python происходят очень быстро. Здесь вы можете увидеть некоторые тайминги: https://jeffknupp.com/blog/2013/02/06/write-cleaner-python-use-exceptions/