装饰器原理与应用
装饰器在 Python 中扮演着'化妆师'的角色,它能在不修改原函数代码的前提下,动态地为函数添加新功能。
本质与机制
从本质上讲,装饰器是一个接收函数作为参数并返回新函数的工具。其核心依赖于两个概念:
- 函数即对象:Python 中的函数可以像变量一样传递,这是装饰器的基础。
- 闭包机制:通过嵌套函数保留原函数引用,并包裹新的逻辑层。
实际调用流程通常是这样的:当你调用被装饰的函数时,Python 执行的是经过装饰器加工后的新函数。新函数会先执行装饰逻辑(如权限检查),再调用原函数。
常见应用场景
| 场景 | 作用 | 类比 |
|---|---|---|
| 权限验证 | 检查用户是否登录后再执行 | 进小区前的门禁系统 |
| 日志记录 | 自动记录调用时间和参数 | 飞机的黑匣子 |
| 性能统计 | 计算函数运行耗时 | 跑步时的计时手表 |
| 缓存结果 | 避免重复计算(如 @lru_cache) | 备忘录复用答案 |
实现方式与注意事项
最常用的方式是函数式装饰器,通过嵌套函数实现。例如给函数添加'呼叫提醒'功能:
def remind_call(func):
def wrapper():
print("【提醒】开始打电话...")
func()
print("【提醒】通话结束")
return wrapper
@remind_call
def call_friend():
print("正在和好友通话中...")
call_friend()
输出结果为:
【提醒】开始打电话...
正在和好友通话中...
【提醒】通话结束
此外还有类装饰器,通过实现 __call__ 方法让类可调用,适合需要维护状态的场景(如重试计数器)。
在实际使用中需要注意几个坑:
- 原函数信息丢失:直接使用装饰器会导致
help(func)显示 wrapper 的信息。解决方法是使用functools.wraps装饰 wrapper 函数。 - 装饰顺序:多个装饰器从下往上执行,顺序不同效果可能不同。
- 带参装饰器:需要三层嵌套,最外层接收装饰器参数。
深浅拷贝与内存管理
理解深浅拷贝对于处理复杂数据结构至关重要。
核心区别
- 浅拷贝:只复制对象本身,不递归复制内部的子对象。就像给房子拍照片,只复制了外观,房间里的家具还是同一套。
- 深拷贝:递归复制对象及其所有子对象。连房子带家具全部克隆一份,新旧对象完全独立。
技术实现
import copy
a = [1, [2, 3]]
b = copy.copy(a) # 浅拷贝
c = copy.deepcopy(a) # 深拷贝
修改 a[1][0] 会影响 b,但不会影响 c。
典型场景
- 合租公寓(浅拷贝):你和室友共用客厅冰箱。你往冰箱放啤酒,室友也能看到;但你换床单不影响室友。
- 买新房(深拷贝):开发商按样板房给你建了完全一样的房子。砸墙或换家具互不影响。
特殊注意事项
- 不可变类型:数字、字符串、元组等不可变类型,深浅拷贝无实质区别。
- 自定义对象:需实现
__copy__()和__deepcopy__()方法控制行为。 - 性能权衡:深拷贝比浅拷贝慢,对复杂结构可能差 10 倍以上。
建议数据没有嵌套或子对象不可变时用浅拷贝节省资源;复杂嵌套且需完全独立副本时用深拷贝。
数据结构对比与选择
Python 内置了多种数据结构,理解它们的特性有助于写出高效的代码。
常用数据结构概览
| 类型 | 特点 | 示例 |
|---|---|---|
| 列表 (list) | 有序、可变、允许重复 | [1, 2, 2, 3] |
| 元组 (tuple) | 有序、不可变、允许重复 | (1, "a", True) |
| 集合 (set) | 无序、可变、不允许重复 | {1, 2, 3} |
| 字典 (dict) | 键值对、键不可重复 | {"name": "Alice"} |
| 字符串 (str) | 不可变字符序列 | "hello" |
List vs Set
| 对比项 | List | Set |
|---|---|---|
| 顺序 | 保持插入顺序 | 无序 |
| 重复元素 | 允许 | 自动去重 |
| 查找速度 | 慢 O(n) | 极快 O(1) |
| 适用场景 | 保留顺序或重复数据 | 去重、快速成员检测 |
数组 vs 链表
虽然 Python 的 list 是动态数组,但理解底层数组与链表的差异很有必要。
| 对比项 | 数组 | 链表 |
|---|---|---|
| 内存分配 | 连续内存,大小固定 | 非连续内存,动态扩展 |
| 访问速度 | 极快 O(1) | 慢 O(n) |
| 插入/删除 | 慢 O(n) | 快 O(1) |
| 适用场景 | 频繁随机访问 | 频繁增删 |
通俗来说,数组像书架上的书,直接拿编号快,但插新书要挪动其他书;链表像藏宝图,改箭头快,但找宝藏要一步步走。
列表去重方案
- 使用
set():简单快捷,但会打乱顺序。unique_list = list(set(my_list)) - 保持顺序去重:遍历并记录已见元素。
seen = set() unique_list = [] for item in my_list: if item not in seen: seen.add(item) unique_list.append(item) dict.fromkeys():Python 3.7+ 推荐,保持顺序且简洁。unique_list = list(dict.fromkeys(my_list))
递归的使用场景
递归是指一个函数调用自身来解决问题,主要为了简化具有'自相似'特征问题的代码结构。
何时使用递归
- 问题可分解为规模更小的子问题。
- 子问题结构与原问题相同(自相似)。
- 有明确的终止条件(递归出口)。
递归 vs 循环
| 对比点 | for 循环 | 递归 |
|---|---|---|
| 代码结构 | 适合线性问题 | 适合分治、树形问题 |
| 可读性 | 简单任务直观 | 复杂结构更直观 |
| 性能开销 | 高效,无额外栈空间 | 消耗栈空间 |
| 错误易发点 | 循环条件控制 | 栈溢出、忘记出口 |
实例分析
以技能树遍历为例,递归写法更符合'每个技能点都做同样的事'的逻辑:
def traverse(skill_node):
print(skill_node.name)
for child in skill_node.children:
traverse(child)
相比之下,用栈模拟的迭代写法虽然避免了递归深度限制,但代码复杂度更高。
注意:递归虽优雅,但若无出口条件易导致栈溢出。性能敏感场景中,可考虑尾递归优化或转为迭代。
Python2 与 Python3 的关键差异
Python2 已于 2020 年停止支持,新项目应统一使用 Python3。以下是核心差异点。
语法与行为变化
-
打印语法:
- Py2:
print是语句,无需括号。 - Py3:
print是函数,必须加括号。
- Py2:
-
字符串编码:
- Py2:默认 ASCII,
str是字节串,unicode独立。 - Py3:默认 Unicode,
str即 Unicode,bytes用于字节。
- Py2:默认 ASCII,
-
整数除法:
- Py2:
5 / 2结果为2(向下取整)。 - Py3:
5 / 2结果为2.5(真实除法),地板除需用//。
- Py2:
-
输入函数:
- Py2:
input()执行表达式,raw_input()读字符串。 - Py3:
input()始终返回字符串。
- Py2:
-
库重构:
- Py3 标准库拆分更细,如
urllib拆分为request,parse等模块。
- Py3 标准库拆分更细,如
总结建议
如果你正在搭建新项目,务必使用 Python3。它不仅解决了旧版本的诸多不一致性,还引入了 f-string、类型注解、async/await 等现代特性,能显著提升开发效率与代码质量。
