简介
在这个实验中,你将学习 Python 面向对象编程的一个基本方面:属性访问。Python 允许开发者通过特殊方法自定义类中属性的访问、设置和管理方式。这为控制对象行为提供了强大的手段。
此外,你将学习如何在 Python 类中自定义属性访问,理解委托(delegation)和继承(inheritance)之间的区别,并练习在 Python 对象中实现自定义属性管理。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将学习 Python 面向对象编程的一个基本方面:属性访问。Python 允许开发者通过特殊方法自定义类中属性的访问、设置和管理方式。这为控制对象行为提供了强大的手段。
此外,你将学习如何在 Python 类中自定义属性访问,理解委托(delegation)和继承(inheritance)之间的区别,并练习在 Python 对象中实现自定义属性管理。
__setattr__
在 Python 中,有一些特殊方法可以让你自定义对象属性的访问和修改方式。其中一个重要的方法是 __setattr__()
。每当你尝试为对象的属性赋值时,这个方法就会发挥作用。它让你能够对属性赋值过程进行精细控制。
__setattr__
?__setattr__(self, name, value)
方法充当所有属性赋值的拦截器。当你编写像 obj.attr = value
这样的简单赋值语句时,Python 并不会直接赋值。相反,它会在内部调用 obj.__setattr__("attr", value)
。这种机制让你能够决定在属性赋值期间应该发生什么。
现在让我们来看一个实际的例子,展示如何使用 __setattr__
来限制可以在类上设置哪些属性。
首先,在 WebIDE 中打开一个新文件。你可以通过点击“File”菜单,然后选择“New File”来完成。将这个文件命名为 restricted_stock.py
,并将其保存到 /home/labex/project
目录中。这个文件将包含我们使用 __setattr__
来控制属性赋值的类定义。
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__
方法中,我们用 name
、shares
和 price
属性初始化对象。__setattr__
方法会检查正在赋值的属性名是否在允许的属性集合(name
、shares
、price
)中。如果不在,它会引发一个 AttributeError
。如果属性是允许的,它会使用父类的 __setattr__
方法来实际设置属性。
创建一个名为 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__
方法是否按预期工作。
在 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__
方法按以下步骤工作:
name
、shares
、price
)中。AttributeError
。这可以防止赋值不需要的属性。super().__setattr__()
来实际设置属性。这确保了允许的属性能够正常进行属性赋值过程。这种方法比我们在之前的例子中看到的 __slots__
更灵活。虽然 __slots__
可以优化内存使用并限制属性,但在处理继承时它有局限性,并且可能与其他 Python 特性冲突。我们的 __setattr__
方法在没有这些局限性的情况下,为我们提供了类似的属性赋值控制。
在这一步中,我们将探索代理类(proxy classes),这是 Python 中非常有用的一种模式。代理类允许你在不修改现有对象原始代码的情况下,改变其行为。这就像是给对象套上一个特殊的包装,以添加新功能或施加限制。
代理是介于你和另一个对象之间的对象。它具有与原始对象相同的一组函数和属性,但可以做额外的事情。例如,它可以控制谁能访问该对象、记录操作(日志记录)或添加其他有用的功能。
让我们创建一个只读代理。这种代理将阻止你更改对象的属性。
首先,我们需要创建一个 Python 文件来定义我们的只读代理。
/home/labex/project
目录。readonly_proxy.py
的新文件。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__
方法在我们尝试更改属性时被调用。它会引发一个错误,以防止任何更改。
现在,我们将创建一个测试文件,以查看我们的只读代理如何工作。
/home/labex/project
目录下创建一个名为 test_readonly.py
的新文件。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
对象创建一个只读代理并打印其信息。最后,我们尝试修改只读代理,并期望得到一个错误。
创建代理类和测试文件后,我们需要运行测试脚本以查看结果。
/home/labex/project
目录:cd /home/labex/project
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
类使用两个特殊方法来实现其只读功能:
__getattr__(self, name)
:当 Python 无法以正常方式找到属性时,会调用此方法。在我们的 ReadonlyProxy
类中,我们使用 getattr()
函数将属性访问请求传递给被包装的对象。因此,当你尝试访问代理的属性时,它实际上会从被包装的对象获取该属性。
__setattr__(self, name, value)
:当你尝试为属性赋值时,会调用此方法。在我们的实现中,我们引发一个 AttributeError
来阻止对代理属性进行任何更改。
在 __init__
方法中,我们直接修改 self.__dict__
来存储被包装的对象。这很重要,因为如果我们使用正常方式分配对象,它会调用 __setattr__
方法,这会引发错误。
这种代理模式允许我们在不更改任何现有对象原始类的情况下,为其添加一个只读层。代理对象的行为就像被包装的对象,但不允许你进行任何修改。
在面向对象编程中,代码的复用和扩展是常见的任务。实现这一目标主要有两种方式:继承和委托。
继承 是一种机制,通过它子类可以从父类继承方法和属性。子类可以选择重写部分继承的方法,以提供自己的实现。
委托 则是指一个对象包含另一个对象,并将特定的方法调用转发给它。
在这一步中,我们将探索委托作为继承的替代方案。我们将实现一个类,将其部分行为委托给另一个对象。
首先,我们需要建立一个基类,供委托类与之交互。
/home/labex/project
目录下创建一个名为 base_class.py
的新文件。该文件将定义一个名为 Spam
的类,它有三个方法:method_a
、method_b
和 method_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"
接下来,我们将创建委托类。
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_a
。method_c
方法则完全重写了原方法。__getattr__
是 Python 中的一个特殊方法,当访问 DelegatingSpam
类中不存在的属性或方法时会调用它,然后将调用委托给 Spam
实例。
现在,让我们创建一个测试文件来验证我们的实现。
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}")
最后,我们将运行测试脚本。
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'
现在,让我们将委托与传统的继承进行比较。
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_a
和 method_c
方法。在 method_a
方法中,我们使用 super()
来调用父类的 method_a
。
接下来,我们将为继承示例创建一个测试文件。
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}")
最后,我们将运行继承测试。
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'
让我们来看看委托和继承之间的异同。
方法重写:委托和继承都允许你重写方法,但语法不同。
super()
来调用父类的方法。方法访问:
__getattr__
方法转发。类型关系:
isinstance(delegating_spam, Spam)
返回 False
,因为 DelegatingSpam
对象不是 Spam
类的实例。isinstance(inheriting_spam, Spam)
返回 True
,因为 InheritingSpam
类继承自 Spam
类。局限性:通过 __getattr__
进行的委托不适用于像 __getitem__
、__len__
等特殊方法。这些方法需要在委托类中显式定义。
委托在以下情况下特别有用:
继承通常在以下情况下更受青睐:
在这个实验中,你学习了用于自定义属性访问和行为的强大 Python 机制。你探索了如何使用 __setattr__
来控制可以在对象上设置哪些属性,从而实现对对象属性的可控访问。此外,你实现了一个只读代理来包装现有对象,在保留其功能的同时防止对其进行修改。
你还深入研究了委托和继承在代码复用和定制方面的区别。通过使用 __getattr__
,你学会了将方法调用转发给被包装的对象。这些技术提供了灵活的方式来控制对象行为,超越了标准继承的范畴,对于创建可控接口、实施访问限制、添加横切行为以及组合多个来源的行为非常有用。理解这些模式有助于你编写更易于维护和更灵活的 Python 代码。