关于Python:魔术方法详解

在 Python 中,魔术方法(Magic Methods)是以  __方法名__  形式存在的特殊函数。它们是 Python 面向对象底层机制的核心,支撑着很多我们平时使用的“语法糖”行为,比如:

  • print(obj) 背后的 __str__

  • obj + other 背后的 __add__

  • for x in obj 背后的 __iter____next__

魔术方法是 Python 自动调用的特殊方法,以双下划线包围(如 __init____str____getitem__)。你无需显式调用它们,而是通过特定操作触发,例如:

class Person:
    def __init__(self, name):
        self.name = name

p = Person("Tom")  # 自动调用 __init__
print(p)           # 如果定义了 __str__,则会调用它

一、查看dir属性

dir() 是 Python 内置函数之一,它用于列出对象的属性和方法名称(字符串形式)

dir([object])
  • 如果 不传入参数:返回当前作用域中的变量名。

  • 如果 传入对象:返回该对象的属性列表(包括方法、魔术方法、类变量等)。

1. 基本用法举例

不带参数:查看当前作用域变量

a = 10
b = "hello"
print(dir())  # ['__builtins__', 'a', 'b']

传入一个对象(例如:list)

print(dir([]))

输出结果中会包含 list 对象的所有可用方法和属性,如:

['__add__', '__class__', '__contains__', '__delattr__', ..., 'append', 'clear', 'extend', 'pop', 'remove', 'reverse']

2. dir() 能看到什么?

dir() 输出的是 对象的属性名称字符串列表,包括:

类型示例
内置魔术方法__init__, __str__, __len__
实例方法append, remove, clear(如 list)
实例属性自定义的属性或数据字段
类属性类变量
继承自父类的属性和方法也会列出
描述器属性__dict__, __class__, __weakref__(有的对象)

3. dir()__dict__ 的对比

比较点dir()__dict__
类型返回字符串列表返回字典
内容包含方法名、继承、魔术方法等只包含对象实例的实际属性
用途查看可用成员查看对象当前实际状态
是否完整比较全面只是一部分
class A:
    x = 1
    def __init__(self):
        self.y = 2

a = A()
print(dir(a))        # 包括 x、y、__init__ 等等
print(a.__dict__)    # 只包含 {'y': 2}

4. 配合反射使用

可以用 dir() 结合 getattr() 做反射操作:

class Test:
    def hello(self):
        print("hi")

obj = Test()
for name in dir(obj):
    if name.startswith("he"):
        getattr(obj, name)()  # 动态调用 hello()

5. 配合魔术方法调试类功能

当你想知道一个对象支持哪些魔术方法时,可以直接:

print(dir([]))
# 找到 '__getitem__' 表示支持索引
# 找到 '__iter__' 表示支持迭代

这在做 重载运算符、容器模拟、上下文管理器设计 时非常有用。

6. 查看函数、类、模块的属性

函数对象

def f(): pass
print(dir(f))  # 包含 __call__、__name__、__annotations__ 等

类对象

class User:
    def login(self): pass

print(dir(User))  # 有 login, __init__, __class__, __mro__ 等

模块对象

import math
print(dir(math))  # ['acos', 'asin', 'pi', 'sqrt'...]

7. 总结

用法作用
dir()查看当前作用域变量
dir(obj)查看对象的属性与方法
配合 getattr()反射调用对象方法
分析 __*__ 魔术方法判断是否支持某种行为
区分 __dict____dict__ 只存储当前属性

二、实例化

实例化:是指通过调用一个类来创建它的对象(实例)的过程。

在 Python 中,写下:

obj = MyClass("参数")

这就是一次实例化行为。但背后发生了很多自动操作,这一切依赖于 Python 提供的两个重要魔术方法:

  • __new__():创建对象(分配内存)

  • __init__():初始化对象(赋属性)

1.实例化背后的底层流程

Python 实例化的完整流程如下:

obj = MyClass("参数")

背后实际执行的是:

# 实际过程
obj = MyClass.__new__(MyClass, "参数")      # 创建对象(返回对象)
MyClass.__init__(obj, "参数")               # 初始化对象(设置属性)

注意:

  • __new__ 是类方法,用来分配内存并创建实例对象

  • __init__ 是实例方法,用来初始化这个实例对象

2.__new____init__ 的区别详解

特性__new____init__
类型类方法实例方法
作用创建对象初始化对象
返回值必须返回一个对象必须返回 None
被调用时机类调用时最先执行__new__ 返回对象后立即执行
常用场景单例模式、继承不可变类型(如 strtuple正常对象初始化

3.流程代码演示

class MyClass:
    def __new__(cls, *args, **kwargs):
        print("1. __new__ 被调用")
        # 用 object.__new__(cls)(因为所有类的最终父类是 object)
        instance = super().__new__(cls)
        return instance

    def __init__(self, name):
        print("2. __init__ 被调用")
        self.name = name

obj = MyClass("ChatGPT")

输出:

1. __new__ 被调用
2. __init__ 被调用

总结执行顺序:

  1. __new__ 被调用:创建对象并返回给解释器。

  2. __init__ 被调用:接收刚创建的对象并进行初始化。

4. __new__ 常见高级用法

 实现单例模式(只创建一个实例)

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, name):
        self.name = name

不可变类型子类(如 tuplestr

# 这里 MyStr 继承自 Python 内建的 str 类型,意味着 MyStr 是一个特殊的字符串类型。
class MyStr(str):
    def __new__(cls, value):
        print("创建字符串对象")
        return super().__new__(cls, value)

    def __init__(self, value):
        print("初始化不执行修改,因为字符串是不可变的")

对于不可变类型,初始化必须放在 __new__ 中完成。

限制类的实例化行为

class AbstractBase:
    def __new__(cls, *args, **kwargs):
        if cls is AbstractBase:
            raise TypeError("AbstractBase 是抽象类,不能实例化")
        return super().__new__(cls)

5.深度理解 Python 类的本质

谁控制了 __new____init__ 的调用? 答:type.__call__

当你写 obj = MyClass(),实际上是执行了:

obj = type.__call__(MyClass, ...)

type.__call__ 的逻辑如下:

def __call__(cls, *args, **kwargs):
    obj = cls.__new__(cls, *args, **kwargs)
    if isinstance(obj, cls):
        cls.__init__(obj, *args, **kwargs)
    return obj

总结:实例化是 Python 中类到对象的转化过程,由 __new__ 控制“出生”,由 __init__ 负责“养育”,共同完成一个完整的对象生命启动周期。


三、可视化和哈希

可视化哈希 相关的魔术方法分别对应了对象如何以字符串形式显示以及如何计算对象的哈希值

1. 魔术方法之可视化

Python 提供了多个魔术方法,用于定义对象在不同场景下的“可视化”表现,常见的魔术方法有:

  • __str__():定义对象的可打印字符串。

  • __repr__():定义对象的开发者可查看的字符串。

__str__() —— 用户友好的字符串表示

__str__ 是用来定义对象打印时的表现,它的目标是让用户能看到一个简洁、友好的字符串。如果你直接打印一个对象,或者在 str() 调用中使用这个对象,Python 会自动调用 __str__()

示例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Person(name={self.name}, age={self.age})"

p = Person("Alice", 30)
print(p)  # Person(name=Alice, age=30)
  • 当你执行 print(p)str(p) 时,Python 会调用 __str__ 方法返回的字符串,最终打印出 "Person(name=Alice, age=30)"

__repr__() —— 开发者友好的字符串表示

__repr__ 主要是为开发者准备的,目标是提供一种明确的、能够唯一标识对象的字符串表达,通常返回一个可以用来重新创建对象的表达式。

  • __repr__ 主要用于交互式环境(比如 Python shell 或调试时)和日志记录中。

  • 如果你没有重写 __repr__,Python 默认会返回类似 <__main__.ClassName object at 0xAddress> 的表示。

示例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"Person('{self.name}', {self.age})"

p = Person("Bob", 25)
print(repr(p))  # Person('Bob', 25)
  • repr(p) 返回的是 Person('Bob', 25),这意味着你可以直接通过这个字符串创建一个新的 Person 对象。

__str__() vs __repr__() 的区别

str()  format()  print()函数调用都优先用__str__(),需要返回对象的字符串表达。

两者的关系

  • 如果你只实现了 __repr__() 而没写 __str__()

    • 那么 print(obj) 会退而调用 __repr__()

    • 如果repr也没有定义,就直接返回object的定义就是显示内存地址信息,默认是<__main__.类名 object at 地址> 这种形式。

  • 如果你两个都写了:

    • print(obj)__str__()

    • repr(obj)、交互式环境中用 __repr__()

注:什么是交互式环境?

打开命令行(Windows 的 cmd / Linux 的 shell / macOS 的 terminal),输入:

python

你会看到:

>>> 1 + 1
2
>>> print("hello")
hello
>>> class A:
...     def __repr__(self):
...         return "我是 A"
...
>>> a = A()
>>> a
我是 A    ← 就是这里,自动调用 __repr__()

或者 

IPython: 更强大的交互式命令行(支持补全、自动缩进、颜色等)。

Jupyter Notebook: 是你可能见过的网页形式的代码笔记本,一格一格地运行代码。

2. 魔术方法之哈希

哈希是 Python 中用于判断对象唯一性的机制,常见的魔术方法与哈希相关的有:

  • __hash__():为对象计算哈希值,通常用于将对象存储在集合(如 set)和字典(dict)中。

  • __eq__():比较两个对象是否相等,返回 TrueFalse,它通常与 __hash__() 配合使用。

__hash__() —— 返回对象的哈希值

哈希值是一个整数,它用于表示对象在内存中的唯一性。哈希值的计算通常基于对象的内容。

  • 如果一个对象可以被哈希(即它是可哈希的),就可以被放入集合(set)和作为字典的键(dict)。

  • 不可变对象(如整数、字符串、元组等)是可哈希的,然而可变对象(如列表、字典等)通常不可哈希,除非你手动实现 __hash__()

示例:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __hash__(self):
        return hash((self.x, self.y))  # 使用坐标元组的哈希值

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

# 创建两个相同的 Point 对象
p1 = Point(1, 2)
p2 = Point(1, 2)

print(hash(p1))  # hash(p1) == hash(p2)
print(p1 == p2)  # True
  • 在这个例子中,我们用 __hash__ 根据 xy 坐标计算了 Point 对象的哈希值。

  • __eq__() 方法用于判断两个 Point 对象是否相等。

__eq__() —— 判断对象是否相等

__eq__ 是用来定义两个对象是否相等的逻辑。它返回布尔值 TrueFalse,当 Python 判断两个对象是否相等时,实际上是在调用 __eq__() 方法。

示例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

p1 = Person("Alice", 30)
p2 = Person("Alice", 30)
p3 = Person("Bob", 25)

print(p1 == p2)  # True
print(p1 == p3)  # False
  • 在这个例子中,我们定义了 __eq__() 来比较两个 Person 对象的 nameage 属性是否相等。

哈希与相等的关系

哈希值用于确定两个对象是否可以认为是相同的,并在集合或字典中快速比较。

  • 如果两个对象相等(通过 __eq__() 判断),则它们的哈希值也应该相同

  • 如果两个对象的哈希值相同,它们不一定相等,但相等的对象必须有相同的哈希值。

注意:

对象的 hash() 值不是内存地址,而是通过 __hash__() 方法计算出来的一个整数值,这个值代表对象的“身份”或“内容特征”,用于快速查找或对比。

函数含义
hash(obj)返回对象的哈希值,用于字典、集合等哈希结构中
id(obj)返回对象的内存地址标识,通常是其地址或唯一编号

自定义哈希与相等的例子

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __hash__(self):
        return hash((self.x, self.y))  # 根据 x, y 坐标生成哈希

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)  # 判断坐标是否相同

# 使用 set 来存储 Point 对象
# set() 是一个无序、不可重复、可变的集合容器。
points = set()
points.add(Point(1, 2))
points.add(Point(1, 2))  # 由于哈希值和坐标相等,第二个 Point 会被视为重复元素

print(len(points))  # 1

在这个例子中,两个具有相同 (x, y) 坐标的 Point 对象会被视为相同的对象,因此它们不会重复出现在 set 中。


四、bool运算符重载与容器化

1. 布尔运算符重载(对象真假性的控制)

Python 在判断一个对象是否为“真”时,会优先调用这个魔术方法:

__bool__(self)  返回 TrueFalse。控制对象在布尔上下文中的真假性。

如果类没有实现 __bool__(),Python 会退而求其次调用:

__len__(self)  返回一个整数。如果结果为 0,则对象为假;否则为真。

举例:

class MyObject:
    def __init__(self, value):
        self.value = value

    def __bool__(self):
        print("调用 __bool__")
        return self.value != 0

obj1 = MyObject(10)
obj2 = MyObject(0)

print(bool(obj1))  # True
print(bool(obj2))  # False

如果注释掉 __bool__() 方法:

    def __len__(self):
        print("调用 __len__")
        return self.value

Python 会使用 __len__() 的结果判断真假性:

  • len(obj1) = 10bool(obj1) == True

  • len(obj2) = 0bool(obj2) == False

2.运算符重载

你可以用这些方法来自定义对象的运算符行为。

基本运算符重载魔术方法一览表:

魔术方法对应操作符说明
__add__(self, other)+加法,例如 a + b
__sub__(self, other)-减法,例如 a - b
__mul__(self, other)*乘法,例如 a * b
__truediv__(self, other)/真除法,例如 a / b(返回浮点数)
__floordiv__(self, other)//地板除法,例如 a // b(向下取整)
__mod__(self, other)%取模,例如 a % b
__pow__(self, other)**幂运算,例如 a ** b

比较运算符魔术方法:

魔术方法对应操作符说明
__eq__==等于
__ne__!=不等于
__lt__<小于
__le__<=小于等于
__gt__>大于
__ge__>=大于等于

示例:自定义 + 运算符

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # 输出:Vector(4, 6)

3. 容器化(实现类的“容器行为”)

如果希望编写的类像 listdictset 一样,可以索引、迭代、in 判断,就可以重写以下魔术方法:

  •  __len__(self)  返回元素个数,支持 len(obj)
  •  __getitem__(self, key)  支持 obj[key] 访问
  •  __setitem__(self, key, value)  支持 obj[key] = value 赋值
  •  __delitem__(self, key)  支持 del obj[key]
  •  __contains__(self, item)  支持 item in obj
  •  __iter__(self)  支持 for x in obj 循环(必须返回迭代器)

示例:自定义一个支持索引和遍历的容器类

class MyList:
    def __init__(self, data):
        self.data = data

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        return self.data[index]

    def __setitem__(self, index, value):
        self.data[index] = value

    def __delitem__(self, index):
        del self.data[index]

    def __contains__(self, item):
        return item in self.data

    def __iter__(self):
        return iter(self.data)  # 返回迭代器

# 使用
mlist = MyList([1, 2, 3, 4])

print(len(mlist))          # 4
print(mlist[2])            # 3
mlist[2] = 30
print(mlist[2])            # 30
del mlist[1]
print(list(mlist))         # [1, 30, 4]
print(30 in mlist)         # True
for i in mlist:
    print(i)

注:bool()函数调用的时候,如果没有__bool__()方法,则会看__len__()方法是否存在,存在返回非0为真。__contains__   in成员运算符,没有实现,就调用__iter__方法遍历。

__missing__ : 字典或其子类使用__getitem__()调用时,key不存在执行该方法

class A(dict):
    def __missing__(self, key):
        print(key, '!!!!!!!!!!!!!')
        # return 100

a = A()
print(a[1]) # key KeyError

五、可调用对象

在 Python 中,只要一个对象可以像函数那样使用 () 调用,它就是一个“可调用对象”

比如:

def foo(): pass
foo()  # 可调用函数

class A:
    def __call__(self): pass
a = A()
a()  # 实例对象 a 也是可调用的

判断对象是否“可调用”:使用内置函数 callable(obj),返回 TrueFalse

1. 核心魔术方法

对一个对象使用 () 运算时,Python 自动调用该对象的 __call__() 方法。

方法定义:

class MyObj:
    def __call__(self, *args, **kwargs):
        print("你调用了我!")

示例:

class Greeter:
    def __init__(self, name):
        self.name = name

    def __call__(self, msg):
        print(f"{self.name} says: {msg}")

g = Greeter("Alice")
g("Hello!")  # Alice says: Hello!

2. 可调用对象的实际用途

用途说明举例
模拟函数行为让对象表现得像函数类对象实例本质是函数(如装饰器)
封装函数状态(闭包)比函数更灵活,可以保存状态计数器、缓存器等
函数式编程风格map() / filter() 等高阶函数配合类似于函数对象的写法
自定义策略类策略模式,面向对象封装“调用逻辑”动态执行行为
装饰器类用类实现装饰器逻辑实现带状态的装饰器

3. 带状态的函数:类比“闭包函数”

class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self):
        self.count += 1
        print(f"当前调用次数: {self.count}")

c = Counter()
c()  # 当前调用次数: 1
c()  # 当前调用次数: 2

和闭包函数效果类似:

def make_counter():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print("调用次数:", count)
    return inner

c = make_counter()
c()
c()

4. 进阶应用:类装饰器(使用 __call__

class MyDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("函数开始前")
        result = self.func(*args, **kwargs)
        print("函数结束后")
        return result

@MyDecorator
def hello(name):
    print(f"Hello, {name}!")

hello("Alice")

输出:

函数开始前
Hello, Alice!
函数结束后

5. 判断是否可调用

callable(print)     # True
callable(42)        # False
callable(Greeter)   # True(类本身是可调用的)
callable(Greeter()) # True(因为定义了 __call__)

六、上下文管理

Python 中的魔术方法之 上下文管理,主要涉及使用 with ... as ... 语法时背后的底层机制。

在 Python 中,with 语句用来简化资源的管理(比如文件、网络连接、锁等),可以确保在使用完毕后正确释放资源,即使中途抛出了异常。

这背后的核心机制是通过两个魔术方法实现的:

  • __enter__(self)

  • __exit__(self, exc_type, exc_val, exc_tb)

1.  基本原理

with expression as var:
    block

Python 执行流程:

  1. 调用表达式对象的 __enter__() 方法,返回值赋给 var

  2. 执行 block 代码块。

  3. 执行完毕或发生异常时,自动调用 __exit__(exc_type, exc_val, exc_tb) 方法,无论是否发生异常,都会执行。

2. 方法说明

__enter__(self)

  • with 语句开始时被调用。

  • 它的返回值会赋值给 as 后的变量(如果有的话)。

__exit__(self, exc_type, exc_val, exc_tb)

  • with 语句块结束时自动调用(无论是否发生异常)。

  • 参数说明:

    • exc_type:异常类型(如 ZeroDivisionError),无异常则为 None

    • exc_val:异常实例对象

    • exc_tb:异常的 traceback 信息对象

  • 如果 __exit__ 返回 True,异常会被吞掉(不会再向上传播);否则继续抛出。

3. 最基本的例子

class MyContext:
    def __enter__(self):
        print("进入上下文")
        return "我是返回值"

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("退出上下文")
        if exc_type:
            print("出现异常:", exc_type, exc_val)
        return False  # 不吞异常

with MyContext() as val:
    print("val =", val)
    # raise ValueError("人为错误")

输出:

进入上下文
val = 我是返回值
退出上下文

如果取消注释 raise 行,依然会退出上下文,然后异常继续抛出。

4. 应用场景

场景说明
文件管理自动关闭文件
数据库连接自动提交/关闭连接
锁机制多线程中自动加锁/释放
性能监控自动统计代码块执行时间
异常捕获/处理局部控制异常逻辑
虚拟资源管理如自动恢复配置/状态

5. 经典例子:自定义计时器上下文管理器

import time

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self  # 可选
    def __exit__(self, exc_type, exc_val, exc_tb):
        end = time.time()
        print(f"耗时:{end - self.start:.2f}秒")

with Timer():
    time.sleep(1.5)

6. 上下文管理器简写方式:使用 contextlib 装饰器

Python 标准库提供了更轻便的方式,不用写类,直接写生成器函数:

from contextlib import contextmanager

@contextmanager
def my_context():
    print("进入")
    yield "我是值"
    print("退出")

with my_context() as v:
    print(v)

7. 文件操作的本质

with open("test.txt", "w") as f:
    f.write("Hello")

实际上就是:

f = open("test.txt", "w")
try:
    f.write("Hello")
finally:
    f.close()

七、反射

在 Python 中,反射(reflection)是一种在运行时动态操作对象的能力——比如:

  • 查看对象是否有某个属性或方法

  • 获取属性或方法的值

  • 设置属性值

  • 调用对象的某个方法

这使得 Python 语言非常灵活,常用于框架、ORM、自动化脚本、插件系统等场景。

1. 反射相关的魔术方法 / 内置函数

方法 / 函数含义
hasattr(obj, name)是否存在某属性或方法(底层会触发 __getattr__
getattr(obj, name, default=None)获取属性或方法
setattr(obj, name, value)设置属性值
delattr(obj, name)删除属性
__getattr__(self, name)当访问不存在的属性时自动调用
__getattribute__(self, name)无论是否存在都调用,慎用(优先级高)
__setattr__(self, name, value)赋值时自动触发
__delattr__(self, name)删除属性时触发

2. 代码演示:基础用法

class Person:
    def __init__(self):
        self.name = "Alice"
    
    def greet(self):
        print("Hello!")

p = Person()

# 是否有属性 name?
print(hasattr(p, "name"))  # True

# 获取 name 的值
print(getattr(p, "name"))  # Alice

# 获取方法并调用
method = getattr(p, "greet")
method()  # Hello!

# 设置属性
setattr(p, "age", 20)
print(p.age)  # 20

# 删除属性
delattr(p, "name")
print(hasattr(p, "name"))  # False

4. __getattr____getattribute__

__getattr__(self, name)

  • 当访问一个不存在的属性时调用。

  • 常用于动态代理、属性默认值。

class Demo:
    def __getattr__(self, name):
        return f"你访问了不存在的属性: {name}"

d = Demo()
print(d.abc)  # 你访问了不存在的属性: abc

__getattribute__(self, name)

  • 访问任何属性时都会调用,不管存不存在。

  • 要小心使用,否则容易造成无限递归。

注意:如果你在 __getattribute__ 方法内部,又用 self.xxx 访问属性,它再次调用了自己 —— 这就是递归调用了自己,没有出口,就无限循环了!

class Demo:
    def __getattribute__(self, name):
        return f"拦截了访问: {name}"

d = Demo()
print(d.anything)  # 拦截了访问: anything

注意:如果你重写 __getattribute__,访问任何属性(包括 self.__dict__)都会触发它。

5. 进阶应用场景

动态调用方法(如命令分发)

class Command:
    def run(self):
        print("运行指令")
    
    def stop(self):
        print("停止指令")

cmd = Command()
action = "stop"
getattr(cmd, action)()  # 调用 cmd.stop()

框架自动注册、动态路由(如 Django、Flask)

class API:
    def get_user(self):
        print("GET 请求:获取用户信息")

    def post_user(self):
        print("POST 请求:创建用户")

api_obj = API()
http_method = "GET"

# 构造方法名:get_user
method_name = f"{http_method.lower()}_user"

# 动态获取方法
handler = getattr(api_obj, method_name)

# 调用方法
handler()  # 输出:GET 请求:获取用户信息

6. 总结

功能方法说明
检查属性hasattr()返回 True/False
获取属性getattr()获取值 / 方法
设置属性setattr()用于动态赋值
删除属性delattr()删除成员属性
魔术处理__getattr__访问不存在属性
魔术处理__getattribute__所有属性访问都拦截

八、描述器

描述器(Descriptor) 是一个在 Python 中用于管理属性访问的机制。简单来说,描述器定义了一些魔术方法,允许我们控制对类属性的获取、设置和删除。

具体来说,描述器通过实现以下几个魔术方法之一,能够影响对属性的访问:

  • __get__(self, instance, owner)

  • __set__(self, instance, value)

  • __delete__(self, instance)

这三个魔术方法允许我们在访问某个属性时,插入自定义的操作逻辑。

1. 描述器的核心魔术方法

描述器最重要的魔术方法有三个,它们允许我们在属性访问时控制行为。

__get__(self, instance, owner)

  • 作用:当访问属性时,__get__ 被调用。

  • instance:实例对象。

  • owner:类对象。

__set__(self, instance, value)

  • 作用:当为属性赋值时,__set__ 被调用。

  • instance:实例对象。

  • value:要设置的新值。

__delete__(self, instance)

  • 作用:当删除属性时,__delete__ 被调用。

  • instance:实例对象。

2. 基本使用

class MyDescriptor:
    def __get__(self, instance, owner):
        print("调用 __get__ 方法")
        return instance._value
    
    def __set__(self, instance, value):
        print(f"调用 __set__ 方法,设置值为 {value}")
        instance._value = value
    
    def __delete__(self, instance):
        print("调用 __delete__ 方法")
        del instance._value

class MyClass:
    attr = MyDescriptor()  # 将描述器作为类属性
    
    def __init__(self, value):
        self._value = value

obj = MyClass(10)
print(obj.attr)  # 调用 __get__
obj.attr = 20    # 调用 __set__
del obj.attr     # 调用 __delete__

输出:

调用 __get__ 方法
10
调用 __set__ 方法,设置值为 20
调用 __delete__ 方法

在这个例子中,我们定义了一个 MyDescriptor 类,它实现了三个魔术方法,用来控制对 MyClass 类中 attr 属性的访问。

3. 描述器与反射的区别

特征描述器(Descriptor)反射(Reflection)
定义方式描述器是通过实现 __get____set____delete__ 等方法来控制属性的访问。反射是通过运行时访问和修改对象属性、方法和类等信息,常用 getattr()setattr() 等函数。
使用场景用于控制类或对象的属性访问、修改、删除。用于动态访问和操作对象的属性和方法,通常在运行时使用。
控制粒度描述器可以精确地控制属性的获取、设置和删除。反射用于动态访问和操作对象的属性和方法,通常没有细粒度的控制。
执行时机描述器在定义类时就确定,访问时触发。反射通常在程序运行时动态触发。
典型用法实现属性的管理(如 ORM 映射、数据验证、延迟加载等)。实现动态属性和方法调用、动态修改类结构等。
效率通常在静态上下文中工作,性能较高。反射可能会有性能损耗,因为它涉及到动态查找和方法调用。

对于描述器:当你访问类的属性时,如果该属性是一个描述器实例,Python 会自动调用该描述器的 __get__ 方法。你可以通过在 __get__ 方法中定义自定义行为来控制属性的获取过程。

而对于反射,比如  getattr()它是一个内置函数,用于在运行时动态获取对象的属性值。它不是描述器的一部分,而是直接调用的函数。当你不知道某个对象的属性名时,可以使用 getattr() 来访问这个属性。

描述器是通过实现特殊方法来控制对象属性的访问、修改和删除,通常用于静态属性管理或字段映射。

反射则是在运行时动态检查和操作对象的属性、方法,提供更大的灵活性,适用于动态编程场景。

4. 描述器的应用场景

属性验证:可以通过描述器对属性的值进行检查或限制。

class PositiveInteger:
    def __get__(self, instance, owner):
        return instance._value
    
    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Value must be positive")
        instance._value = value
    
class MyClass:
    attr = PositiveInteger()

obj = MyClass()
obj.attr = 10  # 正常
obj.attr = -5  # 抛出 ValueError

懒加载:通过描述器延迟加载某些属性(例如,计算属性)。

class Lazy:
    def __init__(self, func):
        self.func = func
        self._value = None
    
    def __get__(self, instance, owner):
        if self._value is None:
            self._value = self.func(instance)
        return self._value

class MyClass:
    def __init__(self):
        self._x = 10
    
    # @Lazy 等同于 expensive_calculation = Lazy(expensive_calculation)
    @Lazy  
    def expensive_calculation(self):
        print("计算中...")
        return self._x * 2

obj = MyClass()
print(obj.expensive_calculation)  # 计算中... 输出 20

属性访问控制:可以通过描述器控制属性的读取、写入和删除操作,比如控制属性的可写性。

class ReadOnly:
    def __get__(self, instance, owner):
        return instance._value
    
    def __set__(self, instance, value):
        raise AttributeError("Cannot modify read-only attribute")

class MyClass:
    attr = ReadOnly()

obj = MyClass()
obj.attr = 10  # 抛出 AttributeError

5. 描述器与 property 的区别

在 Python 中,property 是一个常用的内置函数,它也是描述器的一种特殊形式。使用 property 时,我们可以控制属性的获取、设置和删除,而不需要显式地定义一个类来实现描述器。

class MyClass:
    def __init__(self, value):
        self._value = value
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, value):
        self._value = value

obj = MyClass(10)
print(obj.value)  # 调用 getter
obj.value = 20    # 调用 setter

区别

  • property 是对描述器的一种简化方式,适用于较简单的场景。

  • 描述器提供更高的灵活性,适用于复杂的属性管理需求。

6. 对比:__get____getattr__

特性__get__(描述器)__getattr__(对象方法)
定义位置描述器类目标类自身
触发条件访问类属性时(这个属性是一个描述器)访问一个不存在的实例属性时才触发
应用方式类中定义:attr = Descriptor()类中定义:def __getattr__(self, name)
使用场景精细控制属性读取、写入、删除(如ORM字段)兜底处理未定义属性的访问

九、可迭代对象

在 Python 中,一个对象是“可迭代的”,意味着它可以用在:

  • for 循环中

  • in 操作中(如:x in my_list

  • list()tuple()sum() 等函数中

本质上:
一个对象只要实现了 __iter__() 魔术方法,它就是一个可迭代对象。

1. 判断是否可迭代

from collections.abc import Iterable

print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance(100, Iterable))       # False

2. 魔术方法说明

__iter__(self)

这是最核心的魔术方法,用于返回一个迭代器对象

class MyIterable:
    def __init__(self):
        self.data = [1, 2, 3]
    
    def __iter__(self):
        print("调用 __iter__")
        return iter(self.data)  # 返回一个真正的迭代器(如列表的迭代器)

你可以这么用它:

obj = MyIterable()
for i in obj:
    print(i)

注意:__iter__() 必须返回一个迭代器对象,这个迭代器对象又必须实现 __next__(self), 它是迭代器对象的魔术方法,用于逐步返回下一个值。一个对象要成为迭代器,必须实现:

  • __iter__() 返回自身

  • __next__() 每次返回一个值,直到抛出 StopIteration

自定义完整迭代器示例:

class Counter:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self  # 自己是迭代器

    def __next__(self):
        if self.current > self.end:
            raise StopIteration
        val = self.current
        self.current += 1
        return val

使用:

for i in Counter(1, 3):
    print(i)
# 输出:
# 1
# 2
# 3

3. 总结

角色需要实现的魔术方法作用
可迭代对象__iter__()返回一个迭代器对象
迭代器对象__iter__()__next__()支持 next() 获取下一个元素
for 循环自动调用 __iter__()__next__()遍历所有元素直到结束

十、__slots__

在 Python 中,类的实例默认使用字典(__dict__)来存储属性,这给了我们动态添加属性的能力,但也带来了额外的内存开销。

使用 __slots__ 可以显式声明某个类允许的属性名,避免使用 __dict__,从而节省内存

1. 为什么要用 __slots__

每个 Python 对象都带有一个 __dict__ 属性,用来存储实例变量。这种机制虽然灵活,但会占用更多的内存,尤其是在大量创建对象时。

如果使用 __slots__,Python 就不会为每个实例创建 __dict__,从而节省内存空间。

2. 语法格式

class MyClass:
    __slots__ = ('name', 'age')  # 声明允许的属性

    def __init__(self, name, age):
        self.name = name
        self.age = age

上面这段代码中,实例对象只能拥有 nameage 两个属性,尝试添加其他属性会报错:

obj = MyClass("Tom", 18)
obj.name = "Jerry"     # OK
obj.gender = "male"    # AttributeError!

3. 内存对比示例

来看一个示例对比 __slots__ 使用前后的内存使用情况(使用 sys.getsizeof()tracemalloc 更准确):

import sys

class NoSlots:
    def __init__(self):
        self.x = 1
        self.y = 2

class WithSlots:
    __slots__ = ('x', 'y')
    def __init__(self):
        self.x = 1
        self.y = 2

a = NoSlots()
b = WithSlots()

print(sys.getsizeof(a.__dict__))  # 输出较大,例如:112
# print(b.__dict__)  # 报错,没有 __dict__

如果批量创建 10 万个对象,内存差距会非常明显。

4. 对象内存布局对比

特性__slots__(默认)使用 __slots__
每个对象有 __dict__ 是 否
动态添加属性支持 不支持(除非额外定义)
内存占用 多(因字典结构) 少(通过固定结构优化)
存取属性速度 慢(通过哈希字典) 快(通过偏移表/槽位)

5. 继承中的 __slots__

注意:__slots__ 不会被子类继承!

如果你在父类中定义了 __slots__,子类要使用它也必须显式声明:

class Parent:
    __slots__ = ('x',)

class Child(Parent):
    __slots__ = ('y',)  # 否则又会启用 __dict__

如果你不定义子类的 __slots__,那么子类仍然会使用 __dict__,丧失优化效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值