Python 知识1
在 Python 的学习旅程中,有些坑几乎每个人都会踩,有些特性看似简单却暗藏玄机。这篇笔记整理了函数默认参数、装饰器、字典哈希、并发模型等核心知识点,带你从现象到本质,彻底搞懂这些容易混淆的概念。
一、函数默认参数的"可变对象陷阱"
1.1 问题现象
写过 Python 的人大概率都遇到过这个问题:
defappend_item(x,cache=[]):cache.append(x)returncache append_item(1)# 返回 [1],没问题append_item(2)# 返回 [1, 2],而不是预期的 [2]第二次调用时,列表竟然"记住"了上一次的结果。这不是魔法,而是 Python 的设计特性。
1.2 根本原因
函数的默认参数只在函数定义时初始化一次,而不是每次调用时重新创建。cache=[]这个列表对象在函数定义的那一刻就被创建了,之后所有不传参的调用都共享同一个列表实例。
这就像你在班级里放了一个公共邮箱,每个人都往里面塞东西,自然会越积越多。
1.3 正确的解决方案
最标准的修复方式是将默认参数设为None,在函数内部判断并初始化:
defappend_item(x,cache=None):ifcacheisNone:cache=[]cache.append(x)returncache这样每次不传参调用都会获得一个干净的新列表,问题迎刃而解。
1.4 常见错误选项分析
选项 B:使用copy.deepcopy
这是治标不治本的做法。深拷贝只是复制了内容,但函数每次还是从同一个默认参数开始,而且性能差、逻辑混乱。
选项 C:改用元组()
虽然元组不可变,list(cache)每次都会新建列表,但语义上很奇怪——默认参数传入的是空元组,而函数期望的是"可累积缓存",逻辑不够清晰。
选项 D:在函数末尾调用cache.clear()
这会清空缓存,导致函数返回空列表,完全破坏功能。如果函数外持有该缓存对象的引用,清空还会引发副作用。
经验之谈:Python 中切忌使用可变对象(如列表、字典)作为函数默认参数。这是一个非常经典的面试题,理解它,你的代码就更健壮了。
二、装饰器:给函数"无痛"加功能
2.1 装饰器的本质
装饰器是 Python 中最优雅的语法糖之一,它的核心作用是:在不修改原函数代码的前提下,给函数动态添加功能。
importtimedeftimer(func):definner(*args,**kwargs):start=time.time()result=func(*args,**kwargs)print(f'耗时:{time.time()-start:.6f}s')returnresultreturninner@timerdefhello():print('Hi!')hello()# 输出:Hi! 耗时: 0.000123s2.2@timer到底是什么
@timer是语法糖,等价于:
hello=timer(hello)也就是说,@timer把hello这个函数对象传给timer,再用返回的新函数覆盖原来的hello。以后调用hello(),实际上是在调用inner()。
2.3 为什么要用*args, **kwargs
*args和**kwargs是装饰器的"通用接口",它们能接收任意数量的位置参数和关键字参数,让同一个装饰器可以装饰任何函数。
当原函数无参时,args == (),kwargs == {},等价于func();当原函数有参时,它们会自动展开传递。
2.4return result为什么必不可少
装饰器的责任是"透明包装"——不能改变原函数的行为(包括返回值)。如果省略return result,原函数的返回值会丢失。
@timerdefadd(a,b):returna+b x=add(3,5)# 如果装饰器不return,x会变成None个人见解:装饰器就像给手机贴膜——膜不影响屏幕显示内容,只是增加保护。好的装饰器应该是"无感增强",不该让调用者察觉变化。
2.5functools.wraps的作用
一个容易被忽略的细节:装饰器会覆盖原函数的__name__等元信息。
@timerdefhello():passhello.__name__# 返回 'inner',而不是 'hello'为了解决这个问题,需要使用functools.wraps:
fromfunctoolsimportwrapsdeftimer(func):@wraps(func)definner(*args,**kwargs):# ...returninner2.6 装饰器的应用场景
装饰器的用途非常广泛,常见的有:
- 日志记录:记录函数调用信息
- 性能监控:统计函数执行时间
- 权限校验:检查用户是否有权限访问
- 缓存:缓存函数返回值,避免重复计算
- 重试机制:网络请求失败时自动重试
三、字典与哈希表
3.1 字典的核心规则
Python 字典中,键(key)必须是不可变的,值(value)可以是任意类型。
d={"name":"Alice",# str ✅1:"one",# int ✅(1,2):"tuple_key",# tuple ✅# [1, 2]: "list_key" # list ❌ TypeError}3.2 为什么 key 必须不可变
字典底层是哈希表(hash table)。Python 需要根据key计算出一个 hash 值来决定键值对存储的位置。如果key在放入字典后内容变了,它的 hash 值也会变,Python 就再也找不到它了。
这就像图书馆的书,如果书的编号变了,你就无法通过索引找到它。
3.3 一个常被误解的点
key 不可变 ≠ key 不能被替换。你可以换 key,但不能改 key 的内容。
d={"a":1}d["b"]=d.pop("a")# ✅ 这是"换 key"# d["a"][0] = "c" # ❌ 这是不行的(字符串不可变)3.4 tuple 作为 key 的注意事项
虽然 tuple 是不可变的,但如果 tuple 里包含可变元素,也不能当 key:
d={(1,2,[3]):"oops"# ❌ TypeError}因为 tuple 本身不可变,但它的元素是可变的,导致 hash 值不稳定。
四、魔术方法:让对象支持自定义行为
4.1__getitem__:让对象支持索引
通过实现__getitem__方法,可以让自定义类的实例像列表一样使用索引操作。
classFoo:def__getitem__(self,pos):returnrange(0,30,10)[pos]foo=Foo()foo[0]# 返回 0foo[1]# 返回 10foo[2]# 返回 20当你写foo[1]时,Python 会自动调用Foo.__getitem__(foo, 1),pos参数就是方括号里的数字。
4.2__init__vs__new__:对象创建的两个阶段
这是面试中非常经典的考点,很多人分不清这两个方法的区别。
| 方法 | 职责 | 参数 | 返回值 |
|---|---|---|---|
__new__ | 创建并返回实例 | cls(类) | 新创建的实例 |
__init__ | 初始化已创建的实例 | self(实例) | None |
关键点:
__new__是类方法(隐式),负责分配内存、创建对象;__init__是实例方法,负责对已创建的对象进行初始化;- 如果
__new__没有返回当前类的实例,__init__不会被调用; - 在不可变类型(如 int、str、tuple)的子类中,必须在
__new__中设置值。
classMyInt(int):def__new__(cls,value):returnsuper().__new__(cls,value*2)x=MyInt(5)# 输出 104.3pass语句的作用
pass是 Python 的"占位符语句",表示"什么都不做"。Python 不允许函数体或类体为空,所以必须放一个语句进去。
def__init__(self):pass# 初始化方法什么都不做五、并发模型与 GIL
5.1 GIL 的本质
GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器的一个互斥锁,它确保同一时刻只有一个线程在执行 Python 字节码。
这意味着,即使你启用了多个线程,在 CPython 中它们也不能真正"并行"执行 CPU 计算任务——必须轮流获取 GIL 才能运行。
5.2 GIL 对不同任务的影响
CPU 密集型任务(如数值计算、循环、图像处理):
GIL 会成为瓶颈,多线程无法利用多核优势,性能提升不明显甚至更慢。
I/O 密集型任务(如文件读写、网络请求):
I/O 操作在等待时会主动释放 GIL,其他线程可以抢占执行,因此 GIL 的影响很小。
5.3 正确的并发策略选择
| 场景 | 推荐方案 |
|---|---|
| CPU 密集型 | multiprocessing(多进程,绕开 GIL) |
| I/O 密集型 | threading或asyncio |
| 极致性能 | Cython、Numba、或改用无 GIL 语言 |
multiprocessing创建的是独立的子进程,每个进程都有自己独立的 Python 解释器实例和 GIL,因此可以在不同 CPU 核心上真正并行执行。
面试口诀:“算得快,用多进程;等得久,用协程或线程。”
六、日志系统:从 print 到 logging
6.1 为什么要用 logging
print()是玩具,logging是工业级工具。
| 特性 | logging | |
|---|---|---|
| 级别区分 | ❌ | ✅ DEBUG / INFO / WARNING / ERROR / CRITICAL |
| 输出目标 | 仅屏幕 | 控制台、文件、网络 |
| 上线处理 | 需要删除 | 可统一关闭或降级 |
| 附加信息 | 无 | 时间、模块名、行号 |
6.2 logging 的核心概念
- Logger:日志记录器(入口)
- Handler:输出目的地(控制台 / 文件)
- Formatter:日志格式
- Filter:过滤日志
- Level:日志级别(DEBUG < INFO < WARNING < ERROR < CRITICAL)
6.3 常用配置方式
最简单用法:
importlogging logging.basicConfig(level=logging.INFO,format="%(asctime)s - %(levelname)s - %(message)s")logging.info("程序启动")logging.error("发生错误")同时输出到屏幕和文件:
logger=logging.getLogger("my_app")logger.setLevel(logging.DEBUG)ch=logging.StreamHandler()# 控制台fh=logging.FileHandler("app.log")# 文件formatter=logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")ch.setFormatter(formatter)fh.setFormatter(formatter)logger.addHandler(ch)logger.addHandler(fh)6.4 官方推荐写法
importlogging logger=logging.getLogger(__name__)使用__name__作为 logger 名称,可以自动带上模块名,方便大型项目分层管理。
6.5 记录异常
try:1/0exceptException:logger.exception("发生异常")# 自动记录完整 traceback七、生成器:内存的救星
7.1 生成器的核心价值
生成器是一种特殊的迭代器,它通过yield关键字实现"暂停并返回"值,下次调用时从暂停处继续执行。
最大优势:节省内存。处理百万级、亿级数据时,生成器让你"边读边处理",而不是"先全读进来再处理"。
7.2 生成器的两种创建方式
方式一:生成器函数(使用yield)
defcountdown(n):whilen>0:yieldn n-=1foriincountdown(5):print(i)# 输出 5, 4, 3, 2, 1方式二:生成器表达式(使用圆括号)
squares=(x**2forxinrange(1000000))# 几乎不占内存7.3 常见误区澄清
误区一:生成器对象不占用内存
错误。生成器对象本身会占用少量内存来保存当前执行状态、局部变量、代码指针等,但远小于列表。
误区二:生成器只能返回数值类型
错误。生成器可以yield任何类型的数据:字符串、列表、字典、对象、None,甚至另一个生成器。
误区三:生成器中不能使用循环
错误。生成器中完全可以使用for循环、while循环,很多生成器都依赖循环来产生一系列值。
7.4 内存对比
| 方法 | 是否一次性加载 | 内存占用 | 适用场景 |
|---|---|---|---|
| 列表推导 | ✅ | 高 | 小数据量 |
| 生成器表达式 | ❌ | 极低 | 大数据量、流式处理 |
| tuple() | ✅ | 中高 | 需要不可变序列 |
八、模式匹配:Python 3.10+ 的新特性
8.1 match-case 是什么
match是 Python 3.10 引入的结构化模式匹配语法,不是函数,而是关键字,类似if/for/while。
match(100,200):case(x,y)ifx>y:print("first")case(x,y)ifx<y:print("second")case(x,y):print("third")8.2 执行流程
match后面的值会依次和每个case的模式进行匹配;- 匹配成功后,如果有守卫条件(
if),需要额外检查条件; - 第一个匹配成功的
case会被执行,后续的case不再检查。
8.3 核心能力
解包匹配:
matchpoint:case(x,y):print(f"坐标:{x},{y}")守卫条件:
case(x,y)ifx<y:# 模式匹配成功后,额外检查条件通配符_:
case_:# 匹配任何值,类似 else8.4 与 if-elif-else 的对比
match-case比if-elif-else强在:
- 能匹配"结构",不只是值;
- 自动解包;
- 可读性更好(尤其处理复杂数据结构)。
九、垃圾回收机制
9.1 双轨制回收策略
Python 采用引用计数 + 分代循环垃圾收集器的双轨制策略。
引用计数:
对象被引用一次,计数 +1;引用失效,计数 -1。计数为 0 时立即回收。优点是实时性强、速度快,但无法处理循环引用。
分代 GC:
专门处理循环引用的对象。把对象分为 0 代(新生代)、1 代(中年代)、2 代(老年代),定期扫描,使用标记-清除算法,找出不可达的循环引用对象并回收。
9.2 常见误区
误区一:Python 仅使用引用计数
错误。Python 主要依赖引用计数,但还有 GC 处理循环引用。
误区二:循环引用的对象无法被回收
错误。GC 就是专门处理这种情况的。
误区三:del语句会立即回收对象
错误。del只是删除变量名(引用),让对象的引用计数减 1。只有当引用计数降为 0 时,对象才会立即被回收。
9.3 手动干预 GC
importgcprint(gc.get_count())# 查看当前各代对象数量gc.collect()# 手动触发一次全量垃圾回收print(gc.get_threshold())# 查看 GC 阈值十、模块导入与查找顺序
10.1 导入查找顺序
Python 在使用import导入模块时,查找顺序如下:
- 内建模块:Python 解释器首先查找内建模块(如
sys、math); - 当前路径:即执行脚本所在的目录;
- 环境变量 PYTHONPATH:在
PYTHONPATH指定的目录中查找; - Python 安装路径:最后查找 Python 安装目录下的标准库和第三方库。
结语
Python 是一门看似简单但实则博大精深的语言。这些知识点都是从实践中提炼出来的精华,每一个都对应着实际开发中可能遇到的问题。理解这些概念,不仅能让你写出更健壮的代码,也是面试中展示技术深度的重要素材。
学习 Python 的过程,就像剥洋葱——一层一层深入,每一层都有惊喜。保持好奇心,多写代码,多踩坑,你会越来越接近这门语言的本质。
