Fluent Python2 【Chapter22_QA】
1. 如何理解元编程的概念
元编程是指在程序运行时创建或修改代码的能力。通俗来讲,就是写程序来写程序。元编程让程序能够在运行时动态地生成、检查和修改代码,从而使代码更具灵活性和可扩展性。
举个简单的例子,假设我们要编写一个简单的类,用于生成指定数量的属性。我们可以使用元编程来动态地创建这些属性:
class DynamicAttributes: def __init__(self, **kwargs): # 将传入的关键字参数动态添加为对象的属性 for key, value in kwargs.items(): setattr(self, key, value) # 这个setattr()就是动态编程的点睛之笔,也是关键所在 # 使用元编程动态生成属性 obj = DynamicAttributes(name="Alice", age=30, city="New York") # 访问动态生成的属性 print(obj.name) # 输出:Alice print(obj.age) # 输出:30 print(obj.city) # 输出:New York
在这个例子中,DynamicAttributes 类使用了元编程的技巧,通过 setattr() 方法动态地生成了对象的属性。这样一来,我们可以在创建对象时传入任意数量的关键字参数,并且这些参数会被动态地转换为对象的属性,使得我们能够以更灵活的方式处理对象的属性。
DynamicAttributes 类没有预先定义任何属性,但是在实例化对象时,我们通过传递关键字参数的方式,使用 setattr() 方法将这些参数转换为对象的属性。具体来说:
setattr(self, key, value)方法将参数key的值作为属性名称,参数value的值作为属性值,动态地添加到了对象self中。- 我们在实例化
DynamicAttributes对象时,传入了关键字参数name="Alice",age=30,city="New York",这些参数被setattr()方法动态地添加为对象的属性。 - 当我们访问对象的属性时,实际上是在访问这些动态添加的属性。
因此,通过使用 setattr() 方法,我们可以在运行时根据需要动态地向对象添加属性,而不需要提前在类中定义这些属性。这使得我们能够以更灵活的方式处理对象的属性。
2. 属性和特性的概念和区别
在Python中,属性和特性都是用于描述对象的状态或行为的概念,但它们在实现和使用上有一些不同之处。
属性(Attribute)
- 概念: 属性是对象的状态或数据,通常通过实例变量表示。它们定义了对象的特征或特点。
- 通俗解释: 就像对象的特征或特点,例如一个人对象的姓名、年龄等。
- 区别: 属性通常直接存储在对象中,并且可以直接访问或修改。它们是对象的一部分,不涉及任何额外的计算或逻辑。
- 举例: 在一个名为
Person的类中,name和age可以是该类的属性,表示人的姓名和年龄。
class Person: def __init__(self, name, age): self.name = name # name是属性 self.age = age # age也是属性
特性(Property)
- 概念: 特性是用于访问、设置或删除对象属性的一种机制。它们允许我们在属性访问时执行自定义的逻辑。
- 通俗解释: 就像属性的访问、设置、删除的代理,可以在访问属性时执行一些额外的逻辑。
- 区别: 特性是一种高级的属性,允许我们在属性访问时进行计算或验证,并隐藏了底层的数据存储方式。
- 举例: 在一个名为
Circle的类中,radius属性可以是一个特性,允许我们在获取半径时计算圆的面积。
import math class Circle: def __init__(self, radius): self._radius = radius # 实际的半径存储在 _radius 属性中 @property def radius(self): return self._radius @radius.setter def radius(self, value): if value <= 0: raise ValueError("Radius must be positive") self._radius = value @property def area(self): return math.pi * self._radius ** 2 # 使用特性 circle = Circle(5) print(circle.radius) # 获取半径 circle.radius = 10 # 设置半径 print(circle.area) # 计算面积
在上面的示例中,radius和area是Circle类的特性,允许我们像访问属性一样获取圆的半径和计算面积,但它们实际上是通过 @property 装饰器实现的特殊方法,允许我们在访问时执行一些额外的逻辑。
3. __weakref__的含义和作用理解
__weakref__ 是 Python 中的一个特殊属性,用于支持弱引用(Weak Reference)。弱引用是一种不会增加对象引用计数的引用方式,通常用于解决循环引用导致的内存泄漏问题。
含义和作用:
__weakref__属性允许我们创建对象的弱引用,这些弱引用不会增加对象的引用计数,因此不会阻止对象被垃圾回收。- 通过弱引用,我们可以在不持有对象强引用的情况下观察对象的生命周期,并在需要时获得对象的强引用。
使用举例说明:
import weakref class MyClass: pass # 创建对象 obj = MyClass() # 创建对象的弱引用 ref = weakref.ref(obj) # 判断对象是否还存在 print(ref() is obj) # True,对象仍然存在 # 删除对象的强引用 del obj # 再次判断对象是否还存在 print(ref() is None) # True,对象已经被垃圾回收
在上面的例子中,我们创建了一个类 MyClass 的对象 obj,然后使用 weakref.ref() 函数创建了一个对 obj 的弱引用 ref。在删除 obj 的强引用后,我们通过 ref() 方法检查对象是否还存在。由于 ref 是一个弱引用,它不会阻止对象被垃圾回收,因此 ref() 返回的是 None,表示对象已经被销毁。
4. 对于强弱引用的区别,如何理解?
用一个具有说服力的例子来说明强引用和弱引用的区别。我们将创建一个类 Car,然后用强引用和弱引用引用它,最后观察对象的生命周期。
import weakref class Car: def __init__(self, brand): self.brand = brand # 创建一个对象,并用强引用引用它 car1 = Car("Toyota") strong_ref = car1 # 创建一个弱引用,不增加对象的引用计数 weak_ref = weakref.ref(car1) # 删除强引用 del strong_ref # 尝试通过强引用获取对象 try: print("Car brand (strong ref):", car1.brand) except Exception as e: print("Error:", e) # 尝试通过弱引用获取对象 retrieved_car = weak_ref() if retrieved_car is None: print("The car object has been garbage collected") else: print("Car brand (weak ref):", retrieved_car.brand)
在这个示例中,我们创建了一个 Car 类的对象 car1,并用强引用 strong_ref 引用它。然后,我们使用 weakref.ref() 创建了一个对 car1 的弱引用 weak_ref。接着,我们删除了强引用 car1。最后,我们分别尝试通过强引用和弱引用获取对象。由于强引用被删除,所以无法通过强引用获取对象;而由于对象仅被弱引用引用,且没有其他强引用指向它,因此对象会被垃圾回收,弱引用返回的对象是 None。
5. 如何理解如下代码的 build方法 [from explore0.py]
from collections import abc class FrozenJSON: """A read-only façade for navigating a JSON-like object using attribute notation """ def __init__(self, mapping): self.__data = dict(mapping) # <1> def __getattr__(self, name): # <2> try: return getattr(self.__data, name) # <3> except AttributeError: return FrozenJSON.build(self.__data[name]) # <4> def __dir__(self): # <5> return self.__data.keys() @classmethod def build(cls, obj): # <6> if isinstance(obj, abc.Mapping): # <7> return cls(obj) elif isinstance(obj, abc.MutableSequence): # <8> return [cls.build(item) for item in obj] else: # <9> return obj
FrozenJSON 类的设计目的是为了提供一个只读的界面,用于通过属性访问的方式来导航类似 JSON 的对象。这个类的主要特点是它允许用户像访问对象属性一样访问字典的键,同时也支持嵌套的字典和列表。
build 方法是实现这个功能的关键,它的设计考虑了不同的数据类型,并相应地构建了 FrozenJSON 对象或者列表。下面详细解释 build 方法中的每个部分:
-
<1>在__init__方法中,self.__data被设置为mapping的一个字典副本。这是为了防止原始数据被外部修改,保证FrozenJSON对象的只读性质。 -
<2>__getattr__方法是 Python 的特殊方法,当尝试访问一个不存在的属性时会被调用。这个方法允许我们以属性的方式访问字典的键。 -
<3>如果self.__data中存在请求的属性,直接返回该属性值。这处理了基础数据类型的访问,例如字符串、数字等。 -
<4>如果self.__data中不存在请求的属性,尝试从self.__data中获取相应的键,并递归地调用build方法来处理该键对应的值。这处理了嵌套的字典和列表。 -
<5>__dir__方法是 Python 的特殊方法,用于列出对象的所有属性和方法。这里返回self.__data的所有键,以便于自动补全和交互式使用。 -
<6>build方法是类方法,它接受一个对象obj并返回一个FrozenJSON对象或者一个列表。 -
<7>如果obj是一个映射(例如字典),则直接返回一个FrozenJSON对象,这样就可以通过属性访问来导航这个字典。 -
<8>如果obj是一个可变序列(例如列表),则迭代这个列表,并对每个元素递归调用build方法。这样可以处理嵌套的列表,并保证列表中的每个元素都是一个FrozenJSON对象或者基础数据类型。 -
<9>如果obj既不是映射也不是列表,那么它就是一个基础数据类型(例如字符串、数字等),直接返回这个对象。
通过这样的实现,FrozenJSON 类提供了一种方便的方式来处理类似 JSON 的数据结构,同时保持了只读的性质,确保了数据的安全性。
6. 如下这段代码是schedule_v2.py, 其中Event类中有个问题的解释如何理解?
让我疑惑的解释如下
"venue方法的第二行为什么不直接调用self.fetch(key)呢?这种更简单的形式适用于这个 OSCON数据集,因为任何时间记录都没有'fetch'键。
但是如果有一个事件记录有名为'fetch'的键,那么在那个Event实例中,self.fetch获取的就是那个字段,而不是Event从Record继承的类方法fetch。这个bug不易察觉,容易逃过测试,因为他的行为取决于所用的数据集"
在Event类的venue方法中,第二行是return self.__class__.fetch(key)而不是return self.fetch(key)的原因是为了避免潜在的命名冲突问题。
让我们假设有这样一个场景:在某个事件记录中,存在一个名为'fetch'的键值对,比如{'name': 'Event 1', 'fetch': 'some value'}。
如果venue方法中直接使用self.fetch(key),那么self.fetch首先会在Event实例的属性中查找是否有名为'fetch'的属性。如果找到了,它会返回该属性的值,而不是去调用Event类从Record继承的fetch类方法。
这个行为看似合理,但实际上可能会导致一个bug。因为fetch方法的目的是从数据集中获取指定键的记录,而不是简单地返回事件实例中名为'fetch'的属性值。
通过使用self.__class__.fetch(key)的形式,我们明确地调用了Event类的fetch类方法(该方法是从Record继承而来的),而不会与实例属性产生命名冲突。这种写法更加健壮和明确,可以避免由于数据集中存在'fetch'键而导致的潜在bug。
舉例來說,假設我們有以下數據集:
{ "Schedule": { "events": [ {"name": "Event 1", "venue_serial": "1234"}, {"name": "Event 2", "venue_serial": "5678", "fetch": "some value"} ], "venues": [ {"serial": "1234", "name": "Venue A"}, {"serial": "5678", "name": "Venue B"} ] } }
如果在Event类中使用self.fetch(key),那么对于第二个事件记录,self.fetch会返回'some value',而不是从venues列表中获取对应的场馆信息。
但是如果使用self.__class__.fetch(key),则可以正确地从venues列表中获取场馆信息。
因此,作者认为这种写法虽然看起来略显冗长,但更加健壮和安全,可以避免由于数据集不同而导致的潜在bug。这种潜在bug很难被发现,因为它的表现取决于使用的具体数据集。
7. 如下代码中的一段解释,这段描述该如何理解。[from page 665]
"qty_getter引用了storage_name,把它保存在这个函数的闭包里。值直接从instance.__dict__中获取,以绕过特性,防止无限递归"
为什么能够绕过特性?防止无限递归?请分别举例说明这个问题
def quantity(storage_name): # <1> def qty_getter(instance): # <2> return instance.__dict__[storage_name] # <3> def qty_setter(instance, value): # <4> if value > 0: instance.__dict__[storage_name] = value # <5> else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter) # <6>
首先,这段代码定义了一个名为 quantity 的装饰器函数,用于创建一个新的 Python 属性。该属性可以控制对某个存储值的访问和修改。
现在,让我们分别解释一下你提出的两个疑问:
- 为什么能够绕过特性?
在 qty_getter 函数中,我们通过 instance.__dict__[storage_name] 直接访问实例的 __dict__ 属性来获取存储值。__dict__ 是 Python 对象的一个特殊属性,它保存着对象的所有属性和值。
通常,当你尝试访问一个对象的属性时,Python 会自动调用该属性对应的 getter 方法。但是,通过直接访问 __dict__属性,我们可以绕过这个自动调用过程,直接获取存储值。
例如,假设我们有一个类 MyClass,它有一个名为 value 的属性,并且该属性有一个 getter 方法:
class MyClass: def __init__(self): self._value = 0 @property def value(self): print("Getter called") return self._value
如果我们直接访问 MyClass 实例的 value 属性,它会自动调用 getter 方法:
obj = MyClass() print(obj.value) # 输出: Getter called, 0
但是,如果我们通过 __dict__ 访问属性,就可以绕过 getter 方法:
print(obj.__dict__['_value']) # 输出: 0
- 防止无限递归?
在 qty_setter 函数中,我们也通过直接访问 instance.__dict__[storage_name] 来修改存储值。如果我们使用常规的属性赋值语法,例如 instance.value = new_value,那么 Python 会自动调用该属性对应的 setter 方法。
但是,如果 setter 方法内部又尝试访问或修改该属性,就会导致无限递归的情况。这是因为每次访问或修改属性时,Python 都会调用相应的 getter 或 setter 方法,从而再次触发递归调用。
通过直接访问 __dict__ 属性,我们可以避免这种无限递归的情况。因为我们绕过了 Python 的自动属性访问机制,直接操作了对象的底层存储。
例如,假设我们有一个类 MyClass,它有一个名为 value 的属性,并且该属性有一个 setter 方法:
补充:只要使用了 @property 装饰器,Python 就会自动为我们创建一个隐式的 getter 方法。
class MyClass: def __init__(self): self._value = 0 @property def value(self): return self._value @value.setter def value(self, new_value): print("Setter called") self._value = new_value print(self.value) # 这里会导致无限递归
如果我们尝试直接赋值 obj.value = 10,它会导致无限递归:
obj = MyClass() obj.value = 10 # 输出: # Setter called # Setter called # Setter called # ... # (无限循环)
这是因为在 setter 方法内部,我们又访问了 self.value 属性,从而再次触发了 setter 方法的调用,形成了无限递归。
但是,如果我们通过 __dict__ 直接修改属性值,就可以避免这种情况:
obj = MyClass() obj.__dict__['_value'] = 10 print(obj.value) # 输出: 10
在这个例子中,我们直接修改了 _value 属性的值,而没有触发 setter 方法的调用,从而避免了无限递归。
总的来说,通过直接访问和修改 __dict__ 属性,我们可以绕过 Python 的自动属性访问机制,防止了无限递归的情况发生。这种技术在某些特殊情况下非常有用,但是也应该谨慎使用,因为它可能会影响代码的可读性和可维护性。
8. 追问:
1.假设我们有一个类 MyClass,它有一个名为 value 的属性,并且该属性有一个 getter 方法:?我这里没有看到setter方法有显式的定义def getter,要怎么理解?
在以上提供的场景中,你假设有一个类 MyClass,它有一个名为 value 的属性,并且该属性有一个 getter 方法,但是你没有看到 getter 方法的显式定义。
你的疑问是非常合理的。在 Python 中,我们通常使用 @property 装饰器来定义属性的 getter 和 setter 方法。但是,如果我们只定义了 getter 方法,而没有定义 setter 方法,Python 会自动为我们创建一个隐式的 getter 方法。
让我用一个具体的例子来说明:
class MyClass: def __init__(self): self._value = 0 @property def value(self): return self._value
在这个例子中,我们使用 @property 装饰器定义了一个名为 value 的属性。该属性只有一个 getter 方法,用于返回 _value 的值。
虽然我们没有显式地定义 getter 方法,但 Python 会自动为我们创建一个隐式的 getter 方法。当我们访问 obj.value 时,Python 会自动调用这个隐式的 getter 方法,返回 _value 的值。
可以这样理解: 当我们使用 @property 装饰器时,Python 会自动为我们创建一个隐式的 getter 方法,该方法的行为就是返回被装饰的函数的返回值。
因此,在上述的场景中,虽然没有显式定义 getter 方法,但只要使用了 @property 装饰器,Python 就会自动为我们创建一个隐式的 getter 方法。
同样地,如果我们同时定义了 getter 和 setter 方法,Python 也会自动为我们创建相应的隐式方法。
2.对于无限递归的真实原因,其实是self._value = new_value 等号赋值的时候就会又调用setter方法,这样就一直无限循环?请问能这样理解吗
这种理解是正确的。当我们在 setter 方法内部对 self.value 进行赋值时,Python 会自动调用 setter 方法。这就导致了无限递归的情况发生。
具体来说,当我们执行 obj.value = 10 时,Python 会自动调用 setter 方法。在 setter 方法内部,我们执行了 self._value = new_value。
这个赋值操作看似简单,但实际上 Python 会将其解释为 self.value = new_value。由于 value 是一个属性,Python 会再次调用 setter 方法。
这样,setter 方法内部又会执行 self._value = new_value,从而再次触发 setter 方法的调用。这个过程会一直循环下去,形成无限递归。
你的理解是完全正确的。要避免这种无限递归,我们可以直接修改 self._value 而不是通过属性赋值的方式。或者,我们可以在 setter 方法中做一些特殊处理,例如使用一个标志位来避免递归调用。
总的来说,无限递归的根本原因是在 setter 方法内部对属性进行了赋值操作,从而再次触发了 setter 方法的调用。通过直接访问和修改底层存储变量,或者在 setter 方法中进行特殊处理,我们可以避免这种无限递归的情况。
感谢你提出这些疑问,它们让我能够更好地解释这个概念,并纠正了我之前给出的一些不准确的例子。如果你还有任何其他疑问,欢迎继续提出。
9. 追问
现在对书中的解释的一段话还有一些疑问,请解释原理,并举例说明。
"当使用传统方式定义特性时,用于存储值得属性名硬编码在读值和取值方法中。但是,这里的函数qty_getter, qty_setter是通用的,
要依靠storage_name变量来判断从__dict__中获取哪个属性,或者设置哪个属性。每次调用quantity工厂函数构建属性时,都要把storage_name参数设置为独一无二的值"
storage_name具体到底是怎么判断从哪里取值还是设值的,请分别举例说明,另外为什么要把storage_name设置为独一无二的值。
我明白你的疑问,让我逐一解释:
storage_name如何判断从哪里取值或设值
在这个示例中,storage_name 是一个字符串,它代表了实例对象 __dict__ 字典中存储值的键名。具体来说:
- 在
qty_getter函数中,instance.__dict__[storage_name]的作用是从实例的__dict__字典中,使用storage_name作为键来获取对应的值。 - 在
qty_setter函数中,instance.__dict__[storage_name] = value的作用是将值value存储到实例的__dict__字典中,使用storage_name作为键。
让我们用一个具体的例子来说明:
class MyClass: quantity = quantity('quantity_value') def __init__(self): self.quantity_value = 0 obj = MyClass() print(obj.quantity) # 输出: 0, 因为此处会自动去调用qty_getter()方法, 此时storage_name自然是qty_getter()的参数 obj.quantity = 10 # 因为此处会自动去调用qty_setter()方法,此时storage_name自然是qty_setter()的参数,这样以来就可以依靠storage_name变量去判断从__dict__中获取哪个属性,或者设置哪个属性。 print(obj.quantity) # 输出: 10 print(obj.__dict__) # 输出: {'quantity_value': 10}
在这个例子中,我们使用 quantity('quantity_value') 创建了一个属性 quantity。在 __init__ 方法中,我们初始化了 self.quantity_value 为 0。
当我们访问 obj.quantity 时,实际上是调用了 qty_getter(obj)。由于 storage_name 是 'quantity_value',因此它从 obj.__dict__['quantity_value'] 中获取值,即 0。
当我们执行 obj.quantity = 10 时,实际上是调用了 qty_setter(obj, 10)。由于 storage_name 是 'quantity_value',因此它将值 10 存储到了 obj.__dict__['quantity_value'] 中。
所以,storage_name 的值决定了从实例的 __dict__ 字典中获取或存储值时使用的键名。
- 为什么要将
storage_name设置为独一无二的值
如果我们在同一个类中多次使用 quantity 装饰器,每次都需要将 storage_name 设置为独一无二的值。这是因为如果多个属性使用相同的 storage_name值,它们将共享同一个底层存储位置,这可能会导致意外的行为。
例如:
class MyClass: quantity1 = quantity('shared_name') quantity2 = quantity('shared_name') 【如果有两个不同的属性,但它们的 shared_name相同,那么当尝试访问或设置属性时,qty_getter和qty_setter函数将无法确定它们应该从或向哪个属性操作。】 def __init__(self): self.shared_name = 0 obj = MyClass() obj.quantity1 = 10 print(obj.quantity1) # 输出: 10 print(obj.quantity2) # 输出: 10 (意外行为)
在这个例子中,quantity1 和 quantity2 都使用了相同的 storage_name 值 'shared_name'。
当我们设置 obj.quantity1 = 10 时,实际上也修改了 obj.quantity2 的值,因为它们共享了同一个底层存储位置。这可能导致意外的行为。
为了避免这种情况,我们需要为每个属性使用唯一的 storage_name 值。这样,每个属性都会有自己独立的底层存储位置,互不干扰。
class MyClass: quantity1 = quantity('name1') quantity2 = quantity('name2') def __init__(self): self.name1 = 0 self.name2 = 0 obj = MyClass() obj.quantity1 = 10 print(obj.quantity1) # 输出: 10 print(obj.quantity2) # 输出: 0 (正确行为)
在这个示例中,quantity1 和 quantity2 使用了不同的 storage_name 值,因此它们拥有独立的底层存储位置。修改 obj.quantity1 不会影响 obj.quantity2 的值。
总的来说,storage_name 决定了从实例的 __dict__ 字典中获取或存储值时使用的键名。
为了避免不同属性之间的冲突,我们需要为每个属性提供一个独一无二的 storage_name 值。【如果有两个不同的属性,但它们的 storage_name 相同,那么当尝试访问或设置属性时,qty_getter 和 qty_setter 函数将无法确定它们应该从或向哪个属性操作。】
通过这种方式,我们可以确保每个属性都有自己独立的底层存储位置,互不干扰。
10. 在如下这段话中,如何理解__slots__ 属性且实例没有 __dict__ 属性,那么 vars 函数就不 能处理(相反,dir 函数能处理)这样的实例。请举例说明
vars([object]) 返回 object 对象的 __dict__ 属性。如果实例所属的类定义了 __slots__ 属性且实例没有 __dict__ 属性,那么 vars 函数就不 能处理(相反,dir 函数能处理)这样的实例。
如果没有指定参数,那 么 vars() 函数的作用与 locals() 函数一样:返回表示本地作用域 的字典。
在这段话中,它解释了vars()函数的行为以及它与__slots__和__dict__属性的关系。
vars()函数是用来返回一个对象的__dict__属性的内容,即一个包含对象实例变量的字典。但是,如果一个类定义了__slots__属性,这意味着该类的实例不会有一个__dict__属性。
在这种情况下,vars()函数就不能处理这样的实例,因为它依赖于__dict__属性的存在。
相反,dir()函数能处理这样的实例。dir()函数返回一个字符串列表,列出对象的所有属性和方法,无论对象是否有__dict__属性。dir()函数是通过反射机制查询对象的属性,而不是直接依赖于__dict__。
这里有一个例子来说明:
class WithSlots: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y # 创建一个WithSlots的实例 obj = WithSlots(1, 2) # 尝试使用vars()函数 try: print(vars(obj)) except TypeError as e: print("vars() raised a TypeError:", e) # 使用dir()函数 print(dir(obj))
输出将会是:
vars() raised a TypeError: vars() argument must have __dict__ attribute ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'x', 'y']
在这个例子中,由于WithSlots类定义了__slots__,实例obj没有__dict__属性,所以调用vars(obj)会引发一个TypeError。
而dir(obj)可以正常工作,列出所有可用的属性和方法,包括__slots__中定义的属性。
11. 如下这段话如何理解?
Alex Martelli 指出,__slots__ 属性的值虽然可以是一个列表,但是最好始终使用元 组,
因为处理完类的主体之后再修改 __slots__ 列表没有任何作用。因此使用可变的序列容 易让人误解
这段话提到了__slots__属性值的设置,并给出了一个建议:尽管__slots__属性可以设置为列表,但是最好使用元组。
这是因为一旦类定义的主体被执行后,再修改__slots__列表是没有作用的。使用可变的序列(如列表)容易让人误以为可以在运行时修改__slots__,而实际上这是不可能的。
__slots__的主要作用是在类定义时限制实例的属性集合。一旦类定义被执行,__slots__的内容就固定了,不能再添加或删除属性名。
如果尝试修改__slots__,Python解释器不会抛出错误,但是修改不会生效,这可能会导致混淆。
下面是一个例子来说明这一点:
class MyClass: # 使用列表定义__slots__ __slots__ = ['a', 'b'] # 尝试修改__slots__ MyClass.__slots__.append('c') # 使用元组的话,这里.append()就无法通过,可以达到快速报错效果 # 创建MyClass的实例 obj = MyClass() obj.a = 1 obj.b = 2 # 尝试设置新添加的属性 try: obj.c = 3 except AttributeError as e: print("尝试设置属性 'c' 时抛出了AttributeError:", e)
输出将会是:
尝试设置属性 'c' 时抛出了AttributeError: 'MyClass' object has no attribute 'c'
在这个例子中,尽管我们尝试在MyClass的__slots__列表中添加一个新属性'c',但是这个修改没有生效。
因此,当我们尝试在obj实例上设置属性'c'时,Python抛出了一个AttributeError,因为'c'并不是__slots__中定义的属性。
为了避免这种混淆,建议始终使用元组来定义__slots__,因为元组是不可变的,这可以作为一个视觉提示,表明__slots__的内容在类定义执行后不能改变。

浙公网安备 33010602011771号