简介
在这个实验中,你将学习 Python 中的类变量和类方法。你将了解它们的用途和用法,并学习如何有效地定义和使用类方法。
此外,你将使用类方法实现替代构造函数,探索类变量与继承之间的关系,并创建灵活的数据读取工具。在这个实验过程中,你将修改 stock.py 和 reader.py 文件。
理解类变量和类方法
在第一步中,我们将深入探讨 Python 中的类变量和类方法的概念。这些重要的概念将帮助你编写更高效、更有条理的代码。在开始使用类变量和类方法之前,让我们先看看目前是如何创建 Stock 类的实例的。这将为我们提供一个基础理解,并让我们知道可以在哪些方面进行改进。
什么是类变量?
类变量是 Python 中一种特殊类型的变量。它们由类的所有实例共享。为了更好地理解这一点,让我们将它们与实例变量进行比较。实例变量对于类的每个实例都是唯一的。例如,如果你有一个类的多个实例,每个实例的实例变量都可以有自己的值。另一方面,类变量是在类级别定义的。这意味着该类的所有实例都可以访问并共享类变量的相同值。
什么是类方法?
类方法是作用于类本身,而不是类的单个实例的方法。它们与类绑定,这意味着可以直接在类上调用它们,而无需创建实例。在 Python 中定义类方法时,我们使用 @classmethod 装饰器。与将实例 (self) 作为第一个参数不同,类方法将类 (cls) 作为其第一个参数。这使它们能够操作类级别的数据,并执行与整个类相关的操作。
当前创建 Stock 实例的方法
让我们首先看看目前是如何创建 Stock 类的实例的。在编辑器中打开 stock.py 文件,观察当前的实现:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
这个类的实例通常通过以下方式之一创建:
直接用值初始化:
s = Stock('GOOG', 100, 490.1)在这里,我们通过为
name、shares和price属性提供值,直接创建了Stock类的一个实例。当你事先知道这些值时,这是一种直接创建实例的方法。从 CSV 文件读取的数据创建:
import csv with open('portfolio.csv') as f: rows = csv.reader(f) headers = next(rows) ## Skip the header row = next(rows) ## Get the first data row s = Stock(row[0], int(row[1]), float(row[2]))当我们从 CSV 文件中读取数据时,这些值最初是字符串格式。因此,当从 CSV 数据创建
Stock实例时,我们需要手动将字符串值转换为适当的类型。例如,shares值需要转换为整数,price值需要转换为浮点数。
让我们来试试这个。在 ~/project 目录下创建一个名为 test_stock.py 的新 Python 文件,内容如下:
## test_stock.py
from stock import Stock
import csv
## Method 1: Direct creation
s1 = Stock('GOOG', 100, 490.1)
print(f"Stock: {s1.name}, Shares: {s1.shares}, Price: {s1.price}")
print(f"Cost: {s1.cost()}")
## Method 2: Creation from CSV row
with open('portfolio.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip the header
row = next(rows) ## Get the first data row
s2 = Stock(row[0], int(row[1]), float(row[2]))
print(f"\nStock from CSV: {s2.name}, Shares: {s2.shares}, Price: {s2.price}")
print(f"Cost: {s2.cost()}")
运行这个文件以查看结果:
cd ~/project
python test_stock.py
你应该会看到类似于以下的输出:
Stock: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Stock from CSV: AA, Shares: 100, Price: 32.2
Cost: 3220.0
这种手动转换方法可行,但有一些缺点。我们需要知道数据的确切格式,并且每次从 CSV 数据创建实例时都必须进行转换。这可能容易出错且耗时。在下一步中,我们将使用类方法创建一个更优雅的解决方案。
使用类方法实现替代构造函数
在这一步中,你将学习如何使用类方法实现一个替代构造函数。这将使你能够以更优雅的方式从 CSV 行数据创建 Stock 对象。
什么是替代构造函数?
在 Python 中,替代构造函数是一种实用的模式。通常,你使用标准的 __init__ 方法来创建对象。然而,替代构造函数为你提供了另一种创建对象的方式。类方法非常适合实现替代构造函数,因为它们可以访问类本身。
实现 from_row() 类方法
你将为 Stock 类添加一个类变量 types 和一个类方法 from_row()。这将简化从 CSV 数据创建 Stock 实例的过程。
让我们通过添加高亮代码来修改 stock.py 文件:
## stock.py
class Stock:
types = (str, int, float) ## Type conversions to apply to CSV data
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
@classmethod
def from_row(cls, row):
"""
Create a Stock instance from a row of CSV data.
Args:
row: A list of strings [name, shares, price]
Returns:
A new Stock instance
"""
values = [func(val) for func, val in zip(cls.types, row)]
return cls(*values)
## The rest of the file remains unchanged
现在,让我们逐步了解这段代码的工作原理:
- 你定义了一个类变量
types。它是一个包含类型转换函数(str, int, float)的元组。这些函数将用于将 CSV 行中的数据转换为适当的类型。 - 你添加了一个类方法
from_row()。@classmethod装饰器将此方法标记为类方法。 - 此方法的第一个参数是
cls,它是对类本身的引用。在普通方法中,你使用self来引用类的实例,但在这里你使用cls,因为这是一个类方法。 zip()函数用于将types中的每个类型转换函数与row列表中的相应值配对。- 你使用列表推导式将每个转换函数应用于
row列表中的相应值。这样,你将 CSV 行中的字符串数据转换为适当的类型。 - 最后,你使用转换后的值创建一个新的
Stock类实例并返回它。
测试替代构造函数
现在,你将创建一个名为 test_class_method.py 的新文件来测试新的类方法。这将帮助你验证替代构造函数是否按预期工作。
## test_class_method.py
from stock import Stock
## Test the from_row() class method
row = ['AA', '100', '32.20']
s = Stock.from_row(row)
print(f"Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost()}")
## Try with a different row
row2 = ['GOOG', '50', '1120.50']
s2 = Stock.from_row(row2)
print(f"\nStock: {s2.name}")
print(f"Shares: {s2.shares}")
print(f"Price: {s2.price}")
print(f"Cost: {s2.cost()}")
要查看结果,请在终端中运行以下命令:
cd ~/project
python test_class_method.py
你应该会看到类似于以下的输出:
Stock: AA
Shares: 100
Price: 32.2
Cost: 3220.0
Stock: GOOG
Shares: 50
Price: 1120.5
Cost: 56025.0
请注意,现在你可以直接从字符串数据创建 Stock 实例,而无需在类外部手动进行类型转换。这使你的代码更简洁,并确保数据转换的职责在类内部处理。
类变量与继承
在这一步中,我们将探讨类变量如何与继承相互作用,以及它们如何作为一种定制机制。在 Python 中,继承允许子类从基类继承属性和方法。类变量是属于类本身的变量,而不是类的任何特定实例。理解它们如何协同工作对于创建灵活且可维护的代码至关重要。
继承中的类变量
当子类从基类继承时,它会自动访问基类的类变量。然而,子类可以覆盖这些类变量。通过这样做,子类可以改变其行为而不影响基类。这是一个非常强大的特性,因为它允许你根据特定需求定制子类的行为。
创建专门的 Stock 类
让我们创建 Stock 类的一个子类。我们将其命名为 DStock,代表 Decimal Stock(十进制股票)。DStock 与常规 Stock 类的主要区别在于,DStock 将使用 Decimal 类型来表示价格值,而不是 float。在金融计算中,精度极其重要,与 float 相比,Decimal 类型提供了更精确的十进制算术。
要创建这个子类,我们将创建一个名为 decimal_stock.py 的新文件。以下是你需要放在该文件中的代码:
## decimal_stock.py
from decimal import Decimal
from stock import Stock
class DStock(Stock):
"""
A specialized version of Stock that uses Decimal for prices
"""
types = (str, int, Decimal) ## Override the types class variable
## Test the subclass
if __name__ == "__main__":
## Create a DStock from row data
row = ['AA', '100', '32.20']
ds = DStock.from_row(row)
print(f"DStock: {ds.name}")
print(f"Shares: {ds.shares}")
print(f"Price: {ds.price} (type: {type(ds.price).__name__})")
print(f"Cost: {ds.cost()} (type: {type(ds.cost()).__name__})")
## For comparison, create a regular Stock from the same data
s = Stock.from_row(row)
print(f"\nRegular Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price} (type: {type(s.price).__name__})")
print(f"Cost: {s.cost()} (type: {type(s.cost()).__name__})")
在你使用上述代码创建了 decimal_stock.py 文件后,你需要运行它以查看结果。打开你的终端并按照以下步骤操作:
cd ~/project
python decimal_stock.py
你应该会看到类似于以下的输出:
DStock: AA
Shares: 100
Price: 32.20 (type: Decimal)
Cost: 3220.0 (type: Decimal)
Regular Stock: AA
Shares: 100
Price: 32.2 (type: float)
Cost: 3220.0 (type: float)
关于类变量和继承的关键点
从这个例子中,我们可以得出几个重要的结论:
DStock类继承了Stock类的所有方法,例如cost()方法,而无需重新定义它们。这是继承的主要优点之一,因为它避免了你编写冗余代码。- 只需覆盖
types类变量,我们就改变了创建DStock新实例时数据的转换方式。这展示了类变量如何用于定制子类的行为。 - 基类
Stock保持不变,仍然使用float值。这意味着我们对子类所做的更改不会影响基类,这是一个良好的设计原则。 from_row()类方法在Stock和DStock类中都能正常工作。这展示了继承的强大之处,因为同一个方法可以用于不同的子类。
这个例子清楚地展示了类变量如何作为一种配置机制。子类可以覆盖这些变量来定制其行为,而无需重写方法。
设计讨论
让我们考虑另一种方法,即将类型转换放在 __init__ 方法中:
class Stock:
def __init__(self, name, shares, price):
self.name = str(name)
self.shares = int(shares)
self.price = float(price)
使用这种方法,我们可以从一行数据创建一个 Stock 对象,如下所示:
row = ['AA', '100', '32.20']
s = Stock(*row)
虽然这种方法乍一看可能更简单,但它有几个缺点:
- 它将两个不同的关注点结合在一起:对象初始化和数据转换。这使得代码更难理解和维护。
__init__方法变得不那么灵活,因为它总是转换输入,即使它们已经是正确的类型。- 它限制了子类定制转换过程的方式。如果转换逻辑嵌入在
__init__方法中,子类将更难改变转换逻辑。 - 代码变得更脆弱。如果任何转换失败,对象就无法创建,这可能会导致程序出错。
另一方面,类方法方法将这些关注点分开。这使得代码更易于维护和灵活,因为代码的每个部分都有单一的职责。
创建通用的 CSV 读取器
在这最后一步,我们将创建一个通用函数。该函数能够读取 CSV 文件,并创建任何实现了 from_row() 类方法的类的对象。这展示了将类方法用作统一接口的强大之处。统一接口意味着不同的类可以以相同的方式使用,这使我们的代码更灵活、更易于管理。
修改 read_portfolio() 函数
首先,我们将更新 stock.py 文件中的 read_portfolio() 函数。我们将使用新的 from_row() 类方法。打开 stock.py 文件,并将 read_portfolio() 函数修改如下:
def read_portfolio(filename):
'''
Read a stock portfolio file into a list of Stock instances
'''
import csv
portfolio = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
for row in rows:
portfolio.append(Stock.from_row(row))
return portfolio
这个新版本的函数更简单。它将类型转换的职责交给了 Stock 类,这才是它真正所属的地方。类型转换是指将数据从一种类型转换为另一种类型,比如将字符串转换为整数。通过这样做,我们使代码更有条理、更易于理解。
创建通用的 CSV 读取器
现在,我们将在 reader.py 文件中创建一个更通用的函数。这个函数可以读取 CSV 数据,并创建任何具有 from_row() 类方法的类的实例。
打开 reader.py 文件,并添加以下函数:
def read_csv_as_instances(filename, cls):
'''
Read a CSV file into a list of instances of the given class.
Args:
filename: Name of the CSV file
cls: Class to instantiate (must have from_row class method)
Returns:
List of class instances
'''
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
for row in rows:
records.append(cls.from_row(row))
return records
这个函数接受两个输入:一个文件名和一个类。然后,它返回一个由该类的实例组成的列表,这些实例是根据 CSV 文件中的数据创建的。这非常有用,因为只要类具有 from_row() 方法,我们就可以将该函数与不同的类一起使用。
测试通用的 CSV 读取器
让我们创建一个测试文件,看看我们的通用读取器是如何工作的。创建一个名为 test_csv_reader.py 的文件,内容如下:
## test_csv_reader.py
from reader import read_csv_as_instances
from stock import Stock
from decimal_stock import DStock
## Read portfolio as Stock instances
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print(f"Portfolio contains {len(portfolio)} stocks")
print(f"First stock: {portfolio[0].name}, {portfolio[0].shares} shares at ${portfolio[0].price}")
## Read portfolio as DStock instances (with Decimal prices)
decimal_portfolio = read_csv_as_instances('portfolio.csv', DStock)
print(f"\nDecimal portfolio contains {len(decimal_portfolio)} stocks")
print(f"First stock: {decimal_portfolio[0].name}, {decimal_portfolio[0].shares} shares at ${decimal_portfolio[0].price}")
## Define a new class for reading the bus data
class BusRide:
def __init__(self, route, date, daytype, rides):
self.route = route
self.date = date
self.daytype = daytype
self.rides = rides
@classmethod
def from_row(cls, row):
return cls(row[0], row[1], row[2], int(row[3]))
## Read some bus data (just the first 5 records for brevity)
print("\nReading bus data...")
import csv
with open('ctabus.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
bus_rides = []
for i, row in enumerate(rows):
if i >= 5: ## Only read 5 records for the example
break
bus_rides.append(BusRide.from_row(row))
## Display the bus data
for ride in bus_rides:
print(f"Route: {ride.route}, Date: {ride.date}, Type: {ride.daytype}, Rides: {ride.rides}")
运行这个文件以查看结果。打开终端并使用以下命令:
cd ~/project
python test_csv_reader.py
你应该会看到输出显示投资组合数据被加载为 Stock 和 DStock 实例,公交路线数据被加载为 BusRide 实例。这证明我们的通用读取器可以与不同的类一起使用。
这种方法的主要优点
这种方法展示了几个强大的概念:
- 关注点分离:读取数据与创建对象是分开的。这意味着读取 CSV 文件的代码不会与创建对象的代码混合在一起。这使代码更易于理解和维护。
- 多态性:相同的代码可以与遵循相同接口的不同类一起工作。在我们的例子中,只要一个类具有
from_row()方法,我们的通用读取器就可以使用它。 - 灵活性:我们可以通过使用不同的类轻松更改数据的转换方式。例如,我们可以使用
Stock或DStock以不同的方式处理投资组合数据。 - 可扩展性:我们可以添加与读取器一起工作的新类,而无需更改读取器代码。这使我们的代码更具前瞻性。
这是 Python 中一种常见的模式,它使代码更模块化、可重用和易于维护。
关于类方法的最后说明
在 Python 中,类方法通常用作替代构造函数。你通常可以通过它们的名称来区分,因为它们的名称中通常包含“from”这个词。例如:
## Some examples from Python's built-in types
dict.fromkeys(['a', 'b', 'c'], 0) ## Create a dict with default values
datetime.datetime.fromtimestamp(1627776000) ## Create datetime from timestamp
int.from_bytes(b'\x00\x01', byteorder='big') ## Create int from bytes
遵循这个约定,你可以使代码更具可读性,并与 Python 的内置库保持一致。这有助于其他开发者更轻松地理解你的代码。
总结
在本次实验中,你学习了 Python 的两个关键特性:类变量和类方法。类变量由所有类实例共享,可用于配置。类方法作用于类本身,通过 @classmethod 装饰器进行标记。替代构造函数是类方法的常见应用,它提供了不同的对象创建方式。使用类变量的继承允许子类通过覆盖类变量来定制行为,而使用类方法可以实现灵活的代码设计。
这些概念对于创建结构良好且灵活的 Python 代码非常有用。通过将类型转换置于类内部,并通过类方法提供统一的接口,你可以编写更通用的实用工具。为了进一步拓展学习,你可以探索更多用例、创建类层次结构,并使用类方法构建复杂的数据处理管道。