一、面向对象进阶

OOP 进阶知识点分类:

核心分类 知识点 功能说明
抽象化约束 抽象类(abc 模块) 定义强制实现的抽象方法,限制子类必须实现指定功能,避免 “空实现” 问题
元编程 元类(metaclass) 控制类的创建过程,可统一修改类的属性 / 方法,实现类级别的逻辑管控
动态属性管控 描述符(Descriptor) 自定义属性的访问 / 赋值 / 删除逻辑(如类型校验、值限制)
__getattr__/__setattr__ 动态处理属性的获取 / 赋值(解决 “属性不存在”、统一属性赋值逻辑)
上下文管理 上下文管理器 通过 __enter__/__exit__ 实现资源自动管理(如文件 / 数据库连接自动关闭)
设计模式落地 Mixin 混入模式 基于多继承实现功能复用,避免单继承的层级臃肿
常用设计模式(Python 实现) 单例 / 工厂 / 策略 / 观察者模式,解决特定场景的代码设计问题
类与对象进阶 方法解析顺序(MRO) 多继承下方法的查找规则,避免继承冲突
类型注解与泛型类 给类 / 方法添加类型提示,提升代码可读性和可维护性(typing 模块)
魔术方法进阶 自定义容器 / 迭代器 通过 __getitem__/__iter__/__next__ 实现类的 “容器 / 迭代” 行为

1.抽象基类(ABC, Abstract Base Class)

1.1.核心概念

  • 抽象基类是一种不能被实例化的类,用于定义一组必须由子类实现的抽象方法,强制子类遵循统一的接口规范。
  • 通过 abc 模块实现。

1.2.代码示例

场景:定义一个 “支付接口”,要求所有子类必须实现 pay 方法,避免子类遗漏核心功能。

from abc import ABC, abstractmethod

# 抽象基类(不能实例化)
class Payment(ABC):
    @abstractmethod
    def pay(self, amount: float) -> bool:
        """抽象方法:子类必须实现支付逻辑"""
        pass

# 子类实现微信支付
class WeChatPay(Payment):
    def pay(self, amount: float) -> bool:
        print(f"微信支付 {amount} 元")
        return True

# 子类实现支付宝支付
class AliPay(Payment):
    def pay(self, amount: float) -> bool:
        print(f"支付宝支付 {amount} 元")
        return True

# 测试
# p = Payment()  # 报错:抽象类不能实例化
wechat = WeChatPay()
wechat.pay(100.5)  # 输出:微信支付 100.5 元

# 若子类未实现pay方法,实例化时直接报错(强制约束)
class UnfinishedPay(Payment):
    pass
# up = UnfinishedPay()  # 报错:Can't instantiate abstract class UnfinishedPay with abstract method pay

# 错误:抽象基类不能实例化
# pay = Payment()  # 报错:TypeError: Can't instantiate abstract class Payment without an implementation for abstract method 'pay'

1.3.适用场景

  • 定义接口规范,强制子类实现核心方法
  • 多态场景下的统一接口约束
  • 框架开发中的插件规范定义

2.元类(Metaclass)

2.1.核心概念

元类是 “类的类”,负责创建类对象。Python 中默认的元类是 type,你可以自定义元类来控制类的创建过程,实现更底层的逻辑。

2.2.代码示例

场景:统一给所有子类添加 create_time 属性(类创建时自动赋值),实现类级别的统一管控。

import datetime

# 自定义元类
class CreateTimeMeta(type):
    # __new__ 控制类的创建过程
    def __new__(cls, name, bases, attrs):
        # 给类添加create_time属性(当前时间)
        attrs["create_time"] = datetime.datetime.now()
        # 给类添加统一的前缀方法
        attrs["prefix_func"] = lambda self: f"[{name}] " + self.__class__.__name__
        return super().__new__(cls, name, bases, attrs)

# 使用元类创建类
class User(metaclass=CreateTimeMeta):
    def __init__(self, name):
        self.name = name

class Order(metaclass=CreateTimeMeta):
    def __init__(self, order_id):
        self.order_id = order_id

# 测试
u = User("张三")
print(u.create_time)  # 输出:2026-01-14 08:59:37.499780(类创建时的时间)
print(u.prefix_func())  # 输出:[User] User

o = Order("123456")
print(o.create_time)  # 输出:2026-01-14 08:59:37.499807(与User不同,因为类创建时间不同)
print(o.prefix_func())  # 输出:[Order] Order

2.3.适用场景

  • 统一规范类的属性 / 方法命名
  • 自动注入通用逻辑(如注册子类、添加日志)
  • 实现 ORM 框架(如 SQLAlchemy)

3.描述符(Descriptor)

3.1.核心概念

描述符是实现了 __get____set____delete__ 中至少一个方法的类,用于控制属性的访问逻辑,是 @property 的底层实现。

3.2.代码示例

场景:限制 “年龄” 属性只能是 0-120 的整数,实现属性赋值的校验逻辑。

# 自定义描述符类
class AgeDescriptor:
    def __get__(self, instance, owner):
        # 获取属性时触发
        return instance._age
    
    def __set__(self, instance, value):
        # 赋值属性时触发(核心:校验逻辑)
        if not isinstance(value, int):
            raise TypeError("年龄必须是整数")
        if value < 0 or value > 120:
            raise ValueError("年龄必须在0-120之间")
        instance._age = value

# 使用描述符
class Person:
    age = AgeDescriptor()  # 把age属性交给描述符管控
    
    def __init__(self, name, age):
        self.name = name
        self.age = age  # 触发__set__方法

# 测试
p1 = Person("李四", 25)
print(p1.age)  # 输出:25(触发__get__)

# p1.age = "25"  # 报错:TypeError: 年龄必须是整数
# p1.age = 150   # 报错:ValueError: 年龄必须在0-120之间

3.3.适用场景

  • 复杂属性校验(如价格必须为正)
  • 延迟加载、缓存属性
  • ORM 框架中的字段映射

4.上下文管理器(Context Manager)

4.1.核心概念

上下文管理器用于管理资源的获取与释放,通过 __enter____exit__ 魔法方法实现,配合 with 语句使用,能确保资源在使用后被正确释放。

4.2.代码示例

场景:模拟数据库连接,实现 “进入时连接、退出时自动关闭”,避免资源泄露。

class DBConnection:
    def __init__(self, db_name):
        self.db_name = db_name
        self.connected = False

    def __enter__(self):
        # 进入with语句时触发:建立连接
        print(f"连接数据库 {self.db_name}")
        self.connected = True
        return self  # 返回给as后的变量

    def __exit__(self, exc_type, exc_val, exc_tb):
        # 退出with语句时触发:关闭连接(无论是否报错)
        print(f"关闭数据库 {self.db_name} 连接")
        self.connected = False
        # 返回True可忽略异常,返回False则抛出异常
        return False


# 测试
with DBConnection("mysql") as db:
    print(f"数据库连接状态:{db.connected}")
    # 执行数据库操作
    # 即使这里报错,__exit__也会执行,确保连接关闭

# 简化写法:@contextmanager装饰器
from contextlib import contextmanager


@contextmanager
def db_connect(db_name):
    # __enter__逻辑
    print(f"连接数据库 {db_name}")
    conn = {"connected": True}
    try:
        yield conn  # 返回给as后的变量
    finally:
        # __exit__逻辑
        print(f"关闭数据库 {db_name} 连接")
        conn["connected"] = False


# 使用装饰器版
with db_connect("postgresql") as db:
    print(f"连接状态:{db['connected']}")
    
# 输出:
# 连接数据库 mysql
# 数据库连接状态:True
# 关闭数据库 mysql 连接
# 连接数据库 postgresql
# 连接状态:True
# 关闭数据库 postgresql 连接

4.3.适用场景

  • 文件操作、数据库连接
  • 网络连接、锁管理
  • 任何需要 “获取 - 使用 - 释放” 模式的资源

5.Mixin 模式

5.1.核心概念

Mixin 是一种特殊的类,仅提供一组功能方法,不单独实例化,通过多继承让其他类复用这些功能,是实现 “组合式复用” 的常用方式。

5.2.代码示例

场景:给不同的类复用 “序列化” 和 “日志” 功能,避免多继承的层级混乱。

# 定义Mixin类(仅提供功能,不单独实例化)
class SerializeMixin:
    def to_dict(self):
        """将对象属性转为字典"""
        return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}

class LogMixin:
    def log(self, msg):
        """打印带类名的日志"""
        print(f"[{self.__class__.__name__}] {msg}")

# 业务类:继承基础类 + Mixin类
class User(SerializeMixin, LogMixin):
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Order(SerializeMixin, LogMixin):
    def __init__(self, order_id, amount):
        self.order_id = order_id
        self.amount = amount

# 测试
u = User("王五", 30)
print(u.to_dict())  # 输出:{'name': '王五', 'age': 30}
u.log("用户创建成功")  # 输出:[User] 用户创建成功

o = Order("789", 200.0)
print(o.to_dict())  # 输出:{'order_id': '789', 'amount': 200.0}
o.log("订单支付完成")  # 输出:[Order] 订单支付完成

5.3.适用场景

  • 为类动态添加功能(如折扣、库存、日志)
  • 避免多重继承的菱形问题
  • 实现 “功能组合” 而非 “层级继承”

6.自定义容器(Custom Container)

6.1.核心概念

自定义容器是通过实现 Python 「容器协议」(特定魔法方法),打造贴合业务需求的专属容器类,让自定义类支持原生列表 / 字典 / 集合的核心操作(如下标访问、长度计算、迭代、包含判断等)。

  • 核心协议方法:__len__(长度)、__getitem__(下标取值)、__setitem__(下标赋值)、__delitem__(下标删除)、__iter__(迭代)、__contains__(包含判断);
  • 便捷实现方式:继承 collections.abc 中的抽象容器类(如 MutableSequence/MutableMapping),强制实现核心方法,避免遗漏协议。

6.2.代码示例

场景:实现一个 “有序字典” 风格的容器,支持下标访问和迭代。

from collections.abc import MutableSequence

# 自定义购物车容器(支持列表操作+业务逻辑)
class ShoppingCart(MutableSequence):
    def __init__(self):
        self._goods = []  # 内部存储商品列表(私有化,隐藏实现)

    # -------- 容器协议核心方法(必须实现) --------
    def __len__(self):
        """返回购物车商品数量(支持len(cart))"""
        return len(self._goods)

    def __getitem__(self, index):
        """下标取值(支持cart[index])"""
        if isinstance(index, int) and 0 <= index < len(self._goods):
            return self._goods[index]
        raise IndexError("购物车索引越界")

    def __setitem__(self, index, value):
        """下标赋值(支持cart[index] = goods)"""
        # 强制校验:只能存入Goods对象
        if not isinstance(value, Goods):
            raise TypeError("购物车仅支持存入Goods对象")
        if isinstance(index, int) and 0 <= index < len(self._goods):
            self._goods[index] = value
        else:
            raise IndexError("购物车索引越界")

    def __delitem__(self, index):
        """下标删除(支持del cart[index])"""
        if isinstance(index, int) and 0 <= index < len(self._goods):
            del self._goods[index]
        else:
            raise IndexError("购物车索引越界")

    def insert(self, index, value):
        """插入元素(协议必实现,支持append/insert等方法)"""
        if not isinstance(value, Goods):
            raise TypeError("购物车仅支持存入Goods对象")
        self._goods.insert(index, value)

    # -------- 扩展业务方法(容器定制化) --------
    def __contains__(self, goods_name):
        """包含判断(支持goods_name in cart)"""
        return any(g.name == goods_name for g in self._goods)

    def get_total_price(self):
        """自动计算购物车总价(业务专属逻辑)"""
        return round(sum(g.price * g.number for g in self._goods), 2)

    def clear(self):
        """清空购物车"""
        self._goods.clear()

# 配套商品类
class Goods:
    def __init__(self, name, price, number):
        self.name = name
        self.price = price
        self.number = number

# -------- 使用自定义容器 --------
if __name__ == "__main__":
    cart = ShoppingCart()
    # 添加商品(继承MutableSequence的append方法)
    cart.append(Goods("水杯", 29.9, 2))
    cart.append(Goods("笔记本", 19.9, 3))

    # 原生列表式操作
    print(f"商品数量:{len(cart)}")          # 输出:2
    print(f"是否有水杯:{'水杯' in cart}")   # 输出:True
    print(f"第一个商品:{cart[0].name}")    # 输出:水杯
    print(f"总价:{cart.get_total_price()}")# 输出:119.5

    # 修改商品
    cart[1] = Goods("钢笔", 9.9, 5)
    print(f"修改后总价:{cart.get_total_price()}")  # 输出:79.3

    # 删除商品
    del cart[0]
    print(f"删除后数量:{len(cart)}")                # 输出:1

    # 清空购物车
    cart.clear()
    print(f"清空后数量:{len(cart)}")                # 输出:0

6.3.适用场景

  1. 业务专属容器:如购物车、订单列表、权限列表,在基础容器功能上叠加业务逻辑(如自动计算总价、校验元素类型);
  2. 定制容器行为:限制元素类型、隐藏内部存储结构、动态计算属性(如购物车总价);
  3. 替代原生容器:解决原生列表 / 字典无业务约束的问题(如避免往购物车存入非商品对象)。

7.常用设计模式

7.1.单例模式(Singleton)

7.1.1.核心定义

保证一个类全局只有一个实例,并提供一个全局访问点。无论实例化多少次,返回的都是同一个对象。

核心场景:

  • 配置管理类(全局唯一的配置实例,避免重复加载配置)
  • 数据库连接池(全局唯一的连接池,避免创建过多连接)
  • 日志器对象(全局唯一的日志实例,保证日志输出统一)
7.1.2.实现示例(3 种常用方式)

方式 1:装饰器实现(最简单)

def singleton(cls):
    """
    单例装饰器的核心逻辑:
    - cls:被装饰的类(如 ConfigManager)
    - instances:缓存字典,key=类,value=该类的唯一实例
    """
    instances = {}  # 闭包变量,缓存所有被装饰类的实例

    def wrapper(*args, **kwargs):
        # 1. 检查该类是否已创建过实例
        if cls not in instances:
            # 2. 未创建则调用类的__init__创建实例,并缓存
            instances[cls] = cls(*args, **kwargs)
        # 3. 无论是否创建,都返回缓存的实例
        return instances[cls]

    return wrapper


# 用@singleton装饰ConfigManager类
@singleton
class ConfigManager:
    def __init__(self):
        print("加载配置文件...")
        self.config = {"db_host": "127.0.0.1", "port": 3306}


# 测试
cm1 = ConfigManager()
cm2 = ConfigManager()
print(cm1 is cm2)  # 输出:True(同一个实例)
print(cm1.config)  # 输出:{'db_host': '127.0.0.1', 'port': 3306}
# 仅打印一次"加载配置文件...",说明只实例化一次

执行流程

  1. 程序加载到 @singleton 时,会立即执行 singleton(ConfigManager)
  2. singleton 函数接收 ConfigManager 类作为参数,返回内部定义的 wrapper 函数;
  3. 此时 ConfigManager 这个名字,已经不再指向原类,而是指向 wrapper 函数!
  4. 调用 wrapper(),检查 instances 字典中是否有 ConfigManager 这个 key;
  5. 首次调用时没有,执行 ConfigManager(*args, **kwargs)(原类的实例化),创建第一个实例;
  6. 将实例存入 instances[ConfigManager],并返回这个实例;
  7. 控制台打印 加载配置文件...(仅这一次)。
  8. 再次调用 wrapper(),检查到 ConfigManager 已在 instances 中;
  9. 直接返回缓存的 instances[ConfigManager]不会执行原类的 __init__
  10. cm1cm2 指向同一个对象(cm1 is cm2 → True)。

@singleton 装饰器的核心是拦截类的实例化过程,保证被装饰的类:

  • 无论被实例化多少次,都只创建一个对象
  • 后续所有实例化请求,都返回第一次创建的那个对象。

方式 2:元类实现(最规范)

class SingletonMeta(type):
    """单例元类"""
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class DBConnectionPool(metaclass=SingletonMeta):
    def __init__(self):
        print("创建数据库连接池...")
        self.pool = ["conn1", "conn2"]

# 测试
pool1 = DBConnectionPool()
pool2 = DBConnectionPool()
print(pool1 is pool2)  # 输出:True
7.1.3.优缺点
优点 缺点
全局唯一实例,节省资源 违背 “单一职责原则”(既实现业务逻辑,又控制实例创建)
避免重复初始化(如重复加载配置) 多线程场景需额外加锁(Python 中可通过 threading.Lock 解决)

7.2.工厂模式(Factory)

7.2.1.核心定义

定义一个创建对象的接口,让子类决定实例化哪个类。核心是 “封装对象的创建逻辑”,避免直接使用 new(Python 中是 类名())。

核心场景:

  • 不同类型的支付方式(微信 / 支付宝 / 银行卡),统一通过工厂创建
  • 不同格式的文件解析器(JSON/CSV/XML),根据文件类型创建对应解析器
  • 爬虫中不同网站的解析器,根据域名创建对应解析器
7.2.2.实现示例(简单工厂 + 工厂方法)

场景:支付方式工厂

# 第一步:定义统一的产品接口
class Payment:
    def pay(self, amount: float) -> str:
        """支付接口(子类必须实现)"""
        raise NotImplementedError

# 第二步:实现具体产品类
class WeChatPay(Payment):
    def pay(self, amount: float) -> str:
        return f"微信支付 {amount} 元"

class AliPay(Payment):
    def pay(self, amount: float) -> str:
        return f"支付宝支付 {amount} 元"

class BankPay(Payment):
    def pay(self, amount: float) -> str:
        return f"银行卡支付 {amount} 元"

# 第三步:实现工厂类(封装创建逻辑)
class PaymentFactory:
    @staticmethod
    def create_payment(pay_type: str) -> Payment:
        """根据类型创建对应支付对象"""
        if pay_type == "wechat":
            return WeChatPay()
        elif pay_type == "ali":
            return AliPay()
        elif pay_type == "bank":
            return BankPay()
        else:
            raise ValueError(f"不支持的支付类型:{pay_type}")

# 测试
factory = PaymentFactory()
# 创建微信支付对象
wechat_pay = factory.create_payment("wechat")
print(wechat_pay.pay(100))  # 输出:微信支付 100 元

# 创建支付宝支付对象
ali_pay = factory.create_payment("ali")
print(ali_pay.pay(200))  # 输出:支付宝支付 200 元

执行流程:

  1. 定义Payment接口,约定pay支付方法;
  2. 实现微信 / 支付宝 / 银行卡支付子类,重写pay
  3. 工厂类封装创建逻辑,按类型返回对应支付对象;
  4. 工厂创建支付对象,调用pay执行支付并返回结果。
7.2.3.优缺点
优点 缺点
解耦:创建逻辑与业务逻辑分离 新增产品时需修改工厂类(简单工厂),违背 “开闭原则”
便于扩展:新增支付方式只需加子类,无需修改业务代码 工厂方法模式会增加类的数量(每个产品对应一个工厂子类)
统一接口:调用方无需关心具体实现,只需传入类型 -

7.3.策略模式(Strategy)

7.3.1.核心定义

定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。核心是 “算法与使用算法的逻辑分离”。

核心场景:

  • 排序算法(快速排序 / 冒泡排序 / 归并排序),根据数据规模切换策略
  • 折扣计算(普通用户 / 会员 / VIP 不同折扣),根据用户等级切换策略
  • 数据校验规则(手机号 / 邮箱 / 身份证),根据校验类型切换策略
7.3.2.实现示例(折扣计算)
# 第一步:定义策略接口
class DiscountStrategy:
    def calculate(self, price: float) -> float:
        """计算折扣后价格"""
        raise NotImplementedError

# 第二步:实现具体策略
class NormalDiscount(DiscountStrategy):
    """普通用户:无折扣"""
    def calculate(self, price: float) -> float:
        return price

class MemberDiscount(DiscountStrategy):
    """会员:9折"""
    def calculate(self, price: float) -> float:
        return price * 0.9

class VIPDiscount(DiscountStrategy):
    """VIP:8折 + 满100减10"""
    def calculate(self, price: float) -> float:
        discount_price = price * 0.8
        return discount_price - 10 if discount_price >= 100 else discount_price

# 第三步:定义上下文(使用策略的逻辑)
class PriceCalculator:
    def __init__(self, strategy: DiscountStrategy):
        self.strategy = strategy  # 注入策略
    
    def set_strategy(self, strategy: DiscountStrategy):
        """动态切换策略"""
        self.strategy = strategy
    
    def get_final_price(self, price: float) -> float:
        """计算最终价格"""
        return self.strategy.calculate(price)

# 测试
# 普通用户
calculator = PriceCalculator(NormalDiscount())
print(f"普通用户100元最终价格:{calculator.get_final_price(100)}")  # 100.0

# 切换为会员策略
calculator.set_strategy(MemberDiscount())
print(f"会员100元最终价格:{calculator.get_final_price(100)}")  # 90.0

# 切换为VIP策略
calculator.set_strategy(VIPDiscount())
print(f"VIP100元最终价格:{calculator.get_final_price(100)}")  # 70.0

执行流程:

  1. 定义DiscountStrategy策略接口,约定所有折扣策略必须实现calculate方法,统一折扣算法的调用规范;
  2. 实现普通用户、会员、VIP 三个具体策略类,各自封装对应的折扣逻辑(无折扣 / 9 折 / 8 折 + 满减);
  3. 定义PriceCalculator上下文类,初始化时注入策略对象,支持通过set_strategy动态切换策略,且封装get_final_price方法调用当前策略的calculate
  4. 实例化上下文并依次注入不同策略,调用get_final_price方法,上下文自动适配对应策略计算出折扣后价格。
7.3.3.优缺点
优点 缺点
算法可动态切换(无需修改上下文代码) 策略过多时,会增加类的数量
避免大量 if-else 分支(如 if 会员 /if VIP) 调用方需了解所有策略的差异,才能选择合适的策略
符合 “开闭原则”:新增策略只需加子类 -

7.4.观察者模式(Observer)

7.4.1.核心定义

定义对象间的一对多依赖关系,当一个对象(主题)状态改变时,所有依赖它的对象(观察者)都会收到通知并自动更新。

核心场景:

  • 消息通知系统(用户订阅主题,主题更新时推送消息)
  • 监控系统(监控指标变化时,触发报警 / 日志 / 存储等操作)
  • GUI 开发(按钮点击时,触发多个回调函数)
7.4.2.实现示例(消息订阅)
# 第一步:定义主题(被观察者)
class Subject:
    def __init__(self):
        self._observers = []  # 存储观察者

    def attach(self, observer):
        """添加观察者"""
        if observer not in self._observers:
            self._observers.append(observer)

    def detach(self, observer):
        """移除观察者"""
        self._observers.remove(observer)

    def notify(self, message: str):
        """通知所有观察者"""
        for observer in self._observers:
            observer.update(message)


# 第二步:定义观察者
class UserObserver:
    def __init__(self, name: str):
        self.name = name

    def update(self, message: str):
        """收到通知后的处理逻辑"""
        print(f"用户 {self.name} 收到消息:{message}")


class LogObserver:
    def update(self, message: str):
        """日志观察者:记录消息"""
        print(f"[日志] {message}")


class EmailObserver:
    def update(self, message: str):
        """邮件观察者:发送邮件"""
        print(f"[邮件] 发送消息:{message}")


# 测试
# 创建主题(如“产品更新”主题)
product_topic = Subject()

# 创建观察者
user1 = UserObserver("张三")
user2 = UserObserver("李四")
log_observer = LogObserver()
email_observer = EmailObserver()

# 订阅主题
product_topic.attach(user1)
product_topic.attach(user2)
product_topic.attach(log_observer)
product_topic.attach(email_observer)

# 主题更新,通知所有观察者
product_topic.notify("Python教程已更新!")

# 移除观察者(取消订阅)
product_topic.detach(user2)
print("\n移除李四后:")
product_topic.notify("新增Python设计模式章节!")

# 输出:
# 用户 张三 收到消息:Python教程已更新!
# 用户 李四 收到消息:Python教程已更新!
# [日志] Python教程已更新!
# [邮件] 发送消息:Python教程已更新!
# 
# 移除李四后:
# 用户 张三 收到消息:新增Python设计模式章节!
# [日志] 新增Python设计模式章节!
# [邮件] 发送消息:新增Python设计模式章节!

执行流程:

  1. 定义主题类Subject:初始化观察者列表,封装attach(添加)、detach(移除)、notify(通知)方法,作为被观察者的核心逻辑载体;
  2. 定义观察者类:实现UserObserver(用户)、LogObserver(日志)、EmailObserver(邮件)三类观察者,均实现update方法,各自封装收到通知后的处理逻辑;
  3. 初始化主题与观察者:创建 “产品更新” 主题对象,以及用户、日志、邮件等观察者实例;
  4. 订阅与通知:将观察者添加到主题的观察者列表(订阅),主题调用notify方法时,会遍历列表并触发所有观察者的update方法;移除指定观察者后,再次通知时该观察者将不再收到消息。
7.4.3.优缺点
优点 缺点
解耦:主题与观察者互不依赖,可独立扩展 观察者过多时,通知会耗时,影响性能
支持广播通知:一个主题可通知多个观察者 观察者通知顺序不固定,可能导致逻辑混乱
符合 “开闭原则”:新增观察者只需加子类 -

二、函数进阶

1.高阶函数

1.1.核心概念

满足以下任一条件的函数就是高阶函数:

  1. 接收一个 / 多个函数作为参数;
  2. 返回一个函数。

通俗解释:

  • 就像 “多功能搅拌机”:可以把 “搅拌”(函数 A)、“榨汁”(函数 B)作为参数传入,也可以返回一个 “自定义模式”(新函数)。

使用场景:

  • 批量处理数据(map/filter/reduce);
  • 自定义排序(sorted的 key 参数);
  • 动态生成函数(如上面的create_adder)。

重点提醒

  • Python 内置高阶函数:map/filter/reduce/sorted/functools.partial(偏函数);
  • 高阶函数是函数式编程的核心,核心思想是 “函数作为数据传递”。

1.2.代码示例

# ========== 场景1:函数作为参数(Python内置高阶函数) ==========
# sorted:key参数接收函数,自定义排序规则
nums = [1, 3, 2, 5, 4]
# 按数字的平方排序(lambda是匿名函数,作为key的参数)
sorted_nums = sorted(nums, key=lambda x: x*x)
print(sorted_nums)  # 输出:[1, 2, 3, 4, 5](平方后:1,4,9,16,25)

# map:接收函数和可迭代对象,批量处理数据
# 把列表每个元素乘以2
nums_double = list(map(lambda x: x*2, nums))
print(nums_double)  # 输出:[2, 6, 4, 10, 8]

# filter:接收函数和可迭代对象,过滤数据
# 筛选偶数
even_nums = list(filter(lambda x: x%2 == 0, nums))
print(even_nums)  # 输出:[2, 4]

# ========== 场景2:返回函数(高阶函数) ==========
def create_adder(n):
    """返回一个“加n”的函数"""
    def adder(x):
        return x + n
    return adder

# 创建“加5”的函数
add5 = create_adder(5)
# 创建“加10”的函数
add10 = create_adder(10)

print(add5(3))   # 输出:8
print(add10(3))  # 输出:13

2.装饰器

2.1.核心概念

装饰器是「给函数 / 方法动态加功能的工具」,本质是返回函数的高阶函数,无需修改原函数代码,就能扩展功能(如日志、计时、权限校验)。

通俗解释:

就像给手机贴保护膜:手机(原函数)功能不变,贴膜(装饰器)新增 “防刮” 功能,还能随时换膜(切换装饰器)。

使用场景:

  • 日志记录、性能监控(计时)、权限校验(如登录验证);
  • 缓存(functools.lru_cache 是 Python 内置装饰器);
  • 函数入参 / 返回值校验。

重点提醒:

  • 装饰器会 “覆盖” 原函数的元信息(如 __name__),需用 functools.wraps 修复:

    from functools import wraps
    def timeit(func):
        @wraps(func)  # 保留原函数元信息
        def wrapper(*args, **kwargs):
            # 逻辑不变
            pass
        return wrapper
    
  • 装饰器执行时机:导入模块时就会执行(而非调用函数时),需注意全局变量影响。

2.2.代码示例(基础版 + 进阶版)

# ========== 基础版:无参数装饰器(计时功能) ==========
def timeit(func):
    """装饰器:计算函数执行时间"""
    # wrapper是包装函数,*args/**kwargs兼容任意参数
    def wrapper(*args, **kwargs):
        import time
        start = time.time()  # 记录开始时间
        result = func(*args, **kwargs)  # 执行原函数
        end = time.time()  # 记录结束时间
        # 打印耗时(重点:保留原函数名)
        print(f"函数 {func.__name__} 执行耗时:{end - start:.4f} 秒")
        return result  # 返回原函数结果
    return wrapper

# 使用装饰器(@语法糖,等价于 add = timeit(add))
@timeit
def add(a, b):
    """计算两数之和"""
    import time
    time.sleep(0.1)  # 模拟耗时操作
    return a + b

# 调用函数(自动触发装饰器)
print(add(1, 2))
# 输出:
# 函数 add 执行耗时:0.1001 秒
# 3

# ========== 进阶版:带参数装饰器(自定义日志前缀) ==========
def logger(prefix="INFO"):
    """带参数的装饰器:打印日志(可自定义前缀)"""
    def decorator(func):  # 接收原函数
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] 调用函数:{func.__name__},参数:{args}")
            result = func(*args, **kwargs)
            print(f"[{prefix}] 函数返回:{result}")
            return result
        return wrapper
    return decorator

# 使用带参数装饰器
@logger(prefix="DEBUG")  # 传入自定义前缀
def multiply(a, b):
    return a * b

multiply(3, 4)
# 输出:
# [DEBUG] 调用函数:multiply,参数:(3, 4)
# [DEBUG] 函数返回:12

3.迭代器

3.1.核心概念

迭代器是实现了「迭代器协议」的对象,必须包含两个方法:

  • __iter__():返回迭代器自身;
  • __next__():返回下一个元素,无元素时抛出 StopIteration

通俗解释:

就像 “自动售货机”:每次按 “下一个” 按钮(调用__next__),吐出一个商品(元素),直到商品售罄(抛出异常),且只能往前取,不能回头。

使用场景:

  • 遍历大文件(逐行读取,不一次性加载到内存);
  • 生成无限序列(如自然数序列);
  • 自定义可迭代对象(如自定义数据结构的遍历)。

重点提醒:

  • 迭代器是「惰性取值」:只有调用next()时才生成下一个元素,节省内存;
  • 迭代器只能遍历一次(不可逆),如需重复遍历,需重新创建迭代器;
  • 可迭代对象(如列表、字典)≠ 迭代器,但可通过iter()转换为迭代器。

3.2.代码示例

# ========== 自定义迭代器(模拟自然数生成) ==========
class NumberIterator:
    def __init__(self, max_num):
        self.max_num = max_num  # 最大数
        self.current = 0  # 当前数(初始值)
    
    def __iter__(self):
        """返回迭代器自身(必须实现)"""
        return self
    
    def __next__(self):
        """返回下一个元素(必须实现)"""
        if self.current < self.max_num:
            self.current += 1
            return self.current
        # 无元素时抛出异常(迭代终止)
        raise StopIteration("已到达最大数")

# 创建迭代器对象
num_iter = NumberIterator(3)

# 方式1:手动调用__next__()
print(next(num_iter))  # 输出:1(等价于num_iter.__next__())
print(next(num_iter))  # 输出:2

# 方式2:for循环(自动调用__next__(),捕获StopIteration)
# 注意:迭代器只能遍历一次,这里重新创建
num_iter2 = NumberIterator(3)
for num in num_iter2:
    print(num)  # 输出:1、2、3

# ========== 内置可迭代对象转迭代器 ==========
# 列表是可迭代对象,不是迭代器
lst = [1, 2, 3]
lst_iter = iter(lst)  # 转成迭代器
print(next(lst_iter))  # 输出:1
print(next(lst_iter))  # 输出:2

4.生成器

4.1.核心概念

生成器是「简化版迭代器」,有两种形式:

  1. 生成器函数:使用yield关键字的函数,执行时返回生成器对象;
  2. 生成器表达式:(x for x in range(10))(类似列表推导式,括号不同)。

通俗解释:

就像 “按需生产的工厂”:不提前生产所有产品(节省内存),客户要一个(调用next()),才生产一个,生产完暂停,下次继续。

使用场景:

  • 处理大文件、大数据集(惰性取值,节省内存);
  • 替代列表推导式(数据量大时);
  • 实现无限序列(如def gen_infinite(): while True: yield 1)。

重点提醒:

  • yield的作用:返回值 + 暂停函数执行,下次next()从暂停处继续;
  • 生成器本质是迭代器(实现了迭代器协议),可直接用next()for循环;
  • 生成器表达式比列表推导式省内存(不生成完整列表),但只能遍历一次。

4.2.代码示例

# ========== 生成器函数(核心:yield) ==========
def gen_num(max_num):
    """生成器函数:生成1~max_num的数"""
    current = 0
    while current < max_num:
        current += 1
        # yield:返回值,且暂停函数执行(重点)
        yield current
        print(f"暂停后继续执行,当前current:{current}")

# 创建生成器对象(函数不会立即执行)
gen = gen_num(2)

# 第一次调用next():执行到yield,返回1,暂停
print(next(gen))  # 输出:1
# 第二次调用next():从暂停处继续执行,到下一个yield,返回2
print(next(gen))  # 输出:暂停后继续执行,当前current:1 → 2
# 第三次调用next():无yield,抛出StopIteration
# print(next(gen))  # 报错

# ========== 生成器表达式 ==========
# 生成1~3的平方数(惰性取值)
gen_expr = (x*x for x in range(1, 4))
print(next(gen_expr))  # 输出:1
print(next(gen_expr))  # 输出:4

# ========== 生成器遍历大文件(实战场景) ==========
def read_big_file(file_path):
    """逐行读取大文件(避免内存溢出)"""
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:  # 文件本身是可迭代对象,逐行读取
            yield line.strip()  # 每行作为一个元素返回

# 使用:遍历大文件时,每次只加载一行
# for line in read_big_file("big_file.txt"):
#     print(line)

4.3.yield(生成器的暂停键)

yield 是「生成器函数」的专属关键字,核心作用是:

  • 返回一个值 + 暂停函数执行(保留当前执行状态),下次调用 next() 时,从暂停的位置继续执行。

通俗比喻:把函数变成 “暂停播放的播放器”:

  • 普通函数:像一次性播放完的视频,从头跑到尾,返回结果后函数就 “消失” 了(内部变量全部销毁);
  • yield 的生成器函数:像带暂停键的播放器,按一下播放(调用 next()),播放到 yield 就暂停,再按一下,从暂停处继续播放,直到播放完(抛出 StopIteration)。
4.3.1.分步拆解执行流程
# 定义生成器函数(带yield)
def gen_demo():
    print("第一步:函数开始执行")
    yield 1  # ①返回1 ②暂停函数,保留当前状态
    print("第二步:从yield暂停处继续执行")
    yield 2  # ①返回2 ②再次暂停
    print("第三步:最后一次继续执行")
    yield 3  # ①返回3 ②暂停
    print("第四步:函数执行完毕")

# 1. 创建生成器对象(关键:函数不会立即执行!)
gen = gen_demo()
print("创建生成器后,函数还没执行\n")

# 2. 第一次调用next():触发函数执行,直到第一个yield
print("第一次next()返回:", next(gen))
# 输出:
# 第一步:函数开始执行
# 第一次next()返回: 1

print("\n")

# 3. 第二次调用next():从第一个yield暂停处继续,直到第二个yield
print("第二次next()返回:", next(gen))
# 输出:
# 第二步:从yield暂停处继续执行
# 第二次next()返回: 2

print("\n")

# 4. 第三次调用next():从第二个yield暂停处继续,直到第三个yield
print("第三次next()返回:", next(gen))
# 输出:
# 第三步:最后一次继续执行
# 第三次next()返回: 3

print("\n")

# 5. 第四次调用next():从第三个yield暂停处继续,函数执行完毕,抛出异常
try:
    print("第四次next()返回:", next(gen))
except StopIteration:
    print("触发StopIteration:函数执行完毕,没有更多yield了")
# 输出:
# 第四步:函数执行完毕
# 触发StopIteration:函数执行完毕,没有更多yield了
4.3.2.对比:普通函数 vs 生成器函数
4.3.2.1.普通函数(无 yield):一次性执行完毕
def normal_func():
    print("执行第一步")
    return 1
    print("执行第二步")  # 永远不会执行,因为return后函数结束

# 调用普通函数
print(normal_func())  # 输出:执行第一步 → 1
print(normal_func())  # 重新执行,输出:执行第一步 → 1(变量重新初始化)

特点return 后函数立即结束,后续代码不执行;每次调用都是 “从头开始”。

4.3.2.2.生成器函数(有 yield):暂停 + 继续
def yield_func():
    print("执行第一步")
    yield 1
    print("执行第二步")  # 暂停后能继续执行
    yield 2

gen = yield_func()
print(next(gen))  # 输出:执行第一步 → 1(暂停在第一个yield)
print(next(gen))  # 输出:执行第二步 → 2(暂停在第二个yield)

特点yield 不会结束函数,只是 “暂停”;后续 next() 会从暂停处继续,变量保留上一次的状态。

4.3.3.核心总结
  1. 本质yield 是生成器的 “暂停 / 恢复” 开关,替代了 return,但比 return 强大;
  2. 执行逻辑
    • 生成器对象创建时,函数不执行;
    • 每次 next() 触发函数执行,直到 yield 暂停;
    • 所有 yield 执行完,再次 next() 抛出 StopIteration
  3. 核心优势:惰性取值(用的时候才生成),节省内存,适合处理大数据 / 无限序列;
  4. 新手避坑
    • 生成器函数调用后返回的是「生成器对象」,不是直接返回值;
    • 生成器只能遍历一次,遍历完就 “空了”,需重新创建。

5.闭包

5.1.核心概念

闭包是「嵌套函数中,内层函数引用外层函数变量,且外层函数返回内层函数」的结构,能保留外层变量的上下文(“记忆” 变量值)。

通俗解释:

就像你去咖啡店点单:外层函数是 “点单流程”,内层函数是 “制作咖啡”,内层函数能记住你点的 “甜度 / 温度”(外层变量),即使点单流程结束,制作咖啡时仍能用到这些参数。

使用场景:

  • 实现装饰器(装饰器本质是闭包);
  • 创建 “定制化函数”(如固定初始值的计数器);
  • 数据私有化(外层变量只能通过内层函数访问)。

重点提醒:

  • 内层函数修改外层变量时,需用 nonlocal 声明(修改全局变量用 global);
  • 闭包会保留外层变量的引用,即使外层函数执行完毕,变量也不会被销毁(注意内存占用)。

5.2.代码示例

def outer_func(init_num):
    """外层函数:定义初始值(被内层引用)"""
    count = init_num  # 外层变量,被内层引用
    
    def inner_func():
        """内层函数:使用外层变量,且被返回"""
        nonlocal count  # 声明使用外层的count(不可省略,否则视为局部变量)
        count += 1
        return count  # 每次调用都能保留count的最新值
    
    return inner_func  # 外层返回内层函数

# 创建闭包实例(记住init_num=0)
counter1 = outer_func(0)
# 调用内层函数,count会持续累加(闭包的“记忆性”)
print(counter1())  # 输出:1(count从0→1)
print(counter1())  # 输出:2(count从1→2)

# 新的闭包实例(独立上下文,不影响counter1)
counter2 = outer_func(10)
print(counter2())  # 输出:11
print(counter1())  # 输出:3(counter1的count仍在累加)

5.3.nonlocal(内层修改外层变量)

nonlocal嵌套函数中专用的关键字,核心作用是:

声明当前变量不是内层函数的局部变量,而是来自 “外层嵌套函数” 的变量,允许内层函数修改外层嵌套函数的变量(而非创建新的局部变量)。

5.3.1.先看痛点:没有 nonlocal 会发生什么?

先通过一个 “计数器” 场景,理解为什么需要 nonlocal—— 没有它,内层函数无法修改外层函数的变量:

def outer():
    # 外层函数的变量
    count = 0
    
    def inner():
        # 想修改外层的count,但会报错!
        count += 1  # ❶ 问题:Python认为count是inner的局部变量,但未定义就赋值
        return count
    
    return inner

counter = outer()
print(counter())  # 执行报错:UnboundLocalError: local variable 'count' referenced before assignment

报错原因

Python 有「变量作用域规则」:

  • 内层函数读取外层变量(如 print(count))是允许的;
  • 但内层函数修改外层变量(如 count += 1)时,Python 会默认把 count 当作内层函数的局部变量
  • count += 1 等价于 count = count + 1,右边的 count 还没定义,就会报错。
5.3.2.nonlocal 解决什么问题?

给内层函数的 count 加上 nonlocal 声明,就能明确告诉 Python:“这个 count 不是我的局部变量,是外层嵌套函数的,我要修改它”:

def outer():
    count = 0  # 外层函数变量
    
    def inner():
        nonlocal count  # ❶ 核心:声明count来自外层嵌套函数
        count += 1      # ❷ 现在可以正常修改外层的count了
        return count
    
    return inner

# 测试:闭包的“记忆性”体现
counter1 = outer()
print(counter1())  # 输出:1(count从0→1)
print(counter1())  # 输出:2(count从1→2)
print(counter1())  # 输出:3(count从2→3)

# 新的闭包实例,count独立初始化
counter2 = outer()
print(counter2())  # 输出:1(和counter1的count互不影响)

执行逻辑拆解

  1. 调用 outer() 时,创建外层变量 count=0,返回内层函数 inner(闭包);
  2. 第一次调用 counter1()(即 inner()):
    • nonlocal count 绑定外层的 count
    • count += 1 把外层的 count 从 0 改成 1;
    • 返回 1,且外层的 count 保留为 1(闭包的记忆性);
  3. 第二次调用 counter1()
    • 继续使用外层保留的 count=1
    • 执行 count += 1 后变成 2,返回 2。
5.3.3.nonlocal vs global:别搞混!

很多人会把 nonlocalglobal 弄混,用表格清晰对比:

关键字 作用场景 声明的变量来源 核心区别
nonlocal 嵌套函数(内层→外层) 外层嵌套函数的变量 只作用于 “嵌套函数层级”,不涉及全局
global 任意函数内部→全局 模块级别的全局变量 作用于 “函数→全局” 层级

对比示例

# 全局变量
total = 0

def outer():
    # 外层嵌套函数变量
    count = 0
    
    def inner():
        global total    # 声明total是全局变量
        nonlocal count  # 声明count是外层嵌套函数变量
        
        total += 10     # 修改全局变量
        count += 1      # 修改外层嵌套变量
        
        print(f"count={count}, total={total}")
    
    return inner

counter = outer()
counter()  # 输出:count=1, total=10
counter()  # 输出:count=2, total=20
print(total)  # 全局变量被修改:20

5.4.核心总结(nonlocal 必记要点)

  1. 核心作用:让内层嵌套函数能修改外层嵌套函数的变量,解决 “内层无法修改外层变量” 的痛点;
  2. 使用场景:仅用于嵌套函数(闭包),是实现闭包 “记忆性” 的关键;
  3. 避坑重点
    • 别和 global 混:nonlocal 管 “嵌套层级”,global 管 “全局层级”;
    • 声明的变量必须在外层嵌套函数中存在,否则报错;
  4. 新手简化理解:如果在嵌套函数里想修改外层的变量,先加 nonlocal 声明就对了。

6.函数参数进阶

6.1.核心概念

Python 函数参数的 4 种类型:

  • 默认参数:def func(a, b=10): ...
  • 位置参数:按顺序传入的参数;
  • 可变参数:*args(接收任意数量位置参数,转元组);
  • 关键字参数:**kwargs(接收任意数量关键字参数,转字典)。

通俗解释:

就像 “外卖下单”:

  • 默认参数:默认加辣(可改);
  • 位置参数:先选菜品,再选份数(顺序固定);
  • 可变参数:加 N 个配菜(数量不限);
  • 关键字参数:备注 “少糖、多醋”(键值对形式,不限数量)。

使用场景:

  • 默认参数:简化常用参数的调用(如printend="\n");
  • 可变参数:兼容任意数量的参数(如print(*objects));
  • 关键字参数:接收灵活的键值对参数(如配置项)。

重点提醒:

  • 默认参数的陷阱:默认参数在函数定义时初始化,若默认参数是可变对象(如列表),多次调用会累积值:

    def func(lst=[]):
        lst.append(1)
        print(lst)
    func()  # 输出:[1]
    func()  # 输出:[1,1](而非[1])
    # 修复:默认参数用None,内部初始化
    def func(lst=None):
        if lst is None:
            lst = []
        lst.append(1)
        print(lst)
    
  • 参数顺序:必选参数 → 默认参数 → *args → 关键字参数 → **kwargs

6.2.代码示例

def func(a, b=10, *args, **kwargs):
    """
    综合参数示例
    :param a: 必选位置参数
    :param b: 默认参数(有默认值)
    :param args: 可变位置参数(元组)
    :param kwargs: 可变关键字参数(字典)
    """
    print(f"必选参数a:{a}")
    print(f"默认参数b:{b}")
    print(f"可变位置参数args:{args}(类型:{type(args)})")
    print(f"可变关键字参数kwargs:{kwargs}(类型:{type(kwargs)})")

# 调用方式1:仅传必选参数
func(1)
# 输出:
# 必选参数a:1
# 默认参数b:10
# 可变位置参数args:()(类型:<class 'tuple'>)
# 可变关键字参数kwargs:{}(类型:<class 'dict'>)

# 调用方式2:传位置参数+默认参数覆盖
func(1, 20)
# 输出:
# 必选参数a:1
# 默认参数b:20
# 可变位置参数args:()(类型:<class 'tuple'>)
# 可变关键字参数kwargs:{}(类型:<class 'dict'>)

# 调用方式3:传可变位置参数
func(1, 20, 30, 40)
# 输出:
# 必选参数a:1
# 默认参数b:20
# 可变位置参数args:(30, 40)(类型:<class 'tuple'>)
# 可变关键字参数kwargs:{}(类型:<class 'dict'>)

# 调用方式4:传可变关键字参数
func(1, 20, 30, 40, name="张三", age=20)
# 输出:
# 必选参数a:1
# 默认参数b:20
# 可变位置参数args:(30, 40)(类型:<class 'tuple'>)
# 可变关键字参数kwargs:{'name': '张三', 'age': 20}(类型:<class 'dict'>)

# ========== 参数解包(进阶用法) ==========
# 列表解包为位置参数
lst = [1, 2, 3, 4]
func(*lst)  # 等价于func(1,2,3,4)
# 输出:
# 必选参数a:1
# 默认参数b:2
# 可变位置参数args:(3, 4)(类型:<class 'tuple'>)
# 可变关键字参数kwargs:{}(类型:<class 'dict'>)

# 字典解包为关键字参数
dic = {"name": "李四", "age": 30}
func(1, 2, **dic)  # 等价于func(1,2,name="李四",age=30)
# 输出:
# 必选参数a:1
# 默认参数b:2
# 可变位置参数args:()(类型:<class 'tuple'>)
# 可变关键字参数kwargs:{'name': '李四', 'age': 30}(类型:<class 'dict'>)

6.3.*args **kwargs 区别

6.3.1.*args 应用场景

*args 用于接收任意数量的位置参数(打包成元组),核心场景:

  1. 函数需要兼容不确定数量的位置参数(如求和函数 def sum_nums(*args): return sum(args));
  2. 批量传递参数(如把列表 / 元组解包后传给函数,func(*[1,2,3]));
  3. 封装通用逻辑(如装饰器、高阶函数,适配不同参数个数的函数)。
6.3.2.**kwargs 应用场景

**kwargs 用于接收任意数量的关键字参数(打包成字典),核心场景:

  1. 函数需要接收灵活的键值对参数(如配置项、可选参数,def print_info(**kwargs): print(kwargs));
  2. 传递带名称的可选参数(如封装 API 请求,接收 headers/timeout 等可选配置);
  3. 字典解包传参(func(**{"name":"张三","age":20})),适配需关键字参数的函数;
  4. 类的初始化 / 装饰器中,接收并透传自定义配置参数。
6.3.3.核心区别总结
语法 参数类型 核心场景
*args 位置参数(元组) 兼容不确定数量的无名称参数
**kwargs 关键字参数(字典) 兼容不确定数量的带名称参数

7.递归函数

7.1.核心概念

递归函数是「调用自身的函数」,必须包含两个部分:

  • 递归条件:函数调用自身,拆解问题;
  • 终止条件:停止递归,返回结果(否则无限递归)。

通俗解释:

就像 “剥洋葱”:每次剥一层(递归调用),直到剥到芯(终止条件),再逐层返回结果。

使用场景:

  • 数学问题(阶乘、斐波那契数列);
  • 树形结构遍历(目录、二叉树);
  • 分治算法(快速排序、归并排序)。

重点提醒:

  • 递归深度限制:Python 默认递归深度约 1000 层,超过会抛出RecursionError

  • 尾递归优化:Python 不支持尾递归优化(即使递归是最后一步,仍会占用栈空间),深度大时建议用循环替代;

  • 避免重复计算:如斐波那契数列,可加缓存(functools.lru_cache)优化:

    from functools import lru_cache
    @lru_cache(maxsize=None)
    def fib(n):
        if n <= 1:
            return n
        return fib(n-1) + fib(n-2)
    

7.2.代码示例

# ========== 基础示例:计算阶乘(n! = n × (n-1)!) ==========
def factorial(n):
    """
    计算n的阶乘
    :param n: 非负整数
    :return: n!
    """
    # 终止条件(核心:必须有,否则栈溢出)
    if n == 0 or n == 1:
        return 1
    # 递归条件:拆解问题为n × (n-1)!
    return n * factorial(n-1)

# 执行过程(以factorial(3)为例):
# factorial(3) → 3 × factorial(2)
# factorial(2) → 2 × factorial(1)
# factorial(1) → 1(终止)
# 最终:3×2×1=6
print(factorial(3))  # 输出:6

# ========== 实战示例:遍历目录(递归遍历子目录) ==========
import os
def traverse_dir(path):
    """递归遍历目录下所有文件"""
    # 终止条件:如果是文件,直接返回
    if os.path.isfile(path):
        print(f"文件:{path}")
        return
    # 递归条件:如果是目录,遍历子项
    print(f"目录:{path}")
    for item in os.listdir(path):
        item_path = os.path.join(path, item)
        traverse_dir(item_path)  # 递归调用

# 调用(替换为自己的目录路径)
traverse_dir("./test_dir")
# 输出:
# 目录:./test_dir
# 目录:./test_dir\a1
# 目录:./test_dir\a1\b
# 目录:./test_dir\a2
# 目录:./test_dir\a2\c

8.偏函数

8.1.核心概念

偏函数是「固定函数部分参数,生成新函数」的工具,通过functools.partial实现,简化重复调用。

通俗解释:

就像 “预设参数的快捷键”:比如经常用int(x, base=2)将二进制转整数,偏函数可固定base=2,生成新函数int2,直接调用int2("101")即可。

使用场景:

  • 固定常用参数,简化函数调用(如进制转换、日志前缀);
  • 适配函数参数(如将 3 参数函数转为 2 参数,满足其他接口要求)。

重点提醒:

  • 偏函数固定的是「默认参数」,调用新函数时仍可覆盖:

    print(int2("101", base=10))  # 输出:101(覆盖base=2)
    
  • 偏函数本质是高阶函数(返回新函数),底层基于闭包实现。

8.2.代码示例

from functools import partial

# ========== 基础示例:固定int的base参数 ==========
# 原函数:int(x, base=10)(默认十进制)
# 偏函数:固定base=2,生成二进制转整数的函数
int2 = partial(int, base=2)

# 调用新函数(无需传base)
print(int2("101"))  # 输出:5(等价于int("101", base=2))
print(int2("110"))  # 输出:6

# ========== 进阶示例:固定自定义函数的参数 ==========
def add(a, b, c):
    """计算a+b+c"""
    return a + b + c

# 固定a=10,生成新函数add10(只需传b和c)
add10 = partial(add, 10)
print(add10(2, 3))  # 输出:15(10+2+3)

# 固定a=10,b=20,生成新函数add10_20(只需传c)
add10_20 = partial(add, 10, 20)
print(add10_20(3))  # 输出:33(10+20+3)

三、模块进阶

1.模块导入进阶

1.1.核心概念

基础导入(import os)满足日常需求,但复杂项目中需掌握「别名导入、条件导入、动态导入、包导入」,解决命名冲突、跨目录调用、按需加载等问题。

1.2.通俗解释

就像 “整理工具箱”:基础导入是直接拿工具,进阶导入是给工具改名、按需拿工具、从不同抽屉拿工具,避免工具重名或拿错。

1.3.代码示例

1.3.1.别名导入(解决命名冲突 / 简化名称)
# 场景:模块名太长/多个模块名冲突
import numpy as np  # 给numpy取别名np(行业通用写法)
import pandas as pd
from sklearn.linear_model import LinearRegression as LR  # 给类取别名

# 使用别名调用,简洁且避免冲突
arr = np.array([1,2,3])
model = LR()

重点:别名是项目中统一代码风格的常用方式,如 import matplotlib.pyplot as plt 是固定写法。

1.3.2.条件导入(按需加载模块)
# 场景:不同系统/环境加载不同模块(如Windows/Linux兼容)
import sys

if sys.platform == "win32":
    # Windows系统加载专属模块
    import win32api as api
elif sys.platform == "linux":
    # Linux系统加载专属模块
    import linux_api as api
else:
    # 其他系统加载通用模块
    import common_api as api

# 后续统一使用api,无需区分系统
api.do_something()

重点:避免在不兼容的环境中导入错误模块,提升代码兼容性。

1.3.3.动态导入(运行时按需导入)
# 场景:模块体积大/仅在特定条件下使用,避免启动时全部加载
def load_module(module_name):
    """动态导入模块"""
    # __import__ 是Python内置函数,接收字符串模块名
    module = __import__(module_name)
    return module

# 仅需要时才导入numpy(比如用户触发某个功能时)
if input("是否需要数据分析?(y/n)") == "y":
    np = load_module("numpy")
    print(np.array([1,2,3]))

进阶写法(更推荐)

import importlib
# importlib.import_module 是__import__的封装,更易读
np = importlib.import_module("numpy")

重点:减少程序启动时间和内存占用,适合大型项目。

1.3.4.包导入(跨目录调用模块)

假设项目结构如下:

my_project/
├── main.py          # 主文件
└── utils/           # 工具包
    ├── __init__.py  # 包标识文件(空文件也可以)
    └── tools.py     # 工具模块(含函数add(a,b))
# main.py中导入utils包下的tools模块
# 方式1:导入整个模块
import utils.tools as ut
print(ut.add(1,2))  # 输出3

# 方式2:导入模块中的指定函数
from utils.tools import add
print(add(3,4))  # 输出7

重点__init__.py 是包的标识,缺一不可;包导入的核心是「按目录层级指定模块路径」。

1.4.重点提醒

  • 导入顺序:标准库(如os/sys)→ 第三方库(如numpy)→ 自定义模块,提升代码可读性;
  • 避免循环导入:如 a.py 导入 b.pyb.py 又导入 a.py,会导致报错,解决方法是把导入语句放在函数内部 / 调整代码结构

2.作用域与 __name__ == "__main__"

2.1.核心概念

  • 模块的作用域:模块内的变量 / 函数 / 类默认是 “公开的”,可被其他模块导入;以 _ 开头的变量(如 _private_var)是 “私有” 的,不建议外部导入;
  • __name__:Python 内置变量,标识模块的运行方式:
    • 当模块直接运行时,__name__ == "__main__"
    • 当模块被导入时,__name__ == 模块名

2.2.通俗解释

就像 “剧本角色”:模块直接运行时是 “主角”(执行测试代码),被导入时是 “配角”(只提供功能,不执行测试代码)。

2.3.代码示例

创建 calc.py 模块:

# calc.py
def add(a, b):
    """加法函数"""
    return a + b

# 私有变量(外部导入时不建议使用)
_private_var = "我是私有变量"

# 测试代码:只有直接运行calc.py时才执行
if __name__ == "__main__":
    # 这里写测试代码,验证函数是否正常
    print("测试add函数:", add(1,2))  # 输出3
    print("模块名:", __name__)  # 输出__main__

main.py 中导入:

# main.py
from calc import add

# 调用add函数
print(add(3,4))  # 输出7
# 尝试导入私有变量(不推荐,PEP8规范不建议)
# from calc import _private_var
print("calc模块的__name__:", calc.__name__)  # 输出calc

2.4.核心价值

  • 把测试代码放在 if __name__ == "__main__" 中,模块被导入时不会执行测试代码,避免冗余;
  • 实现 “模块既可以单独运行测试,又可以被导入提供功能”,是 Python 项目的标准写法。

3.缓存机制(提升导入效率)

3.1.核心概念

Python 导入模块时会做缓存:第一次导入模块时,会执行模块代码并将模块对象存入 sys.modules 字典;后续再次导入时,直接从缓存读取,不会重复执行模块代码。

3.2.通俗解释

就像 “缓存零食”:第一次买零食(导入模块)要花钱 + 时间,之后再吃(再次导入)直接从冰箱(缓存)拿,不用再买。

3.3.代码示例

项目结构:

ProjectTest/
├── test_cache.py
└── main.py
  1. test_cache.py
# test_cache.py
print("模块被执行了!")  # 导入时会执行的代码
var = 10
  1. main.py
# main.py
import sys
# 第一次导入:执行模块代码,输出“模块被执行了!”
import test_cache
print(test_cache.var)  # 输出10

# 第二次导入:从缓存读取,不会输出“模块被执行了!”
import test_cache
test_cache.var = 20  # 修改模块变量

# 查看缓存字典
print("缓存中的模块:", "test_cache" in sys.modules)  # 输出True

# 清除缓存(强制重新导入,慎用)
del sys.modules["test_cache"]
# 第三次导入:重新执行模块代码,再次输出“模块被执行了!”
import test_cache
print(test_cache.var)  # 输出10(恢复初始值)

3.4.重点提醒

  • 缓存提升效率,但如果修改了模块代码,重启程序才能生效(缓存不会自动更新);

  • 调试时如需强制重新加载模块,可用 importlib.reload()

    import importlib
    import test_cache
    importlib.reload(test_cache)  # 重新执行模块代码
    

4.自定义包与 __init__.py 进阶

4.1.核心概念

__init__.py 不仅是包的标识,还可以:

  • 控制包的导入内容(简化外部导入);
  • 定义包的版本、作者等元信息;
  • 执行包的初始化逻辑。

4.2.通俗解释

就像 “包的说明书”:告诉外部 “这个包包含哪些功能,怎么快速使用”。

4.3.代码示例

项目结构:

ProjectTest/
├── __init__.py
├── math_utils.py  # 含add(a,b)、sub(a,b)
└── str_utils.py   # 含upper_str(s)、lower_str(s)
  1. math_utils.py
def add(a, b):
    return a + b

def sub(a, b):
    return a - b
  1. str_utils.py
def upper_str(s):
    return s.upper()

def lower_str(s):
    return s.lower()
  1. __init__.py(核心进阶)
# 定义包的元信息
__version__ = "1.0.0"
__author__ = "自学Python的你"

# 控制导入:让外部可以直接从包导入这些函数,无需写具体模块名
from .math_utils import add, sub
from .str_utils import upper_str, lower_str

# 初始化逻辑(包被导入时执行)
print(f"加载my_package v{__version__},作者:{__author__}")
  1. 外部使用包(main.py
# 简化导入:直接从包导入函数,不用关心内部模块结构
from my_package import add, upper_str

print(add(1,2))  # 输出3
print(upper_str("hello"))  # 输出HELLO

# 查看包的元信息
import my_package
print(my_package.__version__)  # 输出1.0.0

# 顺序输出:
# 加载my_package v1.0.0,作者:自学Python的你
# 3
# HELLO
# 1.0.0

4.4.重点提醒

  • __init__.py 中的 . 表示 “当前包”,是相对导入的标识;
  • 避免在 __init__.py 中写过多业务逻辑,只保留 导入控制初始化代码,保持简洁。

5.模块的查找路径(解决 “找不到模块” 报错)

5.1.核心概念

Python 导入模块时,会按顺序在以下路径查找:

  1. 当前执行脚本的目录;
  2. sys.path 列表中的目录(包含标准库、第三方库路径);
  3. 环境变量 PYTHONPATH 指定的目录。

5.2.通俗解释

就像 “找文件”:Python 会按固定路径顺序找模块,找不到就报错 ModuleNotFoundError

5.3.代码示例

项目结构:

ProjectTest/
├── main.py
└── my_modules
	└── my_custom_module.py
  1. my_custom_module.py

    var = "ok"
    
  2. main.py

import sys

# 查看当前模块查找路径
print("模块查找路径:")
for idx, path in enumerate(sys.path):
    print(f"{idx+1}. {path}")

# 场景:添加自定义目录到查找路径(解决“找不到模块”)
import os
# 获取自定义模块所在目录(比如D盘子目录下的my_modules)
custom_path = "D:/Workspaces/python/ProjectTest/my_modules"
# 把自定义目录加入sys.path(临时生效,重启程序失效)
if custom_path not in sys.path:
    sys.path.append(custom_path)

# 现在可以导入 my_modules 下的模块了
import my_custom_module
print(my_custom_module.var)

# 顺序输出:
# 模块查找路径:
# 1. D:\Workspaces\python\ProjectTest
# 2. D:\Workspaces\python\ProjectTest
# 3. C:\Program Files\JetBrains\PyCharm 2024.3.6\plugins\python-ce\helpers\pycharm_display
# 4. D:\Servers\Python313\python313.zip
# 5. D:\Servers\Python313\DLLs
# 6. D:\Servers\Python313\Lib
# 7. D:\Servers\Python313
# 8. D:\Servers\Python313\Lib\site-packages
# 9. C:\Program Files\JetBrains\PyCharm 2024.3.6\plugins\python-ce\helpers\pycharm_matplotlib_backend
# 10. C:\Program Files\JetBrains\PyCharm 2024.3.6\plugins\python-ce\helpers\pycharm_plotly_backend
# ok

各类路径的优先级

当前执行脚本目录(1、2) > sys.path中的IDE辅助路径(3、9、10) > sys.path中的标准库路径(4-7) > sys.path中的第三方库路径(8)

分类类型 对应列表序号 具体路径 核心作用
1. 当前执行脚本目录 1、2 D:\Workspaces\python\ProjectTest Python 优先查找当前运行脚本所在目录(重复出现是 PyCharm 运行机制导致,不影响优先级),优先加载项目内的自定义模块。
2. IDE 辅助路径 3、9、10 PyCharm 插件相关路径(pycharm_display/matplotlib_backend/plotly_backend) 非 Python 原生路径,是 PyCharm 为了支持调试、图表显示等功能额外添加的辅助路径,普通运行(非 IDE)不会出现。
3. Python 标准库路径 4、5、6、7 4:python313.zip(标准库压缩包)5:DLLs(Python 底层依赖 DLL)6:Lib(标准库源码)7:Python 安装根目录 存放 Python 内置标准库(如os/sys/json),是sys.path的核心组成,所有 Python 环境都会包含。
4. 第三方库路径 8 D:\Servers\Python313\Lib\site-packages 存放通过pip install安装的第三方库(如numpy/pandas/requests),是第三方模块的默认安装位置。

5.4.重点提醒

  • 临时添加路径:sys.path.append(),仅当前程序运行时生效;
  • 永久添加路径:修改环境变量 PYTHONPATH,或把自定义模块放到 sys.path 已有的目录中(如 Python 的 site-packages 目录)。

6.模块的重载(调试 / 热更新必备)

6.1.核心概念

importlib.reload() 可以强制重新加载已导入的模块,执行最新的模块代码(突破缓存机制)。

6.2.通俗解释

就像 “刷新页面”:修改模块代码后,不用重启程序,直接刷新(重载)模块即可生效。

6.3.代码示例

import importlib
# 第一次导入模块
import test_reload

# 修改test_reload.py的代码后,重载模块
test_reload = importlib.reload(test_reload)

# 调用重载后的函数,执行最新逻辑
test_reload.my_func()

6.4.重点提醒

  • 重载仅适用于已导入的模块,未导入的模块需先 import
  • 重载不会影响已创建的对象:如重载前创建的 obj = test_reload.MyClass(),仍使用旧代码,需重新创建对象。

7.第三方模块的管理(pip 进阶)

7.1.核心概念

除了基础的 pip install 模块名,还需掌握版本管理、镜像源、导出 / 导入依赖,解决第三方模块的安装 / 兼容问题。

7.2.通俗解释

就像 “管理手机 APP”:安装指定版本、换更快的应用商店(镜像源)、备份已装 APP 列表。

7.3.常用命令

7.3.1.导出/导入依赖
# 1. 安装指定版本的模块(解决版本兼容问题)
pip install numpy==1.24.0

# 2. 升级模块到最新版本
pip install --upgrade numpy
# Looking in indexes: https://mirrors.aliyun.com/pypi/simple/
# Requirement already satisfied: numpy in d:\servers\python313\lib\site-packages (2.4.0)
# Collecting numpy
#   Downloading https://mirrors.aliyun.com/pypi/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl (12.3 MB)
#      ---------------------------------------- 12.3/12.3 MB 4.9 MB/s  0:00:02
# Installing collected packages: numpy
#   Attempting uninstall: numpy
#     Found existing installation: numpy 2.4.0
#     Uninstalling numpy-2.4.0:
#       Successfully uninstalled numpy-2.4.0
# Successfully installed numpy-2.4.1

# 3. 卸载模块
pip uninstall numpy -y

# 4. 使用国内镜像源(提速,推荐阿里云)
pip install numpy -i https://mirrors.aliyun.com/pypi/simple/

# 5. 导出当前环境的依赖包(生成requirements.txt)
pip freeze > requirements.txt
# 导出文件内容:
# numpy==2.4.1

# 6. 批量安装依赖包(部署项目时)
pip install -r requirements.txt

# 7. 查看已安装的模块及版本
pip list
7.3.2.查看配置/修改镜像源
# 查看pip完整配置(包含镜像源)
pip config list

# 简化查看(只看索引源)
pip config get global.index-url

# -v 表示显示详细日志,install xxx --dry-run 表示“模拟安装”(不实际下载)
pip install numpy -v --dry-run
# 关键日志解读:
# Looking in indexes: https://mirrors.aliyun.com/pypi/simple/  # 阿里云源
# 或
# Looking in indexes: https://pypi.org/simple/  # pip默认官方源

# 查看配置文件位置
pip config list -v
# 输出示例(Windows 系统):
# For variant 'global', will try loading 'C:\ProgramData\pip\pip.ini'
# For variant 'user', will try loading 'C:\Users\DELL\pip\pip.ini'
# For variant 'user', will try loading 'C:\Users\DELL\AppData\Roaming\pip\pip.ini'
# For variant 'site', will try loading 'D:\Servers\Python313\pip.ini'
# global.index-url='https://mirrors.aliyun.com/pypi/simple/'
# install.trusted-host='mirrors.aliyun.com'

# 临时 / 永久切换镜像源
# 1. 临时使用(仅本次命令生效)
# 用阿里云源安装numpy
pip install numpy -i https://mirrors.aliyun.com/pypi/simple/

# 2. 永久配置(推荐,一劳永逸)
# 配置阿里云镜像源(Windows/Linux/Mac通用)
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/

# 恢复默认官方源
pip config unset global.index-url

7.4.重点提醒

  • 虚拟环境:不同项目用不同虚拟环境(venv/conda),避免模块版本冲突(如 A 项目需要 numpy1.24,B 项目需要 numpy1.26);
  • 离线安装:下载 .whl 文件,用 pip install 文件名.whl 安装,适合无网络环境。

7.5.WHL文件

7.5.1.国内 WHL 文件下载渠道
优先级 名称 地址 特点 使用方式
1(首选) 清华大学 PyPI 镜像 https://pypi.tuna.tsinghua.edu.cn/simple/ 最全、更新快 1. 打开地址,输入库名回车进入详情页;2. 选择匹配系统 / Python 版本的 WHL 文件下载(如 cp313 对应 Python3.13,win_amd64 对应 Windows 64 位)
2(备选) 阿里云 PyPI 镜像 https://mirrors.aliyun.com/pypi/simple/ 速度快 和清华镜像完全一致,输入库名查找对应 WHL 文件下载
3(专用) Unofficial Windows Binaries for Python Extension Packages https://www.lfd.uci.edu/~gohlke/pythonlibs/ 国外地址(国内可访问),Windows WHL 最全,含 scipy/opencv-python 等难编译库 页面按字母排序,找到对应库,下载匹配 Python 版本 / 系统的 WHL 文件
4(应急) 豆瓣 PyPI 镜像 https://pypi.doubanio.com/simple/ 备用渠道 清华 / 阿里云镜像访问失败时使用,查找方式同清华镜像
4(应急) 中科大 PyPI 镜像 https://pypi.mirrors.ustc.edu.cn/simple/ 备用渠道 清华 / 阿里云镜像访问失败时使用,查找方式同清华镜像
7.5.2.关键技巧:正确选择 WHL 文件

WHL 文件名有严格的命名规则,必须匹配你的环境,否则安装报错!示例:

numpy-1.26.4-cp313-cp313-win_amd64.whl
├─ numpy:库名
├─ 1.26.4:版本号
├─ cp313:Python版本(cp313=Python3.13,cp312=Python3.12)
├─ win_amd64:系统(win_amd64=Windows64位,win32=Windows32位,manylinux_x86_64=Linux)

查看自己的环境参数

# 运行以下代码,获取匹配的WHL标识
import sys
print("Python版本:", sys.version.split()[0])  # 如3.13.0
print("系统架构:", "win_amd64" if sys.maxsize > 2**32 else "win32")  # Windows下判断32/64位
7.5.3.WHL 文件安装方法
# 进入WHL文件下载目录(如下载到D:\Downloads)
cd D:\Downloads

# 用pip安装(替换为你的WHL文件名)
pip install numpy-1.26.4-cp313-cp313-win_amd64.whl
7.5.4.核心总结
  1. 优先选择:清华镜像(最全)> 阿里云镜像(最快),满足 99% 的 WHL 下载需求;
  2. Windows 特殊库:优先用 gohlke 网站(难编译的库都有);
  3. 关键避坑:WHL 文件名必须匹配 Python 版本 / 系统架构,否则安装失败;
  4. 替代方案:如果找不到对应 WHL,可直接用pip install 库名 -i 国内镜像地址,pip 会自动下载适配的 WHL(无需手动找)。

8.模块的文档与自省(提升可维护性)

8.1.核心概念

  • 模块文档:用 __doc__ 查看模块 / 函数的文档字符串,或 help() 函数获取详细说明;
  • 模块自省:通过 dir() 查看模块的所有属性 / 方法,了解模块的功能。

8.2.通俗解释

就像 “查看工具说明书”:不用翻源码,直接查模块的功能和用法。

8.3.代码示例

import numpy

# 查看模块的文档字符串
print("numpy文档:", numpy.__doc__[:100])  # 输出前100个字符

# 查看函数的文档
print("numpy.array文档:", numpy.array.__doc__[:100])

# 用help获取详细说明(交互式环境更友好)
# help(numpy.array)

# 查看模块的所有属性/方法
print("numpy的属性列表(前10个):", dir(numpy)[:10])

8.4.重点提醒

  • 自定义模块要写文档字符串:每个模块 / 函数 / 类都加 """功能说明""",提升代码可维护性;
  • dir() 是调试神器:快速了解陌生模块的可用功能。

四、文件处理

Python 文件操作核心是「读写文件」和「文件系统操作」,常用 open() 函数和 os/pathlib 模块。

1.核心概念

  • 文件模式r(读,默认)、w(写,覆盖)、a(追加)、r+(读写)、b(二进制)、encoding(编码,如utf-8);
  • 上下文管理器with 语句,自动关闭文件,避免资源泄漏。

2.基础操作:文本文件的读写

2.1.打开文件:open() 函数

# 基础语法
f = open("文件路径", mode="打开模式", encoding="编码格式")
参数 说明
文件路径 绝对路径(如D:\test.txt)/ 相对路径(如test.txt,当前脚本目录)
mode(关键) r:只读(默认);w:写入(清空原有内容);a:追加(在末尾加内容);r+:读写
encoding 文本文件必加(推荐utf-8),二进制文件不用加

⭐️ 新手避坑:Windows 路径用\\/,避免转义问题(如D:\\test.txtD:/test.txt)。

2.2.读取文件(3 种方式)

2.2.1.基础读取(逐行 / 全部 / 指定字节)
# 推荐方式:with语句(自动关闭文件)
# 场景1:读取全部内容(小文件适用)
with open("test.txt", mode="r", encoding="utf-8") as f:
    content = f.read()  # 读取全部内容为字符串
    print(content)

# 场景2:逐行读取(大文件适用,节省内存)
with open("test.txt", mode="r", encoding="utf-8") as f:
    for line in f:  # 直接遍历文件对象,逐行读取
        print(line.strip())  # strip()去掉换行符/首尾空格

# 场景3:读取指定行数/字节
with open("test.txt", mode="r", encoding="utf-8") as f:
    line1 = f.readline()  # 读取第一行
    lines = f.readlines()  # 读取剩余所有行,返回列表
    print("第一行:", line1)
    print("剩余行:", lines)

⭐️ 重点

  • 小文件(<100MB)用read()/readlines()
  • 大文件(如 1GB 日志)必须用for line in f逐行读,避免内存溢出。
2.2.2.读取常见问题解决
  • 报错UnicodeDecodeError:编码不匹配,确认文件实际编码(如gbk),修改encoding="gbk"
  • 报错FileNotFoundError:文件路径错误,检查路径拼写 / 绝对路径是否正确。

2.3.写入文件(新增 / 覆盖 / 追加)

# 场景1:覆盖写入(w模式,原有内容会被清空)
with open("test.txt", mode="w", encoding="utf-8") as f:
    f.write("Hello Python\n")  # 写入字符串,\n是换行符
    f.writelines(["第一行\n", "第二行\n"])  # 写入列表(批量写)

# 场景2:追加写入(a模式,推荐写日志/累加内容)
with open("test.txt", mode="a", encoding="utf-8") as f:
    f.write("这是追加的内容\n")

⭐️ 重点

  • w模式:文件不存在则创建,存在则清空;
  • a模式:文件不存在则创建,存在则在末尾追加;
  • 写入后内容不会立即保存到磁盘,with结束时自动刷新(手动刷新用f.flush())。

3.进阶操作:二进制文件处理

处理图片、视频、压缩包等二进制文件,模式用rb(读)/wb(写),无需指定编码:

# 1. 读取二进制文件(如图片)
with open("logo.png", mode="rb") as f:
    img_data = f.read()  # 读取二进制数据

# 2. 写入二进制文件(复制图片)
with open("logo_copy.png", mode="wb") as f:
    f.write(img_data)  # 写入二进制数据,完成复制

⭐️ 应用场景:文件备份、图片 / 视频处理、读取二进制配置文件。

4.实用技巧:文件路径与目录操作(os/pathlib)

新手优先学pathlib(Python3.4 + 推荐,更直观),替代传统os.path

4.1.路径操作(获取 / 拼接 / 判断)

from pathlib import Path

# 1. 创建路径对象(支持绝对/相对路径)
file_path = Path("test.txt")  # 相对路径(当前脚本目录)
abs_path = Path("D:/test.txt")  # 绝对路径

# 2. 核心判断(文件/目录通用)
print("是否存在:", file_path.exists())  # 是否存在: True
print("是否是文件:", file_path.is_file())  # 是否是文件: True
print("是否是目录:", file_path.is_dir())  # 是否是目录: False
print("绝对路径:", file_path.absolute())  # 绝对路径: D:\Workspaces\python\ProjectTest\test.txt
print("文件名:", file_path.name)  # 文件名: test.txt
print("文件名(无后缀):", file_path.stem)  # 文件名(无后缀): test
print("后缀:", file_path.suffix)  # 后缀: .txt

# 3. 路径拼接(新手必学,避免手动拼字符串)
# 拼接文件路径:D:/docs/test.txt
file_new = Path("D:/") / "docs" / "test.txt"
# 拼接目录路径:D:/docs/sub_dir
dir_new = Path("D:/") / "docs" / "sub_dir"
print("文件拼接路径:", file_new)  # 文件拼接路径: D:\docs\test.txt
print("目录拼接路径:", dir_new)  # 目录拼接路径: D:\docs\sub_dir

⭐️ 重点:路径拼接用/(Path 对象的运算符),Windows/Linux/Mac 通用,避免手动写\/导致的错误。

4.2.目录操作(创建 / 遍历 / 删除)

from pathlib import Path
import shutil

# ========== 1. 创建目录 ==========
dir_path = Path("D:/new_dir")
# 创建单级目录(exist_ok=True:目录已存在不报错)
dir_path.mkdir(exist_ok=True)
# 创建多级目录(parents=True:自动创建父目录)
Path("D:/new_dir/sub_dir1/sub_dir2").mkdir(parents=True, exist_ok=True)

# ========== 2. 遍历目录 ==========
# 场景1:遍历目录下所有文件(不含子目录)
print("\n【遍历当前目录文件】")
for file in Path("D:/new_dir").iterdir():
    if file.is_file():
        print("文件:", file.name)
# 输出:
# 文件: a.txt

# 场景2:递归遍历所有文件(含子目录)
print("\n【递归遍历所有文件】")
for file in Path("D:/new_dir").rglob("*"):  # rglob=递归遍历
    if file.is_file():
        print("递归文件:", file)
# 输出:
# 递归文件: D:\new_dir\a.txt
# 递归文件: D:\new_dir\sub_dir1\b.txt

# ========== 3. 删除目录 ==========
# 删除空目录(仅能删空目录)
Path("D:/new_dir/sub_dir1/sub_dir2").rmdir()
# 删除非空目录(谨慎!删除所有子文件/目录)
# shutil.rmtree("D:/new_dir")  # 执行前务必确认路径正确!

4.3.文件操作(创建 / 复制 / 重命名 / 删除)

from pathlib import Path
import shutil

# ========== 1. 创建空文件 ==========
file_path = Path("D:/new_dir/test.txt")
# 方式1:touch(推荐,空文件)
file_path.touch(exist_ok=True)  # exist_ok=True:文件已存在不报错
# 方式2:通过写入创建(含内容)
with open(file_path, "w", encoding="utf-8") as f:
    f.write("测试内容")

# ========== 2. 复制文件 ==========
file_copy = Path("D:/new_dir/test_copy.txt")
shutil.copy(file_path, file_copy)  # 复制文件内容+权限
# 复制文件到其他目录(自动创建文件名)
shutil.copy(file_path, "D:/new_dir/sub_dir1/")

# ========== 3. 重命名/移动文件 ==========
# 重命名(同目录)
file_path.rename("D:/new_dir/test_rename.txt")
# 移动文件(跨目录)
new_file_path = Path("D:/new_dir/sub_dir1/test_move.txt")
file_path.rename(new_file_path)  # 原文件会被移动到新路径

# ========== 4. 删除文件 ==========
# 先判断是否存在,避免报错
if new_file_path.exists():
    new_file_path.unlink()  # 删除文件(仅删文件,不能删目录)

⭐️ 核心避坑

  • 路径拼接用/(Path 对象的运算符),不要手动拼\/
  • 删除操作(unlink()/rmtree())前必须用exists()判断,避免文件 / 目录不存在时报错;
  • 复制 / 移动文件时,目标目录必须存在(可先用mkdir(parents=True)创建)。

4.4.os 模块(传统方式)

import os

# 1. 创建目录
os.makedirs("data/test", exist_ok=True)  # 递归创建,已存在不报错

# 2. 遍历目录
for root, dirs, files in os.walk("data"):
    print(f"根目录:{root},子目录:{dirs},文件:{files}")

# 3. 文件重命名/删除
os.rename("test.txt", "new_test.txt")
os.remove("new_test.txt")  # 删除文件

# 4. 获取文件信息
print(os.path.abspath("test.txt"))  # 绝对路径
print(os.path.exists("test.txt"))   # 是否存在
print(os.path.isfile("test.txt"))   # 是否是文件

4.5.补充总结(文件 + 目录操作核心)

操作类型 核心方法(pathlib) 注意事项
路径拼接 Path("a") / "b" / "c.txt" 不用手动拼\//,跨系统兼容
目录创建 mkdir(parents=True, exist_ok=True) parents=True创建多级目录,exist_ok=True避免重复创建报错
文件创建 touch() / open(..., "w") touch创建空文件,open创建带内容的文件
遍历文件 iterdir()(当前目录)/ rglob("*")(递归) rglob适合遍历所有子目录文件
复制文件 shutil.copy(源路径, 目标路径) 目标目录需存在
删除 文件:unlink();空目录:rmdir();非空目录:shutil.rmtree() rmtree慎用,会删除所有子内容

5.高级场景:CSV/JSON 文件处理

5.1.CSV 文件(表格数据,如 Excel 简化版)

import csv

# 1. 写入CSV
data = [
    ["姓名", "年龄", "城市"],
    ["张三", 20, "北京"],
    ["李四", 25, "上海"]
]
with open("user.csv", mode="w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(data)  # 批量写入

# 2. 读取CSV
with open("user.csv", mode="r", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print("行数据:", row)  # 每行返回列表,如["张三", "20", "北京"]

⭐️ 重点:写入时加newline="",避免 CSV 文件出现空行。

5.2.JSON 文件(配置 / 接口数据常用)

import json

# 1. 写入JSON(Python字典→JSON字符串)
user_data = {
    "name": "张三",
    "age": 20,
    "hobbies": ["读书", "编程"]
}
with open("user.json", mode="w", encoding="utf-8") as f:
    json.dump(user_data, f, ensure_ascii=False, indent=4)
    # ensure_ascii=False:显示中文;indent=4:格式化缩进

# 2. 读取JSON(JSON字符串→Python字典)
with open("user.json", mode="r", encoding="utf-8") as f:
    data = json.load(f)
    print("姓名:", data["name"])
    print("爱好:", data["hobbies"][0])

⭐️ 重点

  • json.dump():把 Python 对象写入文件;json.load():从文件读入 Python 对象;
  • 处理中文必须加ensure_ascii=False,否则中文会变成\uXXXX编码。

6.新手必记的核心规范(避坑指南)

  1. ⭐️ 始终用with语句操作文件,避免忘记close()导致资源泄露;
  2. ⭐️ 文本文件操作必加encoding="utf-8",二进制文件不加;
  3. 路径优先用pathlib.Path,避免手动拼接路径的错误;
  4. 写入文件前确认路径是否存在(用Path.exists());
  5. 大文件读取用for line in f逐行读,不要用read()一次性读入;
  6. 敏感操作(删除 / 覆盖)前先备份文件,避免数据丢失。

五、正则表达式

正则表达式(Regex)是处理字符串的 “瑞士军刀”—— 匹配、提取、替换、校验文本都能高效完成,比如验证手机号、提取网页中的邮箱、清洗日志数据等。以下内容循序渐进,从基础语法到实战场景,重点知识点用「⭐️」标注,确保新手能一步步掌握。

1.核心概念(先理解,再动手)

1.1.什么是正则表达式?

正则表达式是一套描述字符串规则的语法,Python 通过 re 模块实现正则功能,核心作用:

  • 匹配:判断字符串是否符合规则(如是否是手机号);
  • 提取:从字符串中提取符合规则的内容(如提取所有邮箱);
  • 替换:把字符串中符合规则的内容替换成指定内容(如屏蔽敏感词)。

1.2.核心原则(新手必记)

  • 正则匹配是按 “规则” 匹配字符,不是固定字符串;
  • Python 中使用正则需先导入 re 模块:import re
  • 正则语法对特殊字符(如 . * \)需转义(加 \),或使用原始字符串(r"规则")——⭐️ 新手优先用原始字符串,避免转义坑!

2.基础语法:匹配单个字符(入门第一步)

2.1.字符匹配符

正则的最小单位是 “字符匹配规则”,先掌握以下基础,能覆盖 80% 的简单场景:

语法 说明 示例 匹配结果
. 匹配任意单个字符(除换行符 \n r"a.b" 匹配 a1ba+ba b,不匹配 aba\nb
[] 匹配括号内的任意一个字符 r"a[0-9]b" 匹配 a0ba5b,不匹配 aaba*b
[^] 匹配不在括号内的任意一个字符 r"a[^0-9]b" 匹配 aaba*b,不匹配 a0ba5b
\d 匹配数字(0-9) r"\d\d" 匹配 1299,不匹配 ab1a
\D 匹配非数字字符 r"\D\D" 匹配 ab*%,不匹配 129a
\w 匹配字母、数字、下划线(a-z/A-Z/0-9/_) r"\w\w" 匹配 a1_5,不匹配 *%@#
\W 匹配非字母 / 数字 / 下划线的字符 r"\W\W" 匹配 *%@#,不匹配 a1_5
\s 匹配空白字符(空格、制表符 \t、换行 \n r"a\sb" 匹配 a ba\tb,不匹配 a1bab
\S 匹配非空白字符 r"a\Sb" 匹配 a1ba*b,不匹配 a ba\tb

2.2.示例代码(单个字符匹配)

import re

# 1. 匹配任意字符(.)
result = re.match(r"a.b", "a5b")  # match:从字符串开头匹配
print(". 匹配:", result.group() if result else "不匹配")  # 输出:a5b
# group() 是正则匹配成功后(返回 Match 对象),提取匹配到的字符串内容 的方法
# group() / group(0):获取整个正则规则匹配到的全部内容;
# group(n)(n≥1):获取正则中第 n 个 () 分组匹配到的内容(之前讲分组时提过)。

# 2. 匹配数字(\d)
result = re.match(r"\d\d", "123")
print("\d 匹配:", result.group())  # 输出:12(只匹配开头的两个数字)
# 同时报错: SyntaxWarning: invalid escape sequence '\d' print("\d 匹配:", result.group())  # 输出:12(只匹配开头的两个数字)
# 报错原因: print("\d 匹配:...") 里的 \d 没有加 r,Python 会把 \d 当成 “字符串转义符”—— 而 Python 本身没有 \d 这个转义符(Python 支持的转义符是 \n/\t/\\ 等),因此报 “无效转义序列” 警告。
# 解决方法: 给 print 的字符串加 r(最简单)
print(r"\d 匹配:", result.group())  # 加r,\d被当作普通字符,无报错

# 3. 匹配括号内字符([])
result = re.match(r"a[0-9a-z]b", "a7bZ")
print("[] 匹配:", result.group())  # 输出:a7b

⭐️ 重点re.match() 是 “从字符串开头匹配”,如果开头不符合规则,直接返回 None;新手先通过 match() 熟悉语法,后续学更灵活的 search()/findall()

3.进阶语法:匹配多个字符(数量限定)

3.1.数量限定符

单个字符匹配只能匹配固定长度,结合 “数量限定符” 可匹配任意长度的字符,这是正则的核心灵活点:

语法 说明 示例 匹配结果
* 匹配前面的字符 0 次或多次 r"a*b" 匹配 babaaab
+ 匹配前面的字符 1 次或多次 r"a+b" 匹配 abaaab,不匹配 b
? 匹配前面的字符 0 次或 1 次 r"a?b" 匹配 bab,不匹配 aab
{n} 匹配前面的字符恰好 n 次 r"a{3}b" 匹配 aaab,不匹配 abaab
{n,} 匹配前面的字符至少 n 次 r"a{2,}b" 匹配 aabaaab,不匹配 ab
{n,m} 匹配前面的字符 n~m 次 r"a{2,3}b" 匹配 aabaaab,不匹配 abaaaab

3.2.示例代码(数量限定)

import re

# 1. *:0次或多次
result = re.match(r"a*b", "aaab")
print("* 匹配:", result.group())  # 输出:aaab
result = re.match(r"a*b", "b")
print("* 匹配(0次):", result.group())  # 输出:b

# 2. +:1次或多次
result = re.match(r"a+b", "ab")
print("+ 匹配:", result.group())  # 输出:ab
result = re.match(r"a+b", "b")
print("+ 匹配(0次):", result)  # 输出:None(不匹配)

# 3. {n,m}:指定次数(实战高频,如手机号)
# 匹配11位手机号(开头是1,后面10位数字)
phone_pattern = r"1\d{10}"
result = re.match(phone_pattern, "13812345678")
print("手机号匹配:", result.group())  # 输出:13812345678
result = re.match(phone_pattern, "1381234567")
print("手机号匹配(长度不足):", result)  # 输出:None

⭐️ 重点:数量限定符是 “贪婪匹配”(默认匹配最长的结果),比如 r"a.*b" 匹配 a123b456b 时,会匹配整个 a123b456b,而非 a123b;后续会讲非贪婪匹配。

4.高级语法:边界匹配与分组(实战核心)

4.1.边界匹配(精准匹配,避免部分匹配)

4.1.1.边界匹配符
语法 说明 示例 匹配结果
^ 匹配字符串开头 r"^abc" 匹配 abc123,不匹配 123abc
$ 匹配字符串结尾 r"abc$" 匹配 123abc,不匹配 abc123
\b 匹配单词边界(字母 / 数字与非字母 / 数字的分界) r"\bhello\b" 匹配 hello world,不匹配 helloworld
\B 匹配非单词边界 r"\Bhello\B" 匹配 helloworld,不匹配 hello world
4.1.2.示例:精准校验手机号(避免匹配到 12 位数字)
import re

# 错误写法(会匹配138123456789的前11位)
bad_pattern = r"1\d{10}"
print(re.match(bad_pattern, "138123456789"))  # 输出:<re.Match object; span=(0, 11), match='13812345678'>

# 正确写法(^+$ 精准匹配整个字符串)
good_pattern = r"^1\d{10}$"
print(re.match(good_pattern, "13812345678"))  # 输出:<re.Match object; span=(0, 11), match='13812345678'>
print(re.match(good_pattern, "138123456789"))  # 输出:None

⭐️ 重点:校验类场景(手机号、邮箱、身份证)必须加 ^$,否则会出现 “部分匹配” 的错误。

4.2.group(): 分组(提取指定内容,实战高频)

4.2.1.什么时候能调用 group()

只有当 re.match()/re.search() 匹配成功时,才会返回 Match 对象,此时才能调用 group();如果匹配失败(返回 None),调用 group() 会直接报 AttributeError 错误。

✅ 正确习惯:调用前先判空

import re
result = re.match(r"\d\d", "abc123")
if result:  # 先判断匹配成功
    print(result.group())
else:
    print("匹配失败")  # 输出:匹配失败
4.2.2.基础用法(无分组场景)
  1. 核心语法
写法 作用 示例(匹配规则 r"\d\d",字符串 "123"
group() 等价于 group(0),提取整个正则规则匹配到的全部内容 group()"12"
group(0) 同上(显式指定第 0 组) group(0)"12"
  1. 示例:无分组提取
import re

# 规则:匹配开头的2个数字
pattern = r"\d\d"
string = "123456"

# 匹配成功,返回Match对象
result = re.match(pattern, string)

# 提取内容
print("group() →", result.group())    # 输出:12(提取全部匹配内容)
print("group(0) →", result.group(0)) # 输出:12(和group()完全一致)

⭐️ 重点:无分组时,group()group(0) 没有区别,都是提取 “整个规则匹配到的内容”。

4.2.2.进阶用法(分组场景,实战高频)

() 把需要提取的规则 “分组”,匹配后可通过 group(n) 提取第 n 组内容(group(0) 是整个匹配结果)。

示例 1:提取手机号的前缀和主体

import re

# 规则:分组1(开头3位)+ 分组2(后面8位),匹配11位手机号
pattern = r"(1\d{2})(\d{8})"
string = "13812345678"

result = re.match(pattern, string)
if result:
    print("group(0) →", result.group(0))  # 输出:13812345678(全部内容)
    print("group(1) →", result.group(1))  # 输出:138(第1个分组:前缀)
    print("group(2) →", result.group(2))  # 输出:12345678(第2个分组:主体)

示例 2:提取身份证号的多段信息

import re

# 规则:6位地区 + 8位生日 + 4位尾号(3个分组)
pattern = r"(\d{6})(\d{8})(\d{4})"
string = "110101199001011234"

result = re.match(pattern, string)
if result:
    print("完整身份证 →", result.group(0))   # 110101199001011234
    print("地区码 →", result.group(1))       # 110101
    print("出生日期 →", result.group(2))     # 19900101
    print("尾号 →", result.group(3))         # 1234
    # print(result.group(4))  # 报错:IndexError(只有3个分组,没有第4个)

示例 3:嵌套分组(按括号顺序计数)

如果分组嵌套(() 里套 ()),分组编号按 “左括号出现的顺序” 计数:

import re

# 规则:外层分组1 套 内层分组2
pattern = r"((\d{2})-\d{2})"
string = "12-3456"

result = re.match(pattern, string)
if result:
    print("group(0) →", result.group(0))  # 12-34(全部内容)
    print("group(1) →", result.group(1))  # 12-34(外层分组)
    print("group(2) →", result.group(2))  # 12(内层分组)

4.3. 非贪婪匹配(解决贪婪匹配的 “过度匹配”)

在数量限定符后加 ?,变为 “非贪婪匹配”(匹配最短的结果):

import re

# 贪婪匹配(默认):匹配最长的结果
greedy_pattern = r"a.*b"
result = re.match(greedy_pattern, "a123b456b")
print("贪婪匹配:", result.group())  # 输出:a123b456b

# 非贪婪匹配:匹配最短的结果
non_greedy_pattern = r"a.*?b"
result = re.match(non_greedy_pattern, "a123b456b")
print("非贪婪匹配:", result.group())  # 输出:a123b

⭐️ 重点:提取内容时优先用非贪婪匹配(加 ?),避免提取到多余内容。

5.re 模块核心方法(实战必掌握)

re 模块提供了 5 个核心方法,覆盖所有正则场景,按使用频率排序:

5.1.re.match():从字符串开头匹配

  • 语法:re.match(pattern, string)
  • 特点:只匹配开头,匹配成功返回 Match 对象,失败返回 None
  • 适用场景:精准校验整个字符串(如手机号、邮箱)

5.2.re.search():匹配字符串中第一个符合规则的内容

  • 语法:re.search(pattern, string)
  • 特点:扫描整个字符串,返回第一个匹配结果(不要求开头)
  • 适用场景:查找字符串中是否包含某类内容(如提取第一个邮箱)
import re

# 示例:查找第一个数字
result = re.search(r"\d+", "abc123def456")
print("search 匹配:", result.group())  # 输出:123

5.3.re.findall():提取所有符合规则的内容

  • 语法:re.findall(pattern, string)
  • 特点:返回所有匹配结果的列表,无匹配返回空列表
  • 适用场景:批量提取内容(如提取所有手机号、邮箱)
import re

# 示例:提取所有数字
result = re.findall(r"\d+", "abc123def456")
print("findall 匹配:", result)  # 输出:['123', '456']

# 示例:提取所有邮箱(简单规则)
email_pattern = r"\w+@\w+\.\w+"
result = re.findall(email_pattern, "我的邮箱是test1@163.com,备用邮箱是test2@gmail.com")
print("提取邮箱:", result)  # 输出:['test1@163.com', 'test2@gmail.com']

5.4.re.sub():替换符合规则的内容

  • 语法:re.sub(pattern, repl, string, count=0)
  • 参数:repl 是替换后的内容,count 是替换次数(0 = 全部替换)
  • 适用场景:清洗文本(如屏蔽敏感词、替换换行符)
import re

# 示例:屏蔽敏感词(替换为*)
text = "这个产品真垃圾,体验太差了!"
result = re.sub(r"垃圾|太差", "*", text)
print("替换后:", result)  # 输出:这个产品真*,体验*了!

# 示例:去除所有空白字符
text = "  hello \n world \t "
result = re.sub(r"\s+", "", text)
print("去除空白后:", result)  # 输出:helloworld

5.5.re.compile():预编译正则表达式(提升效率)

  • 语法:pattern = re.compile(pattern_str)
  • 特点:将正则规则预编译为 Pattern 对象,后续可重复使用,提升多次匹配的效率
  • 适用场景:多次使用同一正则规则(如循环匹配 1000 条数据)
import re

# 预编译手机号规则
phone_pattern = re.compile(r"^1\d{10}$")

# 重复使用(无需重复编译)
print(phone_pattern.match("13812345678"))  # <re.Match object; span=(0, 11), match='13812345678'>
print(phone_pattern.match("138123456789")) # 不匹配

⭐️ 重点:如果正则规则需要使用多次(如批量校验),一定要用 re.compile() 预编译,效率提升 50% 以上。

6.新手避坑指南(高频错误)

  1. ⭐️ 忘记用原始字符串:正则中的 \d 写成 \d 会被 Python 转义,必须用 r"\d"
  2. ⭐️ 校验时不加 ^$:导致部分匹配(如手机号匹配到 12 位数字的前 11 位);
  3. 贪婪匹配导致提取过多:提取内容时记得加 ? 开启非贪婪匹配;
  4. 特殊字符未转义:匹配 . * ( 等特殊字符时,需加 \(如 r"\." 匹配小数点,而非任意字符);
  5. 混淆 match()search()match() 只匹配开头,search() 匹配整个字符串。

7.核心总结

  1. 正则核心:规则描述字符,Python 用 re 模块实现,新手优先用原始字符串(r"规则");
  2. 高频语法:\d(数字)、\w(字母 / 数字)、{n}(指定次数)、^$(边界)、()(分组)、?(非贪婪);
  3. 核心方法:match()(开头匹配)、search()(第一个匹配)、findall()(所有匹配)、sub()(替换)、compile()(预编译);
  4. 避坑重点:校验加 ^$、提取用非贪婪、特殊字符转义、多次匹配用预编译。
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐