Рубрики
Без рубрики

Исключительный питон

Вещи, которые я узнал, пытаясь реализовать правильное управление ошибками в коде Python .. Tagged с Python.

Первоначально опубликовано на мой блог

Иногда дела идут не так. У нас есть много способов выразить это на английском языке:

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

Вы должны заботиться, потому что ошибки и неожиданные вещи Уилл случаться.

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

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

Наконец, ваши конечные пользователи должны знать, что вы делаете, когда возникает ошибка. Они хотят знать, что случилось и Что еще более важно, что они должны сделать, чтобы исправить это.

Итак, правильная обработка ошибок является ключом. Вы точно не хотите, чтобы ваш продукт был Показано в Ежедневная wtf error’d ряд , ты?

Примечание: этот раздел в основном теоретический. Если вы хотите больше конкретных вещей, не стесняйтесь пропустить непосредственно к части 2.

Когда вы используете Bash, вы обычно запускаете несколько команд Extern, например:

cd project
git pull
pylint project
pytest

Каждая из этих команд имеет код возврата, к которому вы можете получить доступ со специальной переменной с именем $? .

Код возврата равен 0, когда команда преуспевает, и может получить много других значений, когда что -то пойдет не так. Как сказал Толстой:

Счастливые семьи одинаковы; Каждая несчастная семья недоволен по -своему.

Например, pytest как 6 различных возможных кодов возврата:

  • 0 Когда все прошедшие тесты
  • 1 Когда тесты были собраны и запускаются, но Некоторые из тестов не удались
  • 2 Когда пользователь прервал выполнение теста
  • и Подробнее 1

Есть две проблемы с этим подходом:

  • Вы должны все время проверять код возврата, в противном случае код будет продолжать выполнять, игнорируя предыдущие ошибки. (Вы можете исправить это, вызывая set -e в верхней части сценария)
  • Когда что -то терпит неудачу, единственная информация, которая у вас есть, – это число. ( И, может быть, вывод, в зависимости от того, как написан сценарий)

В C.

Когда вы пишете в C, вы должны проверить возвращенное значение каждой функции, которую вы вызовите.

Иногда это целое число. Например:

int size = ...;
FILE* fp = ...;
char buff[size];
int n = fread(buff, size, 1, fp);
if (n < size) {
  // Handle "short read"
}

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

FILE* fp = fopen("foo.cfg", "r");
if (!fp) {
  if (errno == ENOENT) {
    fprintf(stderr, "Could not find foo.cfg\n");
  }
}

Здесь у вас даже нет сообщения об ошибке, все, что вы получаете в номере. (EnoEnt – это просто #define enoent 2 )

Примечание: есть инструмент под названием errno в Morethutils Это может помочь вам, если по какой-то причине все, что вы хотите преобразовать значение Errno в читаемое на человеку сообщение.

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

Если вы используете GCC , вы можете запустить предупреждение, когда вызывающий абонент не проверяет возвращенное значение, как так:

/* in foo.h */
int foo() __attribute__((warn_unused_result));

/* in foo.c */
{
  foo();  // triggers a warning
}

Но тогда люди могут игнорировать предупреждение …

В ходе функции могут вернуть несколько значений.

Функции, которые могут провалиться, должны вернуть Ошибка объект вдоль результата, как так:

file, err := os.Open("foo.cfg")
if err != nil {
  ...
}
// Do something with file

Вопреки C, труднее игнорировать возвращаемое значение.

Вы можете использовать что -то вроде:

file, _ := os.Open("foo.cfg");

Но есть линтеры, которые запрещают вам это делать.

В отличие от C, вы можете добавить все виды метаданных в свою ошибку.

Все, что вам нужно, это объявить пользовательскую структуру и внедрить Ошибка Метод:

type InvalidConfigError struct {
    Path string;
    Details string
}

func (e *InvalidConfig) Error() string {
    return fmt.Sprintf("%s: %s", e.Path, e.Details)
}

func readConf() (Config, error) {
  // ...
  file, errOpen := os.Open("foo.cfg");
  if errOpen := nill {
    return nil, &InvalidConfigError{"foo", "could not open file"};
  }

  parsed, errParse := parseYaml(file);
  if errParse != nill {
    return nil, &InvalidConfigError{"foo", "invalid YAML syntax"};
  }

  return parsed, nil;
}

Использовать его немного больно, потому что вы должны попытаться преобразовать его:

func main() {
    config, err := readConf();
    if err != nil {
        invalidConfig, ok := err.(*InvalidConfig)
        // ok will be false if conversion fails
        if ok {
            fmt.Printf("%s:%s", invalidConfig.Path, invalidConfig.Details);
        }
    }
    // Do someting with config ...
}

В Java вы используете исключения вместо этого, с бросить и поймать ключевые слова.

Как Go, вы можете создать свой собственный тип исключений.

Но есть два ключевых различия:

  • В поймать Блок вы можете указать, какие ошибки вы хотите обработать. Например Catch (fooexception) поймает все исключения, которые являются экземпляром Fooxception класс (Может быть, через наследство)
  • Они также пришли с Backtrace , который перечисляет все функции, которые были вызваны, когда произошло исключение.

Есть улов, хотя. Давайте посмотрим на этот пример:

public class Foo {

    public static void readConf() {
        FileInputStream fs = new FileInputStream("foo.cfg");
    }

    public static void main() {
      readConf();
      ....
    }
}

Если вы попытаетесь скомпилировать это, вы получите ошибку:

Foo.java:5: error: unreported exception FileNotFoundException;
must be caught or declared to be thrown
  FileInputStream fs = new FileInputStream("foo.cfg");

Java говорит вам, что вы можете выбрать:

  • Справиться с ошибкой прямо сейчас
  • Или позволь абоненту сделать это.

Если никто не поймает исключение в цепочке вызывающих абонентов, исключение называется «uncaught», и вся программа заканчивается после печати обратной марки.

Поскольку Java – это Java, вы должны быть явными об этом, как так:

public void readConf() throws FileNotFoundException {
    FileInputStream fs = new FileInputStream("foo.cfg");
}

public static void main(String[] args) throws FileNotFoundException {
    readConf();
    System.out.println("hello");
}

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

/* This error is unchecked because it inherits RuntimeException */
class FooException extends RuntimeException {
  private string path;
  private string details;
    // ...
}

public static void readConf() {
    try {
        FileInputStream fs = new FileInputStream("foo.cfg");
    }
    catch (FileNotFoundException e) {
        throw new FooException("foo.cfg", "file not found");
    }
}

// Look Ma, no 'throws' declaration!
public static void main(String[] args) {
}

Обратите внимание, как мы должны обернуть Filenotfoundexception и пересмотреть нашу собственную ошибку.

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

type alias Person = { name : String , age : Maybe Int }

canBuyAlcohol user =
  case user.age of
    Nothing -> false
    Just age -> (age >= 21)

или о JavaScript, где вы можете обрабатывать ошибки в обратных вызовах:

const prom = new Promise((resolve, reject) => {
  if (ok) {
    resolve('value');
  } else {
    reject('error');
  }
});

Но там так отличается от Python, что мне нечего сказать о них.

С Python вы обычно используете пытаться и кроме Анкет

Но приятно, что у вас много гибкости:

  • Вы можете использовать errno Как в C:
try:
    open("foo.cfg")
except OSError as e:
    if e.errno == errno.ENOENT:
        sys.stderr.write("File not found")

Хотя в Python3 вы бы предпочли сделать:

try:
   open("foo.cfg")
except FileNotFoundError:
   print("File not found", file=sys.stderr)
  • Вы можете использовать кортежи в качестве возвращаемых значений, как в Go:
def run_cmd(cmd):
    result = subprocess.run(cmd, stdout=subprocess.PIPE)
    rc = result.returncode
    out = result.stdout

    return (rc == 0, out.decode().strip())


ok, out = run_cmd(["git", "rev-parse", "--abbrev-ref", "@{u}"])
if ok:
    print("You are tracking", out)
else:
    print("You are not tracking any branch")

Вы также можете передать обратные вызовы, чтобы справиться с ошибками:

def try_something(on_error=None);
  try:
      ...
  except Error as e:
      on_error(e)

def log_error(e):
    log.error(e)

try_something(on_error=log_error)

Пользовательские исключения

В Python2 исключения, используемые для того, чтобы всегда иметь строковый атрибут с именем .message Анкет

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

# Only works in Python2!

def foo():
    raise Exception, "Oh noes!"
    # calls Exception("Oh noes")

try:
    foo()
cacth Exception, e:
    print(e)

В Python3 специальный синтаксис исчез, и все исключения теперь имеют args Атрибут, который является всего лишь кортеем любого типа.

Это означает, что когда вы создаете свой собственный тип исключения, вам нужно будет позвонить Super () .__ init __ () так что args Атрибут правильно установлен, как так:

class InvalidConfigError(RuntimeError)
    def __init__(self, path, details):
        self.path = path
        self.details = details
        super().__init__(path, details)

    def __str__(self):
        return f"{self.path}: {self.details}"


def read_config(path):
    try:
      ...
    except FileNotFoundError:
        raise InvalidConfigError(path, "not found")

    try:
      ...
    except YAMLError:
        raise InvalidConfigError(path, "invalid YAML syntax")

def main():
    try:
        read_config()
    except InvalidConfigError as e:
        sys.exit(e)

Утверждать и выходить

Обратите внимание, что есть и другие способы, чтобы программа Python прекратила другие, что необработанное исключение.

Например, вы можете использовать sys.exit (42) Чтобы заставить выход с данным кодом ошибки.

Потому что обычно отображать сообщение в Stderr Когда вы выходите из ненулевого кода, вы можете использовать sys.exit ("") Чтобы отобразить сообщение об ошибке и немедленно выйти потом.

Вы также можете использовать утверждения, например, так:

def foo(self):
    assert self.bar, "Calling foo() when self.bar is empty"

Однако sys.exit () и утверждение используют исключения под капотом, а именно SystemExit и AssertionError Анкет (Единственное отличие состоит в том, что sys.exit () не печатает Backtrace).

Таким образом, вы можете поймать как сбои утверждений, так и sys.exit звонит как обычно:

try:
   some_script()
except SystemExit as e:
   print("script failed with", e.code)

Итак, учитывая всю эту гибкость, как лучше всего справиться с ошибками в Python?

Вступление

Я собираюсь использовать TSRC Например.

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

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

В этом проекте насчитывается около 2000 строк кода Python, и он используется тремя различными способами:

  • Производственный код: что выполняется при вводе TSRC командование
  • Тесты: что выполняется при выполнении автоматических тестов
  • Библиотека: у нас есть сценарии C.I, написанные на Python, которые повторно используют некоторые из TSRC код. (Например, сбросить все репозитории в правильную ветвь при попытке построить запрос на вытягивание)

Наша цель – найти способ правильно обработать ошибки в этих трех контекстах.

Иерархия ошибок

Несколько созданная практика среди программистов Python – это определить базовый класс для всех исключений, поднятых их собственным проектом.

Обычно имя класса заканчивается «ошибкой». TSRC не отклоняется от этого маршрута:

# in tsrc/errors.py

class Error():
    def __init__(self, *args):
        super().__init__(self, *args)
        self.message = " ".join(str(x) for x in args)

    def __str__(self):
        return self.message

Что следует отметить здесь:

  • Мы определяем __str__ Метод, чтобы ошибки выглядели красиво при печати. Это будет иметь большое значение, когда мы будем рассмотреть ошибки
  • Базовый класс называется Ошибка Анкет Это уже в tsrc.errors Пространство имен, поэтому нет необходимости в имени с префиксом, таким как Tsrcerror Анкет 2

Далее у нас есть несколько пользовательских классов, унаследовавших от TSCR. Ошибка :

class GitCommandError(tsrc.Error):
    def __init__(self, working_path, cmd, *, output=None):


class GitLabError(tsrc.Error):
    def __init__(self, status_code, message):
        ...

Обратите внимание, как каждая ошибка построена с определенным набором атрибутов.

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

И то же самое с Gitlaberror : Вы захотите узнать о коде состояния HTTP и содержании ответа при сбое запроса на API Gitlab.

Когда бросить

Давайте посмотрим на конкретный пример: TSRC Sync командование

Это команда, которая синхронизирует все хранилища текущего рабочего пространства.

При написании реализации этой команды есть ошибки, которые мы можем ожидать:

  • Сеть не работает, поэтому все вызовы git fetch неудача
  • Некоторые из репозиториев грязные, поэтому вызовы Git Merge неудача
  • и так далее.

И это ошибки, которые мы Не Ожидается: произойдет:

  • Синтаксис
  • Индексерра, потому что мы использовали my_list [3] И в списке есть только два элемента
  • AssertionError, потому что мы вызвали утверждение отказ
  • Ошибка в сторонней библиотеке, которую мы забыли поймать

Итак, вот правило, которое мы следовали в TSRC : каждый раз Происходит ошибка, которую мы ожидаем, мы бросаем полученный класс TSRC. Ошибка Анкет

Так, например, вместо того, чтобы позволить Называется ProcessError не участвуют, когда мы бежим git Команды, мы обязательно используем наш собственный GitCommanderror класс:

def run_git(working_dir, *cmd):

    process = subprocess.run(cmd, cwd=working_dir)

    if process.returncode != 0:
        raise GitCommandError(working_dir, cmd)

Это означает, что исключения, поднятые в TSRC не являются «исключительными», в том смысле, что они могут возникнуть даже во время «нормального» использования.

Это также означает, что если ошибка, которая делает не Унаследовать от TSCR. Ошибка поднят, в нашем производственном коде, вероятно, есть ошибка.

Основная () обертка

Различие между «ожидаемыми» и «неожиданными» ошибками ясно ясно в main () точка входа TSRC :

def main_func():
    """ Deals with command-line arguments
    and calls appropriate functions
    """

def main():
    try:
        main_func()
    except tsrc.Error as e:
        # "expected" failure, display it and exit
        if e.message:
            print("Error:", e.message)
        sys.exit(1)

if __name__ == "__main__":
    main()

Таким образом, программа может завершаться следующими способами:

  • A TSCR. Ошибка Экземпляр был поднят и не пойман: отобразите его сообщение, если оно не пустое и выходит с ненулевым кодом возврата.
  • Другой вид исключения был поднят, и мы не поймали его: пусть программа сбой.
  • Кто -то позвонил sys.exit () : Просто заканчивается, как абонент ожидает этого.

Таким образом, до тех пор, пока мы позаботимся о том, как мы строим экземпляры TSRC. Ошибка Занятия, которые мы поднимаем, у конечного пользователя будет хорошее сообщение об ошибке, сообщающее ему, что случилось.

Например:

def load_manifest();
    ....
    raise tsrc.Error("Invalid manifest file ...")

def main_sync():
    ...
    load_manifest()
$ tsrc sync
Error: Invalid manifest file

Обратите внимание, как будет спрятана Backtrace.

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

Обработка ошибок в сценариях CI

Мы можем использовать точно такую же техника при написании CI -кода.

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

Так что очень важно, мы правильно обрабатываем ошибки.

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

Следовательно, мы можем повторно использовать ту же технику, где notify () Является ли функция, которая заботится о том, чтобы рассказать разработчику о результатах сборки (по электронной почте или с комментарием по запросу о слиянии):

class BuildFailed(CIError):
    ...

class TestsFailed(CIError):
    ...

def notify(message):
    ...

def ci():
    fetch_sources()
    build()
    run_tests()

def main():
    try:
      ci()
    except BuildFailed:
        notify("build failed")
    except TestsFailed:
        notify("tests failed")
    except Exception as e:
        print_bactkrace()
        notify("build scripts may be broken! Ask help from a build farm guru")
    notify("Pipeline suceeded. Congrats")

Примечание: это не имеет ничего общего с TSRC сам, но я подумал, что это тоже хорошая идея, чтобы рассказать вам о сценариях CI тоже:)

Использование TSRC в качестве библиотеки

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

Они могут сделать очень мелкозернистую обработку ошибок, например, это:

try:
    push_action = tsrc.PushAction()
    push_action.accept_merge_request()
except tsrc.GitLabError as error:
    if error.status_code == 405:
        # GitLab denied the merge request

Они могут поймать только TSRC. Ошибка Ошибки (поскольку другие типы указывают на ошибку) и повторно обработали свой собственный тип:

try:
    push_action = tsrc.PushAction()
    push_action.accept_merge_request()
except tsrc.Error as error:
    raise FooError()

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

Обработка ошибок тестирования

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

С pytest Мы можем написать код таким образом:

import pytest

def test_reading_config_file():
    with pytest.raises(InvalidConfigError) as e:
        tsrc.config.read_config("nosuchfile")
    assert e.value.path == "nosuchfile"

Обратите внимание, что pytest возвращает ExceptionInfo Экземпляр обертывает исходное исключение, поэтому мы должны использовать .value атрибут. 3

Мы также можем проверить код возврата различных команд, как SO:

def test_sync_with_errors(tsrc_cli):
    # Arrange a workspace where a branch has diverged
    ...

    # Call the sync script:
    with pytest.raises(SystemExit) as e:
        main_sync()

    # Ensure it has failed
    assert e.value.code != 0

Хорошая обработка ошибок довольно простой в Python, как только вы применяете методы, которые я описал здесь.

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

Ваше здоровье!

Спасибо, что прочитали это далеко:)

Я хотел бы услышать, что вы скажете, поэтому, пожалуйста, оставьте комментарий ниже или прочитайте Страница обратной связи Для большего количества способов связаться со мной.

  1. Полный список находится в Документация

  2. Этот анти-паттерн настолько распространен, что у него есть имя: «Конвенция о именовании Смурфа» ↩

  3. Больше информации в Pytest документация

Оригинал: “https://dev.to/dmerejkowsky/exceptional-python”