私有属性和属性(Properties)

PythonPythonBeginner
立即练习

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

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

介绍

在这个实验中,你将学习如何使用私有属性封装对象内部结构,并实现属性装饰器来控制属性访问。这些技术对于维护对象的完整性和确保正确的数据处理至关重要。

你还将了解如何使用 __slots__ 限制属性的创建。我们将在整个实验过程中修改 stock.py 文件,以应用这些概念。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/conditional_statements -.-> lab-132494{{"私有属性和属性(Properties)"}} python/classes_objects -.-> lab-132494{{"私有属性和属性(Properties)"}} python/encapsulation -.-> lab-132494{{"私有属性和属性(Properties)"}} python/raising_exceptions -.-> lab-132494{{"私有属性和属性(Properties)"}} python/decorators -.-> lab-132494{{"私有属性和属性(Properties)"}} end

实现私有属性

在 Python 中,我们使用命名约定来表明一个属性旨在供类内部使用。我们在这些属性前加上下划线 (_)。这向其他开发者发出信号,表明这些属性不是公共 API 的一部分,不应从类外部直接访问。

让我们看看 stock.py 文件中当前的 Stock 类。它有一个名为 types 的类变量。

class Stock:
    ## Class variable for type conversions
    types = (str, int, float)

    ## Rest of the class...

types 类变量在内部用于转换行数据。为了表明这是一个实现细节,我们将它标记为私有。

说明:

  1. 在编辑器中打开 stock.py 文件。

  2. 修改 types 类变量,添加前导下划线,将其更改为 _types

    class Stock:
        ## Class variable for type conversions
        _types = (str, int, float)
    
        ## Rest of the class...
  3. 更新 from_row 方法以使用重命名的变量 _types

    @classmethod
    def from_row(cls, row):
        values = [func(val) for func, val in zip(cls._types, row)]
        return cls(*values)
  4. 保存 stock.py 文件。

  5. 创建一个名为 test_stock.py 的 Python 脚本来测试你的更改。你可以使用以下命令在编辑器中创建文件:

    touch /home/labex/project/test_stock.py
  6. 将以下代码添加到 test_stock.py 文件。此代码创建 Stock 类的实例并打印有关它们的信息。

    from stock import Stock
    
    ## Create a stock instance
    s = Stock('GOOG', 100, 490.10)
    print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}")
    print(f"Cost: {s.cost()}")
    
    ## Create from row
    row = ['AAPL', '50', '142.5']
    apple = Stock.from_row(row)
    print(f"Name: {apple.name}, Shares: {apple.shares}, Price: {apple.price}")
    print(f"Cost: {apple.cost()}")
  7. 使用终端中的以下命令运行测试脚本:

    python /home/labex/project/test_stock.py

    你应该看到类似于以下的输出:

    Name: GOOG, Shares: 100, Price: 490.1
    Cost: 49010.0
    Name: AAPL, Shares: 50, Price: 142.5
    Cost: 7125.0

将方法转换为属性

Python 中的属性(Properties)允许你像访问属性一样访问计算值。这消除了调用方法时使用括号的需要,使你的代码更简洁和一致。

目前,我们的 Stock 类有一个 cost() 方法,用于计算股票的总成本。

def cost(self):
    return self.shares * self.price

要获取成本值,我们必须使用括号调用它:

s = Stock('GOOG', 100, 490.10)
print(s.cost())  ## Calls the method

我们可以通过将 cost() 方法转换为属性来改进这一点,从而允许我们在没有括号的情况下访问成本值:

s = Stock('GOOG', 100, 490.10)
print(s.cost)  ## Accesses the property

说明:

  1. 在编辑器中打开 stock.py 文件。

  2. 使用 @property 装饰器将 cost() 方法替换为属性:

    @property
    def cost(self):
        return self.shares * self.price
  3. 保存 stock.py 文件。

  4. 在编辑器中创建一个名为 test_property.py 的新文件:

    touch /home/labex/project/test_property.py
  5. 将以下代码添加到 test_property.py 文件,以创建一个 Stock 实例并访问 cost 属性:

    from stock import Stock
    
    ## Create a stock instance
    s = Stock('GOOG', 100, 490.10)
    
    ## Access cost as a property (no parentheses)
    print(f"Stock: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price}")
    print(f"Cost: {s.cost}")  ## Using the property
  6. 运行测试脚本:

    python /home/labex/project/test_property.py

    你应该看到类似于以下的输出:

    Stock: GOOG
    Shares: 100
    Price: 490.1
    Cost: 49010.0
✨ 查看解决方案并练习

实现属性验证

属性还允许你控制如何检索、设置和删除属性值。这对于向你的属性添加验证非常有用,确保这些值满足特定标准。

在我们的 Stock 类中,我们希望确保 shares 是一个非负整数,而 price 是一个非负浮点数。我们将使用属性装饰器以及 getter 和 setter 来实现这一点。

说明:

  1. 在编辑器中打开 stock.py 文件。

  2. 将私有属性 _shares_price 添加到 Stock 类,并修改构造函数以使用它们:

    def __init__(self, name, shares, price):
        self.name = name
        self._shares = shares  ## Using private attribute
        self._price = price    ## Using private attribute
  3. sharesprice 定义具有验证的属性:

    @property
    def shares(self):
        return self._shares
    
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError("Expected integer")
        if value < 0:
            raise ValueError("shares must be >= 0")
        self._shares = value
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        if not isinstance(value, float):
            raise TypeError("Expected float")
        if value < 0:
            raise ValueError("price must be >= 0")
        self._price = value
  4. 更新构造函数以使用属性 setter 进行验证:

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares  ## Using property setter
        self.price = price    ## Using property setter
  5. 保存 stock.py 文件。

  6. 创建一个名为 test_validation.py 的测试脚本:

    touch /home/labex/project/test_validation.py
  7. 将以下代码添加到 test_validation.py 文件:

    from stock import Stock
    
    ## Create a valid stock instance
    s = Stock('GOOG', 100, 490.10)
    print(f"Initial: Name={s.name}, Shares={s.shares}, Price={s.price}, Cost={s.cost}")
    
    ## Test valid updates
    try:
        s.shares = 50  ## Valid update
        print(f"After setting shares=50: Shares={s.shares}, Cost={s.cost}")
    except Exception as e:
        print(f"Error setting shares=50: {e}")
    
    try:
        s.price = 123.45  ## Valid update
        print(f"After setting price=123.45: Price={s.price}, Cost={s.cost}")
    except Exception as e:
        print(f"Error setting price=123.45: {e}")
    
    ## Test invalid updates
    try:
        s.shares = "50"  ## Invalid type (string)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting shares='50': {e}")
    
    try:
        s.shares = -10  ## Invalid value (negative)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting shares=-10: {e}")
    
    try:
        s.price = "123.45"  ## Invalid type (string)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting price='123.45': {e}")
    
    try:
        s.price = -10.0  ## Invalid value (negative)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting price=-10.0: {e}")
  8. 运行测试脚本:

    python /home/labex/project/test_validation.py

    你应该看到输出显示成功的有效更新以及无效更新的相应错误消息。

    Initial: Name=GOOG, Shares=100, Price=490.1, Cost=49010.0
    After setting shares=50: Shares=50, Cost=24505.0
    After setting price=123.45: Price=123.45, Cost=6172.5
    Error setting shares='50': Expected integer
    Error setting shares=-10: shares must be >= 0
    Error setting price='123.45': Expected float
    Error setting price=-10.0: price must be >= 0
✨ 查看解决方案并练习

使用 __slots__ 进行内存优化

__slots__ 属性限制了一个类可以拥有的属性。它阻止向实例添加新属性并减少内存使用。

在我们的 Stock 类中,我们将使用 __slots__ 来:

  1. 将属性创建限制为仅我们已定义的属性。
  2. 提高内存效率,尤其是在创建多个实例时。

说明:

  1. 在编辑器中打开 stock.py 文件。

  2. 添加一个 __slots__ 类变量,列出该类使用的所有私有属性名称:

    class Stock:
        ## Class variable for type conversions
        _types = (str, int, float)
    
        ## Define slots to restrict attribute creation
        __slots__ = ('name', '_shares', '_price')
    
        ## Rest of the class...
  3. 保存文件。

  4. 创建一个名为 test_slots.py 的测试脚本:

    touch /home/labex/project/test_slots.py
  5. 将以下代码添加到 test_slots.py 文件:

    from stock import Stock
    
    ## Create a stock instance
    s = Stock('GOOG', 100, 490.10)
    
    ## Access existing attributes
    print(f"Name: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price}")
    print(f"Cost: {s.cost}")
    
    ## Try to add a new attribute
    try:
        s.extra = "This will fail"
        print(f"Extra: {s.extra}")
    except AttributeError as e:
        print(f"Error: {e}")
  6. 运行测试脚本:

    python /home/labex/project/test_slots.py

    你应该看到输出显示你可以访问已定义的属性,但尝试添加新属性会引发 AttributeError(属性错误)。

    Name: GOOG
    Shares: 100
    Price: 490.1
    Cost: 49010.0
    Error: 'Stock' object has no attribute 'extra'
✨ 查看解决方案并练习

协调类型验证与类变量

目前,我们的 Stock 类同时使用 _types 类变量和属性 setter 来处理类型。为了提高一致性和可维护性,我们将协调这些机制,以便它们使用相同的类型信息。

说明:

  1. 在编辑器中打开 stock.py 文件。

  2. 修改属性 setter 以使用 _types 类变量中定义的类型:

    @property
    def shares(self):
        return self._shares
    
    @shares.setter
    def shares(self, value):
        if not isinstance(value, self._types[1]):
            raise TypeError(f"Expected {self._types[1].__name__}")
        if value < 0:
            raise ValueError("shares must be >= 0")
        self._shares = value
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        if not isinstance(value, self._types[2]):
            raise TypeError(f"Expected {self._types[2].__name__}")
        if value < 0:
            raise ValueError("price must be >= 0")
        self._price = value
  3. 保存 stock.py 文件。

  4. 创建一个名为 test_subclass.py 的测试脚本:

    touch /home/labex/project/test_subclass.py
  5. 将以下代码添加到 test_subclass.py 文件:

    from stock import Stock
    from decimal import Decimal
    
    ## Create a subclass with different types
    class DStock(Stock):
        _types = (str, int, Decimal)
    
    ## Test the base class
    s = Stock('GOOG', 100, 490.10)
    print(f"Stock: {s.name}, Shares: {s.shares}, Price: {s.price}, Cost: {s.cost}")
    
    ## Test valid update with float
    try:
        s.price = 500.25
        print(f"Updated Stock price: {s.price}, Cost: {s.cost}")
    except Exception as e:
        print(f"Error updating Stock price: {e}")
    
    ## Test the subclass with Decimal
    ds = DStock('AAPL', 50, Decimal('142.50'))
    print(f"DStock: {ds.name}, Shares: {ds.shares}, Price: {ds.price}, Cost: {ds.cost}")
    
    ## Test invalid update with float (should require Decimal)
    try:
        ds.price = 150.75
        print(f"Updated DStock price: {ds.price}")
    except Exception as e:
        print(f"Error updating DStock price: {e}")
    
    ## Test valid update with Decimal
    try:
        ds.price = Decimal('155.25')
        print(f"Updated DStock price: {ds.price}, Cost: {ds.cost}")
    except Exception as e:
        print(f"Error updating DStock price: {e}")
  6. 运行测试脚本:

    python /home/labex/project/test_subclass.py

    你应该看到基类 Stock 接受价格的浮点数值(float values),而子类 DStock 需要 Decimal 值。

    Stock: GOOG, Shares: 100, Price: 490.1, Cost: 49010.0
    Updated Stock price: 500.25, Cost: 50025.0
    DStock: AAPL, Shares: 50, Price: 142.50, Cost: 7125.00
    Error updating DStock price: Expected Decimal
    Updated DStock price: 155.25, Cost: 7762.50

总结

在这个实验(lab)中,你已经学习了如何使用私有属性、将方法转换为属性(properties)、实现属性验证、使用 __slots__ 进行内存优化,以及协调类型验证与类变量。这些技术通过强制封装和提供清晰的接口,增强了类的健壮性、效率和可维护性。