简介
在这个实验中,你将学习 Python 中的类变量和类方法。你将了解它们的用途和用法,并学习如何有效地定义和使用类方法。
此外,你将使用类方法实现替代构造函数,探索类变量与继承之间的关系,并创建灵活的数据读取工具。在这个实验过程中,你将修改 stock.py 和 reader.py 文件。
在这个实验中,你将学习 Python 中的类变量和类方法。你将了解它们的用途和用法,并学习如何有效地定义和使用类方法。
此外,你将使用类方法实现替代构造函数,探索类变量与继承之间的关系,并创建灵活的数据读取工具。在这个实验过程中,你将修改 stock.py 和 reader.py 文件。
在第一步中,我们将深入探讨 Python 中的类变量和类方法的概念。这些重要的概念将帮助你编写更高效、更有条理的代码。在开始使用类变量和类方法之前,让我们先看看目前是如何创建 Stock 类的实例的。这将为我们提供一个基础理解,并让我们知道可以在哪些方面进行改进。
类变量是 Python 中一种特殊类型的变量。它们由类的所有实例共享。为了更好地理解这一点,让我们将它们与实例变量进行比较。实例变量对于类的每个实例都是唯一的。例如,如果你有一个类的多个实例,每个实例的实例变量都可以有自己的值。另一方面,类变量是在类级别定义的。这意味着该类的所有实例都可以访问并共享类变量的相同值。
类方法是作用于类本身,而不是类的单个实例的方法。它们与类绑定,这意味着可以直接在类上调用它们,而无需创建实例。在 Python 中定义类方法时,我们使用 @classmethod 装饰器。与将实例 (self) 作为第一个参数不同,类方法将类 (cls) 作为其第一个参数。这使它们能够操作类级别的数据,并执行与整个类相关的操作。
让我们首先看看目前是如何创建 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 类的一个子类。我们将其命名为 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 文件,并创建任何实现了 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 类,这才是它真正所属的地方。类型转换是指将数据从一种类型转换为另一种类型,比如将字符串转换为整数。通过这样做,我们使代码更有条理、更易于理解。
现在,我们将在 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() 方法,我们就可以将该函数与不同的类一起使用。
让我们创建一个测试文件,看看我们的通用读取器是如何工作的。创建一个名为 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 实例。这证明我们的通用读取器可以与不同的类一起使用。
这种方法展示了几个强大的概念:
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 代码非常有用。通过将类型转换置于类内部,并通过类方法提供统一的接口,你可以编写更通用的实用工具。为了进一步拓展学习,你可以探索更多用例、创建类层次结构,并使用类方法构建复杂的数据处理管道。