类变量和类方法

Beginner

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

简介

在这个实验中,你将学习 Python 中的类变量和类方法。你将了解它们的用途和用法,并学习如何有效地定义和使用类方法。

此外,你将使用类方法实现替代构造函数,探索类变量与继承之间的关系,并创建灵活的数据读取工具。在这个实验过程中,你将修改 stock.pyreader.py 文件。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 初级 级别的实验,完成率为 100%。获得了学习者 100% 的好评率。

理解类变量和类方法

在第一步中,我们将深入探讨 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

这个类的实例通常通过以下方式之一创建:

  1. 直接用值初始化:

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

    在这里,我们通过为 namesharesprice 属性提供值,直接创建了 Stock 类的一个实例。当你事先知道这些值时,这是一种直接创建实例的方法。

  2. 从 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

现在,让我们逐步了解这段代码的工作原理:

  1. 你定义了一个类变量 types。它是一个包含类型转换函数 (str, int, float) 的元组。这些函数将用于将 CSV 行中的数据转换为适当的类型。
  2. 你添加了一个类方法 from_row()@classmethod 装饰器将此方法标记为类方法。
  3. 此方法的第一个参数是 cls,它是对类本身的引用。在普通方法中,你使用 self 来引用类的实例,但在这里你使用 cls,因为这是一个类方法。
  4. zip() 函数用于将 types 中的每个类型转换函数与 row 列表中的相应值配对。
  5. 你使用列表推导式将每个转换函数应用于 row 列表中的相应值。这样,你将 CSV 行中的字符串数据转换为适当的类型。
  6. 最后,你使用转换后的值创建一个新的 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)

关于类变量和继承的关键点

从这个例子中,我们可以得出几个重要的结论:

  1. DStock 类继承了 Stock 类的所有方法,例如 cost() 方法,而无需重新定义它们。这是继承的主要优点之一,因为它避免了你编写冗余代码。
  2. 只需覆盖 types 类变量,我们就改变了创建 DStock 新实例时数据的转换方式。这展示了类变量如何用于定制子类的行为。
  3. 基类 Stock 保持不变,仍然使用 float 值。这意味着我们对子类所做的更改不会影响基类,这是一个良好的设计原则。
  4. from_row() 类方法在 StockDStock 类中都能正常工作。这展示了继承的强大之处,因为同一个方法可以用于不同的子类。

这个例子清楚地展示了类变量如何作为一种配置机制。子类可以覆盖这些变量来定制其行为,而无需重写方法。

设计讨论

让我们考虑另一种方法,即将类型转换放在 __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)

虽然这种方法乍一看可能更简单,但它有几个缺点:

  1. 它将两个不同的关注点结合在一起:对象初始化和数据转换。这使得代码更难理解和维护。
  2. __init__ 方法变得不那么灵活,因为它总是转换输入,即使它们已经是正确的类型。
  3. 它限制了子类定制转换过程的方式。如果转换逻辑嵌入在 __init__ 方法中,子类将更难改变转换逻辑。
  4. 代码变得更脆弱。如果任何转换失败,对象就无法创建,这可能会导致程序出错。

另一方面,类方法方法将这些关注点分开。这使得代码更易于维护和灵活,因为代码的每个部分都有单一的职责。

创建通用的 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

你应该会看到输出显示投资组合数据被加载为 StockDStock 实例,公交路线数据被加载为 BusRide 实例。这证明我们的通用读取器可以与不同的类一起使用。

这种方法的主要优点

这种方法展示了几个强大的概念:

  1. 关注点分离:读取数据与创建对象是分开的。这意味着读取 CSV 文件的代码不会与创建对象的代码混合在一起。这使代码更易于理解和维护。
  2. 多态性:相同的代码可以与遵循相同接口的不同类一起工作。在我们的例子中,只要一个类具有 from_row() 方法,我们的通用读取器就可以使用它。
  3. 灵活性:我们可以通过使用不同的类轻松更改数据的转换方式。例如,我们可以使用 StockDStock 以不同的方式处理投资组合数据。
  4. 可扩展性:我们可以添加与读取器一起工作的新类,而无需更改读取器代码。这使我们的代码更具前瞻性。

这是 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 代码非常有用。通过将类型转换置于类内部,并通过类方法提供统一的接口,你可以编写更通用的实用工具。为了进一步拓展学习,你可以探索更多用例、创建类层次结构,并使用类方法构建复杂的数据处理管道。