魔术方法与描述符协议
难度:⭐⭐ | 高频指数:🔥🔥
面试回答
常见问法
__new__和__init__什么区别?__slots__有什么用?- 什么是描述符?
property底层是怎么实现的? __getattr__和__getattribute__有什么区别?__repr__和__str__什么时候用哪个?__call__有什么实际用途?
回答
魔术方法(dunder methods)是 Python 对象协议的核心,让自定义类能融入语言的语法和内置操作。按功能分类:
| 类别 | 常用方法 | 触发时机 |
|---|---|---|
| 构造/销毁 | __new__, __init__, __del__ | 创建和初始化对象 |
| 表示 | __repr__, __str__, __format__ | repr(), str(), f"" |
| 容器 | __getitem__, __len__, __contains__, __iter__ | obj[k], len(), in, for |
| 可调用 | __call__ | obj() |
| 属性访问 | __getattr__, __getattribute__, __setattr__ | . 访问属性 |
| 比较 | __eq__, __lt__, __hash__ | ==, <, 哈希 |
| 算术 | __add__, __mul__, __radd__ | +, * |
| 上下文 | __enter__, __exit__ | with 语句 |
描述符协议是属性访问的底层机制,实现了 __get__/__set__/__delete__ 的对象就是描述符。property、classmethod、staticmethod 本质上都是描述符。
追问
1)__new__ vs __init__ 的区别?
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
# __new__ 负责创建实例,返回实例对象
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value):
# __init__ 负责初始化已创建的实例
self.value = value
a = Singleton(1)
b = Singleton(2)
print(a is b) # True,同一个实例
print(a.value) # 2,__init__ 被调用了两次
关键区别:
__new__:类方法,负责创建并返回实例,先于__init__调用__init__:实例方法,负责初始化已创建的实例,不返回值- 用途:
__new__用于单例、不可变类型子类化、元类
2)__slots__ 的作用?
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
# p.z = 3 # AttributeError
效果:不创建 __dict__,省内存(每实例 40-60 字节)、禁止动态属性、略微加速访问。注意 __slots__ 不会被继承到子类。
3)__getattr__ vs __getattribute__?
class LazyProxy:
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
print(f"访问: {name}")
return super().__getattribute__(name)
def __getattr__(self, name):
# 只在属性不存在时调用
return self._data.get(name, None)
核心区别:
__getattribute__:每次属性访问都调用,容易无限递归__getattr__:只在正常查找失败后调用,更安全
4)__repr__ vs __str__?
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, age={self.age})"
def __str__(self):
return f"{self.name} ({self.age}岁)"
u = User("张三", 25)
print(repr(u)) # User(name='张三', age=25) ← 面向开发者
print(str(u)) # 张三 (25岁) ← 面向用户
print([u]) # [User(name='张三', age=25)] ← 容器内用 __repr__
面试结论:优先实现 __repr__,如果没有 __str__,Python 会退回用 __repr__。
5)__call__ 的实际用途?
__call__ 让实例可以像函数一样调用,常见于:装饰器类、策略模式、工厂对象。
class Retry:
def __init__(self, max_retries=3):
self.max_retries = max_retries
def __call__(self, func):
def wrapper(*args, **kwargs):
for i in range(self.max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if i == self.max_retries - 1:
raise
return wrapper
@Retry(max_retries=3)
def fetch_data():
...
原理展开
1. 描述符协议详解
描述符是实现了以下方法的对象:
class Descriptor:
def __get__(self, obj, objtype=None):
"""obj.attr 时调用"""
...
def __set__(self, obj, value):
"""obj.attr = value 时调用"""
...
def __delete__(self, obj):
"""del obj.attr 时调用"""
...
分类:
- 数据描述符:同时实现
__get__+__set__(或__delete__),优先级高于实例__dict__ - 非数据描述符:只实现
__get__,优先级低于实例__dict__
2. 属性查找顺序(MRO + 描述符)
obj.attr 的查找顺序:
1. type(obj).__mro__ 中的数据描述符(有 __set__ 的)
2. obj.__dict__(实例属性)
3. type(obj).__mro__ 中的非数据描述符(只有 __get__)
4. 触发 __getattr__(如果定义了)
5. 抛出 AttributeError
这解释了为什么 property 能拦截赋值——它是数据描述符。
3. property 就是描述符
# property 的简化实现
class MyProperty:
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("不可写")
self.fset(obj, value)
property 是数据描述符(有 __set__),所以优先级高于实例 __dict__,能拦截赋值操作。
4. 实用描述符:类型检查
class Typed:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} 必须是 {self.expected_type.__name__}")
obj.__dict__[self.name] = value
用法:host = Typed("host", str) 作为类属性,自动校验赋值类型。
易错点
-
在
__getattribute__里直接访问self.xxx会无限递归。必须用super().__getattribute__(name)或object.__getattribute__(self, name)。 -
__new__忘记返回实例__new__必须return一个实例,否则__init__不会被调用。 -
__slots__和继承混用 父类有__slots__,子类没声明,子类仍然会有__dict__。 -
描述符放在实例上而非类上 描述符必须定义为类属性才能生效,放在
__init__里赋给self无效。 -
混淆
__repr__和__str__的用途__repr__面向开发者调试,__str__面向用户展示。容器内元素始终用__repr__。 -
__eq__重写后忘记__hash__Python 会自动把__hash__设为None,导致对象不可哈希、不能放入 set/dict。
记忆技巧
__new__是工厂,__init__是装修:先建房子,再装修。- 描述符优先级口诀:数据描述符 > 实例字典 > 非数据描述符。
__getattr__是兜底网:正常找不到才接住;__getattribute__是安检门:每次都过。__slots__像预定座位:只有名单上的属性能坐,省空间但不灵活。__call__让对象变函数:实例后面加括号就能调用。
面试速答版
魔术方法是 Python 对象协议的核心,让自定义类融入语言语法。最高频的区分:__new__ 负责创建实例(类方法,用于单例等),__init__ 负责初始化(实例方法);__repr__ 面向开发者,__str__ 面向用户;__getattr__ 是属性查找的兜底,__getattribute__ 是每次都经过的入口。描述符协议(__get__/__set__/__delete__)是属性访问的底层机制,property 本质就是一个数据描述符。__slots__ 通过去掉实例 __dict__ 来省内存和禁止动态属性。面试中能说清楚这些区别,再举一个描述符或单例的例子,就足够了。