在 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()
是 Python 内置函数之一,它用于列出对象的属性和方法名称(字符串形式)。
dir([object])
如果 不传入参数:返回当前作用域中的变量名。
如果 传入对象:返回该对象的属性列表(包括方法、魔术方法、类变量等)。
不带参数:查看当前作用域变量
a = 10
b = "hello"
print(dir()) # ['__builtins__', 'a', 'b']
传入一个对象(例如:list)
print(dir([]))
输出结果中会包含 list 对象的所有可用方法和属性,如:
['__add__', '__class__', '__contains__', '__delattr__', ..., 'append', 'clear', 'extend', 'pop', 'remove', 'reverse']
dir()
能看到什么?dir()
输出的是 对象的属性名称字符串列表,包括:
类型 | 示例 |
---|---|
内置魔术方法 | __init__ , __str__ , __len__ |
实例方法 | append , remove , clear (如 list) |
实例属性 | 自定义的属性或数据字段 |
类属性 | 类变量 |
继承自父类的属性和方法 | 也会列出 |
描述器属性 | 如 __dict__ , __class__ , __weakref__ (有的对象) |
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}
可以用 dir()
结合 getattr()
做反射操作:
class Test:
def hello(self):
print("hi")
obj = Test()
for name in dir(obj):
if name.startswith("he"):
getattr(obj, name)() # 动态调用 hello()
当你想知道一个对象支持哪些魔术方法时,可以直接:
print(dir([]))
# 找到 '__getitem__' 表示支持索引
# 找到 '__iter__' 表示支持迭代
这在做 重载运算符、容器模拟、上下文管理器设计 时非常有用。
函数对象
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'...]
用法 | 作用 |
---|---|
dir() |
查看当前作用域变量 |
dir(obj) |
查看对象的属性与方法 |
配合 getattr() |
反射调用对象方法 |
分析 __*__ 魔术方法 |
判断是否支持某种行为 |
区分 __dict__ |
__dict__ 只存储当前属性 |
实例化:是指通过调用一个类来创建它的对象(实例)的过程。
在 Python 中,写下:
obj = MyClass("参数")
这就是一次实例化行为。但背后发生了很多自动操作,这一切依赖于 Python 提供的两个重要魔术方法:
__new__()
:创建对象(分配内存)
__init__()
:初始化对象(赋属性)
Python 实例化的完整流程如下:
obj = MyClass("参数")
背后实际执行的是:
# 实际过程
obj = MyClass.__new__(MyClass, "参数") # 创建对象(返回对象)
MyClass.__init__(obj, "参数") # 初始化对象(设置属性)
注意:
__new__
是类方法,用来分配内存并创建实例对象。
__init__
是实例方法,用来初始化这个实例对象。
__new__
与 __init__
的区别详解特性 | __new__ |
__init__ |
---|---|---|
类型 | 类方法 | 实例方法 |
作用 | 创建对象 | 初始化对象 |
返回值 | 必须返回一个对象 | 必须返回 None |
被调用时机 | 类调用时最先执行 | __new__ 返回对象后立即执行 |
常用场景 | 单例模式、继承不可变类型(如 str 、tuple ) |
正常对象初始化 |
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__ 被调用
总结执行顺序:
__new__
被调用:创建对象并返回给解释器。
__init__
被调用:接收刚创建的对象并进行初始化。
__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
不可变类型子类(如 tuple
、str
)
# 这里 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)
谁控制了 __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__
负责“养育”,共同完成一个完整的对象生命启动周期。
可视化 和 哈希 相关的魔术方法分别对应了对象如何以字符串形式显示以及如何计算对象的哈希值。
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: 是你可能见过的网页形式的代码笔记本,一格一格地运行代码。
哈希是 Python 中用于判断对象唯一性的机制,常见的魔术方法与哈希相关的有:
__hash__()
:为对象计算哈希值,通常用于将对象存储在集合(如 set
)和字典(dict
)中。
__eq__()
:比较两个对象是否相等,返回 True
或 False
,它通常与 __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__
根据 x
和 y
坐标计算了 Point
对象的哈希值。
__eq__()
方法用于判断两个 Point
对象是否相等。
__eq__()
—— 判断对象是否相等
__eq__
是用来定义两个对象是否相等的逻辑。它返回布尔值 True
或 False
,当 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
对象的 name
和 age
属性是否相等。
哈希与相等的关系
哈希值用于确定两个对象是否可以认为是相同的,并在集合或字典中快速比较。
如果两个对象相等(通过 __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
中。
Python 在判断一个对象是否为“真”时,会优先调用这个魔术方法:
__bool__(self)
返回 True
或 False
。控制对象在布尔上下文中的真假性。
如果类没有实现 __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) = 10
→ bool(obj1) == True
len(obj2) = 0
→ bool(obj2) == False
你可以用这些方法来自定义对象的运算符行为。
基本运算符重载魔术方法一览表:
魔术方法 | 对应操作符 | 说明 |
---|---|---|
__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)
如果希望编写的类像 list
、dict
、set
一样,可以索引、迭代、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)
,返回 True
或 False
对一个对象使用 ()
运算时,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!
用途 | 说明 | 举例 |
---|---|---|
模拟函数行为 | 让对象表现得像函数 | 类对象实例本质是函数(如装饰器) |
封装函数状态(闭包) | 比函数更灵活,可以保存状态 | 计数器、缓存器等 |
函数式编程风格 | 与 map() / filter() 等高阶函数配合 |
类似于函数对象的写法 |
自定义策略类 | 策略模式,面向对象封装“调用逻辑” | 动态执行行为 |
装饰器类 | 用类实现装饰器逻辑 | 实现带状态的装饰器 |
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()
__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!
函数结束后
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)
with expression as var:
block
Python 执行流程:
调用表达式对象的 __enter__()
方法,返回值赋给 var
。
执行 block
代码块。
执行完毕或发生异常时,自动调用 __exit__(exc_type, exc_val, exc_tb)
方法,无论是否发生异常,都会执行。
__enter__(self)
在 with
语句开始时被调用。
它的返回值会赋值给 as
后的变量(如果有的话)。
__exit__(self, exc_type, exc_val, exc_tb)
在 with
语句块结束时自动调用(无论是否发生异常)。
参数说明:
exc_type
:异常类型(如 ZeroDivisionError
),无异常则为 None
exc_val
:异常实例对象
exc_tb
:异常的 traceback 信息对象
如果 __exit__
返回 True
,异常会被吞掉(不会再向上传播);否则继续抛出。
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
行,依然会退出上下文,然后异常继续抛出。
场景 | 说明 |
---|---|
文件管理 | 自动关闭文件 |
数据库连接 | 自动提交/关闭连接 |
锁机制 | 多线程中自动加锁/释放 |
性能监控 | 自动统计代码块执行时间 |
异常捕获/处理 | 局部控制异常逻辑 |
虚拟资源管理 | 如自动恢复配置/状态 |
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)
contextlib
装饰器Python 标准库提供了更轻便的方式,不用写类,直接写生成器函数:
from contextlib import contextmanager
@contextmanager
def my_context():
print("进入")
yield "我是值"
print("退出")
with my_context() as v:
print(v)
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、自动化脚本、插件系统等场景。
方法 / 函数 | 含义 |
---|---|
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) |
删除属性时触发 |
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
__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__
)都会触发它。
动态调用方法(如命令分发)
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 请求:获取用户信息
功能 | 方法 | 说明 |
---|---|---|
检查属性 | hasattr() |
返回 True/False |
获取属性 | getattr() |
获取值 / 方法 |
设置属性 | setattr() |
用于动态赋值 |
删除属性 | delattr() |
删除成员属性 |
魔术处理 | __getattr__ |
访问不存在属性 |
魔术处理 | __getattribute__ |
所有属性访问都拦截 |
描述器(Descriptor) 是一个在 Python 中用于管理属性访问的机制。简单来说,描述器定义了一些魔术方法,允许我们控制对类属性的获取、设置和删除。
具体来说,描述器通过实现以下几个魔术方法之一,能够影响对属性的访问:
__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)
这三个魔术方法允许我们在访问某个属性时,插入自定义的操作逻辑。
描述器最重要的魔术方法有三个,它们允许我们在属性访问时控制行为。
__get__(self, instance, owner)
作用:当访问属性时,__get__
被调用。
instance
:实例对象。
owner
:类对象。
__set__(self, instance, value)
作用:当为属性赋值时,__set__
被调用。
instance
:实例对象。
value
:要设置的新值。
__delete__(self, instance)
作用:当删除属性时,__delete__
被调用。
instance
:实例对象。
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
属性的访问。
特征 | 描述器(Descriptor) | 反射(Reflection) |
---|---|---|
定义方式 | 描述器是通过实现 __get__ 、__set__ 、__delete__ 等方法来控制属性的访问。 |
反射是通过运行时访问和修改对象属性、方法和类等信息,常用 getattr() 、setattr() 等函数。 |
使用场景 | 用于控制类或对象的属性访问、修改、删除。 | 用于动态访问和操作对象的属性和方法,通常在运行时使用。 |
控制粒度 | 描述器可以精确地控制属性的获取、设置和删除。 | 反射用于动态访问和操作对象的属性和方法,通常没有细粒度的控制。 |
执行时机 | 描述器在定义类时就确定,访问时触发。 | 反射通常在程序运行时动态触发。 |
典型用法 | 实现属性的管理(如 ORM 映射、数据验证、延迟加载等)。 | 实现动态属性和方法调用、动态修改类结构等。 |
效率 | 通常在静态上下文中工作,性能较高。 | 反射可能会有性能损耗,因为它涉及到动态查找和方法调用。 |
对于描述器:当你访问类的属性时,如果该属性是一个描述器实例,Python 会自动调用该描述器的 __get__
方法。你可以通过在 __get__
方法中定义自定义行为来控制属性的获取过程。
而对于反射,比如 getattr()它
是一个内置函数,用于在运行时动态获取对象的属性值。它不是描述器的一部分,而是直接调用的函数。当你不知道某个对象的属性名时,可以使用 getattr()
来访问这个属性。
描述器是通过实现特殊方法来控制对象属性的访问、修改和删除,通常用于静态属性管理或字段映射。
反射则是在运行时动态检查和操作对象的属性、方法,提供更大的灵活性,适用于动态编程场景。
属性验证:可以通过描述器对属性的值进行检查或限制。
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
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
是对描述器的一种简化方式,适用于较简单的场景。
描述器提供更高的灵活性,适用于复杂的属性管理需求。
__get__
和 __getattr__
特性 | __get__ (描述器) |
__getattr__ (对象方法) |
---|---|---|
定义位置 | 在描述器类中 | 在目标类自身中 |
触发条件 | 访问类属性时(这个属性是一个描述器) | 访问一个不存在的实例属性时才触发 |
应用方式 | 类中定义:attr = Descriptor() |
类中定义:def __getattr__(self, name) |
使用场景 | 精细控制属性读取、写入、删除(如ORM字段) | 兜底处理未定义属性的访问 |
在 Python 中,一个对象是“可迭代的”,意味着它可以用在:
for
循环中
in
操作中(如:x in my_list
)
list()
、tuple()
、sum()
等函数中
本质上:
一个对象只要实现了 __iter__()
魔术方法,它就是一个可迭代对象。
from collections.abc import Iterable
print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance(100, Iterable)) # False
__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
角色 | 需要实现的魔术方法 | 作用 |
---|---|---|
可迭代对象 | __iter__() |
返回一个迭代器对象 |
迭代器对象 | __iter__() 和 __next__() |
支持 next() 获取下一个元素 |
for 循环 |
自动调用 __iter__() 和 __next__() |
遍历所有元素直到结束 |
在 Python 中,类的实例默认使用字典(__dict__
)来存储属性,这给了我们动态添加属性的能力,但也带来了额外的内存开销。
使用 __slots__
可以显式声明某个类允许的属性名,避免使用 __dict__
,从而节省内存。
每个 Python 对象都带有一个 __dict__
属性,用来存储实例变量。这种机制虽然灵活,但会占用更多的内存,尤其是在大量创建对象时。
如果使用 __slots__
,Python 就不会为每个实例创建 __dict__
,从而节省内存空间。
class MyClass:
__slots__ = ('name', 'age') # 声明允许的属性
def __init__(self, name, age):
self.name = name
self.age = age
上面这段代码中,实例对象只能拥有 name
和 age
两个属性,尝试添加其他属性会报错:
obj = MyClass("Tom", 18)
obj.name = "Jerry" # OK
obj.gender = "male" # AttributeError!
来看一个示例对比 __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 万个对象,内存差距会非常明显。
特性 | 无 __slots__ (默认) |
使用 __slots__ |
---|---|---|
每个对象有 __dict__ |
是 | 否 |
动态添加属性 | 支持 | 不支持(除非额外定义) |
内存占用 | 多(因字典结构) | 少(通过固定结构优化) |
存取属性速度 | 慢(通过哈希字典) | 快(通过偏移表/槽位) |
__slots__
注意:__slots__
不会被子类继承!
如果你在父类中定义了 __slots__
,子类要使用它也必须显式声明:
class Parent:
__slots__ = ('x',)
class Child(Parent):
__slots__ = ('y',) # 否则又会启用 __dict__
如果你不定义子类的 __slots__
,那么子类仍然会使用 __dict__
,丧失优化效果。