🐍Python 函数与面向对象

魔术方法与描述符协议

难度:⭐⭐ | 高频指数:🔥🔥

面试回答

常见问法

  • __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__ 的对象就是描述符。propertyclassmethodstaticmethod 本质上都是描述符。

追问

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__ 来省内存和禁止动态属性。面试中能说清楚这些区别,再举一个描述符或单例的例子,就足够了。

Related · 函数与面向对象