Перейти к содержанию

Ошибки, обработка ошибок и менеджер контекста

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

Вызов ошибок

Оператор raise вызывает ошибку в вашей программе. Вы предоставляете оператору raise экземпляр ошибки, чтобы указать тип возникшей ошибки. Например:

raise Error("integer overflow")

Для удобства вы можете вместо этого предоставить сообщение об ошибке в виде String или StringLiteral, и raise автоматически использует это для создания экземпляра ошибки. Таким образом, вы можете вызвать то же условие ошибки, что и показано выше, выполнив:

raise "integer overflow"

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

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

stack trace was not collected. Enable stack trace collection with environment variable `MOJO_ENABLE_STACK_TRACE_ON_ERROR`
Unhandled exception caught during execution: integer overflow

Включение трассировки стека на предмет ошибок

По умолчанию Mojo генерирует трассировку стека, когда ваша программа обнаруживает ошибку сегментации. Если вам не нужно такое поведение, вы можете отключить его, установив для переменной окружения MOJO_ENABLE_STACK_TRACE_ON_CRASH значение 0 или false (без учета регистра).

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

Имейте в виду, что при компиляции вашей программы с помощью mojo build компилятор по умолчанию оптимизирует и удаляет символы, поэтому часто трассировка стека не очень полезна.

Давайте рассмотрим эту программу:

stacktrace_error.mojo
def func2() -> None:
    raise Error("Intentional error")


def func1() -> None:
    func2()


def main():
    func1()

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

mojo build stacktrace_error.mojo

MOJO_ENABLE_STACK_TRACE_ON_ERROR=1 stacktrace_error

#0 0x0000000104ef8ecc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xccecc)
#1 0x0000000104e379e4 KGEN_CompilerRT_GetStackTrace (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xb9e4)
#2 0x000000010478868c main (/Users/ken/tmp/stack/stacktrace_error+0x10000068c)

Unhandled exception caught during execution: Intentional error

Чтобы создать более полезную трассировку стека, вам необходимо скомпилировать программу с помощью --debug-level full (или -g), чтобы включить символы отладки:

mojo build --debug-level full stacktrace_error.mojo

MOJO_ENABLE_STACK_TRACE_ON_ERROR=1 ./stacktrace_error

#0 0x0000000102bf4ecc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xccecc)
#1 0x0000000102b339e4 KGEN_CompilerRT_GetStackTrace (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xb9e4)
#2 0x000000010266468c stdlib::builtin::error::Error::__init__[__mlir_type.!kgen.string](::StringLiteral[$0])_REMOVED_ARG open-source/max/mojo/stdlib/stdlib/builtin/error.mojo:159:38
#3 0x000000010266468c stacktrace_error::func2()_REMOVED_ARG /Users/ken/tmp/stack/stacktrace_error.mojo:14:16
#4 0x000000010266468c stacktrace_error::func1() /Users/ken/tmp/stack/stacktrace_error.mojo:18:10
#5 0x000000010266468c stacktrace_error::main() /Users/ken/tmp/stack/stacktrace_error.mojo:22:10
#6 0x000000010266468c stdlib::builtin::_startup::__wrap_and_execute_raising_main[fn() raises -> None](::SIMD[::DType(int32), ::Int(1)],__mlir_type.!kgen.pointer<pointer<scalar<ui8>>>),main_func="stacktrace_error::main()" open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:88:18
#7 0x000000010266468c main open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:103:4

Unhandled exception caught during execution: Intentional error
At this time, running your program directly with mojo run or mojo doesn't include debug symbols in the stack trace even with --debug-level full. For example:

MOJO_ENABLE_STACK_TRACE_ON_ERROR=1 mojo --debug-level full stacktrace_error.mojo

#0 0x000000013a9c4ecc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xccecc)
#1 0x000000013a9039e4 KGEN_CompilerRT_GetStackTrace (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xb9e4)
#2 0x000000032002807c
#3 0x000000010488a0f4 M::KGEN::ExecutionEngine::runProgram(llvm::StringRef, llvm::StringRef, llvm::function_ref<M::ErrorOrSuccess (void*)>) (/Users/ken/tmp/stack/.pixi/envs/default/bin/mojo+0x10039e0f4)
#4 0x0000000104512000 executeMain(M::KGEN::ExecutionEngine&, M::AsyncRT::Runtime&, llvm::ArrayRef<char const*>) (/Users/ken/tmp/stack/.pixi/envs/default/bin/mojo+0x100026000)
#5 0x00000001045118e8 run(M::State const&) (/Users/ken/tmp/stack/.pixi/envs/default/bin/mojo+0x1000258e8)
#6 0x000000010451a240 main (/Users/ken/tmp/stack/.pixi/envs/default/bin/mojo+0x10002e240)
#7 0x0000000186d7ab98
Unhandled exception caught during execution: Intentional error
mojo: error: execution exited with a non-zero result: 1

Как описано в разделе Обработка ошибки, вы можете привязать экземпляр Error к переменной в предложении except. Если вы это сделаете, вы можете вызвать его метод get_stack_trace(), чтобы получить экземпляр StackTrace. StackTrace реализует свойство Stringable, поэтому вы можете создать строку с помощью String(stack_trace), если хотите извлечь трассировку стека в виде строки для дальнейшей обработки. Например:

def func2() -> None:
    raise Error("Intentional error")


def func1() -> None:
    func2()


def main():
    try:
        func1()
    except e:
        print(e)
        print("-" * 20)
        print(String(e.get_stack_trace()))

Когда вы скомпилируете эту программу с использованием символов и запустите ее с включенной генерацией трассировки стека, вы увидите следующий результат:

mojo build --debug-level full stacktrace_error_capture.mojo

MOJO_ENABLE_STACK_TRACE_ON_ERROR=1 ./stacktrace_error_capture

Intentional error
--------------------
#0 0x0000000102d24ecc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xccecc)
#1 0x0000000102c639e4 KGEN_CompilerRT_GetStackTrace (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xb9e4)
#2 0x00000001026d4694 stdlib::builtin::error::Error::__init__[__mlir_type.!kgen.string](::StringLiteral[$0])_REMOVED_ARG open-source/max/mojo/stdlib/stdlib/builtin/error.mojo:159:38
#3 0x00000001026d4694 stacktrace_error_capture::func2()_REMOVED_ARG /Users/ken/tmp/stack/stacktrace_error_capture.mojo:14:16
#4 0x00000001026d4694 stacktrace_error_capture::func1() /Users/ken/tmp/stack/stacktrace_error_capture.mojo:18:10
#5 0x00000001026d4694 stacktrace_error_capture::main() /Users/ken/tmp/stack/stacktrace_error_capture.mojo:23:14
#6 0x00000001026d4694 stdlib::builtin::_startup::__wrap_and_execute_raising_main[fn() raises -> None](::SIMD[::DType(int32), ::Int(1)],__mlir_type.!kgen.pointer<pointer<scalar<ui8>>>),main_func="stacktrace_error_capture::main()" open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:88:18
#7 0x00000001026d4694 main open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:103:4

Без включения генерации трассировки стека выходные данные будут выглядеть следующим образом:

Intentional error
--------------------
stack trace was not collected. Enable stack trace collection with environment variable `MOJO_ENABLE_STACK_TRACE_ON_ERROR`

Объявление raising функции

Функция, определенная с помощью ключевого слова fn, по умолчанию не запускается. Поэтому, если она может вызвать ошибку, вы должны включить ключевое слово raises в определение функции. Например:

fn incr(n: Int) raises -> Int:
    if n == Int.MAX:
        raise "inc: integer overflow"
    else:
        return n + 1

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

# Эта функция не компилируется из-за необработанной ошибки
fn unhandled_error(n: Int):
    print(n, "+ 1 =", incr(n))

# Эта функция компилируется, поскольку она обрабатывает возможную ошибку
fn handled_error(n: Int):
    try:
        print(n, "+ 1 =", incr(n))
    except e:
        print("Handled an error:", e)

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

def incr(n: Int) -> Int:
    if n == Int.MAX:
        raise "inc: integer overflow"
    else:
        return n + 1

Обработка ошибок

Mojo позволяет обнаруживать и обрабатывать ошибки, используя структуру потока управления try-except. Полный синтаксис приведен ниже:

try:
    # Code block to execute that might raise an error
except <optional_variable_name>:
    # Code block to execute if an error occurs
else:
    # Code block to execute if no error occurs
finally:
    # Final code block to execute in all circumstances

Вы должны включить одно или оба предложения except и finally. Предложение else необязательно.

Предложение try содержит блок кода, выполнение которого может привести к ошибке. Если ошибка не возникает, выполняется весь блок кода. При возникновении ошибки выполнение блока кода останавливается в момент возникновения ошибки. Затем ваша программа продолжает выполнение условия except, если оно предусмотрено, или условия finally.

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

Если вы укажете имя переменной после ключевого слова except, то экземпляр Error будет привязан к переменной в случае возникновения ошибки. Тип ошибки реализует свойство, доступное для записи, поэтому вы можете передать его в качестве аргумента функции print(), если хотите вывести сообщение об ошибке на консоль. Он также реализует свойство Stringable, поэтому вы можете создать строку с помощью String(error), если хотите извлечь сообщение об ошибке в виде строки для дальнейшей обработки.

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

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

Если присутствует предложение else, его блок кода выполняется только в том случае, если в предложении try не возникает ошибка. Обратите внимание, что предложение else пропускается, если в предложении try выполняются команды continue, break или return, которые завершают работу в блоке try.

Если присутствует предложение finally, его блок кода выполняется после предложения try и, если применимо, предложения except или else. Предложение finally выполняется даже в том случае, если один из других блоков кода завершается выполнением инструкций continue, break или return или в результате возникновения ошибки. Предложение finally часто используется для освобождения ресурсов, используемых предложением try (например, дескриптора файла), независимо от того, произошла ли ошибка.

В качестве примера рассмотрим следующую программу:

handle_error.mojo:

def incr(n: Int) -> Int:
    if n == Int.MAX:
        raise "inc: integer overflow"
    else:
        return n + 1

def main():
    for value in [0, 1, Int.MAX]:
        try:
            print()
            print("try     =>", value)
            if value == 1:
                continue
            result = "{} incremented is {}".format(value, incr(value))
        except e:
            print("except  =>", e)
        else:
            print("else    =>", result)
        finally:
            print("finally => ====================")

Запуск этой программы приводит к следующему результату:

try     => 0
else    => 0 incremented is 1
finally => ====================

try     => 1
finally => ====================

try     => 9223372036854775807
except  => inc: integer overflow
finally => ====================

Использование контекстного менеджера

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

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

# Получение дескриптора файла для чтения из хранилища
f = open(input_file, "r")
content = f.read()
# Обрабатывайте содержимое по мере необходимости
# Закройте дескриптор файла
f.close()

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

Приведенный выше пример на самом деле включает вызов функции close(), но он игнорирует возможность того, что функция read() может вызвать ошибку, которая предотвратит выполнение функции close(). Чтобы справиться с этим сценарием, вы могли бы переписать код, чтобы использовать try следующим образом:

# Obtain a file handle to read from storage
f = open(input_file, "r")

try:
    content = f.read()
    # Process the content as needed
finally:
    # Ensure that the file handle is closed even if read() raises an error
    f.close()

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

with open(input_file, "r") as f:
    content = f.read()
    # Process the content as needed

Оператор with также позволяет использовать несколько контекстных менеджеров в одном и том же блоке кода. В качестве примера приведенный ниже код открывает один текстовый файл, считывает все его содержимое, преобразует его в верхний регистр, а затем записывает результат в другой файл:

with open(input_file, "r") as f_in, open(output_file, "w") as f_out:
    input_text = f_in.read()
    output_text = input_text.upper()
    f_out.write(output_text)

Файловый дескриптор, пожалуй, является наиболее часто используемым контекстным менеджером. Другими примерами контекстных менеджеров в стандартной библиотеке Mojo являются TemporaryFile, TemporaryDirectory, BlockingScopedLock и assert_raises. Вы также можете создавать свои собственные пользовательские контекстные менеджеры, как описано в разделе Создание пользовательского контекстного менеджера ниже.

Написание пользовательского контекстного менеджера

Написание пользовательского контекстного менеджера - это вопрос определения структуры, которая реализует два специальных метода: __enter__() и __exit__():

  • __enter__() вызывается оператором with для входа в контекст среды выполнения. Метод __enter__() должен инициализировать любое состояние, необходимое для контекста, и возвращать диспетчер контекста.

  • __exit__() вызывается, когда блок кода with завершает выполнение, даже если блок кода with завершается вызовом для продолжения, прерывания или возврата. Метод __exit__() должен освобождать все ресурсы, связанные с контекстом. После возврата метода __exit__() диспетчер контекста уничтожается.

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

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

Вот пример реализации контекстного менеджера таймера, который выводит количество времени, затраченного на выполнение блока with:

context_mgr.mojo:

import sys
import time

@fieldwise_init
struct Timer(ImplicitlyCopyable, Movable):
    var start_time: Int

    fn __init__(out self):
        self.start_time = 0

    fn __enter__(mut self) -> Self:
        self.start_time = Int(time.perf_counter_ns())
        return self

    fn __exit__(mut self):
        end_time = time.perf_counter_ns()
        elapsed_time_ms = round(((end_time - UInt(self.start_time)) / 1e6), 3)
        print("Elapsed time:", elapsed_time_ms, "milliseconds")

def main():
    with Timer():
        print("Beginning execution")
        time.sleep(1.0)
        if len(sys.argv()) > 1:
            raise "simulated error"
        time.sleep(1.0)
        print("Ending execution")

Выполнение этого примера приводит к следующему результату:

$mojo context_mgr.mojo

Beginning execution
Ending execution
Elapsed time: 2010.0 milliseconds
$mojo context_mgr.mojo fail

Beginning execution
Elapsed time: 1002.0 milliseconds
Unhandled exception caught during execution: simulated error

Определение условного метода __exit__()

При создании контекстного менеджера вы можете реализовать форму __exit__(self) метода __exit__() для обработки завершения инструкции with при любых обстоятельствах, включая ошибки. Однако у вас есть возможность дополнительно реализовать перегруженную версию, которая вызывается вместо этого при возникновении ошибки в блоке `with кода:

fn __exit__(self, error: Error) raises -> Bool

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

  • Возвращает True для устранения ошибки
  • Возвращает False для повторного вызова ошибки
  • Выдает новую ошибку

Ниже приведен пример контекстного менеджера, который подавляет только определенный тип ошибки и распространяет все остальные:

conditional_context_mgr.mojo:

import time

@fieldwise_init
struct ConditionalTimer(ImplicitlyCopyable, Movable):
    var start_time: Int

    fn __init__(out self):
        self.start_time = 0

    fn __enter__(mut self) -> Self:
        self.start_time = Int(time.perf_counter_ns())
        return self

    fn __exit__(mut self):
        end_time = time.perf_counter_ns()
        elapsed_time_ms = round(((end_time - UInt(self.start_time)) / 1e6), 3)
        print("Elapsed time:", elapsed_time_ms, "milliseconds")

    fn __exit__(mut self, e: Error) raises -> Bool:
        if String(e) == "just a warning":
            print("Suppressing error:", e)
            self.__exit__()
            return True
        else:
            print("Propagating error")
            self.__exit__()
            return False

def flaky_identity(n: Int) -> Int:
    if (n % 4) == 0:
        raise "really bad"
    elif (n % 2) == 0:
        raise "just a warning"
    else:
        return n

def main():
    for i in range(1, 9):
        with ConditionalTimer():
            print("\nBeginning execution")

            print("i =", i)
            time.sleep(0.1)

            if i == 3:
                print("continue executed")
                continue

            j = flaky_identity(i)
            print("j =", j)

            print("Ending execution")

Выполнение этого примера приводит к следующему результату:

Beginning execution
i = 1
j = 1
Ending execution
Elapsed time: 105.0 milliseconds

Beginning execution
i = 2
Suppressing error: just a warning
Elapsed time: 106.0 milliseconds

Beginning execution
i = 3
continue executed
Elapsed time: 106.0 milliseconds

Beginning execution
i = 4
Propagating error
Elapsed time: 106.0 milliseconds
Unhandled exception caught during execution: really bad