Введение
В этом практическом занятии (лабораторной работе) вы узнаете, как возвращать несколько значений из функций на Python. Вы также поймете, что такое необязательные возвращаемые значения и как эффективно обрабатывать ошибки.
Кроме того, вы рассмотрите концепцию Futures для параллельного программирования. Хотя возврат значения может показаться простой задачей, различные сценарии программирования предполагают разные паттерны и аспекты, которые необходимо учитывать.
Возвращение нескольких значений из функций
В Python, когда вам нужно, чтобы функция вернула более одного значения, есть удобное решение: возвращать кортеж (tuple). Кортеж - это тип структуры данных в Python. Это неизменяемая последовательность, что означает, что после создания кортежа вы не можете изменить его элементы. Кортежи полезны, так как они могут хранить несколько значений разных типов в одном месте.
Давайте создадим функцию для разбора строк конфигурации в формате name=value. Цель этой функции - взять строку в таком формате и вернуть как отдельные элементы как имя, так и значение.
- Сначала вам нужно создать новый файл Python. Этот файл будет содержать код нашей функции и тестовый код. В директории проекта создайте файл с именем
return_values.py. Вы можете использовать следующую команду в терминале для создания этого файла:
touch ~/project/return_values.py
- Теперь откройте файл
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, мы извлекаем имя и значение и возвращаем их в виде кортежа.
- После определения функции нам нужно добавить некоторый тестовый код, чтобы проверить, работает ли функция как ожидается. Тестовый код вызовет функцию
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 и вывести их отдельно.
- После того, как вы напишете функцию и тестовый код, сохраните файл
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' и возврата как имени, так и значения.
- Обновите функцию
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.
- Добавьте тестовый код, чтобы продемонстрировать работу обновленной функции:
## 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 как кортеж, получим ошибку.
- Сохраните файл и запустите его:
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). Этот подход подходит в определенных ситуациях:
- Некорректные входные данные действительно являются исключительными и не являются ожидаемым случаем. Например, если входные данные должны поступать из надежного источника и всегда должны иметь правильный формат.
- Вы хотите заставить вызывающий код обработать ошибку. При возбуждении исключения нормальный поток выполнения программы прерывается, и вызывающий код должен явно обработать ошибку.
- Вам нужно предоставить подробную информацию об ошибке. Исключения могут нести дополнительную информацию об ошибке, которая может быть полезна для отладки.
Пример подхода на основе исключений:
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
Пояснение
- Обычный вызов функции: Когда вы вызываете функцию обычным способом, программа ждет завершения функции и напрямую получает возвращаемое значение.
- Проблема с потоками: Запуск функции в отдельном потоке имеет недостаток. Нет встроенного способа получить возвращаемое значение функции, запущенной в этом потоке.
- Ручное использование объекта
Future: Создав объектFutureи передав его в поток, мы можем установить результат в объектеFutureи затем получить этот результат из главного потока. - ThreadPoolExecutor: Этот класс упрощает параллельное программирование. Он управляет созданием и управлением объектами
Futureза вас, что делает проще запуск функций параллельно и получение их возвращаемых значений.
Объекты Future имеют несколько полезных методов:
result(): Этот метод используется для получения результата функции. Если результат еще не готов, он будет ждать, пока он не станет доступен.done(): Вы можете использовать этот метод, чтобы проверить, завершено ли выполнение функции.add_done_callback(): Этот метод позволяет зарегистрировать функцию, которая будет вызвана, когда результат будет готов.
Этот паттерн очень важен в параллельном программировании, особенно когда вам нужно получить результаты от функций, запущенных параллельно.
Резюме
В этом практическом занятии (lab) вы узнали несколько ключевых шаблонов для возврата значений из функций в Python. Во - первых, функции Python могут возвращать несколько значений, упаковывая их в кортеж, что позволяет чисто и читаемо возвращать и распаковывать значения. Во - вторых, для функций, которые не всегда могут дать корректный результат, возврат None является распространенным способом указать на отсутствие значения, а также был предложен альтернативный подход - возбуждение исключений.
В конце концов, в параллельном программировании объект Future служит временным хранилищем для будущего результата, позволяя получать возвращаемые значения от функций, запущенных в отдельных потоках или процессах. Понимание этих шаблонов повысит надежность и гибкость вашего кода на Python. Для дальнейшей практики поэкспериментируйте с различными стратегиями обработки ошибок, используйте объекты Future с другими типами параллельного выполнения и исследуйте их применение в асинхронном программировании с использованием async/await.