写 Python 写了这么多年,发现一个规律:入门容易,精通难。大部分 Python 开发者停留在「能写 CRUD」的阶段,对语言的高级特性和底层机制一知半解。面试时一问「深拷贝和浅拷贝的区别」「GIL 到底锁的是什么」「描述符协议的优先级」,就支支吾吾。
这篇文章把 Python 高级特性中最常被问、最容易踩坑、最值得深入理解的七个主题串起来:拷贝机制、上下文管理器、内建函数高级用法、面向对象进阶、property、描述符协议、GIL 和垃圾回收。每个主题都从「是什么」讲到「为什么」,最后落到「怎么用」。
一、拷贝机制:深拷贝与浅拷贝
1.1 赋值 ≠ 拷贝
Python 中最基础的认知:赋值只是创建了对同一对象的引用。
python
a = [1, 2, [3, 4]]
b = a # 不是拷贝,是引用
b[0] = 999
print(a) # [999, 2, [3, 4]] ← a 也变了!
print(a is b) # True,同一个对象plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 赋值 vs 浅拷贝 vs 深拷贝 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 赋值 (b = a) │
│ ┌─────┐ ┌──────────────────┐ │
│ │ a │───▶│ [1, 2, [3, 4]] │ │
│ └─────┘ └──────────────────┘ │
│ ┌─────┐ ▲ │
│ │ b │──────────┘ 两个名字指向同一个对象 │
│ └─────┘ │
│ │
│ 浅拷贝 (b = a.copy()) │
│ ┌─────┐ ┌──────────────────┐ │
│ │ a │───▶│ [1, 2, ──────┐ │ │
│ └─────┘ └──────────────┼──┘ │
│ ┌─────┐ ┌──────────────┼──┐ │ │
│ │ b │───▶│ [1, 2, ──────┘ │──────▶ [3, 4] │
│ └─────┘ └──────────────────┘ ▲ │
│ │ │
│ 内嵌对象是共享的!│ │
│ │ │
│ 深拷贝 (b = copy.deepcopy(a)) │
│ ┌─────┐ ┌──────────────────┐ ┌────────┐ │
│ │ a │───▶│ [1, 2, ──────┐ │───▶│ [3, 4] │ a 的 │
│ └─────┘ └──────────────┼──┘ └────────┘ │
│ ┌─────┐ ┌──────────────┼──┐ ┌────────┐ │
│ │ b │───▶│ [1, 2, ──────┘ │───▶│ [3, 4] │ b 的 │
│ └─────┘ └──────────────────┘ └────────┘ │
│ │
│ 所有层级完全独立 │
└─────────────────────────────────────────────────────────────────┘1.2 浅拷贝的三种方式
python
import copy
original = [1, 2, [3, 4]]
# 方式 1:list.copy()
shallow1 = original.copy()
# 方式 2:切片
shallow2 = original[:]
# 方式 3:copy.copy()
shallow3 = copy.copy(original)
# 三种方式效果完全一致浅拷贝的问题——嵌套对象仍然是共享的:
python
original = [1, 2, [3, 4]]
shallow = original.copy()
# 修改外层元素——互不影响
shallow[0] = 999
print(original) # [1, 2, [3, 4]] ← 没变
# 修改内层元素——互相影响!
shallow[2][0] = 888
print(original) # [1, 2, [888, 4]] ← 变了!1.3 深拷贝
python
import copy
original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)
deep[2][0] = 888
print(original) # [1, 2, [3, 4]] ← 没变,完全独立1.4 深拷贝的注意事项
1. 循环引用:deepcopy 能正确处理循环引用,不会死循环:
python
a = []
a.append(a) # 自引用
b = copy.deepcopy(a) # 不会栈溢出,Python 内部用 memo 字典追踪2. 自定义拷贝行为:通过 __copy__ 和 __deepcopy__ 控制:
python
class Database:
def __init__(self, conn_str):
self.conn_str = conn_str
self._connection = self._create_connection()
def _create_connection(self):
return f"Connection({self.conn_str})"
def __copy__(self):
# 浅拷贝:共享连接
new = Database(self.conn_str)
new._connection = self._connection
return new
def __deepcopy__(self, memo):
# 深拷贝:创建新连接
new = Database(self.conn_str)
memo[id(self)] = new # 防止循环引用
return new3. 不可变对象的处理:元组、字符串、数字等不可变对象,深拷贝直接返回原对象(没有拷贝的必要):
python
s = "hello"
s2 = copy.deepcopy(s)
print(s is s2) # True,不可变对象不需要拷贝
t = (1, 2, [3, 4])
t2 = copy.deepcopy(t)
print(t is t2) # False!元组内含可变对象,必须深拷贝1.5 实战原则
表格
python
# ❌ 错误示范
def append_item(item, target=[]):
target.append(item)
return target
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] ← 默认参数被修改了!
# ✅ 正确写法
def append_item(item, target=None):
if target is None:
target = []
target.append(item)
return target二、上下文管理器(Context Manager)
2.1 with 语句的本质
with 语句的核心是确保资源的获取和释放成对出现,即使发生异常也能正确清理。
python
# 不用 with 的写法
f = open("data.txt")
try:
data = f.read()
finally:
f.close()
# 用 with 的写法(等价且更简洁)
with open("data.txt") as f:
data = f.read()
# 离开 with 块后,f.close() 自动调用plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 上下文管理器协议 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ with expression as var: │
│ # 代码块 │
│ │
│ 执行流程: │
│ │
│ 1. expression.__enter__() │
│ ↓ 返回值赋给 var │
│ 2. 执行代码块 │
│ ↓ 正常结束或抛出异常 │
│ 3. expression.__exit__(exc_type, exc_val, exc_tb) │
│ ↓ 无论是否异常都会执行 │
│ ↓ 如果 __exit__ 返回 True,异常被吞掉 │
│ ↓ 如果 __exit__ 返回 False(默认),异常继续传播 │
│ │
└─────────────────────────────────────────────────────────────────┘2.2 自定义上下文管理器(类实现)
python
class Timer:
"""计时上下文管理器"""
def __enter__(self):
import time
self.start = time.perf_counter()
return self # as var 拿到的就是 self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self.start
print(f"耗时: {self.elapsed:.4f}s")
return False # 不吞异常
# 使用
with Timer() as t:
total = sum(range(10_000_000))
# 耗时: 0.2341s
print(t.elapsed) # 0.2341 ← 还可以拿到耗时值2.3 自定义上下文管理器(contextmanager 装饰器)
更简洁的方式,用 contextlib.contextmanager:
python
from contextlib import contextmanager
@contextmanager
def timer(name="代码块"):
import time
start = time.perf_counter()
print(f"[{name}] 开始执行...")
try:
yield # yield 前面是 __enter__,后面是 __exit__
finally:
elapsed = time.perf_counter() - start
print(f"[{name}] 耗时: {elapsed:.4f}s")
# 使用
with timer("数据处理"):
data = [i ** 2 for i in range(1_000_000)]
# [数据处理] 开始执行...
# [数据处理] 耗时: 0.1523s2.4 实战:数据库事务管理器
python
from contextlib import contextmanager
@contextmanager
def db_transaction(connection):
"""自动提交/回滚的事务管理器"""
try:
yield connection
connection.commit()
print("事务提交成功")
except Exception as e:
connection.rollback()
print(f"事务回滚: {e}")
raise # 重新抛出异常
# 使用
with db_transaction(db_conn) as conn:
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
conn.execute("INSERT INTO orders (user_id) VALUES (1)")
# 如果第二条 SQL 出错,第一条也会回滚2.5 实战:临时修改环境变量/工作目录
python
import os
from contextlib import contextmanager
@contextmanager
def temp_env(key, value):
"""临时修改环境变量"""
old = os.environ.get(key)
os.environ[key] = value
try:
yield
finally:
if old is None:
os.environ.pop(key, None)
else:
os.environ[key] = old
@contextmanager
def temp_dir(path):
"""临时切换工作目录"""
old = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old)
# 使用
with temp_env("DATABASE_URL", "sqlite:///test.db"):
# 在这个块里,DATABASE_URL 是测试值
run_tests()
# 出了块,自动恢复原值2.6 嵌套上下文管理器
contextlib.ExitStack 可以动态管理不定数量的上下文:
python
from contextlib import ExitStack
# 需要同时打开多个文件,数量运行时才确定
filenames = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in filenames]
# 所有文件都会在退出 with 块时自动关闭
for f in files:
print(f.read())三、内建函数高级用法
3.1 常被忽视的内建函数
大部分 Python 开发者只知道 len()、range()、print(),但这些才是真正值得掌握的:
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 内建函数能力矩阵 │
├──────────────┬──────────────────────────────────────────────────┤
│ 函数分类 │ 关键函数 │
├──────────────┼──────────────────────────────────────────────────┤
│ 函数式编程 │ map, filter, reduce, lambda, partial │
│ 类型检查 │ isinstance, issubclass, type, callable │
│ 属性操作 │ getattr, setattr, delattr, hasattr, vars │
│ 迭代工具 │ zip, enumerate, iter, next, reversed, sorted │
│ 对象检查 │ dir, id, repr, hash, super │
│ 编码转换 │ chr, ord, bin, oct, hex, ascii │
│ 类创建 │ type(三参数用法), __import__ │
│ 其他 │ any, all, eval, exec, globals, locals │
└──────────────┴──────────────────────────────────────────────────┘3.2 map / filter / reduce
python
from functools import reduce
# map:对每个元素应用函数
names = ["alice", "bob", "charlie"]
upper = list(map(str.upper, names))
# ['ALICE', 'BOB', 'CHARLIE']
# 多序列同时 map
a = [1, 2, 3]
b = [10, 20, 30]
sums = list(map(lambda x, y: x + y, a, b))
# [11, 22, 33]
# filter:过滤元素
nums = range(1, 20)
evens = list(filter(lambda x: x % 2 == 0, nums))
# [2, 4, 6, 8, 10, 12, 14, 16, 18]
# reduce:累计计算
nums = [1, 2, 3, 4, 5]
product = reduce(lambda acc, x: acc * x, nums)
# 120 = 1*2*3*4*5
# 实战:用 reduce 构建嵌套字典
keys = ["a", "b", "c"]
value = 42
result = reduce(lambda d, k: d.setdefault(k, {}), keys[:-1], {})
result[keys[-1]] = value
# {'a': {'b': {'c': 42}}}3.3 getattr / setattr / hasattr:动态属性操作
python
class Config:
debug = False
version = "1.0"
cfg = Config()
# hasattr:检查属性是否存在
hasattr(cfg, "debug") # True
hasattr(cfg, "database") # False
# getattr:安全获取属性,带默认值
getattr(cfg, "database", "sqlite:///default.db")
# 'sqlite:///default.db'
# setattr:动态设置属性
setattr(cfg, "database", "postgresql://prod-db")
print(cfg.database) # postgresql://prod-db
# 实战:从环境变量动态加载配置
import os
class AppConfig:
pass
config = AppConfig()
env_mapping = {
"DB_URL": "database_url",
"REDIS_URL": "redis_url",
"SECRET_KEY": "secret_key",
}
for env_key, attr_name in env_mapping.items():
if value := os.environ.get(env_key):
setattr(config, attr_name, value)3.4 zip / enumerate / itertools
python
# zip:并行迭代
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
for name, age in zip(names, ages):
print(f"{name}: {age}岁")
# zip 构建字典
keys = ["name", "age", "city"]
values = ["Alice", 25, "Beijing"]
person = dict(zip(keys, values))
# zip_longest:不等长并行
from itertools import zip_longest
a = [1, 2, 3]
b = [10, 20]
list(zip_longest(a, b, fillvalue=0))
# [(1, 10), (2, 20), (3, 0)]
# enumerate:带索引迭代
fruits = ["apple", "banana", "cherry"]
for idx, fruit in enumerate(fruits, start=1):
print(f"{idx}. {fruit}")
# itertools 常用工具
from itertools import chain, groupby, islice, product, combinations
# chain:拼接多个可迭代对象
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]
# groupby:分组
data = [("A", 1), ("A", 2), ("B", 3), ("B", 4)]
for key, group in groupby(data, key=lambda x: x[0]):
print(key, list(group))
# product:笛卡尔积
list(product(["a", "b"], [1, 2]))
# [('a', 1), ('a', 2), ('b', 1), ('b', 2)]
# combinations:组合
list(combinations([1, 2, 3, 4], 2))
# [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]3.5 type 三参数用法:动态创建类
python
# type(name, bases, dict) 可以在运行时创建新类
# 等价于 class Dog: ...
Dog = type("Dog", (), {
"species": "Canis familiaris",
"speak": lambda self: "Woof!",
})
dog = Dog()
dog.speak() # 'Woof!'
# 带继承
class Animal:
def __init__(self, name):
self.name = name
Cat = type("Cat", (Animal,), {
"speak": lambda self: "Meow!",
})
cat = Cat("Kitty")
cat.name # 'Kitty'
cat.speak() # 'Meow!'这种用法在 ORM(如 Django Model)、序列化框架中大量使用。
3.6 any / all
python
# any:有一个为 True 就返回 True
any([False, False, True]) # True
any([]) # False
# all:全部为 True 才返回 True
all([True, True, False]) # False
all([]) # True(空序列视为全 True)
# 实战:验证数据
data = [{"name": "Alice", "age": 25}, {"name": "", "age": 30}]
# 检查是否都有名字
all(item.get("name") for item in data) # False
# 检查是否有未成年人
any(item["age"] < 18 for item in data) # False四、面向对象进阶
4.1 类的创建与初始化顺序
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 对象创建与初始化流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ obj = MyClass(arg1, arg2) │
│ │
│ Step 1: MyClass.__call__(arg1, arg2) │
│ ↓ (type 的 __call__ 触发) │
│ Step 2: MyClass.__new__(MyClass, arg1, arg2) │
│ ↓ (分配内存,返回实例) │
│ Step 3: MyClass.__init__(instance, arg1, arg2) │
│ ↓ (初始化属性) │
│ Step 4: 返回 instance │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ 关键点: │
│ • __new__ 是类方法,控制实例创建 │
│ • __init__ 是实例方法,控制实例初始化 │
│ • __new__ 返回的对象如果不是本类实例,__init__ 不会被调用 │
│ • 单例模式用 __new__ 实现 │
│ │
└─────────────────────────────────────────────────────────────────┘4.2 new 与单例模式
python
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="default"):
# 注意:每次 MyClass() 都会走 __init__
# 需要防止重复初始化
if not hasattr(self, "_initialized"):
self.name = name
self._initialized = True
a = Singleton("first")
b = Singleton("second")
print(a is b) # True
print(a.name) # "first"(不会被 second 覆盖)4.3 MRO(方法解析顺序)
Python 使用 C3 线性化算法确定多重继承的方法查找顺序:
python
class A:
def greet(self): return "A"
class B(A):
def greet(self): return "B"
class C(A):
def greet(self): return "C"
class D(B, C):
pass
d = D()
d.greet() # "B" ← 先找 B
# 查看 MRO
print(D.__mro__)
# (D, B, C, A, object)
# 查找顺序:D → B → C → A → objectplaintext
┌─────────────────────────────────────────────────────────────────┐
│ 菱形继承与 MRO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ object │
│ │ │
│ A │
│ / \ │
│ B C │
│ \ / │
│ D │
│ │
│ D.__mro__ = (D, B, C, A, object) │
│ │
│ 规则: │
│ 1. 子类优先于父类 │
│ 2. 按声明顺序从左到右 │
│ 3. 单调性:不改变局部顺序的前提下保证全局一致性 │
│ │
│ super() 不是调用"父类",而是调用 MRO 中的下一个类 │
│ │
└─────────────────────────────────────────────────────────────────┘4.4 super() 的正确理解
super() 不是简单调用父类,而是沿 MRO 链调用:
python
class A:
def __init__(self):
print("A init")
super().__init__() # 不是调用 object,而是 MRO 中的下一个
class B(A):
def __init__(self):
print("B init")
super().__init__()
class C(A):
def __init__(self):
print("C init")
super().__init__()
class D(B, C):
def __init__(self):
print("D init")
super().__init__()
D()
# D init → B init → C init → A init
# 注意 B 中的 super().__init__() 调用的是 C,不是 A!4.5 鸭子类型与协议
Python 的多态不依赖继承,而是依赖协议(Protocol):
python
# 不需要继承同一个基类,只要实现了相同的方法就行
class PDFReport:
def render(self): return "PDF report"
class HTMLReport:
def render(self): return "HTML report"
class MarkdownReport:
def render(self): return "Markdown report"
def generate_report(report):
# 只关心 report 有没有 render() 方法
print(report.render())
# 三种类型都能传
generate_report(PDFReport()) # PDF report
generate_report(HTMLReport()) # HTML report
generate_report(MarkdownReport()) # Markdown reportPython 3.8+ 提供了 typing.Protocol 做结构化类型检查:
python
from typing import Protocol
class Renderable(Protocol):
def render(self) -> str: ...
def generate_report(report: Renderable) -> None:
print(report.render())4.6 魔术方法分类速查
表格
五、@property:优雅的属性访问
5.1 为什么需要 property
python
# ❌ 不好的做法:直接暴露属性
class Student:
def __init__(self, name, score):
self.name = name
self.score = score # 谁都能赋负数
student = Student("Alice", 95)
student.score = -10 # 不合理但不会报错
# ❌ 糟糕的 Java 风格
class Student:
def __init__(self, name, score):
self._score = score
def get_score(self):
return self._score
def set_score(self, value):
if value < 0 or value > 100:
raise ValueError("分数必须在0-100之间")
self._score = value
student.set_score(95) # 用法啰嗦5.2 property 的正确用法
python
class Student:
def __init__(self, name, score):
self.name = name
self.score = score # 走 setter 验证
@property
def score(self):
"""getter:读取时触发"""
return self._score
@score.setter
def score(self, value):
"""setter:赋值时触发"""
if not isinstance(value, (int, float)):
raise TypeError("分数必须是数值")
if value < 0 or value > 100:
raise ValueError("分数必须在0-100之间")
self._score = value
@score.deleter
def score(self):
"""deleter:del 时触发"""
raise AttributeError("分数不能删除")
student = Student("Alice", 95)
print(student.score) # 95,走 getter
student.score = 88 # 走 setter
# student.score = -10 # ValueError!
# del student.score # AttributeError!5.3 计算属性(只读 property)
python
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
"""面积是计算出来的,不需要存储"""
import math
return math.pi * self.radius ** 2
@property
def circumference(self):
import math
return 2 * math.pi * self.radius
c = Circle(5)
print(c.area) # 78.54...
print(c.circumference) # 31.42...
# c.area = 100 # AttributeError: can't set attribute5.4 property 的本质
property 本身就是一个描述符! 这是理解下一节「描述符」的关键:
python
# property 是一个类
print(type(Student.score)) # <class 'property'>
# 等价于手动创建
class Student:
def __init__(self, name, score):
self._score = score
def _get_score(self):
return self._score
def _set_score(self, value):
if value < 0 or value > 100:
raise ValueError("分数必须在0-100之间")
self._score = value
score = property(_get_score, _set_score) # 手动创建 property 对象property 的底层实现就是描述符协议:__get__、__set__、__delete__。
六、描述符协议:Python 最强大的特性之一
6.1 什么是描述符
实现了 __get__、__set__、__delete__ 中任意一个方法的类,就是描述符。描述符用来**控制属性的访问行为 **。
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 描述符协议 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 描述符协议方法: │
│ │
│ __get__(self, obj, objtype=None) → 读取属性时调用 │
│ __set__(self, obj, value) → 设置属性时调用 │
│ __delete__(self, obj) → 删除属性时调用 │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ 两种描述符: │
│ │
│ 数据描述符 (Data Descriptor) │
│ ├── 同时实现 __get__ 和 __set__ │
│ ├── 优先级高于实例属性 │
│ └── property 就是数据描述符 │
│ │
│ 非数据描述符 (Non-Data Descriptor) │
│ ├── 只实现 __get__ │
│ ├── 优先级低于实例属性 │
│ └── 方法(绑定方法)就是非数据描述符 │
│ │
└─────────────────────────────────────────────────────────────────┘6.2 属性查找优先级
这是面试高频题:
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 属性查找优先级(从高到低) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ obj.attr 的查找顺序: │
│ │
│ 1. 数据描述符(类中定义了 __get__ + __set__ 的属性) │
│ ↓ │
│ 2. 实例属性(obj.__dict__ 中的属性) │
│ ↓ │
│ 3. 非数据描述符(类中只定义了 __get__ 的属性) │
│ ↓ │
│ 4. __getattr__(以上都没找到时兜底) │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ 关键区别: │
│ • 数据描述符 > 实例属性 → 设置时被描述符拦截 │
│ • 实例属性 > 非数据描述符 → 实例可以覆盖非数据描述符 │
│ │
└─────────────────────────────────────────────────────────────────┘python
# 数据描述符优先级演示
class DataDescriptor:
def __get__(self, obj, objtype=None):
return "来自数据描述符"
def __set__(self, obj, value):
obj.__dict__["_data"] = value # 存到实例 __dict__
class NonDataDescriptor:
def __get__(self, obj, objtype=None):
return "来自非数据描述符"
class Demo:
data_desc = DataDescriptor() # 数据描述符
non_data_desc = NonDataDescriptor() # 非数据描述符
d = Demo()
# 数据描述符 > 实例属性
d.__dict__["data_desc"] = "实例属性"
print(d.data_desc) # "来自数据描述符" ← 数据描述符赢了
# 实例属性 > 非数据描述符
d.__dict__["non_data_desc"] = "实例属性"
print(d.non_data_desc) # "实例属性" ← 实例属性赢了6.3 实战:类型验证描述符
python
class Typed:
"""类型验证描述符"""
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
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__},"
f"实际收到 {type(value).__name__}"
)
obj.__dict__[self.name] = value
class Range:
"""范围验证描述符"""
def __init__(self, name, min_val=None, max_val=None):
self.name = name
self.min_val = min_val
self.max_val = max_val
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if self.min_val is not None and value < self.min_val:
raise ValueError(f"{self.name} 不能小于 {self.min_val}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"{self.name} 不能大于 {self.max_val}")
obj.__dict__[self.name] = value
# 使用:声明式定义字段约束
class Employee:
name = Typed("name", str)
age = Range("age", min_val=18, max_val=65)
salary = Range("salary", min_val=0)
def __init__(self, name, age, salary):
self.name = name
self.age = age
self.salary = salary
emp = Employee("Alice", 28, 15000)
# Employee("Alice", "28", 15000) # TypeError: name 期望 str... 不对
# Employee("Alice", 15, 15000) # ValueError: age 不能小于 18
# Employee("Alice", 28, -100) # ValueError: salary 不能小于 06.4 实战:延迟加载描述符
python
class LazyProperty:
"""只计算一次,后续从缓存读取"""
def __init__(self, func):
self.func = func
self.attr_name = func.__name__
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 首次访问时计算,结果存入实例 __dict__
value = self.func(obj)
obj.__dict__[self.attr_name] = value
return value
class DataProcessor:
def __init__(self, filepath):
self.filepath = filepath
@LazyProperty
def data(self):
"""首次访问时才加载,后续直接用缓存"""
print("加载中...") # 只会打印一次
with open(self.filepath) as f:
return f.read()
@LazyProperty
def stats(self):
"""依赖 data,首次访问时计算"""
print("计算统计信息...")
lines = self.data.split("\n")
return {"lines": len(lines), "chars": len(self.data)}
processor = DataProcessor("data.txt")
print(processor.stats) # 加载中... 计算统计信息... {lines: 100, chars: 5000}
print(processor.stats) # {lines: 100, chars: 5000} ← 直接从缓存读6.5 描述符 vs property vs slots
表格
python
# __slots__:限制属性 + 节省内存
class Point:
__slots__ = ('x', 'y') # 只允许 x, y 两个属性
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
# p.z = 3 # AttributeError!
# p.__dict__ # AttributeError! slots 没有 __dict__七、GIL:全局解释器锁
7.1 GIL 是什么
GIL(Global Interpreter Lock)是 CPython 中的一个互斥锁,** 同一时刻只允许一个线程执行 Python 字节码 **。
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ GIL 的工作原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Python 进程 │
│ ┌───────────────────────────────────────────────────┐ │
│ │ GIL (全局锁) │ │
│ │ ┌─────────┐ │ │
│ │ │ Lock │ ← 同一时刻只有一把锁 │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ ┌────┴───────────────────────────────┐ │ │
│ │ │ 获取 GIL 的线程 │ │ │
│ │ │ Thread-1: ●●●●●●●●●● │ │ │
│ │ │ Thread-2: 等待... │ │ │
│ │ │ Thread-3: 等待... │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ GIL 切换时机(CPython 默认): │
│ • 每 5ms(sys.getswitchinterval())强制切换 │
│ • I/O 操作时主动释放 │
│ • C 扩展代码可手动释放 │
│ │
└─────────────────────────────────────────────────────────────────┘7.2 GIL 对性能的影响
python
import time
import threading
# CPU 密集型任务
def cpu_bound(n):
total = 0
for i in range(n):
total += i
return total
N = 50_000_000
# 单线程
start = time.perf_counter()
cpu_bound(N)
cpu_bound(N)
print(f"单线程: {time.perf_counter() - start:.2f}s")
# 多线程(有 GIL,不会更快,反而可能更慢)
start = time.perf_counter()
t1 = threading.Thread(target=cpu_bound, args=(N,))
t2 = threading.Thread(target=cpu_bound, args=(N,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"多线程: {time.perf_counter() - start:.2f}s")
# 结果:多线程反而更慢!因为线程切换有开销
# 单线程: 2.3s
# 多线程: 2.5s7.3 GIL 影响总结
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ GIL 对不同任务类型的影响 │
├──────────────┬──────────────────────────────────────────────────┤
│ 任务类型 │ GIL 影响 │
├──────────────┼──────────────────────────────────────────────────┤
│ CPU 密集型 │ ❌ 多线程无法并行,甚至更慢 │
│ │ ✅ 解决方案:多进程 multiprocessing │
├──────────────┼──────────────────────────────────────────────────┤
│ I/O 密集型 │ ✅ 影响不大,I/O 等待时释放 GIL │
│ │ ✅ 多线程有效,也可用 asyncio │
├──────────────┼──────────────────────────────────────────────────┤
│ C 扩展 │ ✅ 可手动释放 GIL(如 NumPy) │
│ │ ✅ 纯 C 计算部分不受 GIL 限制 │
└──────────────┴──────────────────────────────────────────────────┘7.4 绕过 GIL 的策略
python
# 策略 1:多进程
from multiprocessing import Pool
def cpu_bound(n):
return sum(range(n))
with Pool(4) as p:
results = p.map(cpu_bound, [50_000_000] * 4)
# 策略 2:C 扩展释放 GIL
# 在 C 扩展中:
# Py_BEGIN_ALLOW_THREADS ← 释放 GIL
# // 纯 C 计算
# Py_END_ALLOW_THREADS ← 重新获取 GIL
# 策略 3:使用 NumPy / Pandas
# 这些库的底层计算在 C 中执行,会释放 GIL
import numpy as np
a = np.random.rand(10_000_000)
b = np.random.rand(10_000_000)
c = a + b # 这一步在 C 层执行,不受 GIL 限制
# 策略 4:subprocess 调用独立进程
import subprocess
result = subprocess.run(["python", "heavy_task.py"], capture_output=True)7.5 Python 3.13 的 free-threaded 模式
Python 3.13(2024年10月发布)引入了实验性的 free-threaded 模式 (PEP 703),可以禁用 GIL:
bash
# 安装 free-threaded 版本的 Python 3.13+
# 编译时加 --disable-gil 选项
python3.13t -c "import sys; print(sys._is_gil_enabled())"
# False ← GIL 已禁用当前状态 :free-threaded 模式仍为实验性质,大部分 C 扩展尚未适配。生产环境建议继续使用多进程策略。
八、垃圾回收机制
8.1 Python 的三层垃圾回收
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ Python 垃圾回收三层架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 第一层:引用计数 (Reference Counting) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 每个对象维护一个 ob_refcnt 引用计数 │ │
│ │ 引用 +1,解引用 -1 │ │
│ │ 计数归零 → 立即回收 │ │
│ │ ✅ 实时性好,回收及时 │ │
│ │ ❌ 无法处理循环引用 │ │
│ │ ❌ 频繁更新的开销 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 第二层:标记-清除 (Mark & Sweep) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 专门处理循环引用 │ │
│ │ 定期扫描容器对象(list/dict/set/class等) │ │
│ │ 从根对象出发标记可达对象 │ │
│ │ 清除不可达的循环引用组 │ │
│ │ ✅ 解决了引用计数的致命缺陷 │ │
│ │ ❌ 需要暂停执行(STW),有性能影响 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 第三层:分代回收 (Generational GC) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 假设:存活越久的对象,越可能继续存活 │ │
│ │ 三代:第0代(年轻)→ 第1代 → 第2代(老年) │ │
│ │ 新对象进入第0代,存活后晋升到下一代 │ │
│ │ 第0代扫描最频繁,第2代最少 │ │
│ │ ✅ 减少全量扫描的开销 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘8.2 引用计数详解
python
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2(a 引用 + getrefcount 参数引用)
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2
# 引用计数归零 → 对象被立即回收
a = None # 原来的 [1, 2, 3] 没有引用了,被回收引用计数增加的情况:
对象被创建并赋值给变量
对象被传入函数
对象被加入容器(list、dict、set)
对象作为另一个对象的属性
引用计数减少的情况:
变量被重新赋值
del删除变量函数返回,局部变量释放
容器被销毁或移除元素
8.3 循环引用问题
python
# 循环引用:引用计数无法处理
class Node:
def __init__(self, name):
self.name = name
self.next = None
a = Node("A")
b = Node("B")
a.next = b # A → B
b.next = a # B → A(循环引用)
del a
del b
# A 和 B 互相引用,引用计数都不为零
# 但实际上外部已经无法访问它们了 → 内存泄漏!
# 需要标记-清除来处理8.4 标记-清除的工作流程
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 标记-清除过程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 找到所有容器对象 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ ObjA │ │ ObjB │ │ ObjC │ │ ObjD │ │ ObjE │ │
│ │ref=2 │ │ref=2 │ │ref=1 │ │ref=1 │ │ref=0 │ │
│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └──────┘ │
│ │ │ │ │ │
│ Step 2: 模拟删除所有引用(减引用计数) │
│ │ │ │ │ │
│ ┌──┴───┐ ┌──┴───┐ ┌──┴───┐ ┌──┴───┐ │
│ │ref=0 │ │ref=0 │ │ref=0 │ │ref=0 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ Step 3: 从根对象出发,恢复可达对象的引用 │
│ Root → ObjC → ObjD │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ ObjA │ │ ObjB │ │ ObjC │ │ ObjD │ │
│ │ref=0 │ │ref=0 │ │ref=1 │ │ref=1 │ ← 被恢复 │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ Step 4: 清除引用计数仍为 0 的对象 │
│ ObjA, ObjB 被回收(循环引用,无外部可达) │
│ │
└─────────────────────────────────────────────────────────────────┘8.5 分代回收
python
import gc
# 查看分代阈值
print(gc.get_threshold()) # (700, 10, 50)
# 含义:
# 第0代:分配 700 个容器对象后触发第0代回收
# 第1代:第0代回收 10 次后触发第1代回收
# 第2代:第1代回收 50 次后触发第2代回收
# 查看各代对象数量
print(gc.get_count()) # (当前第0代未回收数, 第1代, 第2代)
# 手动触发回收
collected = gc.collect() # 返回回收的对象数
print(f"回收了 {collected} 个对象")
# 手动调整阈值
gc.set_threshold(1000, 15, 75)plaintext
┌─────────────────────────────────────────────────────────────────┐
│ 分代回收示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 新对象 │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 第 0 代(年轻代) │ │
│ │ 触发频率:高 │ │
│ │ 阈值:700 个对象 │ │
│ │ 大部分短命对象在这里被回收 │ │
│ └──────────────┬─────────────────────┘ │
│ │ 存活一次 → 晋升 │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 第 1 代(中年代) │ │
│ │ 触发频率:中 │ │
│ │ 阈值:第0代回收10次后触发 │ │
│ └──────────────┬─────────────────────┘ │
│ │ 存活两次 → 晋升 │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 第 2 代(老年代) │ │
│ │ 触发频率:低 │ │
│ │ 阈值:第1代回收50次后触发 │ │
│ │ 长期存活的对象在这里 │ │
│ └────────────────────────────────────┘ │
│ │
│ 设计哲学:越老的对象越不需要频繁检查 │
│ │
└─────────────────────────────────────────────────────────────────┘8.6 weakref:弱引用
弱引用不增加引用计数,适合做缓存、观察者模式等场景:
python
import weakref
class DataCache:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get(self, key, loader):
"""缓存数据,但不阻止垃圾回收"""
if key in self._cache:
return self._cache[key]
value = loader()
self._cache[key] = value
return value
cache = DataCache()
class ExpensiveObject:
def __init__(self, data):
self.data = data
# 获取对象
obj = cache.get("key1", lambda: ExpensiveObject("expensive_data"))
print(obj.data) # expensive_data
# 当 obj 被删除后,缓存自动清除
del obj
# WeakValueDictionary 中的引用自动消失8.7 gc 模块实战
python
import gc
# 1. 调试内存泄漏
gc.set_debug(gc.DEBUG_LEAK) # 打印无法回收的对象信息
# 2. 关闭自动 GC(极端优化场景,如游戏主循环)
gc.disable()
# ... 执行关键代码 ...
gc.enable()
gc.collect() # 手动回收
# 3. 查看引用链
import gc
obj = some_suspicious_object
referrers = gc.get_referrers(obj)
# 查看谁在引用这个对象,帮助定位内存泄漏
# 4. 强制回收
collected = gc.collect(generation=2) # 只回收第2代
print(f"回收了 {collected} 个对象")
# 5. __del__ 与循环引用的冲突
class Bad:
def __del__(self):
pass # 定义了 __del__ 的对象无法被标记-清除回收!
class Good:
def __del__(self):
pass # Python 3.4+ (PEP 442) 修复了这个问题8.8 内存优化技巧
python
# 1. 使用 __slots__ 减少内存
import sys
class Normal:
def __init__(self, x, y):
self.x = x
self.y = y
class Slotted:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
n = Normal(1, 2)
s = Slotted(1, 2)
print(sys.getsizeof(n.__dict__)) # ~112 bytes
# s 没有 __dict__,节省了这部分内存
# 2. 大量对象用 namedtuple / dataclass(frozen)
from dataclasses import dataclass
@dataclass(frozen=True, slots=True) # Python 3.10+
class Point:
x: float
y: float
# 3. 及时释放大对象
large_data = load_huge_file()
result = process(large_data)
del large_data # 显式释放,不用等 GC
gc.collect() # 立即回收
# 4. 生成器代替列表
# ❌ 一次性加载所有数据
data = [process(item) for item in huge_list]
# ✅ 惰性处理
data = (process(item) for item in huge_list)总结
七个主题,一张图串起来:
plaintext
┌─────────────────────────────────────────────────────────────────┐
│ Python 高级特性知识图谱 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Python 高级特性 │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ 对象生命周期 属性控制机制 并发与内存 │
│ │ │ │ │
│ ┌────┼────┐ ┌────┼────┐ ┌────┼────┐ │
│ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │
│ 拷贝 上下文 OOP prop 描述符 内建 GIL GC 弱引用 │
│ │
│ 核心关联: │
│ • 拷贝 ↔ 描述符(深拷贝处理描述符属性) │
│ • property ↔ 描述符(property 就是数据描述符) │
│ • GIL ↔ GC(GC 需要获取 GIL 才能执行) │
│ • 上下文 ↔ 资源管理(确保 GC 前正确释放) │
│ • 内建函数 ↔ OOP(type 动态创建类,getattr 动态访问属性) │
│ │
│ 学习路径: │
│ 拷贝 → 上下文 → 内建函数 → OOP → property → 描述符 → GIL │
│ │
└─────────────────────────────────────────────────────────────────┘这些知识点不是孤立的,它们之间有深层的关联:
理解描述符才能真正理解
property、classmethod、staticmethod的底层原理理解GIL才能正确选择多线程 vs 多进程 vs 协程
理解垃圾回收才能写出不泄漏内存的代码
理解拷贝才能避免隐晦的数据共享 bug
理解上下文管理器才能写出健壮的资源管理代码
Python 的高级特性不是为了炫技,而是为了**写出更安全、更高效、更 Pythonic 的代码 **。
相关阅读:
评论区