函数参数传递约定

PythonPythonBeginner
立即练习

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

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

简介

在本次实验中,你将学习 Python 函数参数传递的约定。你还将为数据类创建一个可复用的结构,并应用面向对象的设计原则来简化你的代码。

本次练习旨在以更有条理的方式重写 stock.py 文件。在开始之前,将你在 stock.py 中的现有代码复制到一个名为 orig_stock.py 的新文件中,以供参考。你需要创建的文件是 structure.pystock.py


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/keyword_arguments("Keyword Arguments") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/constructor("Constructor") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") subgraph Lab Skills python/arguments_return -.-> lab-132509{{"函数参数传递约定"}} python/keyword_arguments -.-> lab-132509{{"函数参数传递约定"}} python/classes_objects -.-> lab-132509{{"函数参数传递约定"}} python/constructor -.-> lab-132509{{"函数参数传递约定"}} python/inheritance -.-> lab-132509{{"函数参数传递约定"}} python/encapsulation -.-> lab-132509{{"函数参数传递约定"}} end

理解函数参数传递

在 Python 中,函数是一个基本概念,它允许你将一组语句组合在一起以执行特定任务。当你调用一个函数时,通常需要为其提供一些数据,我们将这些数据称为参数。Python 提供了不同的方式将这些参数传递给函数。这种灵活性非常有用,因为它有助于你编写更简洁、更易维护的代码。在将这些技术应用到我们的项目之前,让我们仔细了解一下这些参数传递约定。

备份你的工作

在开始对 stock.py 文件进行修改之前,创建一个备份是个好习惯。这样,如果在实验过程中出现问题,我们总能恢复到原始版本。要创建备份,请打开终端并运行以下命令:

cp stock.py orig_stock.py

此命令使用终端中的 cp(复制)命令。它将 stock.py 文件复制一份,命名为 orig_stock.py。通过这样做,我们确保原始工作得到了安全保存。

探索函数参数传递

在 Python 中,有几种不同类型的参数调用函数的方法。让我们详细探讨每种方法。

1. 位置参数

将参数传递给函数的最简单方法是按位置传递。当你定义一个函数时,需要指定一个参数列表。调用函数时,要按照定义的顺序为这些参数提供值。

以下是一个示例:

def calculate(x, y, z):
    return x + y + z

## Call with positional arguments
result = calculate(1, 2, 3)
print(result)  ## Output: 6

在这个示例中,calculate 函数接受三个参数:xyz。当我们使用 calculate(1, 2, 3) 调用该函数时,值 1 被赋给 x2 被赋给 y3 被赋给 z。然后函数将这些值相加并返回结果。

2. 关键字参数

除了位置参数,你还可以通过参数名来指定参数。这称为使用关键字参数。使用关键字参数时,你不必担心参数的顺序。

以下是一个示例:

## Call with a mix of positional and keyword arguments
result = calculate(1, z=3, y=2)
print(result)  ## Output: 6

在这个示例中,我们首先为 x 传递位置参数 1。然后,使用关键字参数为 yz 指定值。只要提供了正确的名称,关键字参数的顺序无关紧要。

3. 解包序列和字典

Python 提供了一种方便的方式,使用 *** 语法将序列和字典作为参数传递。这称为解包。

以下是将元组解包为位置参数的示例:

## Unpacking a tuple into positional arguments
args = (1, 2, 3)
result = calculate(*args)
print(result)  ## Output: 6

在这个示例中,我们有一个包含值 123 的元组 args。当我们在函数调用中在 args 前使用 * 运算符时,Python 会解包该元组,并将其元素作为位置参数传递给 calculate 函数。

以下是将字典解包为关键字参数的示例:

## Unpacking a dictionary into keyword arguments
kwargs = {'y': 2, 'z': 3}
result = calculate(1, **kwargs)
print(result)  ## Output: 6

在这个示例中,我们有一个包含键值对 'y': 2'z': 3 的字典 kwargs。当我们在函数调用中在 kwargs 前使用 ** 运算符时,Python 会解包该字典,并将其键值对作为关键字参数传递给 calculate 函数。

4. 接受可变参数

有时,你可能想定义一个可以接受任意数量参数的函数。Python 允许你在函数定义中使用 *** 语法来实现这一点。

以下是一个接受任意数量位置参数的函数示例:

## Accept any number of positional arguments
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2))           ## Output: 3
print(sum_all(1, 2, 3, 4, 5))  ## Output: 15

在这个示例中,sum_all 函数使用 *args 参数来接受任意数量的位置参数。* 运算符将所有位置参数收集到一个名为 args 的元组中。然后,函数使用内置的 sum 函数将元组中的所有元素相加。

以下是一个接受任意数量关键字参数的函数示例:

## Accept any number of keyword arguments
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Python", year=1991)
## Output:
## name: Python
## year: 1991

在这个示例中,print_info 函数使用 **kwargs 参数来接受任意数量的关键字参数。** 运算符将所有关键字参数收集到一个名为 kwargs 的字典中。然后,函数遍历字典中的键值对并将它们打印出来。

这些技术将帮助我们在接下来的步骤中创建更灵活、可复用的代码结构。为了更熟悉这些概念,让我们打开 Python 解释器并尝试一些上述示例。

python3

进入 Python 解释器后,尝试输入上述示例。这将让你亲身体验这些参数传递技术。

创建结构基类

既然我们已经很好地理解了函数参数传递,接下来我们将为数据结构创建一个可复用的基类。这一步至关重要,因为当我们创建简单的数据持有类时,它能帮助我们避免反复编写相同的代码。通过使用基类,我们可以简化代码并提高效率。

重复代码的问题

在前面的练习中,你定义了一个 Stock 类,如下所示:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

仔细观察 __init__ 方法,你会发现它相当重复。你必须逐个手动分配每个属性。这会变得非常繁琐且耗时,尤其是当你有许多具有大量属性的类时。

创建灵活的基类

让我们创建一个 Structure 基类,它可以自动处理属性分配。首先,打开 WebIDE 并创建一个名为 structure.py 的新文件。然后,将以下代码添加到该文件中:

## structure.py

class Structure:
    """
    A base class for creating simple data structures.
    Automatically populates object attributes from _fields and constructor arguments.
    """
    _fields = ()

    def __init__(self, *args):
        ## Check that the number of arguments matches the number of fields
        if len(args) != len(self._fields):
            raise TypeError(f"Expected {len(self._fields)} arguments")

        ## Set the attributes
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

这个基类有几个重要的特性:

  1. 它定义了一个 _fields 类变量。默认情况下,这个变量为空。这个变量将保存类将具有的属性名称。
  2. 它会检查传递给构造函数的参数数量是否与 _fields 中定义的字段数量匹配。如果不匹配,它会引发一个 TypeError。这有助于我们尽早捕获错误。
  3. 它使用字段名称和作为参数提供的值来设置对象的属性。setattr 函数用于动态设置属性。

测试我们的结构基类

现在,让我们创建一些继承自 Structure 基类的示例类。将以下代码添加到你的 structure.py 文件中:

## Example classes using Structure
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

class Point(Structure):
    _fields = ('x', 'y')

class Date(Structure):
    _fields = ('year', 'month', 'day')

为了测试我们的实现是否正确,我们将创建一个名为 test_structure.py 的测试文件。将以下代码添加到该文件中:

## test_structure.py
from structure import Stock, Point, Date

## Test Stock class
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}, shares: {s.shares}, price: {s.price}")

## Test Point class
p = Point(3, 4)
print(f"Point coordinates: ({p.x}, {p.y})")

## Test Date class
d = Date(2023, 11, 9)
print(f"Date: {d.year}-{d.month}-{d.day}")

## Test error handling
try:
    s2 = Stock('AAPL', 50)  ## Missing price argument
    print("This should not print")
except TypeError as e:
    print(f"Error correctly caught: {e}")

要运行测试,请打开终端并执行以下命令:

python3 test_structure.py

你应该会看到以下输出:

Stock name: GOOG, shares: 100, price: 490.1
Point coordinates: (3, 4)
Date: 2023-11-9
Error correctly caught: Expected 3 arguments

如你所见,我们的基类按预期工作。它让定义新的数据结构变得容易多了,无需反复编写相同的样板代码。

✨ 查看解决方案并练习

改进对象表示

我们的 Structure 类对于创建和访问对象很有用。然而,目前它没有一种很好的方式将自身表示为字符串。当你打印一个对象或在 Python 解释器中查看它时,你希望看到一个清晰且信息丰富的显示。这有助于你理解对象是什么以及它的值是什么。

理解 Python 的对象表示

在 Python 中,有两个特殊方法用于以不同方式表示对象。这些方法很重要,因为它们允许你控制对象的显示方式。

  • __str__:此方法由 str() 函数和 print() 函数使用。它提供对象的人类可读表示。例如,如果你有一个 Stock 对象,__str__ 方法可能返回类似 "Stock: GOOG, 100 shares at $490.1" 的内容。
  • __repr__:此方法由 Python 解释器和 repr() 函数使用。它提供对象更具技术性且明确的表示。__repr__ 的目标是提供一个可用于重新创建对象的字符串。例如,对于一个 Stock 对象,它可能返回 "Stock('GOOG', 100, 490.1)"。

让我们为我们的 Structure 类添加一个 __repr__ 方法。这将使调试代码变得更容易,因为我们可以清楚地看到对象的状态。

实现良好的表示

现在,你需要更新你的 structure.py 文件。你将为 Structure 类添加 __repr__ 方法。此方法将创建一个字符串,以可用于重新创建对象的方式表示该对象。

def __repr__(self):
    """
    Return a representation of the object that can be used to recreate it.
    Example: Stock('GOOG', 100, 490.1)
    """
    ## Get the class name
    cls_name = type(self).__name__

    ## Get all the field values
    values = [getattr(self, name) for name in self._fields]

    ## Format the fields and values
    args_str = ', '.join(repr(value) for value in values)

    ## Return the formatted string
    return f"{cls_name}({args_str})"

此方法的具体步骤如下:

  1. 它使用 type(self).__name__ 获取类名。这很重要,因为它能告诉你正在处理的是哪种对象。
  2. 它从实例中检索所有字段的值。这能让你获取对象所持有的数据。
  3. 它创建一个包含类名和值的字符串表示。这个字符串可用于重新创建对象。

测试改进后的表示

让我们测试我们改进后的实现。创建一个名为 test_repr.py 的新文件。这个文件将创建我们类的一些实例并打印它们的表示。

## test_repr.py
from structure import Stock, Point, Date

## Create instances
s = Stock('GOOG', 100, 490.1)
p = Point(3, 4)
d = Date(2023, 11, 9)

## Print the representations
print(repr(s))
print(repr(p))
print(repr(d))

## Direct printing also uses __repr__ in the interpreter
print(s)
print(p)
print(d)

要运行测试,请打开终端并输入以下命令:

python3 test_repr.py

你应该会看到以下输出:

Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)
Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)

这个输出比以前更具信息性。当你看到 Stock('GOOG', 100, 490.1) 时,你立即知道该对象代表什么。你甚至可以复制这个字符串并在代码中使用它来重新创建对象。

良好表示的好处

一个良好的 __repr__ 实现对于调试非常有帮助。当你在解释器中查看对象或在程序执行期间记录它们时,清晰的表示能让你更快地识别问题。你可以看到对象的确切状态并理解可能出现的问题。

✨ 查看解决方案并练习

限制属性名称

目前,我们的 Structure 类允许在其实例上设置任何属性。对于初学者来说,这一开始可能看起来很方便,但实际上会导致很多问题。当你使用一个类时,你期望某些属性存在并以特定方式使用。如果用户拼写错误属性名,或者尝试设置并非原始设计一部分的属性,可能会导致难以发现的错误。

属性限制的必要性

让我们看一个简单的场景,来理解为什么需要限制属性名称。考虑以下代码:

s = Stock('GOOG', 100, 490.1)
s.shares = 50      ## Correct attribute name
s.share = 60       ## Typo in attribute name - creates a new attribute instead of updating

在第二行中,有一个拼写错误。我们写的是 share 而不是 shares。在 Python 中,它不会引发错误,而是会简单地创建一个名为 share 的新属性。这可能会导致难以察觉的错误,因为你可能以为自己在更新 shares 属性,但实际上是在创建一个新属性。这会使你的代码行为异常,并且很难调试。

实现属性限制

为了解决这个问题,我们可以重写 __setattr__ 方法。每当你尝试在对象上设置属性时,都会调用这个方法。通过重写它,我们可以控制哪些属性可以设置,哪些不可以。

用以下代码更新 structure.py 中的 Structure 类:

def __setattr__(self, name, value):
    """
    Restrict attribute setting to only those defined in _fields
    or attributes starting with underscore (private attributes).
    """
    if name.startswith('_'):
        ## Allow setting private attributes (starting with '_')
        super().__setattr__(name, value)
    elif name in self._fields:
        ## Allow setting attributes defined in _fields
        super().__setattr__(name, value)
    else:
        ## Raise an error for other attributes
        raise AttributeError(f'No attribute {name}')

这个方法的工作原理如下:

  1. 如果属性名以下划线 (_) 开头,则被视为私有属性。私有属性通常用于类的内部用途。我们允许设置这些属性,因为它们是类内部实现的一部分。
  2. 如果属性名在 _fields 列表中,这意味着它是类设计中定义的属性之一。我们允许设置这些属性,因为它们是类预期行为的一部分。
  3. 如果属性名不满足上述任何一个条件,我们会引发一个 AttributeError。这会告诉用户,他们正在尝试设置一个类中不存在的属性。

测试属性限制

现在我们已经实现了属性限制,让我们测试一下,确保它按预期工作。创建一个名为 test_attributes.py 的文件,内容如下:

## test_attributes.py
from structure import Stock

s = Stock('GOOG', 100, 490.1)

## This should work - valid attribute
print("Setting shares to 50")
s.shares = 50
print(f"Shares is now: {s.shares}")

## This should work - private attribute
print("\nSetting _internal_data")
s._internal_data = "Some data"
print(f"_internal_data is: {s._internal_data}")

## This should fail - invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.share = 60  ## Typo in attribute name
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

要运行测试,请打开终端并输入以下命令:

python3 test_attributes.py

你应该会看到以下输出:

Setting shares to 50
Shares is now: 50

Setting _internal_data
_internal_data is: Some data

Trying to set an invalid attribute:
Error correctly caught: No attribute share

这个输出表明,我们的类现在可以防止意外的属性错误。它允许我们设置有效的属性和私有属性,但当我们尝试设置无效属性时会引发错误。

属性限制的价值

限制属性名称对于编写健壮且可维护的代码非常重要。原因如下:

  1. 它有助于捕获属性名的拼写错误。如果你在输入属性名时出错,代码会引发错误,而不是创建一个新属性。这使得在开发过程的早期更容易发现和修复错误。
  2. 它可以防止尝试设置类设计中不存在的属性。这确保了类按预期使用,并且代码行为可预测。
  3. 它避免了意外创建新属性。创建新属性可能会导致意外行为,使代码更难理解和维护。

通过限制属性名称,我们使代码更可靠,更易于处理。

✨ 查看解决方案并练习

重写 Stock 类

既然我们已经有了一个定义良好的 Structure 基类,现在是时候重写我们的 Stock 类了。通过使用这个基类,我们可以简化代码并使其更有条理。Structure 类提供了一组通用功能,我们可以在 Stock 类中复用这些功能,这对代码的可维护性和可读性非常有帮助。

创建新的 Stock 类

让我们从创建一个名为 stock.py 的新文件开始。这个文件将包含我们重写后的 Stock 类。以下是你需要放在 stock.py 文件中的代码:

## stock.py
from structure import Structure

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

    @property
    def cost(self):
        """
        Calculate the cost as shares * price
        """
        return self.shares * self.price

    def sell(self, nshares):
        """
        Sell a number of shares
        """
        self.shares -= nshares

让我们来详细分析这个新的 Stock 类的功能:

  1. 它继承自 Structure 类。这意味着 Stock 类可以使用 Structure 类提供的所有特性。其中一个好处是,我们不需要自己编写 __init__ 方法,因为 Structure 类会自动处理属性赋值。
  2. 我们定义了 _fields,它是一个元组,指定了 Stock 类的属性。这些属性是 namesharesprice
  3. 定义了 cost 属性,用于计算股票的总成本。它将 shares 的数量乘以 price
  4. sell 方法用于减少股票的数量。当你调用这个方法并传入要卖出的股票数量时,它会从当前的股票数量中减去该数量。

测试新的 Stock 类

为了确保我们的新 Stock 类按预期工作,我们需要创建一个测试文件。让我们创建一个名为 test_stock.py 的文件,代码如下:

## test_stock.py
from stock import Stock

## Create a stock
s = Stock('GOOG', 100, 490.1)

## Check the attributes
print(f"Stock: {s}")
print(f"Name: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost}")

## Sell some shares
print("\nSelling 20 shares...")
s.sell(20)
print(f"Shares after selling: {s.shares}")
print(f"Cost after selling: {s.cost}")

## Try to set an invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.prices = 500  ## Invalid attribute (should be 'price')
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

在这个测试文件中,我们首先从 stock.py 文件中导入 Stock 类。然后,我们创建一个 Stock 类的实例,名称为 'GOOG',有 100 股,价格为 490.1。我们打印出股票的属性,以检查它们是否设置正确。之后,我们卖出 20 股,并打印出新的股票数量和新的成本。最后,我们尝试设置一个无效的属性 prices(应该是 price)。如果我们的 Stock 类工作正常,它应该会引发一个 AttributeError

要运行测试,请打开终端并输入以下命令:

python3 test_stock.py

预期输出如下:

Stock: Stock('GOOG', 100, 490.1)
Name: GOOG
Shares: 100
Price: 490.1
Cost: 49010.0

Selling 20 shares...
Shares after selling: 80
Cost after selling: 39208.0

Trying to set an invalid attribute:
Error correctly caught: No attribute prices

运行单元测试

如果你有之前练习中的单元测试,可以针对新的实现运行这些测试。在终端中输入以下命令:

python3 teststock.py

请注意,有些测试可能会失败。这可能是因为它们期望特定的行为或方法,而我们尚未实现这些。别担心!我们将在未来的练习中继续在此基础上进行构建。

回顾我们的进展

让我们花点时间回顾一下到目前为止我们所取得的成果:

  1. 我们创建了一个可复用的 Structure 基类。这个类:

    • 自动处理属性赋值,这让我们无需编写大量重复的代码。
    • 提供了良好的字符串表示,使打印和调试对象变得更加容易。
    • 限制属性名称以防止错误,这使我们的代码更加健壮。
  2. 我们重写了 Stock 类。它:

    • 继承自 Structure 类以复用通用功能。
    • 仅定义字段和特定领域的方法,这使类的关注点更加集中和简洁。
    • 设计清晰简单,易于理解和维护。

这种方法为我们的代码带来了几个好处:

  • 更易于维护,因为重复代码更少。如果我们需要更改通用功能中的某些内容,只需在 Structure 类中进行更改。
  • 更健壮,因为 Structure 类提供了更好的错误检查。
  • 更具可读性,因为每个类的职责都很明确。

在未来的练习中,我们将继续在此基础上构建一个更复杂的股票投资组合管理系统。

✨ 查看解决方案并练习

总结

在本次实验中,你学习了 Python 中的函数参数传递约定,并将其应用于构建一个更有条理、更易于维护的代码库。你探索了 Python 的参数传递机制,为数据对象创建了可复用的 Structure 基类,并改进了对象表示方式以方便调试。

你还添加了属性验证以防止常见错误,并使用新的结构重写了 Stock 类。这些技术展示了面向对象设计的关键原则,例如通过继承实现代码复用、通过封装保证数据完整性,以及通过公共接口实现多态性。通过应用这些原则,你可以开发出更健壮、更易于维护的代码,减少重复和错误。