__slots__ 在 Python 中是一个用于告知解释器为类的每个实例预留固定属性集合的特殊属性。提供了一种内存优化的手段,特别适用于需要大量创建实例的情况,但它也降低了类的灵活性。
意义
- 减少内存占用:默认情况下,Python 中的每个类实例都有一个 __dict__ 属性,用于动态地存储实例属性。虽然这提供了极大的灵活性(因为可以随时为实例添加新属性),但也带来了额外的内存开销。当定义了 __slots__ 时,Python 会为实例使用更加紧凑的内部表示,不再为每个实例创建 __dict__,从而减少内存占用。
- 加快属性访问速度:当使用 __slots__ 时,访问存储在 __slots__ 中的属性比访问存储在 __dict__ 中的属性要快,因为后者需要进行字典查找。
- 禁止动态添加属性:由于实例不再有 __dict__(除非 __dict__ 明确地包含在 __slots__ 中),因此不能动态地向实例添加不在 __slots__ 中声明的属性。尝试这样做将会抛出 AttributeError。
实现原理
- 固定属性集合:当为一个类定义 __slots__ 时,你需要在 __slots__ 中指定该类实例能够拥有的所有属性名称,这通常是一个包含字符串的元组。Python 解释器会根据 __slots__ 为每个实例预留出固定的空间来存储这些属性,而不是为每个实例创建一个完整的 __dict__。
- 不影响类属性:__slots__ 只影响实例属性,不会限制类属性的添加或访问。
- 继承中的 __slots__:如果一个类继承自定义了 __slots__ 的类,子类实例依然会有 __dict__,除非子类自身也定义了 __slots__。如果子类定义了 __slots__,则它的 __slots__ 会在继承的基础上添加新的属性。
示例1
尽管 MyClass 定义了 __slots__,实例依然可以访问类属性 class_attribute。但是,尝试给实例添加不在 __slots__ 中声明的属性 new_attribute 时,将引发 AttributeError。
class MyClass:
__slots__ = ['instance_attribute'] # 定义允许的实例属性
class_attribute = "This is a class attribute"
instance = MyClass()
instance.instance_attribute = "This is an instance attribute"
# 尝试添加不在 __slots__ 中定义的属性
try:
instance.new_attribute = "Trying to add a new attribute"
except AttributeError as e:
print(e) # 输出: 'MyClass' object has no attribute 'new_attribute'
# 访问类属性
print(instance.class_attribute) # 输出: This is a class attribute
示例2
当类定义了 __slots__ 时,Python 不会为每个实例创建 __dict__,这意味着不能像通常那样给实例动态添加属性。如果想要在描述符中管理属性的值,需要在描述符内部有一个明确的方式来存储每个实例的状态。这就是 self.storage_name 的用途
- self.name 代表属性的公共名称,即用户在代码中使用的名称。
- self.storage_name 用作在实例中存储相应值的属性名称。由于 __slots__ 限制了可以在实例上设置的属性,使用这种命名约定确保每个通过描述符管理的属性都有一个唯一的内部存储位置。
class Field:
def __init__(self, name: str, constructor: Callable) -> None:
if not callable(constructor) or constructor is type(None):
raise TypeError(f'{name!r} type hint must be callable')
self.name = name
self.storage_name = '_' + name
self.constructor = constructor
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self.storage_name)
def __set__(self, instance: Any, value: Any) -> None:
try:
value = self.constructor(value)
except (TypeError, ValueError) as e:
raise
setattr(instance, self.storage_name, value)
class CheckedMeta(type):
def __new__(meta_cls, cls_name, bases, cls_dict):
if '__slots__' not in cls_dict:
slots = []
type_hints = cls_dict.get('__annotations__', {})
for name, constructor in type_hints.items():
field = Field(name, constructor)
cls_dict[name] = field
slots.append(field.storage_name)
cls_dict['__slots__'] = slots
return super().__new__(meta_cls, cls_name, bases, cls_dict)
class Checked(metaclass=CheckedMeta):
__slots__ = () # skip CheckedMeta.__new__ processing
@classmethod
def _fields(cls) -> dict[str, type]:
return get_type_hints(cls)
def __init__(self, **kwargs: Any) -> None:
for name in self._fields():
value = kwargs.pop(name, ...)
setattr(self, name, value)