自定义属性访问

PythonPythonBeginner
立即练习

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

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

简介

在这个实验中,你将学习 Python 面向对象编程的一个基本方面:属性访问。Python 允许开发者通过特殊方法自定义类中属性的访问、设置和管理方式。这为控制对象行为提供了强大的手段。

此外,你将学习如何在 Python 类中自定义属性访问,理解委托(delegation)和继承(inheritance)之间的区别,并练习在 Python 对象中实现自定义属性管理。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") subgraph Lab Skills python/classes_objects -.-> lab-132502{{"自定义属性访问"}} python/inheritance -.-> lab-132502{{"自定义属性访问"}} python/encapsulation -.-> lab-132502{{"自定义属性访问"}} end

理解用于属性控制的 __setattr__

在 Python 中,有一些特殊方法可以让你自定义对象属性的访问和修改方式。其中一个重要的方法是 __setattr__()。每当你尝试为对象的属性赋值时,这个方法就会发挥作用。它让你能够对属性赋值过程进行精细控制。

什么是 __setattr__

__setattr__(self, name, value) 方法充当所有属性赋值的拦截器。当你编写像 obj.attr = value 这样的简单赋值语句时,Python 并不会直接赋值。相反,它会在内部调用 obj.__setattr__("attr", value)。这种机制让你能够决定在属性赋值期间应该发生什么。

现在让我们来看一个实际的例子,展示如何使用 __setattr__ 来限制可以在类上设置哪些属性。

步骤 1:创建一个新文件

首先,在 WebIDE 中打开一个新文件。你可以通过点击“File”菜单,然后选择“New File”来完成。将这个文件命名为 restricted_stock.py,并将其保存到 /home/labex/project 目录中。这个文件将包含我们使用 __setattr__ 来控制属性赋值的类定义。

步骤 2:向 restricted_stock.py 添加代码

将以下代码添加到 restricted_stock.py 文件中。这段代码定义了一个 RestrictedStock 类。

class RestrictedStock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def __setattr__(self, name, value):
        ## Only allow specific attributes
        if name not in {'name', 'shares', 'price'}:
            raise AttributeError(f'Cannot set attribute {name}')

        ## If attribute is allowed, set it using the parent method
        super().__setattr__(name, value)

__init__ 方法中,我们用 namesharesprice 属性初始化对象。__setattr__ 方法会检查正在赋值的属性名是否在允许的属性集合(namesharesprice)中。如果不在,它会引发一个 AttributeError。如果属性是允许的,它会使用父类的 __setattr__ 方法来实际设置属性。

步骤 3:创建一个测试文件

创建一个名为 test_restricted.py 的新文件,并将以下代码添加到其中。这段代码将测试 RestrictedStock 类的功能。

from restricted_stock import RestrictedStock

## Create a new stock
stock = RestrictedStock('GOOG', 100, 490.1)

## Test accessing existing attributes
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")

## Test modifying an existing attribute
print("\nChanging shares to 75...")
stock.shares = 75
print(f"New shares value: {stock.shares}")

## Test setting an invalid attribute
try:
    print("\nTrying to set an invalid attribute 'share'...")
    stock.share = 50
except AttributeError as e:
    print(f"Error: {e}")

在这段代码中,我们首先导入 RestrictedStock 类。然后创建该类的一个实例。我们测试访问现有属性、修改现有属性,最后,我们尝试设置一个无效属性,以查看 __setattr__ 方法是否按预期工作。

步骤 4:运行测试文件

在 WebIDE 中打开一个终端,并执行以下命令来运行 test_restricted.py 文件:

cd /home/labex/project
python3 test_restricted.py

运行这些命令后,你应该会看到类似于以下的输出:

Name: GOOG
Shares: 100
Price: 490.1

Changing shares to 75...
New shares value: 75

Trying to set an invalid attribute 'share'...
Error: Cannot set attribute share

工作原理

我们的 RestrictedStock 类中的 __setattr__ 方法按以下步骤工作:

  1. 它首先检查属性名是否在允许的集合(namesharesprice)中。
  2. 如果属性名不在允许的集合中,它会引发一个 AttributeError。这可以防止赋值不需要的属性。
  3. 如果属性是允许的,它会使用 super().__setattr__() 来实际设置属性。这确保了允许的属性能够正常进行属性赋值过程。

这种方法比我们在之前的例子中看到的 __slots__ 更灵活。虽然 __slots__ 可以优化内存使用并限制属性,但在处理继承时它有局限性,并且可能与其他 Python 特性冲突。我们的 __setattr__ 方法在没有这些局限性的情况下,为我们提供了类似的属性赋值控制。

使用代理创建只读对象

在这一步中,我们将探索代理类(proxy classes),这是 Python 中非常有用的一种模式。代理类允许你在不修改现有对象原始代码的情况下,改变其行为。这就像是给对象套上一个特殊的包装,以添加新功能或施加限制。

什么是代理?

代理是介于你和另一个对象之间的对象。它具有与原始对象相同的一组函数和属性,但可以做额外的事情。例如,它可以控制谁能访问该对象、记录操作(日志记录)或添加其他有用的功能。

让我们创建一个只读代理。这种代理将阻止你更改对象的属性。

步骤 1:创建只读代理类

首先,我们需要创建一个 Python 文件来定义我们的只读代理。

  1. 导航到 /home/labex/project 目录。
  2. 在该目录下创建一个名为 readonly_proxy.py 的新文件。
  3. 打开 readonly_proxy.py 文件,并添加以下代码:
class ReadonlyProxy:
    def __init__(self, obj):
        ## Store the wrapped object directly in __dict__ to avoid triggering __setattr__
        self.__dict__['_obj'] = obj

    def __getattr__(self, name):
        ## Forward attribute access to the wrapped object
        return getattr(self._obj, name)

    def __setattr__(self, name, value):
        ## Block all attribute assignments
        raise AttributeError("Cannot modify a read-only object")

在这段代码中,定义了 ReadonlyProxy 类。__init__ 方法存储我们要包装的对象。我们使用 self.__dict__ 直接存储它,以避免调用 __setattr__ 方法。__getattr__ 方法在我们尝试访问代理的属性时使用。它只是将请求传递给被包装的对象。__setattr__ 方法在我们尝试更改属性时被调用。它会引发一个错误,以防止任何更改。

步骤 2:创建测试文件

现在,我们将创建一个测试文件,以查看我们的只读代理如何工作。

  1. 在同一 /home/labex/project 目录下创建一个名为 test_readonly.py 的新文件。
  2. 将以下代码添加到 test_readonly.py 文件中:
from stock import Stock
from readonly_proxy import ReadonlyProxy

## Create a normal Stock object
stock = Stock('AAPL', 100, 150.75)
print("Original stock object:")
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")
print(f"Cost: {stock.cost}")

## Modify the original stock object
stock.shares = 200
print(f"\nAfter modification - Shares: {stock.shares}")
print(f"After modification - Cost: {stock.cost}")

## Create a read-only proxy around the stock
readonly_stock = ReadonlyProxy(stock)
print("\nRead-only proxy object:")
print(f"Name: {readonly_stock.name}")
print(f"Shares: {readonly_stock.shares}")
print(f"Price: {readonly_stock.price}")
print(f"Cost: {readonly_stock.cost}")

## Try to modify the read-only proxy
try:
    print("\nAttempting to modify the read-only proxy...")
    readonly_stock.shares = 300
except AttributeError as e:
    print(f"Error: {e}")

## Show that the original object is unchanged
print(f"\nOriginal stock shares are still: {stock.shares}")

在这个测试代码中,我们首先创建一个普通的 Stock 对象并打印其信息。然后我们修改它的一个属性并打印更新后的信息。接下来,我们为 Stock 对象创建一个只读代理并打印其信息。最后,我们尝试修改只读代理,并期望得到一个错误。

步骤 3:运行测试脚本

创建代理类和测试文件后,我们需要运行测试脚本以查看结果。

  1. 打开一个终端,并使用以下命令导航到 /home/labex/project 目录:
cd /home/labex/project
  1. 使用以下命令运行测试脚本:
python3 test_readonly.py

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

Original stock object:
Name: AAPL
Shares: 100
Price: 150.75
Cost: 15075.0

After modification - Shares: 200
After modification - Cost: 30150.0

Read-only proxy object:
Name: AAPL
Shares: 200
Price: 150.75
Cost: 30150.0

Attempting to modify the read-only proxy...
Error: Cannot modify a read-only object

Original stock shares are still: 200

代理的工作原理

ReadonlyProxy 类使用两个特殊方法来实现其只读功能:

  1. __getattr__(self, name):当 Python 无法以正常方式找到属性时,会调用此方法。在我们的 ReadonlyProxy 类中,我们使用 getattr() 函数将属性访问请求传递给被包装的对象。因此,当你尝试访问代理的属性时,它实际上会从被包装的对象获取该属性。

  2. __setattr__(self, name, value):当你尝试为属性赋值时,会调用此方法。在我们的实现中,我们引发一个 AttributeError 来阻止对代理属性进行任何更改。

  3. __init__ 方法中,我们直接修改 self.__dict__ 来存储被包装的对象。这很重要,因为如果我们使用正常方式分配对象,它会调用 __setattr__ 方法,这会引发错误。

这种代理模式允许我们在不更改任何现有对象原始类的情况下,为其添加一个只读层。代理对象的行为就像被包装的对象,但不允许你进行任何修改。

委托:继承的替代方案

在面向对象编程中,代码的复用和扩展是常见的任务。实现这一目标主要有两种方式:继承和委托。

继承 是一种机制,通过它子类可以从父类继承方法和属性。子类可以选择重写部分继承的方法,以提供自己的实现。

委托 则是指一个对象包含另一个对象,并将特定的方法调用转发给它。

在这一步中,我们将探索委托作为继承的替代方案。我们将实现一个类,将其部分行为委托给另一个对象。

建立委托示例

首先,我们需要建立一个基类,供委托类与之交互。

  1. /home/labex/project 目录下创建一个名为 base_class.py 的新文件。该文件将定义一个名为 Spam 的类,它有三个方法:method_amethod_bmethod_c。每个方法都会打印一条消息并返回一个结果。以下是要放入 base_class.py 的代码:
class Spam:
    def method_a(self):
        print('Spam.method_a executed')
        return "Result from Spam.method_a"

    def method_b(self):
        print('Spam.method_b executed')
        return "Result from Spam.method_b"

    def method_c(self):
        print('Spam.method_c executed')
        return "Result from Spam.method_c"

接下来,我们将创建委托类。

  1. 创建一个名为 delegator.py 的新文件。在这个文件中,我们将定义一个名为 DelegatingSpam 的类,它将部分行为委托给 Spam 类的一个实例。
from base_class import Spam

class DelegatingSpam:
    def __init__(self):
        ## Create an instance of Spam that we'll delegate to
        self._spam = Spam()

    def method_a(self):
        ## Override method_a but also call the original
        print('DelegatingSpam.method_a executed')
        result = self._spam.method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('DelegatingSpam.method_c executed')
        return "Result from DelegatingSpam.method_c"

    def __getattr__(self, name):
        ## For any other attribute/method, delegate to self._spam
        print(f"Delegating {name} to the wrapped Spam object")
        return getattr(self._spam, name)

__init__ 方法中,我们创建了一个 Spam 类的实例。method_a 方法重写了原方法,但也调用了 Spam 类的 method_amethod_c 方法则完全重写了原方法。__getattr__ 是 Python 中的一个特殊方法,当访问 DelegatingSpam 类中不存在的属性或方法时会调用它,然后将调用委托给 Spam 实例。

现在,让我们创建一个测试文件来验证我们的实现。

  1. 创建一个名为 test_delegation.py 的测试文件。该文件将创建一个 DelegatingSpam 类的实例并调用其方法。
from delegator import DelegatingSpam

## Create a delegating object
spam = DelegatingSpam()

print("Calling method_a (overridden with delegation):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (not defined in DelegatingSpam, delegated):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

最后,我们将运行测试脚本。

  1. 在终端中使用以下命令运行测试脚本:
cd /home/labex/project
python3 test_delegation.py

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

Calling method_a (overridden with delegation):
DelegatingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (not defined in DelegatingSpam, delegated):
Delegating method_b to the wrapped Spam object
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
DelegatingSpam.method_c executed
Result: Result from DelegatingSpam.method_c

Calling non-existent method_d:
Delegating method_d to the wrapped Spam object
Error: 'Spam' object has no attribute 'method_d'

委托与继承的比较

现在,让我们将委托与传统的继承进行比较。

  1. 创建一个名为 inheritance_example.py 的文件。在这个文件中,我们将定义一个名为 InheritingSpam 的类,它继承自 Spam 类。
from base_class import Spam

class InheritingSpam(Spam):
    def method_a(self):
        ## Override method_a but also call the parent method
        print('InheritingSpam.method_a executed')
        result = super().method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('InheritingSpam.method_c executed')
        return "Result from InheritingSpam.method_c"

InheritingSpam 类重写了 method_amethod_c 方法。在 method_a 方法中,我们使用 super() 来调用父类的 method_a

接下来,我们将为继承示例创建一个测试文件。

  1. 创建一个名为 test_inheritance.py 的测试文件。该文件将创建一个 InheritingSpam 类的实例并调用其方法。
from inheritance_example import InheritingSpam

## Create an inheriting object
spam = InheritingSpam()

print("Calling method_a (overridden with super call):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (inherited from parent):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

最后,我们将运行继承测试。

  1. 在终端中使用以下命令运行继承测试:
cd /home/labex/project
python3 test_inheritance.py

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

Calling method_a (overridden with super call):
InheritingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (inherited from parent):
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
InheritingSpam.method_c executed
Result: Result from InheritingSpam.method_c

Calling non-existent method_d:
Error: 'InheritingSpam' object has no attribute 'method_d'

关键差异与注意事项

让我们来看看委托和继承之间的异同。

  1. 方法重写:委托和继承都允许你重写方法,但语法不同。

    • 在委托中,你定义自己的方法,并决定是否调用被包装对象的方法。
    • 在继承中,你定义自己的方法,并使用 super() 来调用父类的方法。
  2. 方法访问

    • 在委托中,未定义的方法通过 __getattr__ 方法转发。
    • 在继承中,未定义的方法会自动继承。
  3. 类型关系

    • 使用委托时,isinstance(delegating_spam, Spam) 返回 False,因为 DelegatingSpam 对象不是 Spam 类的实例。
    • 使用继承时,isinstance(inheriting_spam, Spam) 返回 True,因为 InheritingSpam 类继承自 Spam 类。
  4. 局限性:通过 __getattr__ 进行的委托不适用于像 __getitem____len__ 等特殊方法。这些方法需要在委托类中显式定义。

委托在以下情况下特别有用:

  • 你想自定义对象的行为,而不影响其层次结构。
  • 你想组合多个没有共同父类的对象的行为。
  • 你需要比继承更多的灵活性。

继承通常在以下情况下更受青睐:

  • “是一个”(is-a)关系很明确(例如,汽车是一种交通工具)。
  • 你需要在代码中保持类型兼容性。
  • 需要继承特殊方法。

总结

在这个实验中,你学习了用于自定义属性访问和行为的强大 Python 机制。你探索了如何使用 __setattr__ 来控制可以在对象上设置哪些属性,从而实现对对象属性的可控访问。此外,你实现了一个只读代理来包装现有对象,在保留其功能的同时防止对其进行修改。

你还深入研究了委托和继承在代码复用和定制方面的区别。通过使用 __getattr__,你学会了将方法调用转发给被包装的对象。这些技术提供了灵活的方式来控制对象行为,超越了标准继承的范畴,对于创建可控接口、实施访问限制、添加横切行为以及组合多个来源的行为非常有用。理解这些模式有助于你编写更易于维护和更灵活的 Python 代码。