告别基础!Python 进阶保姆级教程,核心知识点 + 案例全覆盖!
一、面向对象进阶
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.适用场景
- 业务专属容器:如购物车、订单列表、权限列表,在基础容器功能上叠加业务逻辑(如自动计算总价、校验元素类型);
- 定制容器行为:限制元素类型、隐藏内部存储结构、动态计算属性(如购物车总价);
- 替代原生容器:解决原生列表 / 字典无业务约束的问题(如避免往购物车存入非商品对象)。
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}
# 仅打印一次"加载配置文件...",说明只实例化一次
执行流程:
- 程序加载到
@singleton时,会立即执行singleton(ConfigManager); singleton函数接收ConfigManager类作为参数,返回内部定义的wrapper函数;- 此时
ConfigManager这个名字,已经不再指向原类,而是指向wrapper函数! - 调用
wrapper(),检查instances字典中是否有ConfigManager这个 key; - 首次调用时没有,执行
ConfigManager(*args, **kwargs)(原类的实例化),创建第一个实例; - 将实例存入
instances[ConfigManager],并返回这个实例; - 控制台打印
加载配置文件...(仅这一次)。 - 再次调用
wrapper(),检查到ConfigManager已在instances中; - 直接返回缓存的
instances[ConfigManager],不会执行原类的__init__; cm1和cm2指向同一个对象(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 元
执行流程:
- 定义
Payment接口,约定pay支付方法; - 实现微信 / 支付宝 / 银行卡支付子类,重写
pay; - 工厂类封装创建逻辑,按类型返回对应支付对象;
- 工厂创建支付对象,调用
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
执行流程:
- 定义
DiscountStrategy策略接口,约定所有折扣策略必须实现calculate方法,统一折扣算法的调用规范; - 实现普通用户、会员、VIP 三个具体策略类,各自封装对应的折扣逻辑(无折扣 / 9 折 / 8 折 + 满减);
- 定义
PriceCalculator上下文类,初始化时注入策略对象,支持通过set_strategy动态切换策略,且封装get_final_price方法调用当前策略的calculate; - 实例化上下文并依次注入不同策略,调用
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设计模式章节!
执行流程:
- 定义主题类
Subject:初始化观察者列表,封装attach(添加)、detach(移除)、notify(通知)方法,作为被观察者的核心逻辑载体; - 定义观察者类:实现
UserObserver(用户)、LogObserver(日志)、EmailObserver(邮件)三类观察者,均实现update方法,各自封装收到通知后的处理逻辑; - 初始化主题与观察者:创建 “产品更新” 主题对象,以及用户、日志、邮件等观察者实例;
- 订阅与通知:将观察者添加到主题的观察者列表(订阅),主题调用
notify方法时,会遍历列表并触发所有观察者的update方法;移除指定观察者后,再次通知时该观察者将不再收到消息。
7.4.3.优缺点
| 优点 | 缺点 |
|---|---|
| 解耦:主题与观察者互不依赖,可独立扩展 | 观察者过多时,通知会耗时,影响性能 |
| 支持广播通知:一个主题可通知多个观察者 | 观察者通知顺序不固定,可能导致逻辑混乱 |
| 符合 “开闭原则”:新增观察者只需加子类 | - |
二、函数进阶
1.高阶函数
1.1.核心概念
满足以下任一条件的函数就是高阶函数:
- 接收一个 / 多个函数作为参数;
- 返回一个函数。
通俗解释:
- 就像 “多功能搅拌机”:可以把 “搅拌”(函数 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.核心概念
生成器是「简化版迭代器」,有两种形式:
- 生成器函数:使用
yield关键字的函数,执行时返回生成器对象; - 生成器表达式:
(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.核心总结
- 本质:
yield是生成器的 “暂停 / 恢复” 开关,替代了return,但比return强大; - 执行逻辑:
- 生成器对象创建时,函数不执行;
- 每次
next()触发函数执行,直到yield暂停; - 所有
yield执行完,再次next()抛出StopIteration;
- 核心优势:惰性取值(用的时候才生成),节省内存,适合处理大数据 / 无限序列;
- 新手避坑:
- 生成器函数调用后返回的是「生成器对象」,不是直接返回值;
- 生成器只能遍历一次,遍历完就 “空了”,需重新创建。
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互不影响)
执行逻辑拆解:
- 调用
outer()时,创建外层变量count=0,返回内层函数inner(闭包); - 第一次调用
counter1()(即inner()):nonlocal count绑定外层的count;count += 1把外层的count从 0 改成 1;- 返回 1,且外层的
count保留为 1(闭包的记忆性);
- 第二次调用
counter1():- 继续使用外层保留的
count=1; - 执行
count += 1后变成 2,返回 2。
- 继续使用外层保留的
5.3.3.nonlocal vs global:别搞混!
很多人会把 nonlocal 和 global 弄混,用表格清晰对比:
| 关键字 | 作用场景 | 声明的变量来源 | 核心区别 |
|---|---|---|---|
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 必记要点)
- 核心作用:让内层嵌套函数能修改外层嵌套函数的变量,解决 “内层无法修改外层变量” 的痛点;
- 使用场景:仅用于嵌套函数(闭包),是实现闭包 “记忆性” 的关键;
- 避坑重点:
- 别和
global混:nonlocal管 “嵌套层级”,global管 “全局层级”; - 声明的变量必须在外层嵌套函数中存在,否则报错;
- 别和
- 新手简化理解:如果在嵌套函数里想修改外层的变量,先加
nonlocal声明就对了。
6.函数参数进阶
6.1.核心概念
Python 函数参数的 4 种类型:
- 默认参数:
def func(a, b=10): ...; - 位置参数:按顺序传入的参数;
- 可变参数:
*args(接收任意数量位置参数,转元组); - 关键字参数:
**kwargs(接收任意数量关键字参数,转字典)。
通俗解释:
就像 “外卖下单”:
- 默认参数:默认加辣(可改);
- 位置参数:先选菜品,再选份数(顺序固定);
- 可变参数:加 N 个配菜(数量不限);
- 关键字参数:备注 “少糖、多醋”(键值对形式,不限数量)。
使用场景:
- 默认参数:简化常用参数的调用(如
print的end="\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 用于接收任意数量的位置参数(打包成元组),核心场景:
- 函数需要兼容不确定数量的位置参数(如求和函数
def sum_nums(*args): return sum(args)); - 批量传递参数(如把列表 / 元组解包后传给函数,
func(*[1,2,3])); - 封装通用逻辑(如装饰器、高阶函数,适配不同参数个数的函数)。
6.3.2.**kwargs 应用场景
**kwargs 用于接收任意数量的关键字参数(打包成字典),核心场景:
- 函数需要接收灵活的键值对参数(如配置项、可选参数,
def print_info(**kwargs): print(kwargs)); - 传递带名称的可选参数(如封装 API 请求,接收
headers/timeout等可选配置); - 字典解包传参(
func(**{"name":"张三","age":20})),适配需关键字参数的函数; - 类的初始化 / 装饰器中,接收并透传自定义配置参数。
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.py,b.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
test_cache.py
# test_cache.py
print("模块被执行了!") # 导入时会执行的代码
var = 10
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)
math_utils.py
def add(a, b):
return a + b
def sub(a, b):
return a - b
str_utils.py
def upper_str(s):
return s.upper()
def lower_str(s):
return s.lower()
__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__}")
- 外部使用包(
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 导入模块时,会按顺序在以下路径查找:
- 当前执行脚本的目录;
sys.path列表中的目录(包含标准库、第三方库路径);- 环境变量
PYTHONPATH指定的目录。
5.2.通俗解释
就像 “找文件”:Python 会按固定路径顺序找模块,找不到就报错 ModuleNotFoundError。
5.3.代码示例
项目结构:
ProjectTest/
├── main.py
└── my_modules
└── my_custom_module.py
-
my_custom_module.pyvar = "ok" -
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.核心总结
- 优先选择:清华镜像(最全)> 阿里云镜像(最快),满足 99% 的 WHL 下载需求;
- Windows 特殊库:优先用 gohlke 网站(难编译的库都有);
- 关键避坑:WHL 文件名必须匹配 Python 版本 / 系统架构,否则安装失败;
- 替代方案:如果找不到对应 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.txt 或 D:/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.新手必记的核心规范(避坑指南)
- ⭐️ 始终用
with语句操作文件,避免忘记close()导致资源泄露; - ⭐️ 文本文件操作必加
encoding="utf-8",二进制文件不加; - 路径优先用
pathlib.Path,避免手动拼接路径的错误; - 写入文件前确认路径是否存在(用
Path.exists()); - 大文件读取用
for line in f逐行读,不要用read()一次性读入; - 敏感操作(删除 / 覆盖)前先备份文件,避免数据丢失。
五、正则表达式
正则表达式(Regex)是处理字符串的 “瑞士军刀”—— 匹配、提取、替换、校验文本都能高效完成,比如验证手机号、提取网页中的邮箱、清洗日志数据等。以下内容循序渐进,从基础语法到实战场景,重点知识点用「⭐️」标注,确保新手能一步步掌握。
1.核心概念(先理解,再动手)
1.1.什么是正则表达式?
正则表达式是一套描述字符串规则的语法,Python 通过 re 模块实现正则功能,核心作用:
- 匹配:判断字符串是否符合规则(如是否是手机号);
- 提取:从字符串中提取符合规则的内容(如提取所有邮箱);
- 替换:把字符串中符合规则的内容替换成指定内容(如屏蔽敏感词)。
1.2.核心原则(新手必记)
- 正则匹配是按 “规则” 匹配字符,不是固定字符串;
- Python 中使用正则需先导入
re模块:import re; - 正则语法对特殊字符(如
.*\)需转义(加\),或使用原始字符串(r"规则")——⭐️ 新手优先用原始字符串,避免转义坑!
2.基础语法:匹配单个字符(入门第一步)
2.1.字符匹配符
正则的最小单位是 “字符匹配规则”,先掌握以下基础,能覆盖 80% 的简单场景:
| 语法 | 说明 | 示例 | 匹配结果 |
|---|---|---|---|
. |
匹配任意单个字符(除换行符 \n) |
r"a.b" |
匹配 a1b、a+b、a b,不匹配 ab、a\nb |
[] |
匹配括号内的任意一个字符 | r"a[0-9]b" |
匹配 a0b、a5b,不匹配 aab、a*b |
[^] |
匹配不在括号内的任意一个字符 | r"a[^0-9]b" |
匹配 aab、a*b,不匹配 a0b、a5b |
\d |
匹配数字(0-9) | r"\d\d" |
匹配 12、99,不匹配 ab、1a |
\D |
匹配非数字字符 | r"\D\D" |
匹配 ab、*%,不匹配 12、9a |
\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 b、a\tb,不匹配 a1b、ab |
\S |
匹配非空白字符 | r"a\Sb" |
匹配 a1b、a*b,不匹配 a b、a\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" |
匹配 b、ab、aaab |
+ |
匹配前面的字符 1 次或多次 | r"a+b" |
匹配 ab、aaab,不匹配 b |
? |
匹配前面的字符 0 次或 1 次 | r"a?b" |
匹配 b、ab,不匹配 aab |
{n} |
匹配前面的字符恰好 n 次 | r"a{3}b" |
匹配 aaab,不匹配 ab、aab |
{n,} |
匹配前面的字符至少 n 次 | r"a{2,}b" |
匹配 aab、aaab,不匹配 ab |
{n,m} |
匹配前面的字符 n~m 次 | r"a{2,3}b" |
匹配 aab、aaab,不匹配 ab、aaaab |
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.基础用法(无分组场景)
- 核心语法
| 写法 | 作用 | 示例(匹配规则 r"\d\d",字符串 "123") |
|---|---|---|
group() |
等价于 group(0),提取整个正则规则匹配到的全部内容 |
group() → "12" |
group(0) |
同上(显式指定第 0 组) | group(0) → "12" |
- 示例:无分组提取
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.新手避坑指南(高频错误)
- ⭐️ 忘记用原始字符串:正则中的
\d写成\d会被 Python 转义,必须用r"\d"; - ⭐️ 校验时不加
^$:导致部分匹配(如手机号匹配到 12 位数字的前 11 位); - 贪婪匹配导致提取过多:提取内容时记得加
?开启非贪婪匹配; - 特殊字符未转义:匹配
.*(等特殊字符时,需加\(如r"\."匹配小数点,而非任意字符); - 混淆
match()和search():match()只匹配开头,search()匹配整个字符串。
7.核心总结
- 正则核心:规则描述字符,Python 用
re模块实现,新手优先用原始字符串(r"规则"); - 高频语法:
\d(数字)、\w(字母 / 数字)、{n}(指定次数)、^$(边界)、()(分组)、?(非贪婪); - 核心方法:
match()(开头匹配)、search()(第一个匹配)、findall()(所有匹配)、sub()(替换)、compile()(预编译); - 避坑重点:校验加
^$、提取用非贪婪、特殊字符转义、多次匹配用预编译。
更多推荐



所有评论(0)