Возвращение значений из функций

Beginner

This tutorial is from open-source community. Access the source code

Введение

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

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

Это Guided Lab, который предоставляет пошаговые инструкции, чтобы помочь вам учиться и практиковаться. Внимательно следуйте инструкциям, чтобы выполнить каждый шаг и получить практический опыт. Исторические данные показывают, что это лабораторная работа уровня начальный с процентом завершения 91%. Он получил 100% положительных отзывов от учащихся.

Возвращение нескольких значений из функций

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

Давайте создадим функцию для разбора строк конфигурации в формате name=value. Цель этой функции - взять строку в таком формате и вернуть как отдельные элементы как имя, так и значение.

  1. Сначала вам нужно создать новый файл Python. Этот файл будет содержать код нашей функции и тестовый код. В директории проекта создайте файл с именем return_values.py. Вы можете использовать следующую команду в терминале для создания этого файла:
touch ~/project/return_values.py
  1. Теперь откройте файл return_values.py в вашем редакторе кода. Внутри этого файла мы напишем функцию parse_line. Эта функция принимает строку в качестве входных данных, разбивает ее по первому знаку '=' и возвращает имя и значение в виде кортежа.
def parse_line(line):
    """
    Parse a line in the format 'name=value' and return both the name and value.

    Args:
        line (str): Input line to parse in the format 'name=value'

    Returns:
        tuple: A tuple containing (name, value)
    """
    parts = line.split('=', 1)  ## Split at the first equals sign
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## Return as a tuple

В этой функции метод split используется для разделения входной строки на две части по первому знаку '='. Если строка имеет правильный формат name=value, мы извлекаем имя и значение и возвращаем их в виде кортежа.

  1. После определения функции нам нужно добавить некоторый тестовый код, чтобы проверить, работает ли функция как ожидается. Тестовый код вызовет функцию parse_line с примером входных данных и выведет результаты.
## Test the parse_line function
if __name__ == "__main__":
    result = parse_line('email=guido@python.org')
    print(f"Result as tuple: {result}")

    ## Unpacking the tuple into separate variables
    name, value = parse_line('email=guido@python.org')
    print(f"Unpacked name: {name}")
    print(f"Unpacked value: {value}")

В тестовом коде мы сначала вызываем функцию parse_line и сохраняем возвращаемый кортеж в переменной result. Затем мы выводим этот кортеж. Далее мы используем распаковку кортежа, чтобы напрямую присвоить элементы кортежа переменным name и value и вывести их отдельно.

  1. После того, как вы напишете функцию и тестовый код, сохраните файл return_values.py. Затем откройте терминал и выполните следующую команду для запуска Python-скрипта:
python ~/project/return_values.py

Вы должны увидеть вывод, похожий на следующий:

Result as tuple: ('email', 'guido@python.org')
Unpacked name: email
Unpacked value: guido@python.org

Пояснение:

  • Функция parse_line разбивает входную строку по символу '=' с использованием метода split. Этот метод делит строку на части на основе указанного разделителя.
  • Она возвращает обе части в виде кортежа с использованием синтаксиса return (name, value). Кортеж - это способ группировать несколько значений вместе.
  • При вызове функции у вас есть два варианта. Вы можете сохранить весь кортеж в одной переменной, как мы сделали с переменной result. Или вы можете "распаковать" кортеж напрямую в отдельные переменные с использованием синтаксиса name, value = parse_line(...). Это упрощает работу с отдельными значениями.

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

Возвращение необязательных значений

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

Давайте посмотрим, как можно модифицировать функцию для обработки случаев, когда входные данные не соответствуют ожидаемым критериям. Мы будем работать с функцией parse_line, которая предназначена для разбора строки в формате 'name=value' и возврата как имени, так и значения.

  1. Обновите функцию parse_line в файле return_values.py:
def parse_line(line):
    """
    Parse a line in the format 'name=value' and return both the name and value.
    If the line is not in the correct format, return None.

    Args:
        line (str): Input line to parse in the format 'name=value'

    Returns:
        tuple or None: A tuple containing (name, value) or None if parsing failed
    """
    parts = line.split('=', 1)  ## Split at the first equals sign
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## Return as a tuple
    else:
        return None  ## Return None for invalid input

В этой обновленной функции parse_line мы сначала разбиваем входную строку по первому знаку равенства с помощью метода split. Если полученный список содержит ровно два элемента, это означает, что строка имеет правильный формат 'name=value'. Затем мы извлекаем имя и значение и возвращаем их в виде кортежа. Если список не содержит два элемента, это означает, что входные данные недействительны, и мы возвращаем None.

  1. Добавьте тестовый код, чтобы продемонстрировать работу обновленной функции:
## Test the updated parse_line function
if __name__ == "__main__":
    ## Valid input
    result1 = parse_line('email=guido@python.org')
    print(f"Valid input result: {result1}")

    ## Invalid input
    result2 = parse_line('invalid_line_without_equals_sign')
    print(f"Invalid input result: {result2}")

    ## Checking for None before using the result
    test_line = 'user_info'
    result = parse_line(test_line)
    if result is None:
        print(f"Could not parse the line: '{test_line}'")
    else:
        name, value = result
        print(f"Name: {name}, Value: {value}")

Этот тестовый код вызывает функцию parse_line с корректными и некорректными входными данными. Затем он выводит результаты. Обратите внимание, что при использовании результата функции parse_line мы сначала проверяем, равно ли оно None. Это важно, потому что если мы попытаемся распаковать значение None как кортеж, получим ошибку.

  1. Сохраните файл и запустите его:
python ~/project/return_values.py

При запуске скрипта вы должны увидеть вывод, похожий на следующий:

Valid input result: ('email', 'guido@python.org')
Invalid input result: None
Could not parse the line: 'user_info'

Пояснение:

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

Обсуждение дизайна: Альтернативный подход к обработке некорректных входных данных - это возбуждение исключения (raising an exception). Этот подход подходит в определенных ситуациях:

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

Пример подхода на основе исключений:

def parse_line_with_exception(line):
    """Parse a line and raise an exception for invalid input."""
    parts = line.split('=', 1)
    if len(parts) != 2:
        raise ValueError(f"Invalid format: '{line}' does not contain '='")
    return (parts[0], parts[1])

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

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

Работа с объектами Future для параллельного программирования

В Python, когда вам нужно запускать функции одновременно, то есть параллельно, язык предоставляет полезные инструменты, такие как потоки (threads) и процессы (processes). Но здесь возникает общая проблема: как получить значение, возвращаемое функцией, которая запущена в отдельном потоке? Именно здесь концепция объекта Future становится очень важной.

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

Шаг 1: Создание нового файла

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

touch ~/project/futures_demo.py

Шаг 2: Добавление базового кода функции

Теперь откройте файл futures_demo.py и добавьте следующий код Python. Этот код определяет простую функцию и показывает, как работает обычный вызов функции.

import time
import threading
from concurrent.futures import Future, ThreadPoolExecutor

def worker(x, y):
    """A function that takes time to complete"""
    print('Starting work...')
    time.sleep(5)  ## Simulate a time-consuming task
    print('Work completed')
    return x + y

## Part 1: Normal function call
print("--- Part 1: Normal function call ---")
result = worker(2, 3)
print(f"Result: {result}")

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

Шаг 3: Запуск базового кода

Сохраните файл и запустите его с помощью следующей команды в терминале:

python ~/project/futures_demo.py

Вы должны увидеть вывод, похожий на следующий:

--- Part 1: Normal function call ---
Starting work...
Work completed
Result: 5

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

Шаг 4: Запуск функции в отдельном потоке

Далее давайте посмотрим, что произойдет, когда мы запустим функцию worker в отдельном потоке. Добавьте следующий код в файл futures_demo.py:

## Part 2: Running in a separate thread (problem: no way to get result)
print("\n--- Part 2: Running in a separate thread ---")
t = threading.Thread(target=worker, args=(2, 3))
t.start()
print("Main thread continues while worker runs...")
t.join()  ## Wait for the thread to complete
print("Worker thread finished, but we don't have its return value!")

Здесь мы используем класс threading.Thread для запуска функции worker в новом потоке. Главный поток не ждет завершения функции worker и продолжает свое выполнение. Однако, когда поток worker завершает работу, у нас нет простого способа получить возвращаемое значение.

Шаг 5: Запуск кода с потоком

Снова сохраните файл и запустите его той же командой:

python ~/project/futures_demo.py

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

Шаг 6: Ручное использование объекта Future

Чтобы решить проблему получения возвращаемого значения из потока, мы можем использовать объект Future. Добавьте следующий код в файл futures_demo.py:

## Part 3: Using a Future to get the result
print("\n--- Part 3: Using a Future manually ---")

def do_work_with_future(x, y, future):
    """Wrapper that sets the result in the Future"""
    result = worker(x, y)
    future.set_result(result)

## Create a Future object
fut = Future()

## Start a thread that will set the result in the Future
t = threading.Thread(target=do_work_with_future, args=(2, 3, fut))
t.start()

print("Main thread continues...")
print("Waiting for the result...")
## Block until the result is available
result = fut.result()  ## This will wait until set_result is called
print(f"Got the result: {result}")

В этом коде мы создаем объект Future и передаем его в новую функцию do_work_with_future. Эта функция вызывает функцию worker и затем устанавливает результат в объекте Future. Затем главный поток может использовать метод result() объекта Future для получения результата, когда он станет доступен.

Шаг 7: Запуск кода с использованием объекта Future

Сохраните файл и запустите его снова:

python ~/project/futures_demo.py

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

Шаг 8: Использование ThreadPoolExecutor

Класс ThreadPoolExecutor в Python делает работу с параллельными задачами еще проще. Добавьте следующий код в файл futures_demo.py:

## Part 4: Using ThreadPoolExecutor (easier way)
print("\n--- Part 4: Using ThreadPoolExecutor ---")
with ThreadPoolExecutor() as executor:
    ## Submit the work to the executor
    future = executor.submit(worker, 2, 3)

    print("Main thread continues after submitting work...")
    print("Checking if the future is done:", future.done())

    ## Get the result (will wait if not ready)
    result = future.result()
    print("Now the future is done:", future.done())
    print(f"Final result: {result}")

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

Шаг 9: Запуск полного кода

Сохраните файл в последний раз и запустите его:

python ~/project/futures_demo.py

Пояснение

  1. Обычный вызов функции: Когда вы вызываете функцию обычным способом, программа ждет завершения функции и напрямую получает возвращаемое значение.
  2. Проблема с потоками: Запуск функции в отдельном потоке имеет недостаток. Нет встроенного способа получить возвращаемое значение функции, запущенной в этом потоке.
  3. Ручное использование объекта Future: Создав объект Future и передав его в поток, мы можем установить результат в объекте Future и затем получить этот результат из главного потока.
  4. ThreadPoolExecutor: Этот класс упрощает параллельное программирование. Он управляет созданием и управлением объектами Future за вас, что делает проще запуск функций параллельно и получение их возвращаемых значений.

Объекты Future имеют несколько полезных методов:

  • result(): Этот метод используется для получения результата функции. Если результат еще не готов, он будет ждать, пока он не станет доступен.
  • done(): Вы можете использовать этот метод, чтобы проверить, завершено ли выполнение функции.
  • add_done_callback(): Этот метод позволяет зарегистрировать функцию, которая будет вызвана, когда результат будет готов.

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

Резюме

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

В конце концов, в параллельном программировании объект Future служит временным хранилищем для будущего результата, позволяя получать возвращаемые значения от функций, запущенных в отдельных потоках или процессах. Понимание этих шаблонов повысит надежность и гибкость вашего кода на Python. Для дальнейшей практики поэкспериментируйте с различными стратегиями обработки ошибок, используйте объекты Future с другими типами параллельного выполнения и исследуйте их применение в асинхронном программировании с использованием async/await.