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的类中,nameage可以是该类的属性,表示人的姓名和年龄。
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)    # 计算面积

在上面的示例中,radiusareaCircle类的特性,允许我们像访问属性一样获取圆的半径和计算面积,但它们实际上是通过 @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. <1> 在 __init__ 方法中,self.__data 被设置为 mapping 的一个字典副本。这是为了防止原始数据被外部修改,保证 FrozenJSON 对象的只读性质。

  2. <2> __getattr__ 方法是 Python 的特殊方法,当尝试访问一个不存在的属性时会被调用。这个方法允许我们以属性的方式访问字典的键。

  3. <3> 如果 self.__data 中存在请求的属性,直接返回该属性值。这处理了基础数据类型的访问,例如字符串、数字等。

  4. <4> 如果 self.__data 中不存在请求的属性,尝试从 self.__data 中获取相应的键,并递归地调用 build 方法来处理该键对应的值。这处理了嵌套的字典和列表。

  5. <5> __dir__ 方法是 Python 的特殊方法,用于列出对象的所有属性和方法。这里返回 self.__data 的所有键,以便于自动补全和交互式使用。

  6. <6> build 方法是类方法,它接受一个对象 obj 并返回一个 FrozenJSON 对象或者一个列表。

  7. <7> 如果 obj 是一个映射(例如字典),则直接返回一个 FrozenJSON 对象,这样就可以通过属性访问来导航这个字典。

  8. <8> 如果 obj 是一个可变序列(例如列表),则迭代这个列表,并对每个元素递归调用 build 方法。这样可以处理嵌套的列表,并保证列表中的每个元素都是一个 FrozenJSON 对象或者基础数据类型。

  9. <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 属性。该属性可以控制对某个存储值的访问和修改。

现在,让我们分别解释一下你提出的两个疑问:

  1. 为什么能够绕过特性?

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
  1. 防止无限递归?

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 装饰器来定义属性的 gettersetter 方法。但是,如果我们只定义了 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 方法。

  同样地,如果我们同时定义了 gettersetter 方法,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设置为独一无二的值。

我明白你的疑问,让我逐一解释:

  1. 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__ 字典中获取或存储值时使用的键名。

  1. 为什么要将 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 (意外行为)

在这个例子中,quantity1quantity2 都使用了相同的 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 (正确行为)

在这个示例中,quantity1quantity2 使用了不同的 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__的内容在类定义执行后不能改变。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2024-03-25 17:32  AlphaGeek  阅读(29)  评论(0)    收藏  举报