简介
在本次实验中,你将学习 Python 函数参数传递的约定。你还将为数据类创建一个可复用的结构,并应用面向对象的设计原则来简化你的代码。
本次练习旨在以更有条理的方式重写 stock.py
文件。在开始之前,将你在 stock.py
中的现有代码复制到一个名为 orig_stock.py
的新文件中,以供参考。你需要创建的文件是 structure.py
和 stock.py
。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在本次实验中,你将学习 Python 函数参数传递的约定。你还将为数据类创建一个可复用的结构,并应用面向对象的设计原则来简化你的代码。
本次练习旨在以更有条理的方式重写 stock.py
文件。在开始之前,将你在 stock.py
中的现有代码复制到一个名为 orig_stock.py
的新文件中,以供参考。你需要创建的文件是 structure.py
和 stock.py
。
在 Python 中,函数是一个基本概念,它允许你将一组语句组合在一起以执行特定任务。当你调用一个函数时,通常需要为其提供一些数据,我们将这些数据称为参数。Python 提供了不同的方式将这些参数传递给函数。这种灵活性非常有用,因为它有助于你编写更简洁、更易维护的代码。在将这些技术应用到我们的项目之前,让我们仔细了解一下这些参数传递约定。
在开始对 stock.py
文件进行修改之前,创建一个备份是个好习惯。这样,如果在实验过程中出现问题,我们总能恢复到原始版本。要创建备份,请打开终端并运行以下命令:
cp stock.py orig_stock.py
此命令使用终端中的 cp
(复制)命令。它将 stock.py
文件复制一份,命名为 orig_stock.py
。通过这样做,我们确保原始工作得到了安全保存。
在 Python 中,有几种不同类型的参数调用函数的方法。让我们详细探讨每种方法。
将参数传递给函数的最简单方法是按位置传递。当你定义一个函数时,需要指定一个参数列表。调用函数时,要按照定义的顺序为这些参数提供值。
以下是一个示例:
def calculate(x, y, z):
return x + y + z
## Call with positional arguments
result = calculate(1, 2, 3)
print(result) ## Output: 6
在这个示例中,calculate
函数接受三个参数:x
、y
和 z
。当我们使用 calculate(1, 2, 3)
调用该函数时,值 1
被赋给 x
,2
被赋给 y
,3
被赋给 z
。然后函数将这些值相加并返回结果。
除了位置参数,你还可以通过参数名来指定参数。这称为使用关键字参数。使用关键字参数时,你不必担心参数的顺序。
以下是一个示例:
## Call with a mix of positional and keyword arguments
result = calculate(1, z=3, y=2)
print(result) ## Output: 6
在这个示例中,我们首先为 x
传递位置参数 1
。然后,使用关键字参数为 y
和 z
指定值。只要提供了正确的名称,关键字参数的顺序无关紧要。
Python 提供了一种方便的方式,使用 *
和 **
语法将序列和字典作为参数传递。这称为解包。
以下是将元组解包为位置参数的示例:
## Unpacking a tuple into positional arguments
args = (1, 2, 3)
result = calculate(*args)
print(result) ## Output: 6
在这个示例中,我们有一个包含值 1
、2
和 3
的元组 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
函数。
有时,你可能想定义一个可以接受任意数量参数的函数。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)
这个基类有几个重要的特性:
_fields
类变量。默认情况下,这个变量为空。这个变量将保存类将具有的属性名称。_fields
中定义的字段数量匹配。如果不匹配,它会引发一个 TypeError
。这有助于我们尽早捕获错误。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 中,有两个特殊方法用于以不同方式表示对象。这些方法很重要,因为它们允许你控制对象的显示方式。
__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})"
此方法的具体步骤如下:
type(self).__name__
获取类名。这很重要,因为它能告诉你正在处理的是哪种对象。让我们测试我们改进后的实现。创建一个名为 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}')
这个方法的工作原理如下:
_
) 开头,则被视为私有属性。私有属性通常用于类的内部用途。我们允许设置这些属性,因为它们是类内部实现的一部分。_fields
列表中,这意味着它是类设计中定义的属性之一。我们允许设置这些属性,因为它们是类预期行为的一部分。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
这个输出表明,我们的类现在可以防止意外的属性错误。它允许我们设置有效的属性和私有属性,但当我们尝试设置无效属性时会引发错误。
限制属性名称对于编写健壮且可维护的代码非常重要。原因如下:
通过限制属性名称,我们使代码更可靠,更易于处理。
既然我们已经有了一个定义良好的 Structure
基类,现在是时候重写我们的 Stock
类了。通过使用这个基类,我们可以简化代码并使其更有条理。Structure
类提供了一组通用功能,我们可以在 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
类的功能:
Structure
类。这意味着 Stock
类可以使用 Structure
类提供的所有特性。其中一个好处是,我们不需要自己编写 __init__
方法,因为 Structure
类会自动处理属性赋值。_fields
,它是一个元组,指定了 Stock
类的属性。这些属性是 name
、shares
和 price
。cost
属性,用于计算股票的总成本。它将 shares
的数量乘以 price
。sell
方法用于减少股票的数量。当你调用这个方法并传入要卖出的股票数量时,它会从当前的股票数量中减去该数量。为了确保我们的新 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
请注意,有些测试可能会失败。这可能是因为它们期望特定的行为或方法,而我们尚未实现这些。别担心!我们将在未来的练习中继续在此基础上进行构建。
让我们花点时间回顾一下到目前为止我们所取得的成果:
我们创建了一个可复用的 Structure
基类。这个类:
我们重写了 Stock
类。它:
Structure
类以复用通用功能。这种方法为我们的代码带来了几个好处:
Structure
类中进行更改。Structure
类提供了更好的错误检查。在未来的练习中,我们将继续在此基础上构建一个更复杂的股票投资组合管理系统。
在本次实验中,你学习了 Python 中的函数参数传递约定,并将其应用于构建一个更有条理、更易于维护的代码库。你探索了 Python 的参数传递机制,为数据对象创建了可复用的 Structure
基类,并改进了对象表示方式以方便调试。
你还添加了属性验证以防止常见错误,并使用新的结构重写了 Stock
类。这些技术展示了面向对象设计的关键原则,例如通过继承实现代码复用、通过封装保证数据完整性,以及通过公共接口实现多态性。通过应用这些原则,你可以开发出更健壮、更易于维护的代码,减少重复和错误。