Повернення булевого типу, коли успіх чи невдача є єдиною турботою


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не зможе відкритись. Це забезпечує, що програмісти мають об'єкт об'єкта, який знаходиться у дійсному стані, який може бути використаний.

Редагувати:

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


Це підтвердження того, що я роблю, тому мені подобається відповідь :-). Щодо об’єктів, хоча я розумію, звідки ви приїжджаєте, я не бачу, як це допомагає у більшості випадків, я б його використовував. Я хочу бути СУХОМО, тому я збираюся відновити об'єкт єдиним методом, оскільки я хочу зробити це лише одне. Потім мені залишається той самий код, що і зараз, окрім додаткового методу. (також для наведеного прикладу я видаляю файл, тому нульовий об’єкт файлу не говорить дуже багато :-)
Бен

Видалити складно, оскільки це не гарантується. Я ніколи не бачив, щоб метод видалення файлів кидав виняток, але що може зробити програміст, якщо він не працює? Постійно повторюючи цикл? Ні, це проблема ОС. Код повинен записувати результат і рухатися далі.
Реакційний

4

Ваша інтуїція щодо цього правильна, є кращий спосіб зробити це: монади .

Що таке монади?

Монади - це, перефразовуючи Вікіпедію, спосіб з'єднання операцій разом, приховуючи ланцюговий механізм; у вашому випадку механізмом ланцюга є вкладені ifs. Прихойте це, і ваш код буде пахнути набагато приємніше.

Є пара монад, які зроблять саме це ("Можливо" та "Ейбі") і пощастило для вас, що вони є частиною справді гарної бібліотеки монад пітона!

Що вони можуть зробити для вашого коду

Ось приклад використання монади "Either" ("Недоступно" у бібліотеці, з якою пов'язана), де функція може повернути Успіх чи Невдачу, залежно від того, що сталося:

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."))

У кожному з yieldї в process_fileфункції, якщо виклик функції повертає помилку , то process_fileфункція буде вийти з, в той момент , повертаючи значення Відмови від невдалої функції, замість того , щоб продовжувати на до кінця і повертаючиSuccess("All ok.")

Тепер уявіть, що ви робите вище з вкладеними ifs! (Як би ви обробляли повернене значення !?)

Висновок

Монади приємні :)


Примітки:

Я не програміст Python - я використовував бібліотеку монад, пов'язану вище, у сценарії, який я ніндзя мав для автоматизації проектів. Я погоджуюсь, що загалом кращим, ідіоматичним підходом є використання виключень.

IIRC є помилка друку в скрипті lib на сторінці, пов’язаній з тим, що я забуваю, де це банкомат. Я оновлю, якщо пам’ятаю. Я відрізняв свою версію від сторінки та виявив: def failable_monad_examle():-> def failable_monad_example():- pв програмі exampleвідсутні.

Для того, щоб отримати результат функції недоступного декорування (наприклад, process_file), ви повинні зафіксувати результат у variableта зробити це, variable.valueщоб отримати його.


2

Функція - це договір, а його назва повинна підказувати, який контракт він буде виконувати. IMHO, якщо ви назвали його remove_fileтаким чином, він повинен видалити файл, а якщо цього не зробити, це може спричинити виняток. З іншого боку, якщо ви його try_remove_fileназвете, слід "спробувати" видалити і повернути булеве значення, щоб повідомити, видалено файл чи ні.

Це призвело б до іншого питання - це повинно бути remove_fileчи try_remove_file? Це залежить від вашого сайту виклику. Насправді, ви можете мати обидва способи та використовувати їх у різних сценаріях, але я думаю, що видалення файлу per-se має високі шанси на успіх, тому я вважаю за краще remove_fileвиняток лише кинути, коли не вдасться.


0

У цьому конкретному випадку може бути корисно подумати над тим, чому ви не зможете видалити файл. Скажімо, проблема полягає в тому, що файл може існувати або не існувати. Тоді ви повинні мати функцію, doesFileExist()яка повертає істинне чи помилкове, і функцію, removeFile()яка просто видаляє файл.

У своєму коді ви спершу перевірте, чи існує файл. Якщо так, зателефонуйте removeFile. Якщо ні, то робіть інші речі.

У цьому випадку ви все ще removeFileможете скинути виняток, якщо файл неможливо видалити з якоїсь іншої причини, наприклад, дозволів.

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


Це дуже специфічно для прикладу, який я наводив, якого я б краще уникнути. Я ще цього не писав, збирається архівувати файли та записувати той факт, що це сталося в базі даних. Файли можуть бути перезавантажені в будь-який час (хоча один раз завантажені файли набагато рідше перезавантажуються) можливо, що файл може бути заблокований іншим процесом між перевіркою та спробою видалення. Немає нічого виняткового в збої. Це стандартний Python, щоб не турбуватися спочатку перевіряти і виловлювати виняток, коли його піднімають (якщо потрібно), я просто не хочу цього разу нічого з цим робити.
Бен

Якщо в збої немає нічого виняткового, то перевірка того, чи можна вилучити файл, є законною частиною логіки вашої програми. Принцип єдиної відповідальності передбачає, що ви повинні мати функцію перевірки та функцію RemoveFile.
Діма
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.