可以看看本系列开篇:【Python】小子!是魔术方法!-CSDN博客
在对象属性不存在时,__getattr__
魔术方法的作用是什么?
__getattr__
魔术方法是在尝试访问对象不存在的属性时被调用的。如果未定义__getattr__
函数,程序会抛出AttributeError
。但若定义了__getattr__
函数,并在其中返回特定值(如None
),则当读取不存在属性时,程序将不会出现错误,而是返回预设的值。
class MyClassA:
def __init__(self, a=1, b=2):
self.a = a
self.b = b
def __getattr__(self, name):
print(f"试图访问不存在的属性: {name}")
# 可以返回一个默认值,或者抛出自定义错误
return f"属性 '{name}' 不存在,返回默认值"
# 示例
obj_a = MyClassA()
print(f"访问存在的属性 a: {obj_a.a}") # 访问存在的属性 a: 1
print(f"访问存在的属性 b: {obj_a.b}") # 访问存在的属性 b: 2
print(f"访问不存在的属性 c: {obj_a.c}") # 试图访问不存在的属性: c
# 访问不存在的属性 c: 属性 'c' 不存在,返回默认值
print(f"访问不存在的属性 xyz: {obj_a.xyz}") # 试图访问不存在的属性: xyz
# 访问不存在的属性 xyz: 属性 'xyz' 不存在,返回默认值
class MyClassB:
def __init__(self, x=10):
self.x = x
# 没有定义 __getattr__
obj_b = MyClassB()
print(f"访问存在的属性 x: {obj_b.x}") # 访问存在的属性 x: 10
try:
print(obj_b.y)
except AttributeError as e:
print(f"访问不存在的属性 y (无__getattr__): {e}") # 访问不存在的属性 y (无__getattr__): 'MyClassB' object has no attribute 'y'
__getattr__
和__getattribute__
这两个魔术方法有何区别?__delattr__
魔术方法的作用是什么?
__getattr__
函数只有在尝试读取不存在的属性时才会被调用。__getattribute__
函数无论何时尝试读取任何属性都会被调用,其行为更为广泛。
__delattr__
魔术方法是在尝试删除对象属性时被调用。它不会在对象正常生命周期结束时被调用,而是在使用del
关键字删除属性时执行。该方法没有返回值,但在其中可以进行一些操作,如打印提示信息。
class AttributeDemo:
def __init__(self):
self.existing_attr = "这是一个存在的属性"
self._internal_val = 42 # 用于 __getattribute__ 避免递归
def __getattr__(self, name):
print(f"--- __getattr__ 被调用: 尝试访问不存在的属性 '{name}' ---")
return f"属性 '{name}' 通过 __getattr__ 返回"
def __getattribute__(self, name):
print(f"--- __getattribute__ 被调用: 尝试访问属性 '{name}' ---")
# 为了避免无限递归,访问属性时必须使用 super()
# 或者直接访问 __dict__ (但不推荐,因为它绕过了描述符等机制)
try:
return super().__getattribute__(name)
except AttributeError:
# 如果 super().__getattribute__ 抛出 AttributeError,
# 并且定义了 __getattr__,那么 __getattr__ 会被调用。
# 这里我们为了演示清晰,直接在 __getattribute__ 中处理,
# 实际中 Python 会在 super() 失败后自动调用 __getattr__。
# 此处不需要显式调用 self.__getattr__(name)
raise # 重新抛出 AttributeError,让 __getattr__ (如果存在) 被Python解释器调用
def __setattr__(self, name, value):
print(f"--- __setattr__ 被调用: 设置属性 '{name}' = '{value}' ---")
super().__setattr__(name, value)
def __delattr__(self, name):
print(f"--- __delattr__ 被调用: 尝试删除属性 '{name}' ---")
if name == "non_deletable_attr":
print(f"属性 '{name}' 不可删除!")
# 可以抛出 AttributeError 或者什么都不做来阻止删除
# raise AttributeError(f"属性 '{name}' 是受保护的,不能删除")
else:
try:
super().__delattr__(name)
print(f"属性 '{name}' 已删除")
except AttributeError:
print(f"属性 '{name}' 不存在,无法删除")
demo = AttributeDemo()
print("\n1. 设置新属性:")
demo.new_attr = "新属性的值"
# --- __setattr__ 被调用: 设置属性 'new_attr' = '新属性的值' ---
print("\n2. 访问存在的属性:")
print(f"值: {demo.existing_attr}")
# --- __getattribute__ 被调用: 尝试访问属性 'existing_attr' ---
# 值: 这是一个存在的属性
print("\n3. 访问不存在的属性 (会先经过 __getattribute__,失败后调用 __getattr__):")
# 注意:Python 解释器在 __getattribute__ 内部的 super().__getattribute__(name) 失败后,
# 会自动查找并调用 __getattr__。
# 因此,__getattr__ 中的 print 会被触发。
try:
print(f"值: {demo.non_existent_attr}")
except AttributeError: # 如果__getattr__也抛出AttributeError或未定义
print("最终还是 AttributeError")
# --- __getattribute__ 被调用: 尝试访问属性 'non_existent_attr' ---
# --- __getattr__ 被调用: 尝试访问不存在的属性 'non_existent_attr' ---
# 值: 属性 'non_existent_attr' 通过 __getattr__ 返回
print("\n4. 删除存在的属性:")
demo.another_attr = "用于删除测试"
# --- __setattr__ 被调用: 设置属性 'another_attr' = '用于删除测试' ---
del demo.another_attr
# --- __delattr__ 被调用: 尝试删除属性 'another_attr' ---
# 属性 'another_attr' 已删除
print("\n5. 尝试删除不存在的属性:")
try:
del demo.ghost_attr
except AttributeError as e: # __delattr__ 可能会自己处理或 super() 抛出
print(e)
# --- __delattr__ 被调用: 尝试删除属性 'ghost_attr' ---
# 属性 'ghost_attr' 不存在,无法删除
print("\n6. 尝试删除受保护的属性:")
demo.non_deletable_attr = "不可删除"
# --- __setattr__ 被调用: 设置属性 'non_deletable_attr' = '不可删除' ---
del demo.non_deletable_attr # __delattr__ 会阻止
# --- __delattr__ 被调用: 尝试删除属性 'non_deletable_attr' ---
# 属性 'non_deletable_attr' 不可删除!
# 验证属性是否还存在
print(f"non_deletable_attr 仍然存在: {hasattr(demo, 'non_deletable_attr')}")
# --- __getattribute__ 被调用: 尝试访问属性 'non_deletable_attr' ---
# non_deletable_attr 仍然存在: True
如何正确使用__getattribute__
函数以避免无限递归?
在使用__getattribute__
函数时,如果要恢复其默认行为,应使用super().__getattribute__()
,以防止因递归调用__getattr__
函数而导致无限递归。
class SafeGetAttribute:
def __init__(self):
self.my_value = 100
self.another_value = 200
def __getattribute__(self, name):
print(f"SafeGetAttribute: __getattribute__ 正在尝试获取 '{name}'")
# 错误的实现: 直接访问 self.name 会再次调用 __getattribute__,导致无限递归
# if name == "my_value":
# return self.my_value # 错误! 会无限递归调用 __getattribute__('my_value')
# 正确的实现: 使用 super() 来调用父类(object类)的 __getattribute__
# 这样可以安全地获取属性值,而不会再次触发当前的 __getattribute__
if name == "_secret_value": # 假设我们想阻止直接访问某个内部属性
print(f"SafeGetAttribute: 尝试访问受保护属性 '{name}',将返回 None")
return None # 或者 raise AttributeError("...")
try:
value = super().__getattribute__(name)
print(f"SafeGetAttribute: 成功获取 '{name}', 值为: {value}")
return value
except AttributeError:
print(f"SafeGetAttribute: 属性 '{name}' 未找到")
raise # 重新抛出异常,或者可以调用 __getattr__ (如果定义了)
class UnsafeGetAttribute:
def __init__(self):
self.my_value = 10
def __getattribute__(self, name):
print(f"UnsafeGetAttribute: __getattribute__ 正在尝试获取 '{name}'")
# 这是一个会导致无限递归的例子
return self.my_value # 错误! 这里会再次调用 __getattribute__('my_value')
# 安全示例
safe_obj = SafeGetAttribute()
print("\n--- 安全的 __getattribute__ 示例 ---")
print(f"获取 my_value: {safe_obj.my_value}")
# SafeGetAttribute: __getattribute__ 正在尝试获取 'my_value'
# SafeGetAttribute: 成功获取 'my_value', 值为: 100
# 获取 my_value: 100
print(f"获取 another_value: {safe_obj.another_value}")
# SafeGetAttribute: __getattribute__ 正在尝试获取 'another_value'
# SafeGetAttribute: 成功获取 'another_value', 值为: 200
# 获取 another_value: 200
print(f"获取 _secret_value: {safe_obj._secret_value}")
# SafeGetAttribute: __getattribute__ 正在尝试获取 '_secret_value'
# SafeGetAttribute: 尝试访问受保护属性 '_secret_value',将返回 None
# 获取 _secret_value: None
try:
print(f"获取 non_existent: {safe_obj.non_existent}")
except AttributeError as e:
print(e)
# SafeGetAttribute: __getattribute__ 正在尝试获取 'non_existent'
# SafeGetAttribute: 属性 'non_existent' 未找到
# 'SafeGetAttribute' object has no attribute 'non_existent'
# 不安全示例 (会导致 RecursionError)
unsafe_obj = UnsafeGetAttribute()
print("\n--- 不安全的 __getattribute__ 示例 (会导致 RecursionError) ---")
try:
# 注意:这个调用会很快导致 RecursionError,所以可能只会打印几次 __getattribute__ 的消息
print(unsafe_obj.my_value)
except RecursionError as e:
print(f"捕获到错误: {e}")
# UnsafeGetAttribute: __getattribute__ 正在尝试获取 'my_value'
# UnsafeGetAttribute: __getattribute__ 正在尝试获取 'my_value'
# ... (多次重复)
# 捕获到错误: maximum recursion depth exceeded while calling a Python object
__setattr__
魔术方法是如何工作的?
__setattr__
魔术方法有两个参数,name
和value
,当设置对象属性时会被调用。通常会采用super().__setattr__()
来实现默认行为,但也支持自定义实现,例如将name-value
对存储在类级别的字典中,使得所有该类的对象共享这些自定义属性。
class Configurable:
# 类级别的字典,用于存储所有实例共享的属性
_shared_attributes = {}
def __init__(self, initial_val):
# 普通实例属性的设置也会经过 __setattr__
self.initial_value = initial_val # 这会调用下面的 __setattr__
def __setattr__(self, name, value):
print(f"Configurable: __setattr__ 被调用,尝试设置 '{name}' = {value}")
if name.startswith("shared_"):
# 如果属性名以 'shared_' 开头,则存储在类级别字典中
print(f" -> 将 '{name}' 存储为共享属性")
Configurable._shared_attributes[name] = value
else:
# 否则,正常设置实例属性,使用 super() 避免递归
print(f" -> 将 '{name}' 存储为实例属性")
super().__setattr__(name, value)
def __getattr__(self, name): # 用于演示共享属性的获取
print(f"Configurable: __getattr__ 被调用,尝试获取 '{name}'")
if name.startswith("shared_") and name in Configurable._shared_attributes:
print(f" <- 从共享属性获取 '{name}'")
return Configurable._shared_attributes[name]
raise AttributeError(f"属性 '{name}' 未找到")
def __getattribute__(self, name): # 用于更细致地观察属性访问
print(f"Configurable: __getattribute__ 尝试获取 '{name}'")
# 对于非共享属性的常规访问,通过super
if not name.startswith("shared_"):
return super().__getattribute__(name)
# 对于共享属性,我们希望通过 __getattr__ (如果常规查找失败)
# 或者直接在这里检查 _shared_attributes
try:
return super().__getattribute__(name) # 尝试正常获取
except AttributeError:
if name in Configurable._shared_attributes: # 如果不存在于实例,则检查共享
print(f" <- (来自getattribute) 从共享属性获取 '{name}'")
return Configurable._shared_attributes[name]
raise # 如果共享属性里也没有,则让AttributeError继续传播,可能被__getattr__捕获
# 示例
print("\n--- __setattr__ 示例 ---")
conf1 = Configurable(10)
# Configurable: __setattr__ 被调用,尝试设置 'initial_value' = 10
# -> 将 'initial_value' 存储为实例属性
conf2 = Configurable(20)
# Configurable: __setattr__ 被调用,尝试设置 'initial_value' = 20
# -> 将 'initial_value' 存储为实例属性
print(f"\nconf1.initial_value: {conf1.initial_value}")
# Configurable: __getattribute__ 尝试获取 'initial_value'
# conf1.initial_value: 10
print(f"conf2.initial_value: {conf2.initial_value}")
# Configurable: __getattribute__ 尝试获取 'initial_value'
# conf2.initial_value: 20
print("\n设置普通实例属性:")
conf1.instance_specific = "val1"
# Configurable: __setattr__ 被调用,尝试设置 'instance_specific' = val1
# -> 将 'instance_specific' 存储为实例属性
print(f"conf1.instance_specific: {conf1.instance_specific}")
# Configurable: __getattribute__ 尝试获取 'instance_specific'
# conf1.instance_specific: val1
try:
print(f"conf2.instance_specific: {conf2.instance_specific}")
except AttributeError as e:
# Configurable: __getattribute__ 尝试获取 'instance_specific'
# Configurable: __getattr__ 被调用,尝试获取 'instance_specific'
print(e) # '属性 'instance_specific' 未找到'
print("\n设置共享属性:")
conf1.shared_setting = "Global Setting Alpha"
# Configurable: __setattr__ 被调用,尝试设置 'shared_setting' = Global Setting Alpha
# -> 将 'shared_setting' 存储为共享属性
print(f"conf1.shared_setting: {conf1.shared_setting}")
# Configurable: __getattribute__ 尝试获取 'shared_setting'
# Configurable: __getattr__ 被调用,尝试获取 'shared_setting'
# <- 从共享属性获取 'shared_setting'
# conf1.shared_setting: Global Setting Alpha
print(f"conf2.shared_setting: {conf2.shared_setting}") # conf2 也能访问到
# Configurable: __getattribute__ 尝试获取 'shared_setting'
# Configurable: __getattr__ 被调用,尝试获取 'shared_setting'
# <- 从共享属性获取 'shared_setting'
# conf2.shared_setting: Global Setting Alpha
print(f"类级别字典: {Configurable._shared_attributes}")
# 类级别字典: {'shared_setting': 'Global Setting Alpha'}
conf2.shared_setting = "Global Setting Beta" # 修改共享属性
# Configurable: __setattr__ 被调用,尝试设置 'shared_setting' = Global Setting Beta
# -> 将 'shared_setting' 存储为共享属性
print(f"修改后 conf1.shared_setting: {conf1.shared_setting}") # conf1 也看到变化
# Configurable: __getattribute__ 尝试获取 'shared_setting'
# Configurable: __getattr__ 被调用,尝试获取 'shared_setting'
# <- 从共享属性获取 'shared_setting'
# 修改后 conf1.shared_setting: Global Setting Beta
print(f"类级别字典更新后: {Configurable._shared_attributes}")
# 类级别字典更新后: {'shared_setting': 'Global Setting Beta'}
关于作者
- CSDN 大三小白新手菜鸟咸鱼长期更新强烈建议不要关注!