Возврат логического значения, когда успех или неудача являются единственной проблемой

15

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

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

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

Это действительно простой пример, который демонстрирует то, о чем я говорю.

import os

class DoSomething(object):

    def remove_file(self, filename):

        try:
            os.remove(filename)
        except OSError:
            return False

        return True

    def process_file(self, filename):

        do_something()

        if remove_file(filename):
            do_something_else()

Хотя это функционально, мне действительно не нравится этот способ что-то делать, он «пахнет» и может иногда приводить к множеству вложенных ifs. Но я не могу придумать более простой способ.

Я мог бы обратиться к более философии LBYL и использовать os.path.exists(filename) перед попыткой удаления, но нет никаких гарантий, что файл не будет заблокирован за это время (это маловероятно, но возможно), и мне все еще нужно определить, было ли удаление успешным или нет.

Является ли это «приемлемым» дизайном, и если нет, то каким будет лучший способ его разработки?

Бен
источник

Ответы:

11

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

Вы должны бросить, exceptionкогда метод / функция вряд ли будут использоваться в логических решениях.

Вы должны принять решение о том, насколько важен сбой, и нужно ли его устранять или нет. Если вы можете классифицировать ошибку как предупреждение, вернитесь boolean. Если объект входит в плохое состояние, что делает будущие вызовы к нему нестабильными, то бросить exception.

Другая практика - возвращаться objectsвместо результата. Если вы звоните open, то он должен вернуть Fileобъект илиnull если не удается открыть. Это гарантирует, что у программистов есть экземпляр объекта, который находится в допустимом состоянии, которое можно использовать.

РЕДАКТИРОВАТЬ:

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

Reactgular
источник
Это подтверждение того, что я делаю, поэтому мне нравится ответ :-). Что касается объектов, хотя я понимаю, откуда вы пришли, я не понимаю, как это помогает в большинстве случаев, я бы использовал его. Я хочу быть СУХИМЫМ, поэтому я собираюсь перенастроить объект на один метод, поскольку я хочу сделать с ним только одну вещь. Затем я остался с тем же кодом, что и сейчас, но сохранил с помощью дополнительного метода. (также для примера, приведенного выше, я удаляю файл, чтобы нулевой файловый объект ничего не говорил :-)
Бен
Удалить сложно, потому что это не гарантировано. Я никогда не видел, чтобы метод удаления файла вызывал исключение, но что может сделать программист, если он потерпит неудачу? Непрерывно повторять цикл? Нет, это проблема ОС. Код должен записать результат и двигаться дальше.
Reactgular
4

Ваша интуиция в этом верна, есть лучший способ сделать это: монады .

Какие монады?

Монады - это (перефразируя Википедию) способ объединения операций вместе, скрывая механизм объединения; в вашем случае механизм сцепления - это вложенный ifs. Скройте это, и ваш код будет сильно пахнуть лучше.

Есть пара монад, которые будут делать именно это («Возможно» и «Либо»), и, к счастью для вас, они являются частью действительно хорошей библиотеки монад Python!

Что они могут сделать для вашего кода

Вот пример использования монады «Either» («Failable» в связанной библиотеке), где функция может возвращать Success или Failure, в зависимости от того, что произошло:

import os

class DoSomething(object):

    def remove_file(self, filename):
        try:
            os.remove(filename)
            return Success(None)
        except OSError:
            return Failure("There was an OS Error.")

    @do(Failable)
    def process_file(self, filename):
        do_something()
        yield remove_file(filename)
        do_something_else()
        mreturn(Success("All ok."))

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

    def action_that_might_fail_and_returns_something(self):
        # get some random value between 0 and 1 here
        if value < 0.5:
            return Success(value)
        else:
            return Failure("Bad value! Bad! Go to your room!")

    @do(Failable)
    def process_file(self, filename):
        do_something()
        yield remove_file(filename)
        yield action_that_might_fail(somearg)
        yield another_action_that_might_fail(someotherarg)
        some_val = yield action_that_might_fail_and_returns_something()
        yield something_that_used_the_return_value(some_val)
        do_something_else()
        mreturn(Success("All ok."))

На каждом из yields в process_fileфункции, если вызов функции возвращает Failure, process_fileфункция в этот момент завершает работу, возвращая значение Failure из сбойной функции, вместо продолжения через оставшуюся часть и возвратаSuccess("All ok.")

Теперь представьте, что делаете выше с вложенными ifs! (Как бы вы справились с возвращаемым значением !?)

Вывод

Монады хороши :)


Примечания:

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

IIRC В скрипте lib на странице есть ссылка на опечатку, хотя я забыл, где находится банкомат. Я буду обновлять, если я помню. Я сравнил свою версию со страницей и нашел: def failable_monad_examle():-> def failable_monad_example():- pвexample пропускал.

Чтобы получить результат функции Failable (например, process_file), вы должны зафиксировать результат в a variableи сделать a, variable.valueчтобы получить его.

Павел
источник
2

Функция является контрактом, и ее название должно указывать, какой контракт она будет выполнять. ИМХО, если вы назовете его remove_fileтак, чтобы он удалил файл, и если вы не сделаете это, должно произойти исключение. С другой стороны, если вы назовете его try_remove_file, он должен «попытаться» удалить и вернуть логическое значение, чтобы сказать, был ли файл удален или нет.

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

ТИА
источник
0

В этом конкретном случае может быть полезно подумать о том, почему вы не сможете удалить файл. Допустим, проблема в том, что файл может существовать или не существовать. Тогда у вас должна быть функция, doesFileExist()которая возвращает true или false, и функция, removeFile()которая просто удаляет файл.

В вашем коде вы сначала проверите, существует ли файл. Если это так, позвоните removeFile. Если нет, то делай другие вещи.

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

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

Дима
источник
Это очень специфично для приведенного мною примера, которого я бы предпочел избежать. Я еще не написал это, он собирается архивировать файлы и регистрировать тот факт, что это произошло в базе данных. Файлы могут быть перезагружены в любое время (хотя после того, как загруженные файлы будут реже загружаться), существует вероятность, что файл может быть заблокирован другим процессом между проверкой и попыткой удаления. Нет ничего исключительного в неудаче. Это стандартный Python, который не потрудился сначала проверить и перехватить исключение при его возникновении (если требуется), я просто не хочу ничего делать с ним в этот раз.
Бен
Если в сбое нет ничего исключительного, то проверка того, можете ли вы удалить файл, является законной частью логики вашей программы. Принцип единой ответственности требует наличия функции проверки и функции removeFile.
Дима