函数返回值

PythonPythonBeginner
立即练习

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

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习如何在 Python 中从函数返回多个值。你还将了解可选返回值以及如何有效地处理错误。

此外,你将探索并发编程中 Futures 的概念。虽然返回一个值看似简单,但不同的编程场景会呈现出各种模式和需要考虑的因素。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/DataStructuresGroup(["Data Structures"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python(("Python")) -.-> python/PythonStandardLibraryGroup(["Python Standard Library"]) python/DataStructuresGroup -.-> python/tuples("Tuples") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/ErrorandExceptionHandlingGroup -.-> python/catching_exceptions("Catching Exceptions") python/AdvancedTopicsGroup -.-> python/threading_multiprocessing("Multithreading and Multiprocessing") python/PythonStandardLibraryGroup -.-> python/data_collections("Data Collections") subgraph Lab Skills python/tuples -.-> lab-132504{{"函数返回值"}} python/function_definition -.-> lab-132504{{"函数返回值"}} python/arguments_return -.-> lab-132504{{"函数返回值"}} python/catching_exceptions -.-> lab-132504{{"函数返回值"}} python/threading_multiprocessing -.-> lab-132504{{"函数返回值"}} python/data_collections -.-> lab-132504{{"函数返回值"}} end

从函数返回多个值

在 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 变量中。然后我们打印这个元组。接下来,我们使用元组解包(tuple unpacking)将元组的元素直接分配给 namevalue 变量,并分别打印它们。

  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 中,处理这种情况的常见方法是返回 NoneNone 是 Python 中的一个特殊值,表示没有有效的返回值。

让我们看看如何修改一个函数,以处理输入不符合预期标准的情况。我们将对 parse_line 函数进行修改,该函数旨在解析格式为 'name=value' 的行,并返回名称和值。

  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.
    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 值的元素时可能会遇到错误。

设计讨论:
处理无效输入的另一种方法是抛出异常。这种方法在某些情况下很合适:

  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 中,当你需要同时(即并发)运行函数时,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 函数的返回值。

步骤 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 对象中。然后主线程可以使用 Future 对象的 result() 方法在结果可用时获取结果。

步骤 7:运行使用 Future 的代码

保存文件并再次运行它:

python ~/project/futures_demo.py

现在你会看到我们可以成功地从线程中运行的函数获取返回值。

步骤 8:使用 ThreadPoolExecutor

Python 中的 ThreadPoolExecutor 类让处理并发任务变得更加容易。将以下代码添加到 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():这个方法允许你注册一个函数,当结果准备好时会调用该函数。

这种模式在并发编程中非常重要,特别是当你需要从并行运行的函数中获取结果时。

总结

在本次实验中,你学习了 Python 函数返回值的几个关键模式。首先,Python 函数可以通过将多个值打包成元组来返回,这样能实现清晰且易读的值返回和解包操作。其次,对于可能无法始终产生有效结果的函数,返回 None 是一种常见的表示无值的方式,同时也介绍了抛出异常作为另一种可选方法。

最后,在并发编程中,Future 作为未来结果的占位符,使你能够从在单独线程或进程中运行的函数获取返回值。理解这些模式将增强你 Python 代码的健壮性和灵活性。为了进一步练习,你可以尝试不同的错误处理策略,将 Future 与其他并发执行类型结合使用,并探索它们在使用 async/await 的异步编程中的应用。