目 录CONTENT

文章目录

Python 高级进阶

PySuper
2024-10-14 / 0 评论 / 0 点赞 / 5 阅读 / 0 字
温馨提示:
所有牛逼的人都有一段苦逼的岁月。 但是你只要像SB一样去坚持,终将牛逼!!! ✊✊✊

写 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 new

3. 不可变对象的处理:元组、字符串、数字等不可变对象,深拷贝直接返回原对象(没有拷贝的必要):

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 实战原则

表格

场景

选择

列表/字典的简单复制

.copy()

[:]

包含嵌套可变对象

copy.deepcopy()

配置字典的传递(不希望被修改)

copy.deepcopy()

函数参数默认值用可变对象

永远不要!None

代替

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.1523s

2.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 → object

plaintext

┌─────────────────────────────────────────────────────────────────┐
│              菱形继承与 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 report

Python 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 魔术方法分类速查

表格

类别

魔术方法

用途

构造/析构

__new__

, __init__

, __del__

对象创建与销毁

字符串表示

__str__

, __repr__

, __format__

可读/开发者/格式化输出

比较

__eq__

, __lt__

, __gt__

, __le__

, __ge__

比较运算符

算术

__add__

, __sub__

, __mul__

, __truediv__

算术运算符

容器

__len__

, __getitem__

, __setitem__

, __contains__

像容器一样使用

可调用

__call__

让实例像函数一样调用

上下文

__enter__

, __exit__

with 语句支持

迭代

__iter__

, __next__

for 循环支持

属性访问

__getattr__

, __setattr__

, __getattribute__

动态属性

描述符

__get__

, __set__

, __delete__

属性协议

五、@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 attribute

5.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 不能小于 0

6.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

表格

机制

适用场景

复用性

@property

单个类的单个属性验证

低,每个属性都要写一遍

描述符

多个类的多个属性共享验证逻辑

高,描述符类可复用

__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.5s

7.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    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

这些知识点不是孤立的,它们之间有深层的关联:

  • 理解描述符才能真正理解 propertyclassmethodstaticmethod 的底层原理

  • 理解GIL才能正确选择多线程 vs 多进程 vs 协程

  • 理解垃圾回收才能写出不泄漏内存的代码

  • 理解拷贝才能避免隐晦的数据共享 bug

  • 理解上下文管理器才能写出健壮的资源管理代码

Python 的高级特性不是为了炫技,而是为了**写出更安全、更高效、更 Pythonic 的代码 **。

相关阅读

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区