引言

       本文主要是针对python3核心语法体系一个完整介绍,相当于一本完整免费电子书了,python是一门充满陷阱的语言,这些陷阱被美化成了灵活性。在语言面向对象改进过程中,不断打补丁,语法变得稀奇古怪,很多特性依赖于双下划线方法或者属性(比如__init__等一大堆),美其名为魔法。

        python在数据分析、人工智能领域使用得非常多、处于领先地位。脚本语言特性和性能问题日益凸显,语言本身不断在优化。但是因为不是天生强类型、面向对象语言,很多特性使用起了会很别扭。这里列出来一些主要的变化(细节变化很多,最明显的就是print函数):

       1 解释器优化:3.11 引入了新的评估循环(Evaluation Frame),速度提升 10-60%。
       2 JIT 编译实验:3.13/3.14 持续探索即时编译(JIT)技术,进一步提升运行时性能。
       3 自由线程模式 (No-GIL):Python 3.13/3.14 的重大突破。引入了可选的“自由线程”构建版本,移除了全局解释器锁 (GIL),允许真正的多线程并行计算(此前受 GIL 限制,多线程无法利用多核 CPU 进行计算密集型任务

        4 类型提示

        python是靠缩进来组织代码块的,这点要注意,容易出问题(这里思考一个问题,格式化插件格式化代码的标准是什么,会不会把你代码逻辑搞变了😏)。pyhon兼容性相对于像java这类语言差很多,如果对python2或者以前历史版本比较清楚的人,应该能感受到pyhon3和以前版本差异很大。这也是对长期稳定运行大型程序一大挑战,容易给别人埋坑。这玩意早期都是很混乱的,面向对象权限限制更多的是约定,所以有很多东控制不好,没有人管控,出现了很多要求把python改成java实现的,代价可不小.很多公司都是搞烂了才找架构师(或者就是不管),这种到后期都不好搞,代价很大,做过很多系统,从菜鸟开始就是独立设计和开发,前后端都做过,都不知道架构师长啥样。现在要稍微好一些(主要是有环境隔离和依赖管理,但工程规范较差),可以参看我另外一篇博客

          学一门语言,一般都不是一两天就搞得定的,可能都要一周以上(这里不说生态和框架哈,要学这些就扯远了,长的可能一年半载都学不完),所以要淡定。python看起来简单,实际上很复杂,有点像汉语和英语区别,汉语一小本字典你都不用记完,你基本没有不认识的。但是英语你把那个大部头牛津词典记完,你还是经常遇到一些你不熟悉的用法。Python为了爽,想加啥就加啥,代价就是有点烧脑壳。

          备注:实践pyhon版本:3.13.5

1 基本数据类型

Python 的基本数据类型是解释器内置的,无需导入任何模块即可直接使用。主要包括:
数值型:int (整数), float (浮点数), complex (复数), bool (布尔值)。
序列型:str (字符串), list (列表), tuple (元组), range (范围)。
映射型:dict (字典)。
集合型:set (集合), frozenset (冻结集合)。
其他:bytes, bytearray, NoneType (None)。

1.1 数字

       数字和字符串都是属于不可变类型,不过确定的话,可以用id()函数确定其身份。早期python版本可能会缓存一些小整数,导致认为整数是可变类型的的错觉,java也有类似机制

1.1.1 整数

在 Python 2 中:
  int:表示固定精度的整数(通常是 32 位或 64 位,取决于平台)。
  long:表示任意精度的整数,用于处理超出 int 范围的大数。
两者是不同类型,需要显式转换(如 long(x))。


在 Python 3 中:
        移除了 long 类型。int 类型现在直接支持任意精度,自动处理大整数,行为等同于 Python 2 中的 long。不再有 int 和 long 的区别,所有整数都是 int 类型。你不需要担心整数溢出问题(只要内存允许),也不需要区分 int 和 long

1.1.2 浮点数

浮点数在类型系统上也经历了简化
     Python 2:
     存在 float 类型。没有专门的“长浮点数”类型,但底层 C 实现依赖平台。主要问题在于 int 和 long 的分裂导致涉及大整数转换浮点数时行为复杂。


       Python 3:
       只有 float 类型。由于 int 已经统一为任意精度,float 与 int 之间的转换逻辑更加清晰:将巨大的 int 转换为 float 可能会损失精度(因为 float 只有 64 位),但这在两个版本中都会发生,只是 Python 3 的类型提示更明确

特性 Python 2 Python 3
整数除法行为 地板除 (Floor Division)
如果两个操作数都是整数,/ 会截断小数部分,返回整数。
5 / 22
真除法 (True Division)
无论操作数类型,/ 总是返回浮点数。
5 / 22.5
如何获取地板除 使用 / (当操作数为整数时) 或 // (需 from __future__ import division) 必须使用 // 运算符
5 // 22
如何获取真除法 需导入未来特性:
from __future__ import division
之后 / 行为同 Python 3
默认行为,无需导入
混合运算 只要有一个是浮点数,结果就是浮点数。
5.0 / 22.5
同左,行为一致。
# Python 2
print 5 / 2      # 输出: 2  (整数)
print 5.0 / 2    # 输出: 2.5 (浮点数)

# Python 3
print(5 / 2)     # 输出: 2.5 (浮点数)
print(5 // 2)    # 输出: 2   (整数,地板除)

1.1.3 decimal

      需要导入,所以不是基本数据类型,遇到需要考虑到精度的计算场景,比如金融,就可能要使用到decimal,初始化的时候一定要带引号

from decimal import Decimal, ROUND_HALF_UP

# 错误示范 (float)
print(0.1 + 0.2)  # 0.30000000000000004

# 正确示范 (Decimal) - 必须用字符串初始化以保证精度
a = Decimal('0.1')
b = Decimal('0.2')
total = a + b
print(total)  # 0.3

# 设置舍入模式 (例如保留2位小数,四舍五入)
price = Decimal('19.995')
final_price = price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(final_price)  # 20.00

1.1.4 复数

       复数一般可能用的少,就是书上说的复数,是一种内置的核心数据类型,无需导入任何模块即可直接使用,它完全遵循数学中的复数定义 a+bi 。复数运算和书本上一样

z1 = 3 + 4j      # 实部 3, 虚部 4
z2 = 5j          # 实部 0, 虚部 5 (纯虚数)
z3 = 10          # 实部 10, 虚部 0 (自动视为复数 10+0j)
z4 = complex(2, -3) # 使用构造函数:实部 2, 虚部 -3
a = 2 + 3j
b = 1 - 2j

print(a + b)  # (3+1j)
print(a - b)  # (1+5j)
print(a * b)  # (8-1j) -> (2*1 - 3*(-2)) + (2*(-2) + 3*1)j = 8 - 1j
print(a / b)  # (-0.8+1.4j)

1.2 字符串

python 字符串是不可变类型。

1.2.1 字符串定义

       字符串能使用单引号、双引号、三引号。如果字符串内容有双引号,外层就可以使用单引号,减少转义。三引号可以写行数据

# ✅ 推荐:清晰易读
s1 = 'He said, "Hello World!"'

# ❌ 不推荐:需要转义
s2 = "He said, \"Hello World!\""

# 使用三引号定义多行文本
poem = """床前明月光,
疑是地上霜。
举头望明月,
低头思故乡。"""

print(poem)
# 输出会严格保持换行格式:
# 床前明月光,
# 疑是地上霜。
# ...

# 对比:如果用普通引号,必须手动加 \n
poem_bad = "床前明月光,\n疑是地上霜。\n..." 

1.2.2 大小写转换

方法 说明 示例
capitalize() 首字母大写,其余小写 'hello'.capitalize()'Hello'
casefold() 强力转为小写(用于不区分大小写的比较,比 lower() 更彻底) 'Straße'.casefold()'strasse'
lower() 转为小写 'HELLO'.lower()'hello'
upper() 转为大写 'hello'.upper()'HELLO'
swapcase() 大小写互换 'Hello'.swapcase()'hELLO'
title() 每个单词首字母大写 'hello world'.title()'Hello World'

1.2.3 查找与统计

这里要注意的是index方法查找不到会抛出异常

方法 说明 返回值
count(sub[, start, end]) 统计子串出现次数 int
find(sub[, start, end]) 查找子串首次位置,找不到返回 -1 int
index(sub[, start, end]) 查找子串首次位置,找不到抛 ValueError int
rfind(sub[, start, end]) 反向查找(从右向左),找不到返回 -1 int
rindex(sub[, start, end]) 反向查找,找不到抛 ValueError int
startswith(prefix[, start, end]) 检查是否以指定前缀开头 bool
endswith(suffix[, start, end]) 检查是否以指定后缀结尾 bool

1.2.4 清理与填充

方法 说明 示例
strip([chars]) 去除两端空白符(或指定字符) ' hi '.strip()'hi'
lstrip([chars]) 去除左侧空白符 ' hi'.lstrip()'hi '
rstrip([chars]) 去除右侧空白符 'hi '.rstrip()' hi'
center(width[, fillchar]) 居中对齐,用指定字符填充 'hi'.center(5, '*')'*hi*'
ljust(width[, fillchar]) 左对齐,右侧填充 'hi'.ljust(5, '*')'hi***'
rjust(width[, fillchar]) 右对齐,左侧填充 'hi'.rjust(5, '*')'***hi'
expandtabs(tabsize) 将制表符 \t 替换为空格 'a\tb'.expandtabs(4)
zfill(width) 左侧用 0 填充到指定宽度(常用于数字) '42'.zfill(5)'00042'

1.2.3 判断类型

方法 说明
isalnum() 是否只包含字母和数字
isalpha() 是否只包含字母
isdigit() 是否只包含数字(包括上标数字如 ²)
isdecimal() 是否只包含十进制数字(最严格,不包括 ²)
isnumeric() 是否只包含数字字符(包括中文数字如 "四")
isspace() 是否只包含空白字符(空格、\n, \t 等)
islower() 是否所有有大小写的字符都是小写
isupper() 是否所有有大小写的字符都是大写
istitle() 是否符合标题格式(每个单词首字母大写)
isprintable() 是否所有字符都可打印(不含 \n, \t 等控制符)
isidentifier() 是否是合法的 Python 标识符(变量名)

1.2.4 替换与分割

方法 说明 示例
replace(old, new[, count]) 替换子串(可选替换次数) 'aaa'.replace('a', 'b', 1)'baa'
split([sep[, maxsplit]]) 分割字符串为列表(默认按空白符) 'a b c'.split()['a','b','c']
rsplit([sep[, maxsplit]]) 从右向左分割 'a b c'.rsplit(' ', 1)['a b', 'c']
splitlines([keepends]) 按行分割(保留或不保留换行符) 'a\nb'.splitlines()['a', 'b']
partition(sep) 分成三部分:(头, 分隔符, 尾) 'a:b:c'.partition(':')('a', ':', 'b:c')
rpartition(sep) 从右向左分成三部分 'a:b:c'.rpartition(':')('a:b', ':', 'c')
join(iterable) 重要:用该字符串连接列表中的元素 '-'.join(['a','b'])'a-b'

1.2.5 编码与格式化

方法 说明
encode(encoding='utf-8', errors='strict') 将字符串编码为 bytes 对象
format(*args, kwargs) 格式化字符串(配合 {} 使用)
format_map(mapping) 类似 format,但使用字典映射
maketrans(x[, y[, z]]) 创建转换表(配合 translate 使用)
translate(table) 根据转换表替换/删除字符
特性 Python 2 Python 3
'hello' 类型是 str (本质是 bytes/字节串) 类型是 str (本质是 Unicode/字符串)
u'hello' 类型是 unicode (真正的 Unicode/字符串) 类型是 str (本质是 Unicode/字符串)
区别 完全不同。一个是字节,一个是字符。混合使用容易报错。 完全相同。u 前缀被忽略。

python不在需要u开头了

1.2.6 转义字符串

转义字符 描述 (Description) 示例代码 输出效果 (Visual)
\\ 反斜杠 (Backslash) print("C:\\Users\\Name") C:\Users\Name
\' 单引号 (Single Quote) print('It\'s me') It's me
\" 双引号 (Double Quote) print("He said \"Hi\"") He said "Hi"
\n 换行 (Newline) print("Line1\nLine2") Line1
Line2
\r 回车 (Carriage Return) print("Hi\rBye") Bye (覆盖前面的 Hi)
\t 水平制表符 (Tab) print("A\tB") A    B
\b 退格 (Backspace) print("ABC\bD") ABD (C 被删除)
\f 换页 (Formfeed) print("Page1\fPage2") (打印机换页,终端通常显示为空格或换行)
\v 垂直制表符 (Vertical Tab) print("A\vB") (终端通常显示为带间距的换行)
\a 响铃 (Bell/Alert) print("\a") (播放系统提示音/哔声)
\ooo 八进制 ASCII (Octal) print("\101") A (101 是 A 的八进制)
\xhh 十六进制 ASCII (Hex) print("\x41") A (41 是 A 的十六进制)
\N{name} Unicode 字符名 print("\N{SNOWMAN}")
\uhhhh 16位 Unicode print("\u4e2d")
\Uhhhhhhhh 32位 Unicode print("\U0001F600") 😀

1.2.7 原始字符串

       在字符串引号前加上字母 r 或 R(大小写均可,推荐小写 r),这是原始字符串最重要的作用。在普通字符串中,\ 是转义符;在原始字符串中,\ 只是一个普通的反斜杠字符。

# ❌ 错误示范:普通字符串中的 \n 和 \t 被转义了
path_wrong = "C:\new_folder\test.txt"
print("普通字符串:", path_wrong)
# 输出可能类似: C:
# ew_folder    est.txt  (因为 \n 变换行,\t 变制表符)

# ✅ 正确示范:原始字符串保留了所有反斜杠
path_right = r"C:\new_folder\test.txt"
print("原始字符串:", path_right)
# 输出: C:\new_folder\test.txt

1.3 元组

使用()包起来或者逗号给开的数据定义是元组


1.3.1 元组定义

1 使用小括号将元素包裹起来,元素之间用逗号 , 分隔。
特点: 语法清晰,可读性最强。
注意: 括号其实是可选的(见方式 2),但为了代码清晰,强烈建议始终加上

# 空元组
empty = ()

# 普通元组
colors = ("red", "green", "blue")
numbers = (1, 2, 3, 4, 5)

# 混合类型
mixed = (1, "hello", 3.14, True)

print(type(colors)) # <class 'tuple'>

2. 省略括号法 (隐式元组)
在 Python 中,真正定义元组的是逗号 ,,而不是括号 ()。
如果你写出一串用逗号分隔的值,Python 会自动将其识别为元组。
用途: 常用于函数返回多个值、多重赋值。

# 没有括号,依然是元组!
point = 10, 20
print(point)      # (10, 20)
print(type(point))# <class 'tuple'>

# 函数返回多个值时,本质是返回了一个元组
def get_coords():
    return 100, 200  # 等同于 return (100, 200)

x, y = get_coords() # 解包

3. 单元素元组 (⚠️ 易错点)
如果要定义只有一个元素的元组,必须在元素后面加一个逗号 ,。
错误写法: t = (1) -> 这只是一个整数 int,括号被当作数学运算优先级处理了。
正确写法: t = (1,) -> 逗号告诉 Python 这是一个元组。

# ❌ 错误:这只是个整数
wrong = (1)
print(type(wrong)) # <class 'int'>

# ✅ 正确:加上逗号
correct = (1,)
print(type(correct)) # <class 'tuple'>
print(correct)       # (1,)

# 即使不加括号,单元素也必须有逗号
also_correct = 1,
print(type(also_correct)) # <class 'tuple'>

def get_coordsa():
    return (100)  # 等同于 return (100, 200)
    
def get_coordsb():
    return (200,)  # 等同于 return (100, 200)
def get_coordsc():
    return 200,  # 等同于 return (100, 200)
a=get_coordsa()
b=get_coordsb()
c=get_coordsc()
print(f'aType={type(a)},bType={type(b)},cType={type(c)}') #aType=<class 'int'>,bType=<class 'tuple'>,cType=<class 'tuple'>

4.tuple() 构造函数 (类型转换)

将其他可迭代对象(如列表、字符串、范围)转换为元组

# 从列表转换
lst = [1, 2, 3]
t_from_list = tuple(lst)
print(t_from_list) # (1, 2, 3)

# 从字符串转换 (每个字符变成元组的一个元素)
s = "abc"
t_from_str = tuple(s)
print(t_from_str) # ('a', 'b', 'c')

# 从 range 转换
t_from_range = tuple(range(3))
print(t_from_range) # (0, 1, 2)

# 空元组
empty = tuple()
print(empty) # ()

1.3.1 访问方式

主要通过索引位置和切片访问

1.3.2 不可变性

元组表面上是不可变类型,实际上是有可能改变的。

元组可以多层嵌套,还可以类型不同。

1.3.3 深拷贝与浅拷贝

操作方式 代码示例 内存关系 修改子对象的影响 适用场景
直接赋值 b = a 完全同一个对象 影响 (因为是同一个) 不需要副本,只是起别名
浅拷贝 b = a.copy()
b = copy.copy(a)
外层新,内层共享 影响 (内层引用相同) 对象只有一层,或确定内层不会被修改
深拷贝 b = copy.deepcopy(a) 完全独立,递归复制 不影响 (彻底隔离) 嵌套可变对象,需要完全独立的副本

1.3.4 元组方法

因为元组是不可变对象,所以很多修改自身的方法不存在

方法名 语法 功能描述 返回值类型 是否修改原元组
count() t.count(value) 统计指定元素在元组中出现的次数 int (整数) ❌ 否
index() t.index(value[, start[, end]]) 查找指定元素第一次出现的索引位置 int (整数) ❌ 否

1.4 列表

列表是使用[]来定义和初始化的。

1.4.1 列表定义

定义方式 语法示例 核心用途 注意事项
方括号 [1, 2, 3] 通用定义 最直观,支持任意类型
构造函数 list("abc") 类型转换 参数必须是可迭代对象
推导式 [x*2 for x in data] 高效生成/过滤 代码简洁,性能优于循环
乘法 [0] * 10 快速初始化 慎用于可变对象(引用陷阱)
解包 [*list1, *list2] 灵活合并 Python 3.5+ 特性

💡 最佳实践建议
1 手动写数据: 用方括号 [...]。
2 由旧数据生成新数据: 优先用列表推导式(既快又优雅)。
3 初始化固定长度:
6 存数字/字符串等不可变值:用 [0] * n。
7 存列表/字典等可变值:必须用 [..., ...] 推导式,避免引用坑。
8 合并列表: 小列表用 + 或 [*a, *b],大列表频繁合并建议用 extend() 方法(原地修改,省内存)。

1. 方括号法 [ ] (最常用)
使用方括号将元素包裹起来,元素之间用逗号 , 分隔。
特点: 直观、简洁,支持混合数据类型。
适用: 绝大多数场景。

# 空列表
empty_list = []

# 整数列表
numbers = [1, 2, 3, 4, 5]

# 混合类型列表 (Python 特色)
mixed = [1, "hello", 3.14, True, None]

# 嵌套列表 (二维数组/矩阵)
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

print(type(numbers)) # <class 'list'>

2. list() 构造函数 (类型转换)
将其他可迭代对象(如字符串、元组、集合、range)转换为列表。
特点: 用于数据清洗或类型转换

# 从字符串转换 (每个字符成为一个元素)
s = "python"
lst_from_str = list(s)
print(lst_from_str) # ['p', 'y', 't', 'h', 'o', 'n']

# 从元组转换
t = (10, 20, 30)
lst_from_tuple = list(t)
print(lst_from_tuple) # [10, 20, 30]

# 从 range 转换 (生成数字序列)
lst_from_range = list(range(5))
print(lst_from_range) # [0, 1, 2, 3, 4]

# 从集合转换 (注意:集合无序,结果顺序可能不固定)
st = {1, 2, 3}
lst_from_set = list(st) 

3. 列表推导式 (List Comprehension)
Python 最强大的特性之一。用于基于现有序列生成新列表,语法紧凑且执行效率高。
语法: [表达式 for 变量 in 可迭代对象 if 条

# 场景 A: 简单生成 (0-9 的平方)
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, ..., 81]

# 场景 B: 带条件过滤 (只保留偶数的平方)
even_squares = [x**2 for x in range(10) if x % 2 == 0]
print(even_squares) # [0, 4, 16, 36, 64]

# 场景 C: 嵌套循环 (展平二维列表)
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6]

# 场景 D: 类型转换 + 处理
str_nums = ["1", "2", "3"]
int_nums = [int(x) * 10 for x in str_nums]
print(int_nums) # [10, 20, 30]

4. 乘法操作符 * (快速初始化)
用于快速创建一个包含重复元素的列表。
适用: 初始化固定大小的占位列表。
⚠️ 高危陷阱: 如果元素是可变对象(如列表),所有位置会指向同一个对象(引用共享)

# 场景 A: 不可变对象 (安全)
zeros = [0] * 5
print(zeros) # [0, 0, 0, 0, 0]

names = ["None"] * 3
print(names) # ['None', 'None', 'None']

# 场景 B: ⚠️ 可变对象 (危险!)
# 错误写法:创建 3 个指向同一个列表的引用
bad_matrix = [[]] * 3
bad_matrix[0].append(1)
print(bad_matrix) 
# ❌ 输出: [[1], [1], [1]] (所有子列表都变了!)

# ✅ 正确写法:使用列表推导式创建独立对象
good_matrix = [[] for _ in range(3)]
good_matrix[0].append(1)
print(good_matrix) 
# ✅ 输出: [[1], [], []] (只有第一个变了)

5. * 解包操作符 (Python 3.5+)
在列表字面量中使用 * 将另一个可迭代对象“展开”并入当前列表。
适用: 合并多个列表、在列表中间插入元素。

list1 = [1, 2]
list2 = [3, 4]

# 合并列表 (比 list1 + list2 更灵活,可插入中间)
combined = [0, *list1, 99, *list2, 100]
print(combined) 
# [0, 1, 2, 99, 3, 4, 100]

# 将 range 展开
nums = [*range(3), 100, *range(3, 5)]
print(nums) # [0, 1, 2, 100, 3, 4]

1.4.2 可变性

1.4.3 列表与元组转换

这里要注意深拷贝问题

1.4.4 列表方法

类别 方法名 语法示例 功能描述 返回值 是否修改原列表
append() lst.append(x) 在末尾添加一个元素 None ✅ 是
extend() lst.extend(iterable) 在末尾追加另一个可迭代对象的所有元素 None ✅ 是
insert() lst.insert(i, x) 在指定索引 i 处插入元素 None ✅ 是
remove() lst.remove(x) 删除第一个匹配的值 x None ✅ 是
pop() lst.pop([i]) 删除并返回指定索引的元素(默认最后一个) 元素值 ✅ 是
clear() lst.clear() 清空列表所有元素 None ✅ 是
index() lst.index(x) 返回第一个匹配值 x 的索引 int ❌ 否
count() lst.count(x) 统计值 x 出现的次数 int ❌ 否
改/序 sort() lst.sort() 原地排序(升序) None ✅ 是
reverse() lst.reverse() 原地反转列表 None ✅ 是
复制 copy() lst.copy() 返回列表的浅拷贝 新列表 ❌ 否

重要提示:除了 copy()index()count()pop() (返回值) 外,绝大多数列表方法(如 appendsortremove)都返回 None千万不要写成 new_lst = lst.sort(),这会导致 new_lst 变为 None

  可以用列表本身的方法实现队列功能

1.4.5 itertools 模块

除了range,这里给出一个更强大的模块工具

函数 描述 对应 range 的场景 示例代码
count(start, step) 无限计数器 无限版 range (range 必须有终点) list(itertools.islice(itertools.count(0, 0.5), 5))[0, 0.5, 1.0, 1.5, 2.0] (支持浮点步长)
repeat(obj, times) 重复某个值 生成常数列表 list(itertools.repeat('A', 3))['A', 'A', 'A']
cycle(iterable) 无限循环遍历 循环序列 list(itertools.islice(itertools.cycle([1, 2]), 5))[1, 2, 1, 2, 1]
chain(*iterables) 连接多个序列 拼接多个 range list(itertools.chain(range(2), range(5, 7)))[0, 1, 5, 6]
islice(iterable, stop) 切片迭代器 截取无限序列 (配合 count 使用) count 示例
product() 笛卡尔积 多重嵌套 range list(itertools.product(range(2), range(2)))[(0,0), (0,1), (1,0), (1,1)]
permutations() 排列 生成所有排列顺序 list(itertools.permutations([1, 2, 3], 2))
combinations() 组合 生成所有不重复组合 list(itertools.combinations([1, 2, 3], 2))

1.4.6 array

非内置,array可以支持同类型限制,因为相关方法较少,可能不太好用,可以使用numpy包的数组,更强大

import array
# 创建一个只存整数的数组 ('i' 表示 signed int)
arr = array.array('i', [1, 2, 3, 4])
# arr * 2 会报错,不支持向量化乘法
# arr + arr 可以拼接,但不能数学运算

1.4.7 numpy

       numpy主要用于数据分析,支持多维数组和更强大的功能,也更复杂。感兴趣的可以自行学习实践。不适合做基础学习

1.5 序列

字符串、元组、列表都可以归为序列一类,所以有一些共有的方法

1.5.1 内置函数

这些函数接受一个序列作为参数,返回计算结果或新对象。它们不会修改原序列(因为字符串和元组本身不可变,列表虽可变但这些函数设计为非原地操作,如果不确定,可以验证一下)

函数名 语法 功能描述 返回值类型 适用示例
len() len(s) 返回序列的长度(元素个数) int len([1,2])2
max() max(s) 返回序列中的最大元素 元素类型 max("abc")'c'
min() min(s) 返回序列中的最小元素 元素类型 min((3,1,2))1
sum() sum(s) 返回序列中所有元素的和 数字类型 sum([1,2,3])6
sorted() sorted(s) 返回排序后的新列表 list sorted("cba")['a','b','c']
reversed() reversed(s) 返回反转后的迭代器 iterator list(reversed([1,2]))[2,1]
enumerate() enumerate(s) 返回带索引的迭代器 (index, value) iterator list(enumerate("ab"))[(0,'a'), (1,'b')]
zip() zip(s1, s2) 将多个序列打包成元组序列 iterator list(zip([1,2], ['a','b']))[(1,'a'), (2,'b')]
list() list(s) 将任意序列转换为列表 list list("abc")['a','b','c']
tuple() tuple(s) 将任意序列转换为元组 tuple tuple([1,2])(1,2)
str() str(s) 将序列转换为字符串表示 str str([1,2])"[1, 2]"
all() all(s) 所有元素为真则返回 True bool all([1, True])True
any() any(s) 只要有一个元素为真则返回 True bool any([0, False, 1])True
count()* s.count(x) 注意:这是方法不是函数,但所有序列都有 int "aba".count('a')2
index()* s.index(x) 注意:这是方法不是函数,但所有序列都有 int (1,2).index(2)1

这里比较有意思的是zip函数

1.5.2 通用操作

操作符 语法 功能描述 示例
索引 s[i] 获取第 i 个元素(从 0 开始) [10, 20][0]10
负索引 s[-i] 获取倒数第 i 个元素 "abc"[-1]'c'
切片 s[start:end:step] 截取子序列 [1,2,3,4][1:3][2, 3]
拼接 s1 + s2 连接两个同类型序列 [1] + [2][1, 2]
重复 s * n 将序列重复 n "A" * 3'AAA'
成员检查 x in s 判断 x 是否在序列中 3 in [1, 2, 3]True
成员检查 x not in s 判断 x 是否不在序列中 5 not in (1, 2)True
比较 s1 == s2 判断两个序列内容是否相等 [1, 2] == (1, 2)False (类型不同)
比较 s1 < s2 字典序比较(逐个元素比较) "ab" < "ac"True

注意s1 == s2比较的是内容,不是引用

1.5.3 collections

这个包可以参考一下,简单来说:当内置的 listdictsettuple 无法满足特定需求(如:需要有序字典、需要计数、需要双端队列)时,collections 就是你的最佳选择。

需求 推荐工具 核心优势
统计频率 Counter 语法简洁,支持数学运算
避免 KeyErrors / 分组 defaultdict 自动初始化默认值,代码更干净
队列 / 栈 / 滑动窗口 deque 两端操作 O(1) 极速,支持 maxlen
轻量级数据结构 / 记录 namedtuple 像对象一样访问,内存占用极低,不可变
严格顺序控制 / LRU OrderedDict 显式的顺序控制方法 (move_to_end)
多层级配置合并 ChainMap 逻辑合并多个字典,无需复制数据
普通键值对 dict 通用,Python 3.7+ 已保序
普通列表 list 通用

这里举一些例子:

1.5.3.1 Counter  (计数器)

用途:快速统计可哈希对象出现的次数。它是 dict 的子类。
场景:词频统计、找出出现最多的元素、数据分布分析。

from collections import Counter

data = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']

# ✅ 初始化并统计
c = Counter(data)
print(c) 
# 输出: Counter({'apple': 3, 'banana': 2, 'orange': 1})

# ✅ 获取最常见的 N 个元素
print(c.most_common(2)) 
# 输出: [('apple', 3), ('banana', 2)]

# ✅ 数学运算 (支持加减并集交集)
c2 = Counter(['apple', 'kiwi'])
print(c + c2) 
# 输出: Counter({'apple': 4, 'banana': 2, 'orange': 1, 'kiwi': 1})

# ✅ 访问不存在的键不会报错,返回 0
print(c['grape']) # 输出: 0
1.5.3.2 defaultdict (默认字典)

用途:为字典的键提供默认值。当访问不存在的键时,自动创建默认值,而不是抛出 KeyError
场景:分组数据、构建邻接表、避免大量的 if-else 检查

from collections import defaultdict

# ❌ 原生 dict 做法
d = {}
# d['a'].append(1) # 报错 KeyError

# ✅ defaultdict 做法
# 参数是一个工厂函数:int(), list(), set(), lambda: "default"
dd_list = defaultdict(list)
dd_list['fruits'].append('apple')
dd_list['fruits'].append('banana')
dd_list['nums'].append(1) # 自动初始化为空列表 []

print(dd_list) 
# 输出: defaultdict(<class 'list'>, {'fruits': ['apple', 'banana'], 'nums': [1]})

# ✅ 统计计数 (默认值为 int,即 0)
dd_count = defaultdict(int)
for char in "hello":
    dd_count[char] += 1 # 不需要判断 key 是否存在
print(dd_count)
# 输出: defaultdict(<class 'int'>, {'h': 1, 'e': 1, 'l': 2, 'o': 1})
1.5.3.3 deque  (双端队列)

用途:线程安全的、支持两端高效添加/删除元素的队列。
场景:实现队列 (FIFO)、栈 (LIFO)、滑动窗口、最近浏览记录。
优势:在列表头部插入/删除 (pop(0)insert(0)) 是 O(n) 复杂度,而 deque 是 O(1)

from collections import deque

q = deque(['a', 'b', 'c'])

# ✅ 右侧操作 (类似 list)
q.append('d')      # 右边加
q.pop()            # 右边删

# ✅ 左侧操作 (list 做这个很慢!)
q.appendleft('z')  # 左边加
q.popleft()        # 左边删

print(q) # deque(['b', 'c', 'd'])

# ✅ 限制最大长度 (自动丢弃旧元素)
# 适合做“最近 N 条记录”
history = deque(maxlen=3)
for i in range(5):
    history.append(i)
print(history) 
# 输出: deque([2, 3, 4], maxlen=3) (0和1被自动丢弃)
1.5.3.4 namedtuple (命名元组)
from collections import namedtuple

# ✅ 定义一个名为 Point 的类,有 x 和 y 两个字段
Point = namedtuple('Point', ['x', 'y'])

p = Point(10, 20)

# ✅ 通过属性访问 (比元组索引 p[0] 可读性强太多)
print(p.x, p.y)  # 输出: 10 20

# ✅ 依然拥有元组的特性
print(len(p))    # 输出: 2
# p.x = 5        # 报错:AttributeError (不可变)

# ✅ 转换为字典
print(p._asdict()) # 输出: OrderedDict([('x', 10), ('y', 20)])

# ✅ 解包
x, y = p

Python 3.7+ 替代方案:对于可变对象,现在更推荐使用 dataclasses 模块 (@dataclass),但 namedtuple 在需要不可变轻量的场景下依然无敌

1.5.3.5 OrderedDict (有序字典)

用途:记住键值对插入顺序的字典。
现状⚠️ 注意,从 Python 3.7 开始,标准的 dict 已经默认保持插入顺序
场景

  1. 需要兼容 Python 3.6 及以下版本。
  2. 需要使用 move_to_end() 方法(例如实现 LRU 缓存逻辑)。
  3. 需要比较两个字典是否完全相等(包括顺序),标准 dict 比较只看内容,OrderedDict 比较看顺序。
from collections import OrderedDict

od = OrderedDict()
od['first'] = 1
od['second'] = 2
od['third'] = 3

# ✅ 移动某个键到末尾 (常用于实现 LRU 缓存)
od.move_to_end('first') 
print(list(od.keys())) 
# 输出: ['second', 'third', 'first']

# ✅ 弹出第一个元素 (标准 dict 在 3.7+ 也可以 popitem(last=False),但 OrderedDict 语义更明确)
od.popitem(last=False) 
print(list(od.keys())) 
# 输出: ['third', 'first']
1.5.3.6 ChainMap (映射链)

用途:将多个字典/映射组合成一个逻辑单元。查找时会按顺序在各个字典中搜索。
场景:管理多层级配置(命令行参数 > 环境变量 > 默认配置)、合并命名空间。

from collections import ChainMap

defaults = {'color': 'red', 'user': 'guest'}
env_vars = {'user': 'admin'}
cmd_args = {'color': 'blue'}

# ✅ 优先级:cmd_args > env_vars > defaults
config = ChainMap(cmd_args, env_vars, defaults)

print(config['color']) # 输出: 'blue' (来自 cmd_args)
print(config['user'])  # 输出: 'admin' (来自 env_vars)
print(config['port'])  # 如果都没有,报 KeyError

# ✅ 动态更新
# 修改 ChainMap 会直接修改底层的第一个字典
config['color'] = 'green' 
print(cmd_args) # 输出: {'color': 'green'} (原字典被改了!)

1.6 字典

字典主要是用{}定义的键值对数据结构。访问主要通过key

方式 语法示例 最佳适用场景
字面量 {"a": 1} 静态数据,键值明确
关键字参数 dict(a=1) 键是合法变量名,追求简洁
Zip/列表 dict(zip(k, v)) 两个列表配对,或从元组列表构建
推导式 {k:v for ...} 数据转换、过滤、动态生成
Fromkeys dict.fromkeys(k, val) 批量初始化相同默认值 (注意可变对象陷阱)
合并 (` `) d1 | d2

1.6.1 定义方法

1. 字面量直接定义 (Literal Syntax)

最常用、最直观的方式。适用于键值对已知且固定的场景

# 基本用法
user = {
    "name": "Alice",
    "age": 25,
    "is_active": True
}

# 键可以是任何不可变类型 (字符串、数字、元组)
data = {
    "id": 101,
    3.14: "Pi Value",
    ("x", "y"): "Coordinate Key" 
}

print(user["name"])  # 输出: Alice
print(data) # 输出: {'id': 101, 3.14: 'Pi Value', ('x', 'y'): 'Coordinate Key'}

2. dict() 构造函数 + 关键字参数

适用于键是合法变量名(字符串且无空格、特殊字符)的场景。代码非常简洁。

# 键自动转为字符串
config = dict(host="localhost", port=8080, debug=True)

print(config) 
# 输出: {'host': 'localhost', 'port': 8080, 'debug': True}
# 注意:键必须是合法的标识符,不能是 "my-port" 或 "1st"

3. dict() + 可迭代对象 (列表/元组)

适用于从外部数据(如数据库查询结果、CSV 行)批量构建字典。数据源通常是包含 (key, value) 元组的列表。

# 来源:列表中包含元组
items = [("apple", 1.5), ("banana", 0.8), ("orange", 2.0)]
prices = dict(items)

print(prices)
# 输出: {'apple': 1.5, 'banana': 0.8, 'orange': 2.0}

# 来源:zip() 函数动态生成 (非常常用!)
keys = ["name", "role", "level"]
values = ["Bob", "Admin", 5]
user_info = dict(zip(keys, values))

print(user_info)
# 输出: {'name': 'Bob', 'role': 'Admin', 'level': 5}

4. 字典推导式 (Dictionary Comprehension)

类似于列表解析,用于根据现有数据动态生成转换字典。功能最强大

# 场景 A: 生成平方数字典 {1:1, 2:4, ...}
squares = {x: x**2 for x in range(1, 6)}
print(squares) 
# 输出: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# 场景 B: 过滤数据 (只保留值大于 2 的项)
filtered = {k: v for k, v in squares.items() if v > 10}
print(filtered)
# 输出: {4: 16, 5: 25}

# 场景 C: 交换键和值
original = {"a": 1, "b": 2}
swapped = {v: k for k, v in original.items()}
print(swapped)
# 输出: {1: 'a', 2: 'b'}

5. dict.fromkeys() (初始化默认值)

当你需要创建一个新字典,所有键都有相同的初始值(常用于计数器初始化或占位符)时使用。

keys = ["red", "green", "blue"]

# 所有键的默认值为 0
color_counts = dict.fromkeys(keys, 0)
print(color_counts)
# 输出: {'red': 0, 'green': 0, 'blue': 0}

# 所有键的默认值为空列表 (⚠️ 陷阱警告:见下方注意事项)
# 如果默认值是可变对象,所有键会共享同一个对象引用!
shared_list = dict.fromkeys(keys, []) 
shared_list["red"].append(1)
print(shared_list) 
# 输出: {'red': [1], 'green': [1], 'blue': [1]} (通常这不是你想要的!)

6. 合并操作符 | (Python 3.9+ 新特性)

如果你使用的是 Python 3.9 或更高版本,可以使用 | 运算符轻松合并字典。旧版本需使用 {**d1, **d2} 或 update()

# 假设运行环境 >= Python 3.9
defaults = {"theme": "dark", "lang": "en"}
user_prefs = {"lang": "zh", "font_size": 14}

# 合并:后者覆盖前者
config = defaults | user_prefs

print(config)
# 输出: {'theme': 'dark', 'lang': 'zh', 'font_size': 14}

# 原地更新
defaults |= user_prefs 

1.6.2 访问方法

1 字典访问主要通过key

# 键自动转为字符串
config = dict(host="localhost", port=8080, debug=True)

print(config['host']) #输出localhost
#print(config[host]) 不加引号要报错
#print(config.host) 也不能这样用

2 遍历keys

# 方式 A: 省略 .keys() (最常用,Pythonic)
print("学生名单:")
for name in user_scores:
    print(f"- {name}")

# 方式 B: 显式调用 .keys() (代码可读性稍好,但功能相同)
for name in user_scores.keys():
    pass 

3 编列values

user_scores = {
    "Alice": 95,
    "Bob": 82,
    "Charlie": 88,
    "David": 95
}

print("所有分数:")
total = 0
count = 0

for score in user_scores.values():
    print(f"分数: {score}")
    total += score
    count += 1

print(f"平均分: {total / count:.2f}")

#输出
tip = """
所有分数:
分数: 95
分数: 82
分数: 88
分数: 95
平均分: 90.00
"""

4 遍历key,value

user_scores = {
    "Alice": 95,
    "Bob": 82,
    "Charlie": 88,
    "David": 95
}

print("成绩单:")
for name, score in user_scores.items():
    print(f"{name}: {score}分")

#输出
tip = """
成绩单:
Alice: 95分
Bob: 82分
Charlie: 88分
David: 95分
"""

5 使用get访问

user = {"name": "Alice", "age": 25}

# 场景 A: 键存在 -> 返回值
name = user.get("name") 
print(name)  # 输出: Alice

# 场景 B: 键不存在 -> 返回 None (默认)
email = user.get("email")
print(email)  # 输出: None (程序继续运行,不报错)

# 场景 C: 键不存在 -> 返回自定义默认值 (常用!)
role = user.get("role", "guest") 
print(role)  # 输出: guest

1.6.3 无key异常

字典在无key时访问会抛出异常,这点需要特别注意

有以下几个处理办法:

使用get访问(上面已经介绍过)

使用in or not in提前判断

user = {"name": "Alice"}

if "email" in user:
    print(f"邮箱是: {user['email']}")
else:
    print("邮箱未设置,使用默认值")
    # 在这里可以执行备用逻辑

if "email" not in user:
    user["email"] = "default@example.com"

使用try...except异常捕获

user = {"name": "Alice"}

try:
    email = user["email"]
    print(f"邮箱: {email}")
except KeyError:
    print("键 'email' 不存在,使用默认逻辑")
    email = "no-reply@example.com"

1.6.4 字典修改

       “修改字段”通常指三种操作:更新已有值添加新键值对批量合并/更新。由于字典是可变对象,这些操作都会直接改变原字典。

方法 代码示例 是否修改原字典 适用场景
.get() d.get("k", default) ❌ 否 通用首选,获取值并提供默认值
in 检查 if "k" in d: ❌ 否 需要根据键是否存在执行不同逻辑块
try-except try: d["k"] ... ❌ 否 键大概率存在,或逻辑复杂时
defaultdict dd = defaultdict(type) ✅ 是 (自动) 计数器、分组聚合,需频繁处理缺失键
setdefault() d.setdefault("k", v) ✅ 是 需要确保键存在并初始化(如嵌套列表)

1. 直接赋值法 (Direct Assignment) - 最基础

user = {"name": "Alice", "age": 25}

# 场景 A: 修改已存在的值
user["age"] = 26 
print(user) # {'name': 'Alice', 'age': 26}

# 场景 B: 添加新字段
user["city"] = "Beijing"
print(user) # {'name': 'Alice', 'age': 26, 'city': 'Beijing'}

# 场景 C: 值是可变对象时的“深层”修改
user["skills"] = ["Python"]
print(user)
user["skills"].append("Java") # 修改列表内容,字典本身结构未变,但数据变了
print(user["skills"]) # ['Python', 'Java']

#输出
tip='''
{'name': 'Alice', 'age': 26}
{'name': 'Alice', 'age': 26, 'city': 'Beijing'}
{'name': 'Alice', 'age': 26, 'city': 'Beijing', 'skills': ['Python']}
['Python', 'Java']
'''

2. .update() 方法 - 批量修改/合并

user = {"name": "Alice", "age": 25, "role": "user"}

# 方式 A: 传入另一个字典 (常用)
updates = {"age": 26, "city": "Shanghai"}
user.update(updates)
print(user) 
# {'name': 'Alice', 'age': 26, 'role': 'user', 'city': 'Shanghai'}

# 方式 B: 传入关键字参数 (适合键名合法时)
user.update(role="admin", status="active")
print(user)
# {..., 'role': 'admin', 'status': 'active'}

# 方式 C: 传入元组列表
user.update([("level", 5), ("score", 90)])
print(user)

3 .setdefault() 方法 - “仅当不存在时”修改

如果键不存在,则设置默认值并返回;如果键已存在,则什么都不做,直接返回原值。
用途: 防止覆盖已有数据,常用于初始化嵌套结构。

user = {"name": "Alice"}

# 如果 "email" 不存在,则设置为 "unknown" 并返回 "unknown"
# 如果 "email" 已存在,则返回原值,不做修改
email = user.setdefault("email", "unknown")

print(email)   # 输出: unknown
print(user)    # 输出: {'name': 'Alice', 'email': 'unknown'} (字典被修改了!)

1.6.5 字典key删除

删除字典(Dictionary)中的键(Key)主要有 4 种常用方法,以及一种批量删除的技巧。选择哪种方法取决于你是否需要获取被删除的值,以及键是否存在

1. pop(key[, default]) - 最推荐 (安全且返回值)
这是最灵活的方法。它会删除指定的键,并返回该键对应的值。
特点: 可以指定默认值,防止键不存在时报错。
适用: 需要拿到被删除的值,或者希望代码健壮(不报错)。

data = {"a": 1, "b": 2, "c": 3}

# 场景 A: 键存在 -> 删除并返回值
val = data.pop("b")
print(val)   # 输出: 2
print(data)  # 输出: {'a': 1, 'c': 3}

# 场景 B: 键不存在 + 提供默认值 -> 不报错,返回默认值
val = data.pop("z", "Not Found")
print(val)   # 输出: Not Found
print(data)  # 输出: {'a': 1, 'c': 3} (字典未变)

# 场景 C: 键不存在 + 无默认值 -> 抛出 KeyError (慎用)
# data.pop("z") 

2. del 语句 - 最快但危险
使用 Python 的 del 关键字直接删除。
特点: 语法简洁,执行速度快,不返回被删除的值。
风险: 如果键不存在,必定抛出 KeyError 导致程序崩溃。
适用: 你100% 确定键存在,且不需要返回值。

data = {"a": 1, "b": 2}

# 场景 A: 键存在 -> 成功删除
del data["a"]
print(data) # {'b': 2}

# 场景 B: 键不存在 -> 报错! (Runtime Error)
# del data["z"]  # KeyError: 'z'

# ✅ 安全用法:配合 in 检查
if "z" in data:
    del data["z"]

3. popitem() - 删除最后一项 (LIFO)
删除并返回字典中的最后一个键值对(元组形式)。
特点: Python 3.7+ 保证按插入顺序删除最后加入的一项(栈行为)。
风险: 如果字典为空,抛出 KeyError。
适用: 需要逐个处理并移除元素,或实现栈结构。

data = {"first": 1, "last": 2}

# 删除最后一项 ("last": 2)
key, value = data.popitem()
print(f"Deleted: {key} = {value}") # Deleted: last = 2
print(data) # {'first': 1}

# 空字典调用会报错
# {}.popitem() # KeyError

4. clear() - 清空所有
一次性删除字典中的所有键值对。
特点: 字典变为空 {},原地修改,不返回内容。
适用: 重置配置、清理缓存

data = {"a": 1, "b": 2}
data.clear()
print(data) # {}

5. 高级技巧:批量删除 (过滤)
Python 没有内置的“批量删除指定键”的方法,通常使用字典推导式创建一个新字典(排除掉不想要的键)。

data = {"a": 1, "b": 2, "c": 3, "d": 4}
keys_to_remove = {"b", "d"}

# 方法:保留那些 不在 删除列表中的键
new_data = {k: v for k, v in data.items() if k not in keys_to_remove}

print(new_data) # {'a': 1, 'c': 3}
# 注意:原字典 data 未变,如果需要替换,执行 data = new_data

⚠️ 重要警告:遍历中删除

永远不要在直接遍历字典(for k in d:)的同时使用 del 或 pop 删除当前遍历的键,这会引发 RuntimeError

❌ 错误写法

for key in data:
    if data[key] < 0:
        del data[key]  # 报错:RuntimeError: dictionary changed size during iteration

1.7 集合(set)

集合(Set) 是一个无序、不重复的元素序列。它基于数学中的集合论,主要用于去重和关系运算(交集、并集等)。定义集合主要有 3 种方式,其中空集合的定义是新手最容易犯错的地方.

1.7.1 集合定义

定义方式 语法示例 适用场景 关键点
花括号 {1, 2, 3} 定义非空集合 自动去重,元素必须不可变
构造函数 set() 定义空集合 / 去重转换 唯一定义空集的方法
推导式 {x for x in data} 生成规律性集合 类似列表推导式,用 {}
冻结集合 frozenset(...) 需要不可变集合时 可作为字典的 Key


⚠️ 新手避坑指南
1 永远不要写 {} 来创建空集合,请始终使用 set()。
2 不要尝试对集合进行索引操作(s[0]),如果需要索引,请先转为列表 list(s)。
3 如果需要存储“列表”在集合中,请先将其转换为“元组”

1. 花括号法 { } (最常用)
使用花括号将元素包裹起来,元素之间用逗号 , 分隔。
特点: 自动去重,无序。
⚠️ 重要陷阱: 不能用 {} 定义空集合,因为 {} 默认是空字典。

# ✅ 普通集合 (自动去重)
numbers = {1, 2, 3, 2, 1}
print((numbers)) # 输出顺序可能不同,且重复项消失: {1, 2, 3}
print(type(numbers)) #<class 'set'>
# ✅ 混合类型 (必须是可哈希的/不可变的)
mixed = {1, "hello", 3.14, (1, 2)} 
# 注意:列表 [1,2] 或字典 {} 不能作为集合元素,会报错 TypeError

# ❌ 错误:这不是空集合,这是空字典!
empty_wrong = {}
print(type(empty_wrong)) # <class 'dict'>

2. 可变 vs 不可变集合
set: 可变集合。可以添加 (add) 或删除 (remove) 元素。不能作为字典的键或另一个集合的元素。
frozenset: 不可变集合。创建后不能修改。可以作为字典的键或集合的元素

# 定义冻结集合
f_set = frozenset([1, 2, 3])
# f_set.add(4) # ❌ 报错:AttributeError

# 用途:作为字典的键
d = {frozenset([1, 2]): "value"} 

1.7.2 集合访问

目的 推荐方法/操作符 关键点
判断存在 x in s 速度最快 (O(1))
遍历所有 for x in s: 无序,不可控顺序
取出一个 s.pop() 随机取出并删除
安全删除 s.discard(x) 不存在也不报错
危险删除 s.remove(x) 不存在会报错
找共同点 s1 & s2intersection() 交集
找不同点 s1 - s2difference() 差集
合并去重 s1 | s2union() 并集
转列表索引 list(s)[index] 不推荐,除非万不得已

1 成员检测 (in / not in) 
这是访问集合最高效的方式(时间复杂度 O(1)),用于判断元素是否存在。

s = {"apple", "banana", "cherry"}

if "apple" in s:
    print("Found it!") # ✅ 快速判断

if "grape" not in s:
    print("Not found")

2 遍历 (for 循环)
由于无序,只能按顺序(逻辑顺序,非插入顺序)逐个访问

s = {1, 2, 3}
for item in s:
    print(item) 
    # 输出顺序不确定,例如可能是 2, 1, 3

3 获取/提取元素 (Extracting)

方法 语法 功能描述 返回值 备注
pop() s.pop() 随机移除并返回一个元素 被移除的元素 唯一能直接“拿出”元素的方法;若集合为空抛 KeyError
copy() s.copy() 浅拷贝集合 新集合 用于在不修改原集合的情况下操作

1.7.3 集合元素添加

方法 语法 功能描述 返回值 备注
add() s.add(elem) 添加单个元素 None 若元素已存在,不执行任何操作(不报错)
update() s.update(iterable) 批量添加多个元素 None 参数可以是列表、元组、字符串或其他集合

1.7.4 集合元素删除

方法 语法 功能描述 返回值 备注
remove() s.remove(elem) 删除指定元素 None 若元素不存在,抛 KeyError ⚠️
discard() s.discard(elem) 删除指定元素 None 若元素不存在,不报错 (✅ 推荐用于安全删除)
clear() s.clear() 清空所有元素 None 集合变为空集 set()

1.7.4 集合运算

数学中的交、并、差、子集等判断

方法 对应运算符 功能描述 示例
intersection() & 交集:返回两个集合共有的元素 s1.intersection(s2)
union() | 并集:返回两个集合所有不重复元素 s1.union(s2)
difference() - 差集:返回在 s1 但不在 s2 的元素 s1.difference(s2)
symmetric_difference() ^ 对称差集:只在其中一个集合出现的元素 s1.symmetric_difference(s2)
issubset() <= 子集判断:s1 是否包含于 s2 s1.issubset(s2) (返回 bool)
issuperset() >= 父集判断:s1 是否包含 s2 s1.issuperset(s2) (返回 bool)
isdisjoint() - 无交集判断:两个集合是否完全没有共同元素 s1.isdisjoint(s2) (返回 bool)

有一点要注意,同类行|,操作结果类型不变,一个set和一个frozenset要变,看以下代码

s = {1, 2, 3}             # set (可变)
fs = frozenset([3, 4, 5]) # frozenset (不可变)
result1 = fs|s
print(f"结果: {result1}")
print(f"结果类型: {type(result1)}") 

result2 = s|fs
print(f"结果: {result2}")
print(f"结果类型: {type(result2)}")

tip='''
结果: frozenset({1, 2, 3, 4, 5})
结果类型: <class 'frozenset'>
结果: {1, 2, 3, 4, 5}
结果类型: <class 'set'>
'''

1.7.5 速查表

目的 推荐方法/操作符 关键点
判断存在 x in s 速度最快 (O(1))
遍历所有 for x in s: 无序,不可控顺序
取出一个 s.pop() 随机取出并删除
安全删除 s.discard(x) 不存在也不报错
危险删除 s.remove(x) 不存在会报错
找共同点 s1 & s2intersection() 交集
找不同点 s1 - s2difference() 差集
合并去重 s1 | s2union() 并集
转列表索引 list(s)[index] 不推荐,除非万不得已

2 语句

2.1 if

1 缩进 (Indentation):Python 使用缩进来定义代码块,而不是大括号 {}。if、elif 和 else 后面的代码必须缩进(通常使用 4 个空格)。
2 冒号 (:):if、elif 和 else 语句末尾必须有冒号。
3 布尔值:条件表达式的结果必须是布尔值 (True 或 False)。在 Python 中,以下值被视为 False:
  False
  None
  零值:0, 0.0, 0j
  空序列/集合:'' (空字符串), [] (空列表), () (空元组), {} (空字典)
  其他所有值都视为 True


2.1.1  if 

age = 20
if age >= 18:
    print("你已成年。")
# 输出:你已成年。

2.1.3 if else

score = 45

if score >= 60:
    print("及格")
else:
    print("不及格")
# 输出:不及格

2.1.4 if-elif-else

temperature = 25

if temperature > 30:
    print("天气很热")
elif temperature > 20:
    print("天气舒适")
elif temperature > 10:
    print("天气有点凉")
else:
    print("天气很冷")
# 输出:天气舒适

2.1.5 if嵌套

user_logged_in = True
user_is_admin = False

if user_logged_in:
    print("用户已登录")
    if user_is_admin:
        print("欢迎管理员!")
    else:
        print("欢迎普通用户。")
else:
    print("请先登录。")

2.1.6 if三元运算

score=70
# 语法: 值1 if 条件 else 值2
status = "通过" if score >= 60 else "失败"
print(status)

2.1.7 常见运算符搭配

在 if 条件中,常配合以下逻辑运算符使用:
and: 与 (两边都为 True 才为 True)
or: 或 (只要有一边为 True 即为 True)
not: 非 (取反)

x = 5
y = 10

if x > 0 and y > 0:
    print("x 和 y 都是正数")

if not (x > 10):
    print("x 不大于 10")

2.1.8 赋值与判断

# 1. 先赋值
def getValue():
    return True
b =  getValue()
# 2. 再判断
if b:
    # 执行逻辑
    print('hhhh')

#上面的写法等价于
if b:= getValue():
   print('hhhh')

python看起来简单,实际上很复杂,想加啥就加啥,有点像英语

2.2 while

       语句用于在条件为真(True)时重复执行代码块。只要条件保持为 True,循环就会一直继续;一旦条件变为 False,循环终止

2.2.1 基本使用

count = 0
while count < 5:
    print(f"当前计数: {count}")
    count += 1  # 重要:更新计数器,否则死循环
# 输出: 0, 1, 2, 3, 4

2.2.2 配合else

        while正常结束、才会执行else。这个玩意慎用。虽然能满足一些特殊需求,用起来总感觉怪怪的。就像最初人只有男人和女人、后面多了个人妖一样。

print("--- 场景 A: 循环正常结束 ---")
count = 0
while count < 3:
    print(f"计数: {count}")
    count += 1
else:
    # 因为 count 变成 3,条件 count < 3 变为 False,循环自然结束
    print(f"循环自然结束,执行 else 块。count={count}")

print("\n--- 场景 B: 循环被 break 中断 ---")
count = 0
while count < 3:
    print(f"计数: {count}")
    if count == 1:
        print("检测到特定条件,触发 break!")
        break  # 强制退出
    count += 1
else:
    # 因为遇到了 break,这里会被跳过
    print("这行永远不会打印。")

print("程序继续运行...")

tip=='''
--- 场景 A: 循环正常结束 ---
计数: 0
计数: 1
计数: 2
循环自然结束,执行 else 块。count=3

--- 场景 B: 循环被 break 中断 ---
计数: 0
计数: 1
检测到特定条件,触发 break!
程序继续运行...
'''

2.3 for

基本用发是用于遍历可迭代对象、如元组、列表、等

for 变量 in 可迭代对象:
    # 对每个元素执行这里的代码
    语句块

2.3.1 遍历列表、元组和字符串

# 遍历列表
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

# 遍历字符串
for char in "Python":
    print(char, end="-") 
# 输出: P-y-t-h-o-n-

2.3.2 配合 range() 进行数字循环

# range(停止值): 0 到 4
for i in range(5):
    print(i, end=" ") 
# 输出: 0 1 2 3 4

# range(开始, 停止): 2 到 4
for i in range(2, 5):
    print(i, end=" ") 
# 输出: 2 3 4

# range(开始, 停止, 步长): 0, 2, 4, 6, 8
for i in range(0, 10, 2):
    print(i, end=" ") 

2.3.3 配合enumerate使用

colors = ["red", "green", "blue"]

# 错误做法: 手动维护 index
# index = 0
# for color in colors: ... index += 1

# 正确做法:
for index, color in enumerate(colors):
    print(f"索引 {index}: 颜色 {color}")
# 输出:
# 索引 0: 颜色 red
# 索引 1: 颜色 green
# 索引 2: 颜色 blue

2.3.4 遍历字典

user = {"name": "Alice", "age": 25, "city": "Beijing"}

# 只遍历键
for key in user:
    print(key) 

# 遍历键和值 (推荐)
for key, value in user.items():
    print(f"{key}: {value}")

# 只遍历值
for value in user.values():
    print(value)

2.3.5 配合else使用

与 while...else 类似。如果循环正常结束(即遍历完所有元素,没有遇到 break),则执行 else 块

numbers = [1, 3, 6, 7]
for n in numbers:
    if n % 2 == 0:
        print(f"找到偶数: {n}")
        break
else:
    # 只有当循环没有被 break 打断时才执行
    print("列表中没有偶数")
# 输出: 列表中没有偶数

2.4 continue

       它的作用是立即结束当前这一轮循环,跳过循环体中剩余的代码,直接进入下一次循环的判断和执行。

位置:只能用在 for 或 while 循环内部。
行为:
遇到 continue 时,不再执行该行之后、当前缩进块内的任何代码。
直接跳回到循环的开始处。
如果是 for 循环:获取序列中的下一个元素。
如果是 while 循环:重新判断条件表达式。

对比 break:
break:彻底终止整个循环(不再进行任何迭代)。
continue:仅跳过本次迭代(循环还会继续处理剩下的元素)

       不支持带标签,也就是只能继续当前层循环。

2.4.1 基本示例

for i in range(10):
    if i == 5:
        print("跳过 5")
        continue  # 遇到 5,跳过下面的 print(i),直接进入下一轮 i=6
    
    print(f"当前数字: {i}")

print("循环结束")

2.5 break

它的作用是立即彻底终止当前所在的整个循环,不支持跳出多层循环
一旦执行到 break:
1 循环立即结束,不再进行任何后续的迭代。
2 程序流程跳转到循环体之后的第一行代码继续执行。
3 如果是嵌套循环,它只跳出最内层的那一层循环,外层循环不受影响(除非配合其他逻辑)

2.5.1 基本示例

numbers = [1, 5, 8, 12, 3, 9]
target = 8

found_index = -1

for i, num in enumerate(numbers):
    if num == target:
        found_index = i
        print(f"找到了!{target} 在索引 {i} 处")
        break  # 找到后立刻停止循环,后面的 12, 3, 9 不再检查
    
    print(f"正在检查: {num}")

print(f"循环结束,最终索引: {found_index}")

2.6 with

        with 语句用于简化资源管理(如文件操作、网络连接、数据库连接、锁等)。它确保资源在使用完毕后会被自动、正确地释放,即使在使用过程中发生了异常(错误)

        它的核心机制是上下文管理器(Context Manager),基于两个魔法方法:__enter__ 和 __exit__。

2.6.1 传统写法

     如果不使用 with,你需要手动打开和关闭文件。如果在读写过程中发生异常,close() 可能永远不会被执行,导致资源泄露。

f = open('data.txt', 'r')
try:
    content = f.read()
    # 假设这里发生了错误...
    # 1/0  <-- 如果这里报错,下面的 close() 就不会执行!
except Exception as e:
    print(f"出错了: {e}")
finally:
    f.close()  # 必须写在 finally 中确保执行,代码繁琐

2.6.2 with写法

   with 语句会自动处理 try...finally 逻辑。无论代码是否报错,离开 with 代码块时,文件都会自动关闭

# 语法:with 表达式 as 变量:
with open('data.txt', 'r') as f:
    content = f.read()
    # 即使这里发生错误 (如 1/0)
    # Python 也会自动调用 f.close()
    
print("文件已自动关闭,程序继续运行")

2.6.3 lock中的应用

import threading

lock = threading.Lock()

# 传统写法需要 lock.acquire() 和 lock.release()
with lock:
    # 临界区代码
    print("正在访问共享资源...")
    # 离开缩进块后,锁自动释放

2.6.4 数据库连接

确保数据库连接或事务在结束后正确关闭或回滚

import sqlite3

# 假设 conn 是一个数据库连接对象
with sqlite3.connect('example.db') as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
    # 离开块后,连接自动提交事务并关闭(具体行为取决于库的实现)

2.6.5 临时改变环境设置

import os
from contextlib import chdir

# 临时进入 /tmp 目录
with chdir('/tmp'):
    print(os.getcwd())  # 输出: /tmp
    # 在这里创建文件...

# 离开块后,自动回到原来的目录
print(os.getcwd()) 

2.6.6 同时管理多个资源

可以在一行 with 中管理多个资源(Python 2.7+ / 3.1+ 支持)

# 同时打开两个文件
with open('input.txt', 'r') as f_in, open('output.txt', 'w') as f_out:
    content = f_in.read()
    f_out.write(content.upper())
# 两个文件都会自动关闭

2.6.7 原理

with 语句背后的逻辑等价于以下代码结构:

manager = EXPR  # with 后面的表达式
value = manager.__enter__()  # 进入上下文,返回值赋给 as 后的变量
try:
    BLOCK  # 执行 with 块内的代码
except:
    # 如果发生异常
    manager.__exit__(type, value, traceback) # 处理异常
else:
    # 如果没有异常
    manager.__exit__(None, None, None)

__enter__: 在进入 with 块时执行。通常用于初始化资源(如打开文件),并返回资源对象。
__exit__: 在离开 with 块时执行(无论是否发生异常)。通常用于清理资源(如关闭文件)。如果发生异常,它会接收异常类型、值和追踪信息;如果它返回 True,异常会被抑制(不抛出

2.6.8 自定义with

class MyResource:
    def __enter__(self):
        print("资源已获取 (Enter)")
        return self  # 返回给 as 后面的变量
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("资源已释放 (Exit)")
        if exc_type:
            print(f"检测到异常: {exc_val}")
        # 返回 True 可以吞掉异常,返回 False 或 None 则让异常继续抛出
        return False 

# 使用自定义的上下文管理器
with MyResource() as res:
    print("正在使用资源...")
    # raise ValueError("模拟错误") 

print("程序结束")

2.7 pass

pass 是一个空操作语句(null operation)。当它被执行时,什么也不会发生

2.7.1 定义空函数或类(作为开发草稿)

当你设计好程序架构,定义了函数名或类名,但还没想好具体实现逻辑时,可以用 pass 避免报错,很多脚本语言都具有类似的一些奇葩特性,简直无法理解。有人说天才都有些不正常的地方。

def future_feature():
    # TODO: 以后在这里实现具体功能
    pass

class MyNewClass:
    # 暂时还没有属性和方法
    pass

# 调用它们不会报错,虽然也没什么实际效果
future_feature()
obj = MyNewClass()

2.7.2  在条件判断中忽略特定情况

         当你希望某些条件满足时什么都不做,直接跳过,可以使用 pass。这比写注释更明确地表示“这里是故意留空的”

value = 10

if value < 5:
    print("值很小")
elif value == 10:
    # 暂时不需要处理等于 10 的情况,但不想删除这个分支
    pass 
else:
    print("值很大")

2.7.3 在循环中跳过特定逻辑

for i in range(5):
    if i == 2:
        # 遇到 2 时暂时不做任何处理,继续下一次循环
        pass 
    else:
        print(i)
# 输出: 0, 1, 3, 4
# 注意:这里其实直接用 continue 效果更好,pass 常用于逻辑尚未确定的占位。

当然上面代码换成下面方式会更简洁

for i in range(5):
    if i != 2:
      print(i)

2.7.4 捕获异常但不处理

try:
    result = 10 / 0
except ZeroDivisionError:
    # 捕获了除零错误,但决定忽略它,程序继续运行
    pass 

print("程序继续运行...")

2.8 迭代器

       python中的迭代会抛出StopIteration,这是个陷阱,要注意。很多脚本语言都容易整些语法陷阱,让你防不胜防,这就是我吐槽这类语言的原因。如果是大型长期稳定运行的程序,不是有特殊需求,使用这类语言存粹就是有点智障。

2.8.1 基本用法

python2中的和python用法不一样

#python 2中写法不能用于python3
testTuple=(123,'hello',45.67)
ittest=iter(testTuple)
ittest.next()
ittest.next()
ittest.next()

#python3写法
testTuple=(123,'hello',45.67)
ittest=iter(testTuple)
next(ittest)
next(ittest)
next(ittest)
next(ittest)# 这句会抛出StopIteration

2.8.2 文件迭代

手动控制,代码看起来有点恶心

filename = 'data.csv'

with open(filename, 'r', encoding='utf-8') as f:
    # 1. 读取第一行 (通常是标题)
    header = next(f) 
    print(f"标题: {header.strip()}")
    
    # 2. 读取第二行
    first_row = next(f)
    print(f"第一行数据: {first_row.strip()}")
    
    # 3. 继续用循环读取剩余部分
    # 注意:文件指针已经移动到了第三行,循环会从那里开始
    for line in f:
        print(f"剩余数据: {line.strip()}")
        
    # 4. 如果文件读完再调用 next(),会抛出 StopIteration
    try:
        next(f)
    except StopIteration:
        print("文件已读取完毕!")

实在是受不了,所以搞出个with来

优点:
内存高效:即使文件有 10GB,内存占用也极小。
代码简洁:不需要手动管理索引或读取状态。

# 假设有一个文件 'example.txt'
# 内容:
# Line 1
# Line 2
# Line 3

filename = 'example.txt'

# ✅ 最佳实践:直接在 for 循环中使用文件对象
with open(filename, 'r', encoding='utf-8') as f:
    # f 本身就是一个迭代器
    for line in f:
        # line 包含换行符 '\n',通常需要用 .strip() 去除
        print(f"当前行: {line.strip()}")

手动控制

2.9 生成器

       生成器 (Generator) 是一种特殊的迭代器,它允许你按需生成值,而不是一次性将所有值加载到内存中。它是处理大数据流、无限序列或节省内存的神器

特性 列表 (List) 生成器 (Generator)
定义符号 方括号 []list() 圆括号 () 或含 yield 的函数
内存占用
一次性将所有数据加载到内存中。数据量过大易导致 OOM (内存溢出)。
极低
惰性求值,每次只生成一个值,内存占用恒定(仅保存当前状态)。
计算时机 立即执行 (Eager)
创建时立刻计算所有元素。
延迟执行 (Lazy)
只有在迭代请求 (next) 时才计算当前值。
遍历次数 可重复遍历
可以多次 for 循环或通过索引访问。
仅限一次
遍历完成后即耗尽 (Exhausted),无法再次使用,需重新创建。
索引/切片 支持
支持 lst[0], lst[-1], lst[1:5] 等随机访问。
不支持
无法通过索引访问,只能按顺序逐个获取。
长度查询 支持
len(lst) 可立即返回长度。
不支持
无法直接获取 len() (除非遍历完计数,但这会耗尽它)。
数据规模 适合小规模数据集。 适合大规模数据集、无限序列或流式数据。
性能特点 首次创建慢 (需计算全部),但后续随机访问极快。 创建极快,但逐个访问有微小的函数调用开销。
典型应用 需要多次随机访问、修改、排序或切片的数据。 读取大文件、数据库分页、实时数据流、管道处理。
代码示例 data = [x*2 for x in range(100)] data = (x*2 for x in range(100))

def gen(): yield ...

2.9.1 生成器定义

表达式

# 列表推导式 (立即执行,占内存)
squares_list = [x**2 for x in range(5)] 
# 结果: [0, 1, 4, 9, 16] (列表对象)

# 生成器表达式 (延迟执行,省内存)
squares_gen = (x**2 for x in range(5)) 
# 结果: <generator object <genexpr> at 0x...> (生成器对象)

# 使用时逐个获取
print(next(squares_gen)) # 0
print(next(squares_gen)) # 1
# 或者遍历
for num in squares_gen:
    print(num) # 依次输出 4, 9, 16

函数

在函数中使用 yield 关键字代替 return。
return:返回值并终止函数。
yield:返回值并暂停函数,保留当前局部变量的状态,下次调用从暂停处继续执行

def fibonacci_generator(n):
    """生成前 n 个斐波那契数"""
    a, b = 0, 1
    count = 0
    while count < n:
        yield a  # 暂停在这里,返回 a
        a, b = b, a + b
        count += 1

# 调用函数不会立即执行代码,而是返回一个生成器对象
fib = fibonacci_generator(5)

print(type(fib)) # <class 'generator'>

# 遍历生成器
for num in fib:
    print(num) 
# 输出: 0, 1, 1, 2, 3

生成器每次创建都是一个独立的,调用close之后就,再次调用就会抛出异常

方法 功能描述 典型用途
.send(value) 向生成器内部发送值,并恢复执行。 协程、双向数据流、动态调整参数。
.throw(type, value, traceback) 在生成器暂停处抛出异常。 错误注入、强制中断逻辑、资源清理。
.close() 关闭生成器,抛出 GeneratorExit 异常。 释放外部资源(如文件、网络连接)。

2.9.2 高级用法

1 处理无限序列
由于生成器不存储所有值,它可以表示无限长的序列。

def infinite_counter():
    i = 0
    while True:
        yield i
        i += 1

counter = infinite_counter()

print(next(counter)) # 0
print(next(counter)) # 1
print(next(counter)) # 2
# 可以一直 next 下去,不会爆内存

2 管道式数据处理 (Data Pipeline)
生成器可以串联起来,形成高效的数据处理流,中间不产生临时大列表。

# 1. 读取行 (模拟)
def read_lines(filename):
    for i in range(1000000): # 假设文件有 100 万行
        yield f"Line {i}: data"

# 2. 过滤
def filter_lines(lines):
    for line in lines:
        if "Line 5" in line: # 只保留包含 "Line 5" 的行
            yield line

# 3. 转换
def parse_lines(lines):
    for line in lines:
        yield line.upper()

# 串联管道
data_stream = read_lines("dummy.txt")
filtered_stream = filter_lines(data_stream)
final_stream = parse_lines(filtered_stream)

# 只有在这里遍历时,整个链条才会开始流动
for item in final_stream:
    print(item) 
    # 处理完一个就丢弃一个,内存占用极低

3 send() 和 throw() (双向通信)

       生成器不仅可以产出值,还可以接收外部传入的值(协程的基础)。这个 value = yield total稍微有点难理解

def accumulator():
    total = 2
    while True:
        # yield 左边可以接收 send 传来的值
        value = yield total 
        if value is None:
            print('None')
            break
        total += value

acc = accumulator()
print(next(acc))       # 启动生成器,运行到 yield,返回 2
#print(next(acc))
print(acc.send(10)) # 发送 10 给 value,total 变为 10,返回 10
print(acc.send(20)) # 发送 20 给 value,total 变为 30,返回 30
print(acc.send(20)) # 发送 20 给 value,total 变为 30,返回 30
步骤 代码执行 动作详解 内部变量变化 外部输出 (Print) 生成器状态
1 next(acc) 启动生成器。
执行到 yield total
返回当前的 total 值。
total = 2
value = (未定义)
2 暂停
(停在 value = yield ... 处,等待接收值)
2 acc.send(10) 发送 10。
1. yield 表达式接收到 10。
2. 赋值:value = 10
3. 判断 if value is None (False)。
4. 累加:total += 10 (2 + 10)。
5. 循环回到开头。
6. 再次遇到 yield total,返回新 total
value = 10
total = 12
12 暂停
(停在下一轮循环的 yield 处)
3 acc.send(20) 发送 20。
1. yield 表达式接收到 20。
2. 赋值:value = 20
3. 判断 if (False)。
4. 累加:total += 20 (12 + 20)。
5. 循环,遇到 yield,返回新 total
value = 20
total = 32
32 暂停
4 acc.send(20) 发送 20。
1. yield 表达式接收到 20。
2. 赋值:value = 20
3. 判断 if (False)。
4. 累加:total += 20 (32 + 20)。
5. 循环,遇到 yield,返回新 total
value = 20
total = 52
52 暂停
def safe_processor():
    try:
        while True:
            x = yield "Waiting..."
            print(f"处理数据: {x}")
    except ValueError as e:
        print(f"捕获到注入的错误: {e}")
        return "已安全退出"
    except GeneratorExit:
        print("生成器被强制关闭")
        raise

gen = safe_processor()
next(gen)  # 启动

try:
    # 在 yield 处抛出一个 ValueError
    gen.throw(ValueError("数据格式错误!"))
except StopIteration as e:
    # 如果生成器内部 return 了,会触发 StopIteration
    print(f"生成器返回: {e.value}") 

# 输出:
# Waiting... (来自 next)
# 捕获到注入的错误: 数据格式错误!
# 生成器返回: 已安全退出

2.10 三个点用法

... 被称为 Ellipsis(省略号)。它不仅仅是一个符号,而是 Python 的一个内置常量对象(单例对象),其类型是 types.EllipsisType

  • 它是一个对象:你可以直接打印它,也可以把它赋值给变量。
  • 它不是 pass:pass 是一个空语句,什么都不做;而 ... 是一个表达式,它会被求值为 Ellipsis 对象。
print(...)          # 输出: Ellipsis
print(type(...))    # 输出: <class 'ellipsis'>
print(... is Ellipsis) # 输出: True
场景 含义 示例
代码占位 待实现/省略逻辑 def foo(): ...
NumPy 切片 补全所有剩余维度 arr[0, ..., -1]
类型提示 任意参数列表 Callable[..., int]
终端交互 等待继续输入 >>> if True: ...

             python这玩意要多爽就有多爽,反过来要多恶心就有多,你以为你精通python了,冷不丁的冒出一个你不认识的东西。看看你下面代码,你是不不是看不懂!

from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model

class Movie(BaseModel):
    """A movie with details."""
    title: str = Field(description="The title of the movie")
    year: int = Field(description="The year the movie was released")

3 文件操作

3.1 open

       Python3 的文件操作非常直观且功能强大,主要内置在 open() 函数中。现代 Python 开发强烈推荐使用 with 语句(上下文管理器),因为它能自动处理文件关闭,即使发生错误也能确保资源被释放

3.1.1  基础用法:使用 with 语句(推荐)

# 读取文件 三个参数:文件路径,操作模式,编码
#在 Python 3 中处理文本文件时,强烈建议始终指定 encoding='utf-8'
with open('example.txt', 'r', encoding='utf-8') as f:
    content = f.read()
    print(content)
# 离开 with 块后,文件会自动关闭,无需 f.close()

3.1.2 模式

open() 函数的第二个参数决定了对文件的操作方式

模式 描述 文件不存在时 文件存在时
'r' 只读 (默认) 报错 (FileNotFoundError) 保留原内容,指针在开头
'w' 只写 创建新文件 清空原内容,重新写入
'a' 追加 创建新文件 保留原内容,指针在末尾
'x' 独占创建 创建新文件 报错 (FileExistsError)
'b' 二进制 (配合使用,如 'rb', 'wb') - 用于图片、音频等非文本文件
'+' 更新 (读写同时,如 'r+', 'w+') 取决于主模式 -

3.1.3 文件句柄方法汇总

1 读取方法 

方法名 描述 返回值
read([size]) 读取指定字节数(二进制)或字符数(文本)。若省略 size,则读取直到文件末尾。 str (文本) 或 bytes (二进制)
readline([size]) 读取一行(包括换行符 \n)。若指定 size,则最多读取该长度的字符。 strbytes
readlines([hint]) 读取所有行并返回一个列表。hint 用于限制读取的总字节/字符数(近似值)。 list[str]list[bytes]
readable() 检查文件是否支持读取操作。 bool

2 写入方法

方法名 描述 返回值
write(s) 将字符串 s (文本) 或字节串 s (二进制) 写入文件。 写入的字符数或字节数 (int)
writelines(lines) 将一个字符串列表写入文件。注意:它不会自动添加换行符,列表中每个元素需自带 \n None
writable() 检查文件是否支持写入操作。 bool
flush() 强制刷新缓冲区,将数据立即写入磁盘。常用于日志记录或长运行程序。 None

3 定位

方法名 描述 返回值
seek(offset, whence=0) 移动文件指针到新位置。
- offset: 偏移量
- whence: 参考点 (0: 开头, 1: 当前位置, 2: 文件末尾)
新的绝对位置 (int)
tell() 返回当前文件指针的位置。 int
truncate([size]) 截断文件。若指定 size,则保留前 size 字节/字符;若省略,则在当前指针处截断。 文件大小 (int)
seekable() 检查文件是否支持随机访问(即是否支持 seektell)。 bool

4 属性状态

方法名 描述 返回值
close() 关闭文件。使用后无法再进行读写操作。(使用 with 语句时会自动调用) None
closed (属性) 检查文件是否已关闭。 bool
fileno() 返回底层的文件描述符整数(File Descriptor),用于底层系统调用。 int
isatty() 检查文件是否连接到终端(TTY)设备。 bool
name (属性) 返回文件名。 str
mode (属性) 返回打开文件的模式(如 'r', 'wb')。 str
encoding (属性) 返回文本文件的编码格式(如 'utf-8')。二进制文件无此属性。 str

3.2 os模块

       os 模块是 Python 标准库中用于与操作系统交互的核心模块。它提供了大量方法来处理文件、目录、环境变量、进程等。虽然现代 Python 开发中推荐使用 pathlib 模块来处理路径(因为它更面向对象且跨平台兼容性更好),但 os 模块在许多底层操作、环境变量管理和遗留代码中仍然不可或缺

3.2.1 路径操作

方法 描述 示例
os.path.join(path, *paths) 智能拼接路径。自动处理不同操作系统的路径分隔符(Windows 用 \,Linux/Mac 用 /)。

os.path.join('home', 'user', 'file.txt')

'home\\user\\file.txt'
os.path.exists(path) 判断路径(文件或目录)是否存在。 os.path.exists('data.txt')
os.path.isfile(path) 判断是否为文件。 os.path.isfile('script.py')
os.path.isdir(path) 判断是否为目录。 os.path.isdir('images')
os.path.abspath(path) 返回绝对路径。 os.path.abspath('..')
os.path.basename(path) 返回路径中的文件名部分。 os.path.basename('/a/b/c.txt')'c.txt'
os.path.dirname(path) 返回路径中的目录部分。 os.path.dirname('/a/b/c.txt')'/a/b'
os.path.split(path) 将路径分割为 (目录名, 文件名) 元组。 os.path.split('/a/b.txt')('/a', 'b.txt')
os.path.splitext(path) 分离文件名和扩展名,返回 (文件名, 扩展名) os.path.splitext('img.png')('img', '.png')
os.path.getsize(path) 返回文件大小(字节)。 os.path.getsize('log.txt')
os.path.getmtime(path) 返回最后修改时间(时间戳)。 os.path.getmtime('data.csv')

3.2.2 目录操作

方法 描述 示例
os.getcwd() 获取当前工作目录 (Get Current Working Directory)。 os.getcwd()
os.chdir(path) 切换当前工作目录 (Change Directory)。 os.chdir('/tmp')
os.mkdir(path) 创建单个目录。如果父目录不存在会报错。 os.mkdir('new_folder')
os.makedirs(path) 递归创建多级目录。如果中间目录不存在会自动创建(推荐)。 os.makedirs('a/b/c')
os.rmdir(path) 删除空目录。如果目录非空会报错。 os.rmdir('empty_folder')
os.removedirs(path) 递归删除空目录(从叶子节点向上删,直到遇到非空目录)。 os.removedirs('a/b/c')
os.listdir(path='.') 列出指定目录下的所有文件和子目录名称(不包含 ...)。 os.listdir('.')
os.scandir(path='.') 高效迭代目录条目(返回 DirEntry 对象,可快速判断是文件还是目录,比 listdir 性能更好)。 for entry in os.scandir(): ...
os.walk(top) 递归遍历目录树。生成一个三元组 (dirpath, dirnames, filenames) for root, dirs, files in os.walk('.'): ...

3.2.3 文件操作

方法 描述 示例
os.remove(path) 删除文件。不能删除目录。 os.remove('old.txt')
os.unlink(path) os.remove 的别名,功能完全相同。 os.unlink('temp.log')
os.rename(src, dst) 重命名文件或移动文件(如果 dst 包含新路径)。 os.rename('old.txt', 'new.txt')
os.replace(src, dst) 重命名或移动,如果目标存在则强制覆盖(原子操作,更安全)。 os.replace('tmp.txt', 'final.txt')
os.stat(path) 获取文件状态信息(大小、权限、时间等),返回 stat_result 对象。 os.stat('file.txt').st_size
os.access(path, mode) 检查当前用户是否有权限访问文件(读/写/执行)。 os.access('script.sh', os.X_OK)

3.2.4 环境与系统信息

方法 描述 示例
os.getenv(key, default=None) 获取环境变量。如果不存在返回 default os.getenv('HOME', '/root')
os.environ 包含所有环境变量的字典对象,可读写。 os.environ['PATH']
os.putenv(key, value) 设置环境变量(通常直接修改 os.environ 即可)。 os.environ['MY_VAR'] = '123'
os.system(command) 执行 Shell 命令字符串。不推荐用于获取输出,仅用于执行。 os.system('ls -l')
os.popen(command) 执行 Shell 命令并返回文件对象以读取输出(较老,推荐用 subprocess)。 os.popen('date').read()
os.name 返回操作系统名称 ('nt' for Windows, 'posix' for Linux/Mac)。 print(os.name)
os.sep 当前系统的路径分隔符 ('\\''/')。 path = 'a' + os.sep + 'b'
os.linesep 当前系统的行结束符 ('\r\n''\n')。 -

3.3 pathlib

pathlib 是 Python 3.4+ 引入的标准库模块,它提供了面向对象的路径操作方式。相比 os.path 的字符串操作,pathlib 更加直观、易读,且自动处理不同操作系统的路径分隔符问题。

3.3.1 基础创建与表示

方法/属性 描述 示例
Path(...) 创建路径对象。可以传入多个部分自动拼接。 p = Path('home', 'user', 'docs')
Path.cwd() 获取当前工作目录的绝对路径对象。 p = Path.cwd()
Path.home() 获取当前用户的主目录路径对象。 p = Path.home()
p.as_posix() 将路径转换为 POSIX 风格(使用 / 分隔符),常用于生成 URL 或跨平台配置。 p.as_posix()'C:/Users/...' (即使在 Windows)
str(p) 将路径对象转换为普通字符串(用于传给不支持 Path 的旧函数)。 file_path = str(p)

3.3.2 路径拼接与分解

操作 描述 示例
p / 'subdir' 拼接路径 (重载了 / 运算符)。自动处理分隔符。 Path('data') / 'logs' / 'app.txt'
p.name 获取路径最后的文件名(含扩展名)。 Path('/a/b.py').name'b.py'
p.stem 获取文件名不含扩展名的部分。 Path('/a/b.py').stem'b'
p.suffix 获取文件扩展名(含点)。 Path('/a/b.py').suffix'.py'
p.suffixes 获取所有扩展名列表(针对 .tar.gz 等多重后缀)。 Path('archive.tar.gz').suffixes['.tar', '.gz']
p.parent 获取父目录路径对象。 Path('/a/b/c').parentPosixPath('/a/b')
p.parents 获取所有祖先目录的序列(可索引)。 p.parents[0] (父), p.parents[1] (祖父)
p.parts 将路径拆分为元组。 Path('/a/b').parts('/', 'a', 'b')

3.3.3 检查与状态

方法/属性 描述 示例
p.exists() 判断路径是否存在(文件或目录)。 if p.exists(): ...
p.is_file() 判断是否为文件。 p.is_file()
p.is_dir() 判断是否为目录。 p.is_dir()
p.is_symlink() 判断是否为符号链接。 p.is_symlink()
p.is_absolute() 判断是否为绝对路径。 p.is_absolute()
p.stat() 获取文件状态信息(类似 os.stat),返回 stat_result 对象。 p.stat().st_size (大小), p.stat().st_mtime (修改时间)
p.samefile(other) 判断是否与另一个路径指向同一个文件。 p.samefile(q)

3.3.4 目录操作

方法 描述 示例
p.mkdir(mode=0o777, parents=False, exist_ok=False) 创建目录。
- parents=True: 递归创建缺失的父目录。
- exist_ok=True: 如果目录已存在不报错。
p.mkdir(parents=True, exist_ok=True)
p.iterdir() 迭代当前目录下的所有条目(返回 Path 对象生成器)。比 os.listdir 更高效且直接返回对象。 for child in p.iterdir(): print(child)
p.glob(pattern) 根据通配符模式查找文件(非递归)。返回生成器。
支持 *, ?, [...]
list(p.glob('*.txt'))
p.rglob(pattern) 递归查找文件(相当于 `` 前缀)。 list(p.rglob('*.log'))
p.rename(target) 重命名或移动此路径到 target p.rename(new_name)
p.replace(target) 强制重命名或移动(如果目标存在则覆盖)。 p.replace(backup_path)
p.unlink() 删除文件。不能删除目录。 p.unlink()
p.rmdir() 删除空目录。如果目录非空会报错。 p.rmdir()

3.3.5 文件读写

方法 描述 示例
p.read_text(encoding='utf-8') 读取文件全部内容为字符串。 content = p.read_text()
p.read_bytes() 读取文件全部内容为字节串。 data = p.read_bytes()
p.write_text(data, encoding='utf-8') 写入字符串到文件(覆盖模式)。自动创建文件。 p.write_text('Hello')
p.write_bytes(data) 写入字节串到文件。 p.write_bytes(b'\x00\x01')
with p.open(...) 以传统方式打开文件句柄(支持 mode, buffering 等参数)。 with p.open('r') as f: ...

3.4 sys

3.4.1 命令行参数与程序控制

属性/方法 描述 示例/用法
sys.argv 命令行参数列表。argv[0] 是脚本文件名,后续元素是传入的参数。 python script.py arg1 arg2
sys.argv['script.py', 'arg1', 'arg2']
sys.exit([arg]) 退出程序。抛出 SystemExit 异常。
- 0 或省略:正常退出
- 非零整数:异常退出(错误码)
sys.exit(1) # 报错退出
sys.stdin 标准输入流(文件对象)。通常对应键盘输入,可被重定向。 for line in sys.stdin: ...
sys.stdout 标准输出流(文件对象)。通常对应屏幕打印,可被重定向。 sys.stdout.write("Hello\n")
sys.stderr 标准错误流(文件对象)。通常对应屏幕错误信息,默认不缓冲。 sys.stderr.write("Error!\n")

3.4.2 模块搜索路径与环境

属性/方法 描述 示例/用法
sys.path 模块搜索路径列表。Python 导入模块时会按顺序查找这些目录。包含当前目录、标准库路径等。 sys.path.insert(0, '/my/libs')
(优先从自定义路径导入)
sys.modules 字典,记录已加载的模块。键是模块名,值是模块对象。用于高级元编程或检查模块是否已加载。 if 'numpy' in sys.modules: ...
sys.prefix 站点级目录前缀(通常是虚拟环境或 Python 安装根目录)。 用于构建特定于安装的路径。
sys.executable 当前运行 Python 解释器的绝对路径。 用于在脚本中调用自身或其他脚本:subprocess.run([sys.executable, 'other.py'])

3.4.3 版本与平台信息

属性/方法 描述 示例返回值
sys.version Python 版本字符串(包含编译信息)。 '3.9.7 (default, Sep 16 2021)...'
sys.version_info Python 版本元组 (major, minor, micro, releaselevel, serial)。推荐用于版本判断。 sys.version_info >= (3, 8)
sys.platform 平台标识符。用于判断操作系统类型。 'win32', 'linux', 'darwin' (MacOS)
sys.byteorder 系统字节序。 'little''big'

3.4.4 递归限制与内存管理

属性/方法 描述 示例/用法
sys.getrecursionlimit() 获取当前最大递归深度限制(默认通常为 1000)。 防止无限递归导致栈溢出。
sys.setrecursionlimit(limit) 设置最大递归深度。慎用,设得太高可能导致 C 栈溢出崩溃。 sys.setrecursionlimit(2000)
sys.getsizeof(obj) 返回对象在内存中占用的字节数(仅计算对象本身,不包含引用对象的大小)。 sys.getsizeof([1, 2, 3])
sys.getrefcount(obj) 返回对象的引用计数(注意:调用此函数本身会临时增加一次引用)。 用于调试内存泄漏。
sys.gc() 强制触发垃圾回收(通常不需要手动调用,Python 会自动处理)。 sys.gc()

3.4.5 钩子与异常处理 (高级)

属性/方法 描述 示例/用法
sys.exc_info() 返回当前正在处理的异常信息元组 (type, value, traceback)。仅在 except 块中有效。 用于自定义日志记录。
sys.tracebacklimit 设置打印 traceback 时的最大层数。设为 0 可隐藏 traceback。 sys.tracebacklimit = 0
sys.displayhook 交互式解释器中用于显示表达式结果的函数。可被重写以改变 REPL 行为。 -
sys.stdin.encoding 获取标准输入的编码格式(如 'utf-8', 'cp936')。 用于处理编码兼容性问题。

3.5 对象序列化

   pickle 和 marshal 都是 Python 标准库中用于序列化(将 Python 对象转换为字节流)和反序列化(将字节流恢复为 Python 对象)的模块

特性 pickle marshal
主要用途 通用对象序列化(保存/加载复杂对象) Python 内部编译字节码 (.pyc)
支持的数据类型 极广:基本类型、列表、字典、自定义类/实例、函数、递归结构等 有限:基本类型、列表、元组、字典、代码对象 (code object)。不支持自定义类实例。
版本兼容性 较好:高版本 Python 通常能读取低版本生成的 pickle(需指定 protocol),但低版本无法读取高版本的新特性。 极差:格式随 Python 小版本(如 3.8 vs 3.9)变化。严禁在不同 Python 版本间交换 marshal 数据。
安全性 ❌ 不安全:加载恶意 pickle 数据可执行任意代码(RCE)。 ❌ 不安全:同样可执行任意代码(通过构造特殊的 code object)。
性能 中等(取决于 protocol 版本,protocol 5 较快) 极快(因为结构简单,专为内部优化)
人类可读性 二进制(不可读),但有文本协议(protocol 0,已过时且不推荐) 纯二进制(完全不可读)
跨语言支持 无(仅限 Python,虽有第三方库但非标准) 无(仅限 Python 内部)
处理循环引用 支持 支持

3.5.1 pickle基本用法

import pickle

data = {
    'name': 'Alice',
    'scores': [90, 85, 88],
    'active': True
}

# 序列化 (Dump) - 写入文件
# protocol=pickle.HIGHEST_PROTOCOL 推荐使用最新协议以获得最佳性能和功能
with open('data.pkl', 'wb') as f:
    pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)

# 反序列化 (Load) - 读取文件
with open('data.pkl', 'rb') as f:
    loaded_data = pickle.load(f)

print(loaded_data) 
# 输出: {'name': 'Alice', 'scores': [90, 85, 88], 'active': True}

支持自定义对象,这是 pickle 最大的优势

class User:
    def __init__(self, name):
        self.name = name

user = User("Bob")

# 保存对象
with open('user.pkl', 'wb') as f:
    pickle.dump(user, f)

# 恢复对象 (注意:运行时必须定义相同的 User 类)
with open('user.pkl', 'rb') as f:
    restored_user = pickle.load(f)

print(restored_user.name) # 输出: Bob

3.5.2 marshal 基本用法

marshal 主要用于 Python 解释器内部,将编译后的代码对象写入 .pyc 文件,不支持自定义类

import marshal

data = {'a': 1, 'b': [1, 2, 3]} # 只能是基本类型

# 序列化
with open('data.marshal', 'wb') as f:
    marshal.dump(data, f)

# 反序列化
with open('data.marshal', 'rb') as f:
    loaded_data = marshal.load(f)

print(loaded_data)

3.5.3 shelve

       shelve 是 Python 标准库中的一个模块,它提供了一个简单的持久化字典(persistent dictionary)功能。简单来说,它允许你像操作普通 Python 字典一样操作一个文件,但数据会被自动保存到磁盘上。当你关闭程序再重新打开时,数据依然存在。它是 pickle 模块的封装,支持存储几乎所有 Python 对象(列表、字典、类实例等),而不仅仅是字符串。

import shelve

# 'mydata.db' 是文件名前缀 (实际可能生成 .db, .dir, .bak 等多个文件)
# writeback=True 是一个重要参数,稍后解释
with shelve.open('mydata', writeback=True) as db:
    # 像字典一样赋值
    db['user_settings'] = {'theme': 'dark', 'font_size': 14}
    db['user_list'] = ['Alice', 'Bob', 'Charlie']
    db['count'] = 100
    
    print("数据已保存。")

with shelve.open('mydata') as db:
    # 检查键是否存在
    if 'user_settings' in db:
        settings = db['user_settings']
        print(f"读取到的设置: {settings}")
    
    # 遍历所有键
    print("\n所有键:")
    for key in db:
        print(f"{key}: {db[key]}")

4 异常和错误处理

       Python 的异常体系是面向对象的,所有异常都继承自基类 BaseException。在实际开发中,我们通常捕获继承自 Exception 的子类

4.1 异常等级

和早期版本有些差异,StandardError 已经不存在了

BaseException
 +-- SystemExit        (sys.exit() 抛出)
 +-- KeyboardInterrupt (Ctrl+C 抛出)
 +-- GeneratorExit     (生成器关闭)
 +-- Exception         (⚠️ 绝大多数普通异常都继承自这里)
      +-- StopIteration
      +-- ArithmeticError
      |    +-- ZeroDivisionError
      |    +-- OverflowError
      +-- AssertionError
      +-- AttributeError
      +-- EOFError
      +-- ImportError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- NameError
      +-- OSError ( IOError, EnvironmentError )
      |    +-- FileNotFoundError
      |    +-- PermissionError
      +-- RuntimeError
      +-- SyntaxError
      +-- TypeError
      +-- ValueError
      ...

4.2 常用内置异常详解

4.2.1 逻辑与类型错误 (最常见)

异常类型 描述 触发示例
TypeError 操作或函数应用于不适当类型的对象。 len(10)
"hello" + 5
func(arg=None) (当 arg 不能为 None)
ValueError 类型正确,但值不合适。 int("abc")
math.sqrt(-1)
list.remove(x) (x 不在列表中)
AttributeError 引用或赋值属性失败(对象没有该属性)。 None.length
[1,2].foo()
NameError 尝试访问未定义的变量。 print(undefined_var)
IndexError 序列下标超出范围。 lst = [1,2]; lst[5]
KeyError 字典中键不存在。 d = {'a': 1}; d['b']

4.2.2 算术与计算错误

异常类型 描述 触发示例
ZeroDivisionError 除法或取模运算的除数为零。 10 / 0
10 % 0
OverflowError 算术运算结果太大,无法表示。 math.exp(1000) (在某些系统上)
FloatingPointError 浮点运算出错(通常需开启 float_exception 才会触发,默认忽略)。 -

4.2.3 文件与操作系统错误 (OSError 子类)

异常类型 描述 触发示例
FileNotFoundError 文件或目录不存在。 open('missing.txt')
PermissionError 没有权限执行操作。 打开一个只读文件进行写入
IsADirectoryError 期望是文件,但实际是目录。 open('/home/user/') (作为文件打开)
NotADirectoryError 期望是目录,但实际是文件。 os.listdir('file.txt')
InterruptedError 系统调用被信号中断。 -

4.2.4 导入与模块错误

异常类型 描述 触发示例
ImportError 导入模块失败。 import non_existent_module
ModuleNotFoundError ImportError 的子类,特指找不到模块 (Python 3.6+)。 同上

4.2.5 其他常见异常

异常类型 描述 触发示例
AssertionError assert 语句失败。 assert 1 == 2
StopIteration 迭代器没有更多元素(for 循环自动处理,手动 next() 时需注意)。 next(iter([]))
RuntimeError 检测到不属于任何特定类别的错误。 递归过深(有时)、自定义通用错误
NotImplementedError 抽象方法未实现(常用于基类)。 在基类中定义但未重写的方法
SyntaxError 语法错误(解析器抛出,无法在运行时 try...except 捕获,必须在编码阶段修复)。 if True print("hi") (缺冒号)
IndentationError 缩进错误 (SyntaxError 的子类)。 混合使用 Tab 和空格导致对齐错误

4.3 异常处理

4.3.1 基础格式

try:
    # 可能出错的代码
    result = 10 / 0
except ZeroDivisionError as e:
    # 仅当发生 ZeroDivisionError 时执行
    print("不能除以零!",e)

#早期版本是这样写的
try:
    # 可能出错的代码
    result = 10 / 0
except ZeroDivisionError,e:
    # 仅当发生 ZeroDivisionError 时执行
    print("不能除以零!",e)

4.3.2 try finally

try:
    # 可能出错的代码
    result = 10 / 0
finally:
    # 仅当发生 ZeroDivisionError 时执行
    print("不能除以零!")

4.3.3 捕获多异常(元组形式)

try:
    # 可能出错的代码
    value = int("abc")  # 可能触发 ValueError
    result = 10 / 0     # 可能触发 ZeroDivisionError
except (ValueError, ZeroDivisionError) as e:
    # 任意一个异常发生时都会进入这里
    print(f"发生错误: {type(e).__name__} - {e}")

4.3.2 捕获多个异常(多个 except 块)

try:
    # ...
    pass
except FileNotFoundError:
    print("文件没找到,请检查路径。")
except PermissionError:
    print("权限不足,无法操作文件。")
except Exception as e:
    # 捕获其他所有未被上面处理的异常 (兜底)
    print(f"发生了未知错误: {e}")

4.3.3 获取异常信息 (as 子句)

try:
    open('missing.txt')
except IOError as e:
    print(f"错误类型: {type(e)}")
    print(f"错误信息: {e}")
    print(f"错误参数: {e.args}")
    # 在 Python 3 中,通常直接打印 e 即可看到详细信息

4.3.4  完整结构:try - except - else - finally

这是最完整的语法结构,包含所有可选部分。

这是最完整的语法结构,包含所有可选部分。
try: 放置可能出错的代码。
except: 处理发生的异常。
else: 仅当 try 块没有发生任何异常时执行。通常用于放置那些依赖于 try 成功但不应该被 try 包裹的代码(避免捕获不该捕获的错误)。
finally: 无论是否发生异常,最后都会执行。通常用于资源清理(关闭文件、断开数据库连接等)

def divide_and_write(a, b, filename):
    try:
        result = a / b  # 风险代码 1
    except ZeroDivisionError:
        print("除数不能为 0")
        return
    else:
        # 只有除法成功才执行这里
        print(f"计算结果: {result}")
        try:
            with open(filename, 'w') as f:
                f.write(str(result)) # 风险代码 2 (如果在 try 里,会被上面的 except 捕获,这通常不是我们想要的)
        except IOError:
            print("写入文件失败")
    finally:
        # 无论成功、除法错误还是写入错误,都会执行
        print("操作结束,进行清理工作。")

# 调用示例
divide_and_write(10, 2, 'output.txt')

4.4 异常堆栈

需要用到traceback,看起来有点怪

4.4.1 简单打印

import traceback

try:
    1 / 0
except Exception:
    # 自动打印当前异常的完整堆栈到 stderr
    traceback.print_exc()

4.4.2 获取堆栈为字符串

如果你不想直接打印到屏幕,而是想记录到日志文件、发送到网络或存入数据库,使用此方法。它返回包含堆栈信息的字符串

import traceback
import logging

logging.basicConfig(level=logging.ERROR)

try:
    raise ValueError("某个数据错误")
except Exception:
    # 获取堆栈字符串
    error_msg = traceback.format_exc()
    
    # 写入日志
    logging.error(f"发生异常:\n{error_msg}")
    
    # 或者存入变量用于后续处理
    print(f"捕获到的堆栈信息:\n{error_msg}")

4.4.3 打印特定异常对象

import traceback
import sys

try:
    int("abc")
except Exception as e:
    exc_type, exc_value, exc_tb = sys.exc_info()
    
    # 手动传入异常信息进行打印
    traceback.print_exception(exc_type, exc_value, exc_tb)
    
    # 或者在 Python 3.10+ 可以直接传 exception 对象 (部分版本支持)
    # traceback.print_exception(e) 

4.4.4 在程序崩溃时打印

import traceback

# 模拟一个未捕获的异常流程(通常在 IDE 或交互模式下有用)
# 注意:如果在 try-except 外部运行此代码,程序会先崩溃,这行可能执行不到
# 此函数主要用于调试已发生的未处理异常
try:
    raise RuntimeError("测试错误")
except:
    pass # 吞掉异常,但保留现场

# 打印刚才发生的最后一个异常
traceback.print_last()

4.4.5 限制栈深度

import traceback

try:
    def a(): b()
    def b(): c()
    def c(): 1/0
    a()
except:
    # 只打印最后 2 层堆栈
    traceback.print_exc(limit=2)

4.4.6 提取栈摘要

import traceback
import sys

try:
    1 / 0
except:
    tb_list = traceback.extract_tb(sys.exc_info()[2])
    
    print("自定义格式的堆栈摘要:")
    for frame in tb_list:
        print(f"文件: {frame.filename}, 行号: {frame.lineno}, 函数: {frame.name}, 代码: {frame.line}")

4.5 异常触发

  raise 是 Python 中用于主动触发异常的关键字。它允许你在检测到错误条件、无效输入或特定业务逻辑失败时,中断当前程序流程并抛出异常

4.5.1 基础用法

抛出一个新的异常实例

# 语法:raise ExceptionType("错误消息")

def set_age(age):
    if age < 0:
        # 抛出一个 ValueError,提示年龄不能为负
        raise ValueError("年龄不能是负数!")
    if age > 150:
        # 抛出一个自定义消息的 ValueError
        raise ValueError(f"年龄 {age} 超出了合理范围 (0-150)。")
    
    print(f"年龄设置成功: {age}")

# 调用
try:
    set_age(-5)
except ValueError as e:
    print(f"捕获到错误: {e}")

4.5.2 重新抛出当前异常

raise 不带参数

def process_data(data):
    try:
        result = 10 / data
        return result
    except ZeroDivisionError:
        print("日志:检测到除以零错误,正在记录...")
        # 记录完日志后,将异常再次抛给上层调用者
        raise  # 等价于 raise ZeroDivisionError(...) (保留原始堆栈)

try:
    process_data(0)
except ZeroDivisionError:
    print("主程序:接收到下层传来的除零错误,程序终止或降级处理。")

4.5.3 异常链

raise ... from ... (Python 3+)

class DatabaseConnectionError(Exception):
    pass

class ServiceUnavailableError(Exception):
    pass

def connect_to_db():
    try:
        # 模拟底层错误
        raise FileNotFoundError("配置文件丢失") 
    except FileNotFoundError as e:
        # 抛出一个新的业务异常,但指明是由 e 引起的
        raise ServiceUnavailableError("数据库服务启动失败") from e

try:
    connect_to_db()
except ServiceUnavailableError as e:
    print(f"业务错误: {e}")
    # 访问原始异常
    print(f"根本原因: {e.__cause__}") 

tip='''
业务错误: 数据库服务启动失败
根本原因: 配置文件丢失
'''

4.5.4 错误用法

写法 行为 堆栈跟踪 (Traceback) 推荐场景
raise 重新抛出当前异常 保留原始位置 (指向最初出错的那一行) ✅ 推荐。用于透传异常。
raise e 抛出变量 e 代表的异常 重置位置 (指向 raise e 这一行,丢失原始出错行号) ❌ 不推荐。除非你故意想隐藏原始出错位置(极少见)。

4.5.5 自定义异常

# 定义自定义异常
class InsufficientFundsError(Exception):
    """余额不足异常"""
    def __init__(self, balance, amount):
        super().__init__(f"余额不足: 当前余额 {balance}, 需要 {amount}")
        self.balance = balance
        self.amount = amount

def withdraw(balance, amount):
    if amount > balance:
        # 抛出自定义异常,携带更多上下文信息
        raise InsufficientFundsError(balance, amount)
    return balance - amount

try:
    withdraw(100, 200)
except InsufficientFundsError as e:
    print(e) # 输出: 余额不足: 当前余额 100, 需要 200
    print(f"差额: {e.amount - e.balance}")

tip='''
余额不足: 当前余额 100, 需要 200
差额: 100
'''

4.5.6 常见应用场景总结

1 参数验证:函数入口处检查参数类型、范围、格式,不合法则 raise ValueError 或 TypeError。
2 状态检查:对象处于无效状态时(如文件未打开却尝试读取),raise RuntimeError。
3 抽象方法实现:在基类中定义方法,强制子类重写,否则 raise NotImplementedError

class Animal:
    def speak(self):
        raise NotImplementedError("子类必须实现 speak 方法")

4.6 断言

  assert(断言)是 Python 中用于调试和内部逻辑检查的关键字。它的核心作用是:“如果这个条件为假(False),程序就立刻崩溃并报错”,一般是调试时使用,或者单元测试中。Python 解释器有一个优化模式(Optimized Mode)。当你使用 -O (大写 O) 或 -OO 标志运行脚本时,所有的 assert 语句都会被忽略(相当于被删除了)。

4.6.1 assert vs except

特性 assert (断言) raise / try...except (异常处理)
目的 调试、检查代码逻辑假设 (Sanity Check)。 错误处理、应对用户输入、网络故障、文件缺失等。
生产环境 可以被禁用 (使用 -O 优化标志运行 Python 时,所有 assert 会被移除)。 永远生效,无论是否优化。
副作用 断言表达式中不能包含必要的业务逻辑副作用。 可以包含任何逻辑。
适用场景 “这种情况理论上绝不应该发生,如果发生了说明代码有 Bug”。 “这种情况可能会发生(如用户输错密码),我们需要优雅地处理”。

4.6.2 基本语法

assert condition, "错误消息"

condition: 一个表达式,结果应为 True。"错误消息" (可选): 当 cond

ition 为 False 时,显示给用户的提示信息。
执行逻辑:
1 如果 condition 为 True:程序继续正常运行,无任何输出。
2 如果 condition 为 False:程序立即中断,抛出 AssertionError,并显示可选的错误消息

age = -5

# 如果 age < 0 为 False (即 age >= 0),则通过
# 如果 age < 0 为 True (即 age 是负数),则断言失败 -> 实际上我们要检查的是 age >= 0
assert age >= 0, "年龄不能是负数!"

print("年龄检查通过") 
# 如果 age = -5,上面这行不会执行,直接报错:
# AssertionError: 年龄不能是负数!

5 函数

       定义函数的方式非常灵活,从最基础的 def 到高级的匿名函数、装饰器甚至类型提示,共有多种形态。大型正规程序最好都使用面向对象方法,不要使用函数。

5.1 函数定义

5.1.1 基本定义方式

def function_name(arg1, arg2):
    """文档字符串 (Docstring)"""
    return arg1 + arg2

5.1.2 嵌套定义

def funA(arg1, arg2):
    """文档字符串 (Docstring)"""
    def funB():
        print("call B")
    funB()
    print("call A")
    return arg1 + arg2
sum = funA(1,2)
#funB() 不可直接调用
print(sum)

5.1.3 lambda定义

# 语法: lambda 参数: 表达式
add = lambda x, y: x + y
print(add(1,2))
# 常用场景:排序
data = [(1, 'b'), (3, 'a'), (2, 'c')]
data.sort(key=lambda x: x[1]) # 按元组第二个元素排序

5.2 函数属性

属性名 类型 描述 是否可写
__name__ str 函数的名称。 ✅ 可写
__doc__ str 函数的文档字符串(Docstring)。如果没有文档,则为 None ✅ 可写
__module__ str 函数定义的模块名称(例如 '__main__''my_module')。 ✅ 可写
__qualname__ str 函数的限定名称(Qualified Name)。对于嵌套函数或类方法,它包含路径(如 OuterClass.methodouter.<locals>.inner)。Python 3.3+ 引入。 ✅ 可写
__defaults__ tuple 包含函数位置参数的默认值元组。如果没有默认值,则为 None ✅ 可写
__kwdefaults__ dict 包含函数关键字_only 参数的默认值字典。如果没有,则为 None。Python 3+。 ✅ 可写
__code__ code object 函数的代码对象,包含编译后的字节码、变量名、常量等底层信息。 ❌ 只读 (但可替换)
__globals__ dict 函数定义时所在的全局命名空间字典的引用。 ❌ 只读 (引用不可变,内容可变)
__closure__ tuple 包含闭包单元(cell objects)的元组,用于访问自由变量。如果函数不是闭包,则为 None ❌ 只读
__annotations__ dict 函数的参数和返回值类型注解字典。Python 3+。 ✅ 可写
__dict__ dict 用于存储自定义属性的命名空间字典。 ✅ 可写
__wrapped__ function 如果被 @wraps 装饰器包装过,该属性指向被包装的原始函数。Python 3.2+ (functools)。 ✅ 可写

5.2.1 属性查看

def test(): pass

#添加属性,这个很容易乱搞,如果是对象的话更糟糕,每次脚本语言都喜欢用灵活来开拓
#实际上语言的核心就是短、频繁、快,维护性不是脚本语言最关心的问题,只是后面
#用得多了,发现不好维护,就各种查缺补漏,搞得不论不类的
test.xx = 'fff' 
print(f'text.xx={test.xx}')
print(dir(test))
# 输出包含: ['__annotations__', '__call__', '__code__',

5.2.2 基本信息

def my_func():
    """这是一个测试函数"""
    pass

print(my_func.__name__)      # 'my_func'
print(my_func.__doc__)       # '这是一个测试函数'
print(my_func.__module__)    # '__main__' (如果在脚本直接运行)

# 嵌套函数示例
def outer():
    def inner():
        pass
    return inner

f = outer()
print(f.__name__)            # 'inner'
print(f.__qualname__)        # 'outer.<locals>.inner' (显示了层级关系)

5.2.3 默认参数 

#* 是个分割符号,强制后面的是关键字参数,又出了个奇葩
def demo(a, b=10, *, c=20):
    pass

print(demo.__defaults__)     # (10,)  -> 对应 b
print(demo.__kwdefaults__)   # {'c': 20} -> 对应 c
#demo() 要报错
demo(1)
demo(1,5)
#demo(1,5,9) 要报错
demo(1,5,c=9)

5.2.4 代码对象

def demo(a, b=10,c=20):
    pass

code_obj = demo.__code__
print(code_obj.co_argcount)    # 参数个数 (不含 *args, **kwargs)
print(code_obj.co_varnames)    # 局部变量名元组 ('a', 'b', 'c')
print(code_obj.co_filename)    # 文件路径
print(code_obj.co_firstlineno) # 函数定义的行号
print(code_obj.co_name)        # 函数名 (同 __name__)

tip='''
3
('a', 'b', 'c')
F:\Users\admin2\AppData\Local\Temp\ipykernel_14176\3873755586.py
1
demo
'''

5.2.5 闭包

def make_adder(n):
    def adder(x):
        return x + n  # 引用了外部变量 n
    return adder

func = make_adder(5)
print(func.__closure__)        
# (<cell at 0x...: int object at 0x...>,)
print(func.__closure__[0].cell_contents) # 5 (获取闭包变量的值)

5.2.6 全局空间

global_var = 100
def check_global():
    return global_var

print(check_global.__globals__['global_var']) # 100
# 甚至可以动态修改:
check_global.__globals__['global_var'] = 200
print(check_global()) # 200
print(global_var) # 200

5.2.7 自定义属性

def counter():
    count = 0
    def inner():
        nonlocal count
        count += 1
        return count
    return inner

fn = counter()

# 给函数动态添加属性
fn.description = "这是一个计数器函数"
fn.version = "1.0.0"
fn.call_count = 0

print(fn.description) # "这是一个计数器函数"
print(fn.__dict__)    # {'description': '...', 'version': '...', 'call_count': 0}

5.2.8 特殊属性:__wrapped__

from functools import wraps

def my_decorator(f):
    @wraps(f)  # 自动设置 f.__wrapped__ = original_f
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper

# @my_decorator
def original_func():
    """Original Doc"""
    pass

print(original_func.__name__)      # 'original_func' (被 wraps 修复了)
print(original_func.__wrapped__)   # <function original_func at ...> (原始函数)

5.3 函数传参规则

5.3.1 位置传参

def greet(name, age):
    print(f"{name} is {age}")

greet("Alice", 25)  # name="Alice", age=25

5.3.2 关键字传参

通过 参数名=值 的形式传递,顺序可以打乱。

greet(age=25, name="Alice")  # 效果同上

5.3.3 序列解包传参

def greet(name, age):
    print(f"{name} is {age}")
params = ["Bob", 30]
greet(*params)  # 等价于 greet("Bob", 30)

paramsb = ("Bob", 30)
greet(*paramsb)  # 等价于 greet("Bob", 30)

5.3.4 字典传参

params_dict = {"name": "Charlie", "age": 35}
greet(**params_dict)  # 等价于 greet(name="Charlie", age=35)

5.3.5 参数匹配优先级

这点容易出问题

当多种传参方式混合使用时,Python 解释器按以下顺序进行匹配:
1 位置参数 优先匹配 位置形参。
2 关键字参数 精确匹配对应的形参名。
3 *args 收集剩余未匹配的 位置参数 (打包成元组)。
4 **kwargs 收集剩余未匹配的 关键字参数 (打包成字典)。

冲突检测:
1 如果同一个参数既通过位置传递,又通过关键字传递,会报错 TypeError: got multiple values for argument 'x'。
2如果缺少必填的位置参数或命名关键字参数,会报错 TypeError: missing required argument。

5.3.6 默认参数陷阱

❌ 错误写法

def add_item(item, box=[]):  # box 只在定义时创建一次
    box.append(item)
    return box

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2]  <-- 意外!累积了上一次的值

✅ 正确写法

def add_item(item, box=None):
    if box is None:
        box = []  # 每次调用都创建新列表
    box.append(item)
    return box

5.3.7 强制关键字参数

       如果你希望某些参数必须使用关键字传参(防止位置传参导致的歧义),可以将它们放在 *args 之后,或者单独放一个 *。

# 场景:create_user 函数,role 必须明确指定
def create_user(name, *, role, active=True):
    pass

# create_user("Alice", "admin")  ❌ 报错:role 必须用关键字
create_user("Alice", role="admin")  # ✅ 正确
create_user("Alice", role="admin", active=False) # ✅ 正确

5.4 函数式编程

        函数式编程中主要是使用lambda表达式,函数式编程(Functional Programming)主要依赖一些内置的高阶函数以及 functools 和 operator 标准库模块。虽然 Python 是多范式语言,但这些工具能帮助你写出更简洁、声明式的代码

5.4.1 核心内置高阶函数

这些函数直接内置在 Python 解释器中,无需导入即可使用(reduce 除外,和老版本有些区别)。

函数名 说明 返回值类型 (Python 3) 示例用途
map(function, iterable, ...) 将指定函数应用于可迭代对象的每个元素,返回结果。 map 对象 (迭代器) 批量转换数据(如将所有数字平方)。
filter(function, iterable) 使用函数过滤序列,仅保留使函数返回 True 的元素。 filter 对象 (迭代器) 筛选偶数、非空字符串等。
zip(*iterables) 将多个可迭代对象打包成元组迭代器。常用于并行遍历。 zip 对象 (迭代器) 同时遍历两个列表。
enumerate(iterable, start=0) 将一个可遍历的数据对象组合为一个索引序列,同时列出数据和下标。 enumerate 对象 需要在循环中获取索引时。
sorted(iterable, *, key=None, reverse=False) 对所有可迭代对象进行排序并返回新列表(不修改原对象)。 list 函数式风格的排序操作。
reversed(seq) 返回一个反向迭代器。 reversed 对象 反向遍历序列。

5.4.2 functools 模块中的关键函数

函数名 说明 示例用途
functools.reduce(function, iterable[, initializer]) 对一个序列进行累积操作(如求和、求积)。它将函数作用于前两个元素,然后将结果与下一个元素继续运算,直到序列结束。Python 3 已将其从内置移除,移至此模块。 计算列表所有元素的乘积、扁平化嵌套列表。
functools.partial(func, *args, kwargs) 偏函数应用。固定函数的某些参数,生成一个新的函数。 创建一个专门用于转换二进制到整数的函数 int2 = partial(int, base=2)
functools.lru_cache(maxsize=128, typed=False) 最近最少使用缓存装饰器。用于优化递归或重复计算的函数。 优化斐波那契数列递归计算。
functools.total_ordering 类装饰器。只要定义了 __eq__ 和另一个比较方法(如 __lt__),它会自动生成其余的比较方法。 简化自定义类的比较逻辑。

5.4.3 operator 模块中的常用函数

函数名 对应操作 示例
operator.add(a, b) a + b reduce(operator.add, list) (求和)
operator.mul(a, b) a * b reduce(operator.mul, list) (求积)
operator.itemgetter(n) 获取对象的第 n 个元素 sorted(list_of_tuples, key=itemgetter(1))
operator.attrgetter('name') 获取对象的属性 按对象属性排序
operator.not_, operator.and_, operator.or_ 逻辑非、与、或 filter 中进行复杂逻辑判断

5.4.4 简单示例与选择

from functools import reduce
import operator

numbers = [1, 2, 3, 4, 5]

# 1. map: 将所有数字平方
squared = list(map(lambda x: x**2, numbers)) 
# 或者使用 operator (如果操作符支持),但平方通常用 lambda 或自定义函数
# squared = list(map(lambda x: operator.pow(x, 2), numbers))

# 2. filter: 筛选出偶数
evens = list(filter(lambda x: x % 2 == 0, numbers))

# 3. reduce: 计算所有数字的乘积 (需导入 functools)
# Python 3 中必须从 functools 导入
product = reduce(operator.mul, numbers, 1) 

# 4. 组合使用: 先筛选偶数,再平方,最后求和
result = reduce(operator.add, map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers)), 0)

print(f"原列表: {numbers}")
print(f"平方后: {squared}")
print(f"偶数: {evens}")
print(f"乘积: {product}")
print(f"偶数平方和: {result}")

首选列表推导式:在现代 Python 开发中,对于简单的映射和过滤,列表推导式(List Comprehensions)通常比 map 和 filter 更具可读性。
1 map 替代: [x**2 for x in numbers]
2 filter 替代: [x for x in numbers if x % 2 == 0]
何时使用函数式工具:
1 当需要链式调用多个操作时(如 map -> filter -> reduce)。
2 当处理大型数据流且希望利用迭代器的惰性求值特性(节省内存)时。
3 当使用 partial 进行参数固化或 lru_cache 进行性能优化时。
4 当配合 operator 模块使代码更简洁时(如 key=operator.itemgetter(1))。

5.5 偏函数

       偏函数(Partial Function) 是指通过固定原函数的某些参数,从而生成一个新的、参数更少的函数的技术。Python 实现这一功能的核心工具是标准库 functools 中的 partial 函数。

5.5.1 基本用法

from functools import partial

# 定义一个普通函数
def multiply(x, y, z):
    return x * y * z

# 创建一个偏函数:固定 x=2, y=3
# 新函数 double_triple 只需要接收 z
double_triple = partial(multiply, 2, 3)
#其实类似下面写法
def double_triple2(z):
    return multiply(2,3,z)
# 调用偏函数
result = double_triple(4) 
result2 = double_triple2(4)
# 等价于 multiply(2, 3, 4)
# 计算过程: 2 * 3 * 4 = 24

print(result)  # 输出: 24
print(result2)  # 输出: 24

5.5.2 参数匹配规则

当你调用 partial(func, *args, **kwargs) 时:
1 *args (位置参数):会依次填充原函数最左边的参数。你不能跳过前面的参数直接用位置参数去绑定后面的参数。
2 **kwargs (关键字参数):可以通过参数名直接绑定原函数中的任何参数,无论它在前还是在后

def demo_func(a, b, c, d):
    return f"a={a}, b={b}, c={c}, d={d}"

# 这样写会把 10 赋值给 a,而不是 d!
# p = partial(demo_func, 10) 
# 调用 p(1, 2, 3) -> a=10, b=1, c=2, d=3 (完全错了)
from functools import partial

# 使用关键字参数固定 d=100
p = partial(demo_func, d=100)

# 调用时只需提供 a, b, c
result = p(1, 2, 3)
print(result) 
# 输出: a=1, b=2, c=3, d=100
绑定方式 能否绑定后面的参数? 说明
位置参数 (partial(f, val)) 不能 必须从左到右依次填充,无法跳过。
关键字参数 (partial(f, param=val)) 可以指定任意参数名进行绑定,无论位置前后。

5.6 高阶函数

高阶函数(Higher-Order Function) 是指满足以下任一条件的函数:
1 接收一个或多个函数作为参数。
2 返回一个函数作为结果

5.6.1 基本示例

def process_data(data_list, operation_func):
    """
    高阶函数:接收一个列表和一个操作函数
    """
    result = []
    for item in data_list:
        # 调用传入的函数
        transformed = operation_func(item)
        result.append(transformed)
    return result

# 定义具体的操作函数
def add_10(x):
    return x + 10

def square(x):
    return x * x

numbers = [1, 2, 3]

# 传入不同的函数,实现不同的行为
print(process_data(numbers, add_10))   # 输出: [11, 12, 13]
print(process_data(numbers, square))   # 输出: [1, 4, 9]
# 甚至可以直接传 lambda
print(process_data(numbers, lambda x: x * 3)) # 输出: [3, 6, 9]

5.6.2 装饰器

装饰器(Decorator) 是 Python 中的一种高阶函数。
输入:它接收一个函数(或类)作为参数。
输出:它返回一个新的函数(通常是对原函数的增强或包装)。
目的:在不修改原函数源代码、不改变原函数调用方式的前提下,为函数动态地添加额外功能(如日志记录、性能测试、事务处理、权限校验等)

这应该就是代理模式

@my_decorator
def my_func():
    pass

#等价于
def my_func():
    pass
my_func = my_decorator(my_func)
5.6.2.1 无参装饰器
# 1. 定义装饰器
def add_greeting(func):
    """
    这是一个简单的装饰器:
    在调用原函数前,先打印一句问候语。
    """
    # 内部函数(闭包),用于包裹原函数
    def wrapper():
        print("👋 你好!我是装饰器添加的问候:")
        # 执行原函数
        func()
        print("👋 再见!")
    
    # 返回包装后的新函数
    return wrapper

# 2. 使用装饰器 (语法糖写法)
@add_greeting
def say_hello():
    print("      >>> 这是原函数在说:Hello World! <<<")

# 3. 调用函数
say_hello()
5.6.2.2 带参数函数装饰器
import functools

def logger(func):
    @functools.wraps(func)  # 保留原函数名,不然就会被wrapper替换
    def wrapper(*args, **kwargs):  # 接收任意参数
        print(f"[日志] 正在调用函数: {func.__name__}")
        result = func(*args, **kwargs)  # 将参数透传给原函数
        print(f"[日志] 函数执行完毕,返回值: {result}")
        return result  # 返回原函数的结果
    return wrapper

@logger
def add(a, b):
    return a + b

add(3, 5)
#加了这个 @functools.wraps(func) 输出是add,不加就是wrapper
print(f"函数名: {add.__name__}")

output='''
[日志] 正在调用函数: add
[日志] 函数执行完毕,返回值: 8
函数名: add
'''

实际场景中的灾难
场景 A:日志系统 (Logging)
很多日志库会自动记录 func.__name__。如果不加 wraps,所有被装饰的函数在日志里都会显示为 wrapper,你根本不知道是哪个业务函数出错了。

import logging
logging.warning(f"{bad_login.__name__} 执行失败")
# 日志输出: [WARNING] wrapper 执行失败  <-- 运维人员一脸懵逼:哪个 wrapper?

场景 B:Web 框架路由 (如 Flask)
在 Flask 中,路由通常基于函数名生成端点名称。如果名字都变成了 wrapper,可能会导致路由冲突或调试困难。

# 伪代码示例
# @app.route('/login')
# def login(): ...
# 如果装饰器没加 wraps,Flask 内部注册的名字可能是 'wrapper',
# 导致 url_for('login') 找不到对应的端点,或者多个路由都叫 'wrapper' 引发冲突。

场景 C:序列化 (Pickle)
如果你尝试保存(pickle)这个函数,可能会失败,因为 pickle 默认通过 模块名.函数名 来查找函数。如果名字变成了 wrapper,而模块里没有叫 wrapper 的全局函数(因为它只是局部变量),反序列化时会报错。

import pickle

try:
    data = pickle.dumps(bad_login)
    # 尝试加载时可能报错:
    # pickle.UnpicklingError: Can't get attribute 'wrapper' on <module '__main__' ...>
except Exception as e:
    print(f"序列化失败: {e}")
特性 不加 @functools.wraps 加上 @functools.wraps
__name__ 'wrapper' (装饰器内部函数名) 'original_func' (原函数名)
__doc__ wrapper 的文档 (或 None) 原函数的文档字符串
__module__ 当前模块 (通常没问题) 原函数定义的模块
help() 显示 显示包装器信息,误导用户 显示原函数信息,清晰准确
日志/监控 所有函数名混淆为 wrapper 保留真实业务函数名
调试体验 堆栈跟踪难以阅读 堆栈跟踪清晰
兼容性 可能导致序列化或框架集成失败

兼容性好,符合 Python 规范

5.6.2.3 带参装饰器

要实现带参数的装饰器,你需要三层函数嵌套:
1 最外层:接收装饰器的参数(如 repeat_times=3)。
2 中间层:接收被装饰的函数对象(func)。
3 最内层 (wrapper):接收被装饰函数的调用参数(*args, **kwargs),执行逻辑并返回结果。

这玩意嵌套有点深少用,嵌套深的代码都有点坏的味道,模板如下

要实现带参数的装饰器,你需要三层函数嵌套:
最外层:接收装饰器的参数(如 repeat_times=3)。
中间层:接收被装饰的函数对象(func)。
最内层 (wrapper):接收被装饰函数的调用参数(*args, **kwargs),执行逻辑并返回结果。

实例

import functools

# 1. 定义带参数的装饰器
def add_title(title_text):
    """
    title_text: 你想要添加的标题字符串
    """
    # 第一层:接收装饰器的参数 (title_text)
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 2. 执行额外逻辑:先打印标题
            print(f"=== {title_text} ===")
            
            # 3. 执行原函数
            return func(*args, **kwargs)
        return wrapper
    return decorator

# --- 使用示例 ---

# 使用装饰器,传入参数 "用户登录"
@add_title("用户登录")
def login():
    print("正在验证用户名和密码...")

# 使用装饰器,传入参数 "系统设置"
@add_title("系统设置")
def settings():
    print("正在加载配置项...")

# --- 调用 ---
login()
print("-" * 20)
settings()
5.6.2.4 装饰器堆叠

当你在一个函数上应用多个装饰器时:
加载顺序(从上到下):Python 解释器会从下往上依次应用装饰器。
即:@decorator_A 在最上面,@decorator_B 在下面。
实际执行逻辑是:func = decorator_A(decorator_B(func))。
离函数最近的那个装饰器先被应用。
调用顺序(从外到内):当你调用函数时,执行流程像剥洋葱一样,从最外层(最上面的装饰器)开始,一层层向内,直到执行原函数,然后再一层层返回。

这玩意有先后顺序,不能随意调换,非常危险,要注意,谨慎使用,执行过程有点拦截器链

import time
import functools

# --- 装饰器 1: 性能计时 (最内层) ---
def timing(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        print(f"timing start [计时] {func.__name__}")
        result = func(*args, **kwargs)  # 执行下一层
        end = time.time()
        print(f"timing end [计时] {func.__name__} 耗时: {end - start:.4f} 秒")
        return result
    return wrapper

# --- 装饰器 2: 日志记录 (中间层) ---
def log_execution(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"log_execution start [日志] >>> 开始执行: {func.__name__}")
        result = func(*args, **kwargs)  # 执行下一层
        print(f"log_execution end [日志] <<< 结束执行: {func.__name__}")
        return result
    return wrapper

# --- 装饰器 3: 权限验证 (最外层) ---
def login_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 模拟全局登录状态
        is_logged_in = True 
        
        if not is_logged_in:
            return "login_required [错误] 请先登录!"
        
        print("login_required start [权限] 用户已验证通过。")
        result = func(*args, **kwargs) # 执行下一层
        print("login_required end [权限] 用户已验证通过。")
        return result  
    return wrapper

# --- 应用多个装饰器 ---
# 等价于: process_order = login_required(log_execution(timing(process_order)))
@login_required
@log_execution
@timing
def process_order(order_id):
    print(f"process_order [业务] 正在处理订单 {order_id}...")
    time.sleep(0.5)  # 模拟耗时操作
    return "订单处理成功"

# --- 调用 ---
print("=== 开始调用 ===")
result = process_order("ORD-2026-001")
print(f"最终结果: {result}")

output='''
=== 开始调用 ===
login_required start [权限] 用户已验证通过。
log_execution start [日志] >>> 开始执行: process_order
timing start [计时] process_order
process_order [业务] 正在处理订单 ORD-2026-001...
timing end [计时] process_order 耗时: 0.5001 秒
log_execution end [日志] <<< 结束执行: process_order
login_required end [权限] 用户已验证通过。
最终结果: 订单处理成功
'''

5.7 变量作用域

       名称空间 (Namespace) 本质上是一个从名称到对象的映射字典。简单来说,它决定了你在代码的某个特定位置看到的变量名 x 到底指向内存中的哪个对象。名称空间有全局,内建和局部名称空间。名称空间就是为了找到对对应对象,和作用域有很大关系。函数,模块,对象等都可以作为名称空间隔离变量。

       在 Python 中,变量的作用域(Scope)决定了变量在代码中的哪些部分是可见和可访问的。
当你在代码中使用一个变量名时,Python 遵循 LEGB 规则 来查找变量:
L (Local):局部作用域。当前函数内部定义的变量。
E (Enclosing):嵌套作用域。外层函数的局部作用域(即闭包环境)。
G (Global):全局作用域。当前模块(文件)顶层定义的变量。
B (Built-in):内置作用域。Python 内置模块(如 len, print, Exception)中的名称。
查找原则:一旦在某一层找到变量,查找立即停止。如果在所有层都找不到,则抛出 NameError。

      作用域的效果不同版本可能在一些场景不太一样,不确定的化需要自己测试一下

5.7.1 局部变量

def my_func():
    x = 10  # Local 变量
    print(x)

my_func()  # 输出: 10
# print(x) # 报错: NameError: name 'x' is not defined

5.7.2 全局变量

在文件顶层定义的变量,或者在函数内使用 global 关键字声明的变量

y = "yGlobal"  # Global 变量
x = "xGlobal"
def test_global():
    #这里覆盖了全局变量作用域,x式函数内部新的独立变量
    x="xLocal"
    # 这里读取的是 Global 变量 y 要是使用语句提前声明,看起来有点怪怪的
    #还不能写成一行。比如global y = 'yyyGlobal'
    global y
    y='yyyGlobal'
    print(f"test_global:x={x},y={y}")

print(f"bufore call out:x={x},y={y}")
test_global()
print(f"after call out:x={x},y={y}")

tip='''
bufore call out:x=xGlobal,y=yGlobal
test_global:x=xLocal,y=yyyGlobal
after call out:x=xGlobal,y=yyyGlobal
'''

5.7.3 闭包作用域

闭包作用域有点不同于全局和局部,所以需要单独对待

x='globalX'
def outer():
    num = 10  # Enclosing
    def inner():
        nonlocal num  # 声明我们要修改的是外层的 num
        num += 5
        global x
        x='innerGlobal'
        print(f"Inner: num={num},x={x}")
    
    inner()
    print(f"Outer: {num},x={x}") # 这里的 num 也被修改了

print(f"before: x={x}") 
outer()
print(f"after: x={x}") 

output='''
before: x=globalX
Inner: num=15,x=innerGlobal
Outer: 15,x=innerGlobal
after: x=innerGlobal
'''

5.7.4 常见陷阱

陷阱1:延迟绑定与循环中的闭包

x=5
def test():
    y=10
    bar=lambda : x+y
    print(bar())
    y=11 
    print(bar())
test() # 输出 15 16 符合预期,早期版本y重新赋值后不一定生效,奇葩问题很多

funcs = []
for i in range(3):
    # 这里的 i 是自由变量,引用的是全局/外层作用域的 i
    funcs.append(lambda: i) 

# 调用时,i 已经是 2 了
print([f() for f in funcs]) 
# 输出: [2, 2, 2] (期望可能是 [0, 1, 2])

# ✅ 修正方法:利用默认参数捕获当前值
funcs_fixed = []
for i in range(3):
    #这问题有点像早期版本的问题,看来式没有处理彻底,js也有类似问题,喜欢拿来考人,防不胜防
    funcs_fixed.append(lambda x=i: x) 

print([f() for f in funcs_fixed])
# 输出: [0, 1, 2]

陷阱2:可变对象 vs 不可变对象

不可变对象(整数、字符串、元组):在函数内修改(如 x += 1)通常需要 global 或 nonlocal,因为这实际上是重新赋值。
可变对象(列表、字典):可以在不声明 global 的情况下修改其内容,因为变量名本身没有变,只是对象内部状态变了。

my_list = [1, 2]

def modify_list():
    # 不需要 global,因为没有重新绑定 my_list 这个名字
    my_list.append(3) 
    # my_list = [4]  <-- 这行需要 global,因为是重新赋值

modify_list()
print(my_list)  # 输出: [1, 2, 3]

6 模块

python模块是一个深坑,如果不是研究学习、需要做好工程化处理。

6.1 定义

      模块(Module) 是一个包含 Python 定义和语句的文件。文件的名称就是模块名加上 .py 后缀。模块允许你将代码逻辑组织成独立的文件,以便在其他程序中复用、维护和管理。模块这里就和python文件对应。

6.2 模块命名规范

模块命名规范是python一个比较坑的地方,这个玩意到处都是坑,有点混账。

类别 ✅ 推荐 (Good) ❌ 避免/错误 (Bad) 原因
大小写 config.py Config.py, CONFIG.py PEP 8 规定模块名全小写
分隔符 user_auth.py userauth.py, user-auth.py 下划线提高可读性;连字符非法
标准库冲突 my_math.py math.py, random.py 会覆盖标准库,导致导入错误
数字开头 module_v2.py 2nd_module.py 语法错误,无法导入
包初始化 __init__.py init.py 只有双下划线版本会被识别为包入口

6.2.1 核心命名规则 (PEP 8)

全小写字母:模块名应全部使用小写字母。
✅ mymodule.py, utils.py, data_processor.py
❌ MyModule.py, Utils.py


下划线分隔:如果名字由多个单词组成,使用下划线 _ 分隔,以提高可读性。
✅ file_utils.py, http_client.py
❌ fileutils.py (难读), file-utils.py (非法字符), fileUtils.py (驼峰式,通常用于类名)简短且具有描述性:名字应能清晰反映模块的功能,但不要过长。


6.2.2 严格禁止的命名 (避免冲突)

这是新手最容易犯的错误。绝对不要使用 Python 标准库模块或内置函数的名字作为你的模块名,否则会导致“阴影遮蔽”(Shadowing),使标准库无法导入。
❌ 严禁使用的名字示例:
math.py (覆盖标准库 math 模块)
random.py (覆盖标准库 random 模块)
string.py (覆盖标准库 string 模块)
os.py, sys.py, json.py, datetime.py
list.py, dict.py, str.py (虽然这些不是模块文件,但也不要这样命名,容易混淆)
test.py (如果你运行 python test.py,有时会与 unittest 的测试发现机制冲突,建议命名为 test_myfeature.py)
后果示例:
如果你创建了一个名为 math.py 的文件,并在其中写 import math,Python 会导入你自己的文件而不是标准库,导致 math.sqrt() 等函数不可用,甚至引发无限递归导入错误。


6.2.3 字符限制

只能包含:小写字母 (a-z)、数字 (0-9) 和下划线 (_)。
必须以字母或下划线开头:不能以数字开头。
✅ module1.py, _private.py
❌ 1module.py
不能有特殊字符:如 - (连字符), @, $, 空格 等。
❌ my-module.py (这在导入时会报错,因为 - 会被解析为减号)
❌ my module.py


6.2.4 特殊模块名约定

__init__.py:
这是包(Package)的标识文件。如果一个目录中包含 __init__.py,Python 就会将该目录视为一个包。该文件可以为空,也可以包含包的初始化代码。


__main__.py:
允许将一个目录或 zip 文件当作脚本直接运行(例如:python my_package)。


以下划线开头 (如 _helper.py):
这是一种约定,表示该模块是“内部使用”的,不建议被外部直接导入(尽管 Python 并不强制阻止导入)。通常配合 from package import * 使用时,以下划线开头的名字不会被导出。实际上也可以通过其他方式使用,权限控制不硬核


6.2.5 操作系统兼容性考虑

大小写敏感性:
Linux/macOS 文件系统通常是大小写敏感的 (Module.py 和 module.py 是两个不同的文件)。
Windows 文件系统通常是大小写不敏感的。
最佳实践:始终使用全小写,可以避免在不同操作系统间迁移代码时出现 ImportError 或奇怪的缓存问题。

6.3 包

     包(Package) 是一种组织模块(.py 文件)的方式,它允许你使用“点式命名法”(如 package.module)来构建分层级的命名空间。

6.3.1 目录结构

物理结构:包是一个目录。

  • 关键标识:该目录下必须包含一个名为 __init__.py 的文件(在 Python 3.3+ 中,为了支持“命名空间包”,这个文件不再是绝对强制的,但在常规开发中,强烈建议保留它以明确这是一个普通包)。
  • 作用:避免不同模块之间的命名冲突(例如 sound.effects.echo 和 image.effects.echo 可以共存)。
sound/                      <-- 顶层包
    __init__.py             <-- 初始化文件,标志这是包
    formats/                <-- 子包
        __init__.py
        wavread.py          <-- 模块
        wavwrite.py         <-- 模块
        aiffread.py
        aiffwrite.py
    effects/                <-- 子包
        __init__.py
        echo.py             <-- 模块
        surround.py
        reverse.py
    filters/                <-- 子包
        __init__.py
        equalizer.py
        vocoder.py

在这个结构中:

  • sound 是主包。
  • sound.formats 和 sound.effects 是子包。
  • sound.effects.echo 是一个具体的模块

6.3.2 __init__.py作用

这个文件非常重要,它有两个主要功能:
1 标识作用:告诉 Python 解释器“把这个目录当作一个包来处理”,而不是普通的文件夹。
2 初始化代码:当包被导入时,这个文件中的代码会被执行。通常用于:
          1 定义 __all__ 变量(控制 from package import * 时导出哪些内容)。
          2 初始化包级别的全局变量。
          3 简化导入(例如在 __init__.py 中导入子模块,让用户可以直接 import sound 而无需深入层级)

建议是每个包都加__init__.py,但是在高版本加了一个隐式包。可以在最顶层加一个就行了。

6.3.3 __all__

__all__ 是 Python 模块(包括 __init__.py)中的一个特殊变量(也可以在模块文件中),它是一个字符串列表。它的核心作用只有一个:控制 from module import * 的行为。
当你执行 from module import * 时:
        1 如果定义了 __all__:Python 只导入 __all__ 列表中列出的名字。
        2 如果没有定义 __all__:Python 会导入模块中所有不以下划线 _ 开头的名字。

# utils.py

# 1. 公开函数:用户可以直接使用
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

# 2. 内部辅助函数:通常以下划线开头,默认不会被 * 导入
def _internal_log(msg):
    print(f"[LOG]: {msg}")

# 3. 敏感/私有功能:虽然没以下划线,但我不想让用户通过 * 导入
def delete_database():
    print("⚠️ 危险操作:删除数据库!")

# ✅ 关键步骤:定义 __all__
# 只有这里列出的名字,才能被 "from utils import *" 导入
__all__ = ['add', 'subtract'] 

当你执行 import * 时,有两种场景
场景 A:from my_package.utils import *
Python 读取 my_package/utils.py。
检查 utils.py 里有没有 __all__。
my_package/__init__.py 里的 __all__ 完全被无视。

场景 B:from my_package import *
Python 读取 my_package/__init__.py。
检查 __init__.py 里有没有 __all__。
子模块(如 utils.py)里的 __all__ 完全被无视(除非 __init__.py 显式地根据子模块的 __all__ 做了特殊处理,但这需要写代码,不是自动的)。

 6.3.4 __main__.py

sound/
├── __init__.py       # 负责包的初始化和简化导入
├── __main__.py       # 👉 程序的入口点 (Entry Point)
├── player.py         # 播放逻辑
└── recorder.py       # 录音逻辑

python -m sound

6.4 模块导入

       模块导入主要有相对导入和绝对导入,相对导入需要注意验证一下是否受到执行路径影响。

       绝对导入就是从最顶层包开始导入。导入时尽量避免使用*号

6.4.1 基本导入格式

格式 示例 说明 使用方式
import 模块 import os 导入整个模块 os.path.join()
import 模块 as 别名 import numpy as np 导入模块并起别名 np.array()
from 模块 import 成员 from os import path 导入模块中的特定成员 path.join()
from 模块 import 成员 as 别名 from os import path as p 导入成员并起别名 p.join()
from 模块 import * from os import * 导入模块所有公共成员 path.join() ⚠️

6.4.2 导入包

格式 示例 说明 使用方式
import 包 import my_package 只执行 __init__.py my_package.xxx
import 包.子模块 import my_package.utils 导入包下的子模块 my_package.utils.func()
from 包 import 子模块 from my_package import utils 从包导入子模块 utils.func()
from 包.子模块 import 成员 from my_package.utils import add 从子模块导入成员 add()
from 包.子包 import 模块 from my_package.subpkg import module 跨包子包导入 module.func()

默认情况下不会导入包下的模块,只会执行__init__.py

# my_package/__init__.py
print("📦 __init__.py 被执行了!")

# my_package/module_a.py
print("📄 module_a.py 被加载了!")
def func_a(): pass

# my_package/module_b.py
print("📄 module_b.py 被加载了!")
def func_b(): pass
# 测试导入主程序
import my_package

# 输出:
# 📦 __init__.py 被执行了!
# (只有这一行!)

、
# 查看包的内容
print(dir(my_package))
# 输出:['__all__', '__builtins__', '__cached__', '__doc__', '__file__', ...]
# 只有 __init__.py 中定义的内容,没有 module_a, module_b

# 尝试访问子模块
my_package.module_a  # ❌ AttributeError!

在__init__py中加导出语句

这是个绕圈圈,很多时候应该没有必要这样搞

6.4.3 相对导入

相对导入都是from开头,使用相对路径

格式 示例 说明 使用场景
from . import 模块 from . import utils 导入同包下的模块 包内部
from .模块 import 成员 from .utils import add 导入同包模块的成员 包内部
from .. import 模块 from .. import config 导入父包的模块 子包内部
from ..父包 import 模块 from ..parent import module 导入指定父包的模块 子包内部
from ... 导入 from ...config import CFG 导入祖父包的成员 深层子包

6.4.4 其他导入场景

条件导入格式

格式 示例 说明
尝试导入 try: import xxx except ImportError: ... 可选依赖
版本判断导入 if sys.version_info >= (3, 8): import xxx 版本兼容
平台判断导入 if platform.system() == 'Windows': import xxx 跨平台

延迟导入

格式 示例 说明
函数内导入 def func(): import xxx 按需加载
条件分支导入 if condition: import xxx 减少启动时间
importlib 动态导入 importlib.import_module('xxx') 运行时导入

6.4.5 名称隔离

# # main.py
# # 不建议这样用,这相当于是全路径导入,绕了一下
# from test1_project.sub_pkg_1.sub_pkg_01.test_in_pk01 import show,f
# # ✅ 测试可用的函数
# show()
# f='kkkk' # 并没有修改到test_in_pk01 f值
# show()


#上面问题的解决办法
from test1_project.sub_pkg_1.sub_pkg_01 import test_in_pk01
import test1_project.sub_pkg_1.sub_pkg_01.test_in_pk01 as test_in_pk01
#import test1_project.sub_pkg_1.sub_pkg_01.test_in_pk01 这句不行

# ✅ 测试可用的函数
test_in_pk01.show() # 输出f=fffff
f='kkkk'
test_in_pk01.show() # 输出f=fffff
test_in_pk01.f='kkkk' 
test_in_pk01.show() # 输出f=kkkk


#test_in_pk01.py内容


f='fffff'

def show():
    print(f'f={f}')

6.4.5 防止导入

属性是_开头可以防止import * 导入,这意义不大,python的权限控制很不到位,不要太依赖

6.4.6 大小写

PYTHONCASEOK 是控制大小写的环境变量,命名不最好不要依赖大小写。PYTHONCASEOK=1有值就会忽略大小写。

6.4.7 导入循环

循环导入(Circular Import)是 Python 开发中常见的问题,通常表现为 ImportError: cannot import name 'X' from 'Y' 或者程序行为异常(如属性为 None 或函数未定义)。
这通常发生在两个模块互相依赖时:
module_a.py 需要 module_b 的内容。
module_b.py 也需要 module_a 的内容。
以下是解决循环导入的 5 种主要方法,按推荐程度排序

1. 重构代码

这是最根本的解决方法。循环导入通常意味着代码结构不合理,存在过高的耦合度

2. 延迟导入

如果无法重构代码(例如逻辑确实紧密耦合),可以将 import 语句从文件顶部移动到函数内部。
原理:Python 只有在执行到该函数时才会执行导入语句。此时,另一个模块可能已经完成了初始化,从而打破循环。
缺点:导入语句分散在代码中,稍微影响可读性;每次调用函数都会检查导入(虽然有缓存,性能影响极小)。

# module_a.py
def func_a():
    # 延迟导入:只有调用 func_a 时才导入 module_b
    from module_b import func_b 
    return func_b()

# module_b.py
def func_b():
    from module_a import func_a # 同样延迟
    return "Done"


#注意:这种写法在解决“运行时”循环依赖非常有效,但在模块顶层变量互相引用时依然要小心。

3 使用 TYPE_CHECKING 处理类型注解(⭐ 类型提示专用)
如果你是因为类型提示(Type Hinting)导致的循环导入(例如 A 需要 B 的类型,B 需要 A 的类型),这是标准解法。
做法:利用 typing.TYPE_CHECKING 常量。它只在静态类型检查工具(如 mypy, Pyright)运行时为 True,在运行时为 False。

6.5 模块执行

模块执行时需要得到顶层包目录执行,需要加-m参数

6.6 模块内建函数

6.6.1 __import__

主要是用来做自定义导入,一般用不上

参数 类型 必填 说明
name str 要导入的模块名称(字符串)
globals dict 当前全局命名空间,默认 globals()
locals dict 当前局部命名空间,默认 locals()
fromlist list 类似 from ... import 中的导入列表
level int 相对导入级别,0=绝对导入,>0=相对导入

# 简单示例,等价于:import math
math_module = __import__('math')
print(math_module.sqrt(16))  # 4.0

6.6.2 globals和locals

globals() 和 locals() 是 Python 中两个非常重要的内置函数,它们用于访问当前作用域的名称空间字典。理解它们的用法对于调试、动态编程以及深入理解 Python 的作用域机制至关重要

特性 globals() locals()
返回内容 全局名称空间的字典 局部名称空间的字典
范围 模块级别(整个 .py 文件) 函数内部、类定义内部或当前执行帧
可修改性 可修改。修改字典会直接改变全局变量。 不可靠/只读。在函数内修改字典通常不会改变局部变量。
生命周期 随模块加载而存在,程序结束销毁。 随函数调用创建,函数返回销毁。
典型用途 动态访问全局配置、检查已导入模块、调试。 调试函数内部状态、动态获取参数值。

globals() 返回一个字典,表示当前的全局符号表。
在模块顶层,它包含该模块定义的所有变量、函数、类和导入的模块。
在函数内部调用 globals(),返回的仍然是定义该函数的模块的全局字典,而不是函数内部的局部变量。

x = 10
name = "Alice"

def my_func():
    y = 20
    # 在函数内部调用 globals(),看到的依然是模块级的变量
    print(globals()['x'])      # 输出: 10
    print(globals()['name'])   # 输出: Alice
    # print(globals()['y'])    # 报错: KeyError,因为 y 是局部变量,不在全局字典中
    
    # 【重要】修改 globals() 字典会直接改变全局变量
    globals()['x'] = 999
    globals()['new_var'] = "I am global now"

my_func()

print(x)          # 输出: 999 (被函数修改了)
print(new_var)    # 输出: "I am global now" (动态添加了新全局变量)

⚠️ 警告:虽然可以通过 globals() 动态修改变量,但这通常被视为坏实践(Bad Practice),因为它会让代码流程难以追踪,破坏代码的可读性和安全性

locals() 返回一个字典,表示当前的局部符号表。
在函数内部:包含函数的参数和局部定义的变量。
在模块顶层:行为与 globals() 相同(因为模块顶层既是全局也是局部)。
在类定义体内:包含类属性。

def calculate(a, b):
    c = a + b
    d = a * b
    
    # 获取当前局部变量的字典
    local_vars = locals()
    
    print(local_vars['a'])  # 输出: 传入的 a 值
    print(local_vars['c'])  # 输出: 计算结果
    print(local_vars.keys()) # 输出: dict_keys(['a', 'b', 'c', 'd', ...])
    
    return c

calculate(3, 4)

⚠️ 关键陷阱:修改 locals() 的行为,这是面试和实际开发中最容易踩的坑:
在函数内部:修改 locals() 返回的字典,通常不会影响实际的局部变量值。
原因:Python 为了优化速度,局部变量存储在栈帧的特定槽位(slots)中,而不是完全依赖字典。locals() 在函数内只是生成一个快照(副本)。

def test_modify():
    x = 10
    print(f"初始 x: {x}")
    
    # 尝试修改 locals 字典
    locals()['x'] = 999
    
    # 再次打印 x
    print(f"修改后 x: {x}") 
    # 输出仍然是 10!修改字典并没有改变真正的变量 x
    
    # 但是,如果是动态生成的变量名,有时会有不同表现,但强烈不建议依赖此行为。

test_modify()

6.6.3 reload

重新导入模块

import importlib
import my_module  # 假设这是你已经导入的模块

# ... 修改了 my_module.py 的代码 ...

# 重新加载模块
importlib.reload(my_module)

# 现在可以使用更新后的代码
my_module.some_function()

6.5 路径搜索

6.5.1 基本搜索流程

1 检查缓存 (sys.modules):首先检查该模块是否已经被导入并缓存在 sys.modules 字典中。如果是,直接使用缓存对象,不再进行后续搜索。
2 检查内置模块:如果不在缓存中,检查它是否是 Python 的内置模块(如 sys, math, time 等)。
3 搜索 sys.path:如果前两步都没找到,解释器会遍历 sys.path 列表中的每一个目录路径,寻找名为 my_module.py 的文件或名为 my_module 的包(文件夹)。
一旦找到,搜索立即停止。这意味着 sys.path 中靠前的路径具有更高的优先级。如果两个路径下都有同名模块,排在前面的那个会被导入(这被称为“路径遮蔽”)。
4 抛出异常:如果遍历完所有路径仍未找到,抛出 ModuleNotFoundError。

6.5.2 sys.path

sys.path 是一个字符串列表,其内容在 Python 启动时动态生成。它的默认组成通常包括以下几个部分(按优先级从高到低):
(1) 脚本所在目录 (或当前工作目录)
直接运行脚本时 (python script.py):
脚本文件所在的绝对路径会被添加到 sys.path 的最前端(索引 0)。
注意:这不是“当前工作目录”,而是脚本文件本身的目录。
交互式命令行 (python 回车) 或 使用 -c 参数时:
当前工作目录 (Current Working Directory, CWD) 会被添加到最前端。
使用 -m 参数运行时 (python -m package.module):
当前工作目录 会被添加到最前端,而不是脚本所在的子目录。这是运行包内模块时的常见陷阱。

(2) PYTHONPATH 环境变量
如果在操作系统中设置了 PYTHONPATH 环境变量,其中包含的所有目录路径会被插入到 sys.path 中(通常在脚本目录之后,标准库之前)。
这是一个全局配置,允许用户在不修改代码的情况下扩展模块搜索范围。
格式:在 Windows 上用分号 ; 分隔,在 macOS/Linux 上用冒号 : 分隔。

(3) 标准库路径
Python 安装目录下的标准库路径(例如 .../lib/python3.x 和 .../lib/python3.x/lib-dynload)。

(4) 第三方包路径 (site-packages)
通过 pip 安装的第三方包默认存放的位置(例如 .../lib/python3.x/site-packages)。
这也是虚拟环境(venv/conda)隔离依赖的关键目录,每个虚拟环境有自己独立的 site-packages

运行时可以动态修改

import sys
import os

# 将特定目录添加到搜索路径的最前端 (最高优先级)
sys.path.insert(0, '/path/to/my/custom/module')

# 或者添加到末尾
sys.path.append('/another/path')

# 现在可以导入该路径下的模块了
# import my_custom_module

7 面向对象

7.1 类与实例

类定义关键字是class,类名是首字母大写,以便在使用时区别于函数调用。类实例化不需要使用new

class ClassName(ParentClass1, ParentClass2, ...):
    """类的文档字符串 (Docstring),用于说明类的用途"""
    
    # 类属性 (Class Attributes) - 所有实例共享
    class_variable = "默认值"

    def __init__(self, param1, param2, ...):
        """构造方法 (Constructor)
        在创建对象时自动调用,用于初始化实例属性
        """
        # 实例属性 (Instance Attributes) - 每个实例独有
        self.instance_param1 = param1
        self.instance_param2 = param2

    def instance_method(self, arg1):
        """实例方法
        第一个参数必须是 self,代表实例本身
        """
        # 方法逻辑
        return f"{self.instance_param1} 处理 {arg1}"

    @classmethod
    def class_method(cls, arg1):
        """类方法
        第一个参数必须是 cls,代表类本身
        使用 @classmethod 装饰器
        """
        return f"类方法被调用: {cls.class_variable}"

    @staticmethod
    def static_method(arg1):
        """静态方法
        不需要 self 或 cls 参数
        使用 @staticmethod 装饰器
        """
        return f"静态方法计算: {arg1 * 2}"

#实例化,参数会传入__init__
obj = ClassName(param1, param2, ...)

7.1.1 基础类与新式类

python3 中的类默认都继承至object,属于新式类

经典类:实例的 type 是 <type 'instance'>,类本身的类型是 <type 'class'>。类和类型是分开的概念。
新式类:实例的 type 就是它的类。type(obj) 返回 obj.__class__。实现了“类也是对象”的概念,使得元类(Metaclass)编程更加一致和强大

class MyClass:
    pass

class MyExplicitObjectClass(object):
    pass

# 检查基类
print(MyClass.__bases__)        # 输出: (<class 'object'>,)
print(MyExplicitObjectClass.__bases__) # 输出: (<class 'object'>,)

# 检查 MRO
print(MyClass.__mro__)          
# 输出: (<class '__main__.MyClass'>, <class 'object'>)

# 检查类型
obj = MyClass()
print(type(obj))                # 输出: <class '__main__.MyClass'>
print(type(obj) is obj.__class__) # 输出: True

7.1.2 __init__

比较严格的说法是初始化方法,还有一个__new__方法,该方法更像构造器,但是一般都不用。

__init__没有返回值。该方法中定义的self开头的是实例变量

7.1.3 变量访问权限

python中是没有权限修饰的,是通过一些约定或者是转弯磨角方法实现。

1. 单下划线开头 (_var)
含义:内部使用约定 (Internal Use Convention)
语义:告诉程序员和其他开发者,“这个变量或方法是供类内部使用的,请不要在类外部直接访问它”。
实际效果:Python 解释器完全忽略这个前缀。你仍然可以在类外部自由访问和修改它。它不会触发任何特殊的名称转换。

主要用途:
作为代码文档的一部分,提示该成员是“受保护的”(Protected,类似 Java/C++ 的概念,但 Python 没有强制的访问控制)。
在使用 from module import * 时,以单下划线开头的名字不会被导入到当前命名空间。

class MyClass:
    def __init__(self):
        self._internal_var = "我是内部变量"

obj = MyClass()
print(obj._internal_var)  # ✅ 可以正常访问,不会报错
# 输出: 我是内部变量

2 双下划线开头 (__var)
含义:名称修饰 (Name Mangling)
语义:旨在避免子类意外覆盖父类的属性或方法。它通常用于表示“私有”(Private)成员。
实际效果:Python 解释器会主动干预。它会将变量名重写(修饰)为 _ClassName__var 的形式。这使得在类外部直接通过原名访问变得困难(虽然不是不可能)。

主要用途:
防止命名冲突:当你在一个复杂的继承体系中,确保子类的 __var 不会意外覆盖父类的 __var。
模拟私有属性:虽然 Python 没有真正的私有属性,但这是一种强烈的信号,表明“除非万不得已,否则不要在外部访问”。

class MyClass:
    def __init__(self):
        self.__private_var = "我是私有变量"

obj = MyClass()

# print(obj.__private_var) 
# ❌ 报错: AttributeError: 'MyClass' object has no attribute '__private_var'

# 但是,可以通过修饰后的名字访问(不推荐这样做):
print(obj._MyClass__private_var) 
# ✅ 可以访问,输出: 我是私有变量

7.1.4 类变量与实例变量

这里有个特别需要注意的是最好不要通过实例访问类变量,通过实例给类变量赋值存在语言陷阱,参看下面实例

特性 类变量 (Class Variable) 实例变量 (Instance Variable)
定义位置 在类的方法外部,直接定义在类体中。 在方法内部(通常是 __init__),通过 self 定义。
访问方式 类名.变量名实例名.变量名 只能通过 实例名.变量名 (即 self.变量名)
代码示例 class A: count = 0 def __init__(self): self.name = "..."
class Dog:
    # 【类变量】所有狗共享的物种名称
    species = "Canine" 
    __varA  = 'test'
    def __init__(self, name):
        # 【实例变量】每只狗独有的名字
        self.name = name

# 创建两个实例
dog1 = Dog("Buddy")
dog2 = Dog("Max")

# 1. 读取:初始状态
print(f"1. {dog1.name} is a {dog1.species}")  # Buddy is a Canine
print(f"   {dog2.name} is a {dog2.species}")  # Max is a Canine

# 2. 修改【类变量】(正确方式:通过类名)
Dog.species = "Feline" 
print("\n2. 修改类变量后 (Dog.species = 'Feline'):")
print(f"   {dog1.name} is a {dog1.species}")  # Buddy is a Feline (变了!)
print(f"   {dog2.name} is a {dog2.species}")  # Max is a Feline (变了!)

# 3. 修改【实例变量】
dog1.name = "Old Buddy"
print("\n3. 修改实例变量后 (dog1.name = 'Old Buddy'):")
print(f"   {dog1.name} is a {dog1.species}")  # Old Buddy is a Feline
print(f"   {dog2.name} is a {dog2.species}")  # Max is a Feline (没变)

# 4. 【陷阱】通过实例修改类变量名
# 这不会改变类变量,而是给 dog1 创建了一个新的实例变量叫 'species'
dog1.species = "Reptile" 

print("\n4. 陷阱操作后 (dog1.species = 'Reptile'):")
print(f"   {dog1.name} is a {dog1.species}")  # Old Buddy is a Reptile (dog1 独有)
print(f"   {dog2.name} is a {dog2.species}")  # Max is a Feline (dog2 依然共享类的值)
print(f"   类本身的值: {Dog.species}")        # Feline (类变量本身没变)

print("\n4.:")
#print(Dog.__varA 报错
print(Dog._Dog__varA) # test
print(dog1._Dog__varA)# test
Dog._Dog__varA = 'mmm' 
print(Dog._Dog__varA) #mmm
print(dog1._Dog__varA) #mmm

7.1.5 糟糕的实例外挂

除了,类属性和实例属性,还有一种实例外挂属性,这个特性相当糟糕。对代码可维护性会造成很大破坏。

class Dog:
    # 【类变量】所有狗共享的物种名称
    species = "Canine" 
    __varA  = 'test'
    def __init__(self, name):
        # 【实例变量】每只狗独有的名字
        self.name = name

# 创建两个实例
dog1 = Dog("Buddy")
dog2 = Dog("Max")
# 如果你不看类定义,你并不知道ff是类中定义的还是,添加的,相当恶心,代码很难维护
#每次遇到恶心的规则,就会用灵活性来恶心人。这种在实例上附加属性的做法完全可以禁止
#作为一个靠谱的程序员,程序的可维护性,是前期就必须要考虑的问题
dog2.ff='kkk'
print(dog2.ff)
#print(dog1.ff) 要报错 实例属性不存在
  1. 可读性灾难:看到 dog2.ff,如果不翻遍整个文件甚至整个项目,根本不知道 ff 是哪里来的。是类里定义的?是父类继承的?还是某行代码临时挂上去的?
  2. 隐蔽的 Bug:最常见的情况是拼写错误。你想写 self.age,结果手滑写成 self.aeg。在 Java 里编译器直接报错,程序跑不起来;在 Python 默认模式下,它只是默默创建了一个新属性 aeg,程序正常运行,但逻辑全错,这种 Bug 最难排查。
  3. 架构腐蚀:如果团队里有人习惯随手给对象“打补丁”(动态加属性),久而久之,类的结构就会变得支离破碎,任何人都无法确信一个对象到底包含什么数据
  4. 结构腐蚀:也容易造成类似上文中的属性覆盖

另一种是可变对象

class Dog:
    # 【类变量】所有狗共享的物种名称
    species = "Canine" 
    listVar = [1,2,3]
    def __init__(self, name):
        # 【实例变量】每只狗独有的名字
        self.name = name

# 创建两个实例
dog1 = Dog("Buddy")
dog2 = Dog("Max")

# 1. 读取:初始状态
print(f'dog1.listVar={dog1.listVar} of {id(dog1.listVar)},Dog.listVar={Dog.listVar} of {id(Dog.listVar)}')
dog1.listVar[0]=4
print(f'dog1.listVar={dog1.listVar} of {id(dog1.listVar)},Dog.listVar={Dog.listVar} of {id(Dog.listVar)}')
dog1.listVar=[7,8,9] #整个赋值就会遮蔽
print(f'dog1.listVar={dog1.listVar} of {id(dog1.listVar)},Dog.listVar={Dog.listVar} of {id(Dog.listVar)}')

output='''
dog1.listVar=[1, 2, 3] of 2540229032960,Dog.listVar=[1, 2, 3] of 2540229032960
dog1.listVar=[4, 2, 3] of 2540229032960,Dog.listVar=[4, 2, 3] of 2540229032960
dog1.listVar=[7, 8, 9] of 2540229101632,Dog.listVar=[4, 2, 3] of 2540229032960
'''

7.1.6 实例方法

        实例方法都要传一个self,这个也有点恶心。号称简洁的语言,有时候就喜欢来些乌七八糟的,自相矛盾。实例方法只能在实例上调用。

7.1.7 类方法与静态方法

实例和类都可以调用,是通过装饰器实现,原生语法并不支持

class Demo:
    data = "我是类变量"  # 大家共享的

    def __init__(self,name):
        self.name=name
    # 1. 实例方法 (默认,不用写装饰器)
    # 第一个参数是 self (代表具体的对象)
    # 用途:操作具体对象的数据
    def instance_method(self):
        return f"实例方法:我能访问self.data={self.data},Demo.data={Demo.data}"

    # 2. 类方法 (加 @classmethod)
    # 第一个参数是 cls (代表类本身)
    # 用途:操作共享数据,或者用来创建新对象
    @classmethod
    def class_method(cls):
        return f"实例方法:我能访问cls.data={cls.data},Demo.data={Demo.data}"

    # 3. 静态方法 (加 @staticmethod)
    # 不需要 self 或 cls
    # 用途:纯工具函数,跟类或对象都没关系,只是借个名字放这里
    @staticmethod
    def static_method():
        return f"实例方法:我能访问Demo.data={Demo.data}"

# --- 怎么调用 ---

obj = Demo('11')

print(obj.instance_method())  # ✅ 必须用对象调用 (通常)
print(Demo.class_method())    # ✅ 推荐用类调用 (也能用对象)
print(Demo.static_method())   # ✅ 推荐用类调用 (也能用对象)

output='''
实例方法:我能访问self.data=我是类变量,Demo.data=我是类变量
实例方法:我能访问cls.data=我是类变量,Demo.data=我是类变量
实例方法:我能访问Demo.data=我是类变量
'''

7.1.8 如何实现抽象方法

python没有语法上支持,可以在要实现的方法上抛出异常来逼迫子类实现,但是语法不会报错,只有运行时才会报错。

7.1.9 __slots__ 属性

可以使用slot冻结属性,但是又会引发一些新问题,如果没有别要就不要用。

场景 建议
创建数百万个小对象 (如坐标点、粒子、数据库行映射) ✅ 强烈推荐。显著节省内存,提升性能。
作为数据结构或 DTO (Data Transfer Object) ✅ 推荐。固定字段,防止拼写错误,代码更健壮。
需要严格限制属性,防止动态修改 ✅ 推荐。作为一种防御性编程手段。
普通的业务逻辑类,数量不多 ⚠️ 可选。收益不明显,但能增加代码约束性。
需要频繁动态添加属性的类 ❌ 不要用。会给自己找麻烦。
需要多重继承且结构复杂 ⚠️ 谨慎。需仔细处理 __slots__ 的继承关系。
class StrictDog:
    # 只允许这两个属性,这里是个坑,忘记添加怎么办,又不会报错
    __slots__ = ['name', 'age']
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

dog = StrictDog("Buddy", 3)

# 1. 访问已声明的属性:正常
print(dog.name)  # Buddy

# 2. 尝试动态添加新属性:❌ 报错!
try:
    dog.color = "Brown" 
except AttributeError as e:
    print(f"错误: {e}")
    # 输出: 错误: 'StrictDog' object has no attribute 'color'

# 3. 尝试拼写错误赋值:❌ 报错!
try:
    dog.nmae = "Max"
except AttributeError as e:
    print(f"错误: {e}")
    # 输出: 错误: 'StrictDog' object has no attribute 'nmae'

# 4. 检查 __dict__:❌ 不存在!
try:
    print(dog.__dict__)
except AttributeError as e:
    print(f"错误: {e}")
    # 输出: 错误: 'StrictDog' object has no attribute '__dict__'

继承中的 __slots__
如果父类定义了 __slots__,子类必须也定义 __slots__ 才能添加新的属性。如果子类不定义,它将无法添加任何新属性(只能使用父类声明的)。

class Animal:
    __slots__ = ['name']

class Dog(Animal):
    # 如果想让 Dog 有 'breed' 属性,必须在这里声明
    __slots__ = ['breed'] 
    
    def __init__(self, name, breed):
        self.name = name   # 来自父类 slots
        self.breed = breed # 来自子类 slots

d = Dog("Buddy", "Golden")
# d.age = 3  # ❌ 依然报错,因为既不在 Animal 也不在 Dog 的 slots 中

注意:如果子类想允许动态添加属性(打破限制),可以在子类的 __slots__ 中加入 '__dict__'

class FlexibleDog(Animal):
    __slots__ = ['breed', '__dict__'] # 允许 breed + 其他动态属性
    
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

fd = FlexibleDog("Max", "Pug")
fd.trick = "roll over" # ✅ 现在允许了,因为有了 __dict__

_slots__ 可以减少内存,当需要创建大量轻量级对象(如数据点、节点、游戏实体)时,效果惊人。

import sys

class NoSlots:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class WithSlots:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 创建 100 万个实例
list_no_slots = [NoSlots(i, i) for i in range(1000000)]
list_with_slots = [WithSlots(i, i) for i in range(1000000)]

# 估算内存 (简单计算单个对象大小 * 数量)
# 注意:sys.getsizeof 只计算对象本身,不包含引用的对象,但在本例中足以展示差异
size_no = sys.getsizeof(NoSlots(1, 1))
size_yes = sys.getsizeof(WithSlots(1, 1))

print(f"无 slots 实例大小: ~{size_no} bytes")
print(f"有 slots 实例大小: ~{size_yes} bytes")
print(f"节省比例: {(1 - size_yes/size_no) * 100:.2f}%")

# 典型输出 (具体数值因系统而异):
# 无 slots: ~56-64 bytes (包含字典指针)
# 有 slots: ~32-48 bytes (紧凑存储)
# 节省比例通常在 30% - 50% 甚至更多

常见陷阱
类变量依然可以动态添加:
__slots__ 限制的是实例属性。你依然可以随时给类本身添加属性(这会影响所有实例)。

class S:
    __slots__ = ['x']

s1 = S()
# s1.y = 1  # ❌ 实例不行
S.z = 100   # ✅ 类可以
print(S.z)  # 100

多重继承冲突:
如果多个父类都有非空的 __slots__,且内容不兼容,可能会导致问题。通常建议在多重继承中,只有一个基类定义 __slots__,或者所有子类都妥善处理。
无法使用 __dict__ 和 __weakref__:
默认情况下,定义了 __slots__ 的类不支持弱引用(weakref)。如果需要支持弱引用,必须显式在 __slots__ 中加入 '__weakref__'

7.1.10 property()

原始写法

class Person:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        if not value: raise ValueError("不能为空")
        self._name = value

    # 手动将方法绑定到属性
    name = property(get_name, set_name) 

p = Person('ZhangShan')
print(p.name)
print(p.get_name())
p.name='Lisi'
print(p.name)
print(p.get_name())

output='''
ZhangShan
ZhangShan
Lisi
Lisi
'''

新式装饰器写法

class Person:
    def __init__(self, name):
        # 注意:这里用 _name 存储真实数据,避免和属性名冲突
        self._name = name 

    # 1. Getter (获取值)
    # 当用户执行 p.name 时,自动调用这个方法
    @property
    def name(self):
        print("👀 正在读取名字...")
        return self._name.upper()  # 可以动态处理数据

    # 2. Setter (设置值)
    # 当用户执行 p.name = "Tom" 时,自动调用这个方法
    @name.setter
    def name(self, value):
        print("✍️ 正在设置名字...")
        if not value:
            raise ValueError("名字不能为空!")
        self._name = value

    # 3. Deleter (删除值) - 可选
    # 当用户执行 del p.name 时,自动调用
    @name.deleter
    def name(self):
        print("🗑️ 正在删除名字...")
        del self._name

# --- 使用演示 ---
p = Person("alice")

# ✅ 像访问变量一样读取(实际触发了 @property 方法)
print(p.name)  
# 输出: 
# 👀 正在读取名字...
# ALICE

# ✅ 像修改变量一样赋值(实际触发了 @name.setter 方法)
p.name = "bob" 
# 输出: ✍️ 正在设置名字...
# (此时 p._name 变成了 "bob")

# ❌ 如果赋空值,会触发验证逻辑报错
# p.name = ""  -> 抛出 ValueError

# ✅ 删除
del p.name

7.1.11 is与isinstance

is 和 isinstance() 是 Python 中两个非常容易混淆但用途完全不同的操作符/函数。
简单总结:
is:判断身份(是不是同一个对象?内存地址一样吗?)。
isinstance():判断类型(是不是某一种类或其子类的实例?)

7.2 类的继承

支持单继承、多继承以及复杂的方法解析顺序(MRO)                                        

7.2.1 基本用法

class A:pass
class B:pass
class C(A,B):pass #多重继承

class Parent:
    def __init__(self, val):
        self.val = val
    
    def show(self):
        print(f"Parent: {self.val}")

class Child(Parent):
    def __init__(self, val, extra):
        super().__init__(val)  # 继承初始化
        self.extra = extra     # 新增属性
    
    def show(self):            # 重写方法
        super().show()         # 调用父类逻辑
        print(f"Child Extra: {self.extra}")

# 使用
obj = Child(100, "ABC")
obj.show()

7.2.2 菱形依赖

核心冲突:
如果类 A 有一个方法 do_something(),而 B 和 C 都重写了这个方法。当我们在 D 的实例上调用 do_something() 时:
D 应该调用 B 的版本还是 C 的版本?
如果 B 和 C 都通过 super() 调用了 A 的方法,A 的方法会被执行一次还是两次?
执行两次:通常是错误的(例如:初始化代码跑了两次,资源被重复分配)。
执行一次:通常是正确的期望。

      A
     / \
    B   C
     \ /
      D

Python 的解决方案:C3 线性化算法 (MRO)
在 旧式类(Python 2.2 之前,不继承 object)中,Python 使用深度优先搜索(DFS),这会导致 A 的方法被调用两次,引发严重 Bug。
在 新式类(Python 3 中所有类默认都是新式类,显式或隐式继承 object)中,Python 采用了 C3 线性化算法 来计算 方法解析顺序 (MRO, Method Resolution Order)。
C3 算法的核心保证:
1  单调性:子类永远在父类之前被检查。
2 局部优先次序:如果在定义类时写了 class D(B, C),那么 B 永远在 C 之前被检查。
3 唯一性:每个类在 MRO 列表中只出现一次。这意味着父类的方法只会被调用一次。
代码演示:验证“只调用一次”

class A:
    def do(self):
        print("A: do called")
        # 注意:这里通常不需要 super(),因为 A 是顶层基类,但为了演示链式调用,我们加上
        # 如果 A 继承自 object,super().do() 会最终调用 object 的方法(通常什么都不做)
        # 为了清晰,这里我们假设 A 是终点,或者我们观察调用顺序

class B(A):
    def do(self):
        print("B: do called")
        super().do()  # 调用下一个 MRO 中的类

class C(A):
    def do(self):
        print("C: do called")
        super().do()  # 调用下一个 MRO 中的类

# 关键点:定义顺序是 (B, C)
class D(B, C):
    def do(self):
        print("D: do called")
        super().do()  # 触发 MRO 链

# 查看 MRO 顺序
print("MRO 顺序:", [cls.__name__ for cls in D.__mro__])
# 输出: ['D', 'B', 'C', 'A', 'object']
# 注意:A 只出现了一次!

print("\n开始调用 d.do():")
d = D()
d.do()

output='''
MRO 顺序: ['D', 'B', 'C', 'A', 'object']

开始调用 d.do():
D: do called
B: do called
C: do called
A: do called
'''

7.3 内建函数

7.3.1 检查与判断

这些函数用于在运行时确认对象的类型和结构

函数 语法 作用 典型场景
isinstance() isinstance(obj, cls) 判断 obj 是否是 cls 类或其子类的实例。 类型检查,多态处理。
isinstance(dog, Animal)True
issubclass() issubclass(cls_a, cls_b) 判断 cls_a 是否是 cls_b 的子类。 验证继承关系。
issubclass(Dog, Animal)True
hasattr() hasattr(obj, 'name') 判断对象 obj 是否拥有名为 'name' 的属性。 安全检查,避免报错。
if hasattr(user, 'vip'): ...
callable() callable(obj) 判断对象 obj 是否可被调用(即是否有 __call__ 方法)。 区分方法和普通属性,或检查类本身是否可实例化。
class Animal: pass
class Dog(Animal): pass

dog = Dog()

print(isinstance(dog, Dog))      # True
print(isinstance(dog, Animal))   # True (支持继承链检查)
print(issubclass(Dog, Animal))   # True
print(hasattr(dog, 'name'))      # False (除非定义了 name)
print(callable(Dog))             # True (类是可以被调用来创建实例的)
print(callable(dog))             # False (除非定义了 __call__)

7.3.2 获取与动态操作

这些函数允许你通过字符串名称来动态地访问或修改属性,是反射(Reflection)机制的基础

函数 语法 作用 典型场景
getattr() getattr(obj, 'name', default) 获取属性值。如果不存在,返回 default(若不填则报错)。 动态配置读取,处理可选属性。
val = getattr(config, 'timeout', 30)
setattr() setattr(obj, 'name', value) 设置属性值。相当于 obj.name = value 动态添加属性,批量初始化。
setattr(user, 'role', 'admin')
delattr() delattr(obj, 'name') 删除属性。相当于 del obj.name 清理敏感数据,重置状态。
dir() dir(obj) 列出对象的所有属性和方法(包括继承的)。 调试,查看对象能力,自动补全提示。
vars() vars(obj) 返回对象的 __dict__ 字典(仅实例变量)。 查看对象内部状态,序列化。
type() type(obj) 返回对象的确切类型(类)。 精确类型判断(不考虑继承)。
class Dog:
    def __init__(self, name):
        self.name = name

dog = Dog("Buddy")

# 1. 动态获取 (安全模式)
print(getattr(dog, 'breed', 'Unknown'))  # Unknown (因为没定义)

# 2. 动态设置 (你之前提到的“恶心”操作通常由它完成)
setattr(dog, 'ff', 'kkk') 
print(dog.ff) # kkk 整个和外挂属性一样,只对当前实例生效
# 3. 动态删除
delattr(dog, 'ff')
# print(dog.ff) # 这里会报错

# 4. 查看所有内容
print(dir(dog)) 
# ['__class__', ..., 'name'] (包含所有内置和自定义属性)

# 5. 查看实例字典 (只包含动态绑定的实例变量)
print(vars(dog)) 
# {'name': 'Buddy'} (注意:ff 已经被删除了)

# 6. type vs isinstance
print(type(dog) == Dog)      # True
print(type(dog) == Animal)   # False (即使 Dog 继承 Animal,type 也只认直接类)

7.3.3 元编程与高级控制

这些函数用于在运行时创建类或控制属性查找逻辑

函数 语法 作用 典型场景
super() super()super(cls, obj) 返回一个代理对象,用于调用父类的方法。 多重继承中调用父类逻辑,避免硬编码父类名。
property() property(fget, fset, fdel, doc) 将方法转换为属性访问(装饰器 @property 的底层实现)。 封装 getter/setter,实现计算属性。
classmethod() classmethod(func) 将方法转换为类方法(第一个参数是 cls)。 工厂模式,替代构造函数。
staticmethod() staticmethod(func) 将方法转换为静态方法(无 selfcls)。 工具函数,逻辑上属于类但不依赖实例。
globals() / locals() globals(), locals() 返回当前全局或局部符号表的字典。 极其动态的场景,如从字符串动态查找类并实例化。
exec() / eval() exec(code), eval(expr) 执行动态生成的 Python 代码。 慎用!常用于框架加载插件或配置文件。

特别提及:type() 作为类构造器

type 不仅是查询类型的函数,它还是所有类的元类。你可以用它动态创建一个全新的类:

# 动态创建一个类:type(类名, 基类元组, 参数字典)
MyDynamicClass = type('MyDynamicClass', (object,), {'x': 1, 'say_hi': lambda self: "Hi"})

obj = MyDynamicClass()
print(obj.x)       # 1
print(obj.say_hi()) # Hi

7.4 类的特殊方法

python所谓的魔法,很多是通过这种方式实现,非常多的方法,这里只是列举一些,没法记忆,太多了

7.4.1 构造与初始化

方法 触发时机 作用
__new__(cls, ...) 实例化之前 真正创建对象的地方。通常用于单例模式或继承不可变类型(如 str, tuple)时使用。一般不需要重写。
__init__(self, ...) 实例化之后 初始化对象。new 创建好对象后,自动调用它来赋值属性。最常用。
__del__(self) 对象被销毁时 析构函数。当垃圾回收机制回收对象前调用(如关闭文件、断开连接)。注意: 由于垃圾回收时间不确定,重要资源建议用 with 上下文管理。
class Demo:
    def __new__(cls, *args, **kwargs):
        print("1. __new__: 正在创建实例...")
        return super().__new__(cls)

    def __init__(self, name):
        print("2. __init__: 正在初始化...")
        self.name = name

    def __del__(self):
        print(f"3. __del__: {self.name} 被销毁了")

d = Demo("测试") # 输出 1 -> 2
# 脚本结束或 del d 时输出 3

7.4.2 字符串表示

方法 触发时机 作用 推荐写法
__str__(self) print(obj)str(obj) 给用户看的友好字符串。 返回易读的中文或简略信息。
__repr__(self) 直接输入 objrepr(obj) 给开发者看的调试字符串。 返回能还原对象的代码格式(如 <Person name='Tom'>)。
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __str__(self):
        return f"用户:{self.name}"  # print(p) 显示这个
    
    def __repr__(self):
        return f"Person('{self.name}', {self.age})" # 直接敲 p 显示这个

p = Person("张三", 18)
print(p)        # 输出:用户:张三
print(repr(p))  # 输出:Person('张三', 18)

7.4.3 运算符重载

运算符 对应方法 示例
+ __add__(self, other) a + b
- __sub__(self, other) a - b
* __mul__(self, other) a * b
== __eq__(self, other) a == b (判断相等)
< __lt__(self, other) a < b (排序时用)
!= __ne__(self, other) a != b
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        # 定义向量相加:(1,2) + (3,4) = (4,6)
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other):
        # 定义相等判断
        #print('call eq')
        return self.x == other.x and self.y == other.y
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # 输出:Vector(4, 6) 调用__add__
print(v1 == v2) # 输出:False 调用 __eq__

7.4.4 容器行为

让你的对象支持 len()[]infor 循环

功能 对应方法 示例
长度 __len__(self) len(obj)
读取 __getitem__(self, key) obj[key]
写入 __setitem__(self, key, value) obj[key] = val
删除 __delitem__(self, key) del obj[key]
成员检查 __contains__(self, item) item in obj
迭代器 __iter__(self) for x in obj:
class MyList:
    def __init__(self):
        self._data = [10, 20, 30]

    def __len__(self):
        return len(self._data)

    def __getitem__(self, index):
        return self._data[index]

    def __contains__(self, item):
        return item in self._data

ml = MyList()
print(len(ml))      # 2. 输出:3
print(ml[1])        # 2. 输出:20
print(20 in ml)     # 3. 输出:True

for x in ml:        # 4. 如果没写 __iter__ 但写了 __getitem__,也能遍历
    print(x)

7.4.5 上下文管理

class FileManager:
    def __enter__(self):
        print("🔓 打开文件")
        self.file = open("data.txt", "w")
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("🔒 关闭文件")
        self.file.close()
        # 如果返回 True,可以吞掉异常;返回 False (或 None) 则让异常抛出
        return False

# 使用
with FileManager() as f:
    f.write("Hello")
# 即使里面报错,__exit__ 也会执行,确保文件关闭

7.4.6 可调用对象

这玩意有点搞扯

方法 触发时机 示例
__call__(self, ...) obj(...) my_obj()
class Adder:
    def __init__(self, base):
        self.base = base

    def __call__(self, x):
        return self.base + x

add_5 = Adder(5)
print(add_5(10))  # 输出:15 (像调用函数一样)

7.4.7 比较运算

运算符 特殊方法 说明
== __eq__(self, other) 等于 (最常用,用于判断相等)
!= __ne__(self, other) 不等于 (通常不需要写,Python 会自动对 __eq__ 取反)
< __lt__(self, other) 小于 (排序 sort() 的核心)
> __gt__(self, other) 大于
<= __le__(self, other) 小于等于
>= __ge__(self, other) 大于等于

7.4.8 序列相关

方法 对应操作 作用
__len__(self) len(obj) 返回序列长度(整数)。如果返回 0,对象在布尔判断中为 False
__getitem__(self, key) obj[key] 读取元素。支持整数索引(obj[0])和 切片(obj[1:5])。
__setitem__(self, key, value) obj[key] = val 修改/添加元素。如果不实现,序列就是只读的。
__delitem__(self, key) del obj[key] 删除元素。
class MyRange:
    def __init__(self, start, end):
        self._data = list(range(start, end))

    def __len__(self):
        return len(self._data)

    def __getitem__(self, key):
        print(f"🔍 获取数据,key 类型: {type(key)}")
        
        # 情况 A: 如果是切片对象 (例如 [1:3])
        if isinstance(key, slice):
            print("   -> 检测到切片操作")
            # 直接利用内部列表的切片功能返回新列表,或者返回一个新的 MyRange 对象
            return self._data[key] 
        
        # 情况 B: 如果是整数索引 (例如 [0])
        elif isinstance(key, int):
            print("   -> 检测到索引操作")
            if key < 0:
                key += len(self) # 支持负数索引
            if 0 <= key < len(self):
                return self._data[key]
            raise IndexError("索引超出范围")
        
        else:
            raise TypeError("无效的索引类型")

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]

    def __repr__(self):
        return f"MyRange({self._data})"

# --- 测试 ---
mr = MyRange(10, 20) # [10, 11, ..., 19]

print(len(mr))       # 10
print(mr[0])         # 10 (触发 __getitem__, key=0)
print(mr[-1])        # 19 (触发 __getitem__, key=-1)
print(mr[1:4])       # [11, 12, 13] (触发 __getitem__, key=slice(1,4,None))

mr[0] = 99           # 修改第一个值
print(mr[0])         # 99

del mr[1]            # 删除第二个值
print(len(mr))       # 9

7.4.9 属性相关

这四个方法是属性拦截的基石

方法 触发时机 作用 典型场景
__getattr__(self, name) 访问不存在的属性时 当正常查找(包括 __dict__ 和类属性)都失败后,最后调用此方法。 实现动态属性、默认值、API 代理。
__getattribute__(self, name) 访问任何属性时 无条件拦截所有属性访问。优先级最高。 强制日志记录、权限控制、调试。慎用,易导致死循环。
__setattr__(self, name, value) 赋值任何属性时 拦截所有 obj.name = value 操作。 数据验证、类型检查、只读属性模拟。
__delattr__(self, name) 删除任何属性时 拦截 del obj.name 操作。 防止删除关键属性、清理关联资源。
class Config:
    def __init__(self):
        self._data = {"timeout": 30, "retries": 3}

    def __getattr__(self, name):
        # 只有当 _data 里没有这个 key,且实例字典里也没有时才会触发
        print(f"⚠️ 属性 '{name}' 不存在,尝试动态获取...")
        
        # 模拟从环境变量或远程配置获取
        if name.startswith("env_"):
            return f"DynamicValue_{name}"
        
        # 如果真的找不到,抛出标准异常
        raise AttributeError(f"配置项 '{name}' 未找到")

cfg = Config()
#print(cfg.timeout)   # 30 (正常查找,不触发 __getattr__)
print(cfg.env_api)   # 触发 __getattr__ -> 输出: DynamicValue_env_api
# print(cfg.unknown) # 触发 __getattr__ -> 抛出 AttributeError

警告:由于它拦截所有访问(包括 self.__dict__),使用时必须非常小心,通常直接调用 super() 来避免破坏内部机制

class LoggedObject:
    def __getattribute__(self, name):
        # 避免拦截内部特殊方法导致死循环 (如 __class__, __dict__)
        if name.startswith('_'):
            return super().__getattribute__(name)
        
        print(f"📝 [LOG] 用户访问了属性: {name}")
        return super().__getattribute__(name)

obj = LoggedObject()
obj.x = 10
print(obj.x) 
# 输出: 📝 [LOG] 用户访问了属性: x
# 输出: 10
方法 所在类 触发时机
__get__(self, instance, owner) 描述符类 当通过实例或类访问该属性时
__set__(self, instance, value) 描述符类 当通过实例赋值该属性时
__delete__(self, instance) 描述符类 当删除该属性时
class TypedProperty:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.name} 必须是 {self.expected_type.__name__}")
        instance.__dict__[self.name] = value

class Product:
    # 复用 TypedProperty,无需在 Product 中写 __setattr__
    name = TypedProperty("name", str)
    price = TypedProperty("price", float)
    count = TypedProperty("count", int)

p = Product()
p.name = "Apple"      # ✅
p.price = 1.5         # ✅
p.price = "expensive" # ❌ TypeError
# p.count = 1.5       # ❌ TypeError

7.4.10 迭代器

概念 英文 必须实现的方法 作用 例子
可迭代对象 Iterable __iter__(self) 返回一个迭代器对象。可以被 for 循环遍历。 list, str, dict, 自定义容器
迭代器 Iterator __iter__(self)
__next__(self)
记录当前状态,每次调用 __next__ 返回下一个值。当没有值时抛出 StopIteration list_iterator, file object, 生成器
class Fibonacci:
    def __init__(self, max_count):
        self.max_count = max_count
        self.count = 0
        self.a, self.b = 0, 1

    # 1. 实现 __iter__ (让它成为 Iterable)
    # 对于迭代器来说,__iter__ 通常直接返回 self
    def __iter__(self):
        return self

    # 2. 实现 __next__ (让它成为 Iterator)
    def __next__(self):
        if self.count >= self.max_count:
            # 🛑 关键:没有更多数据时,必须抛出 StopIteration
            raise StopIteration
        
        # 计算当前值
        result = self.a
        
        # 更新状态 (准备下一次调用)
        self.a, self.b = self.b, self.a + self.b
        self.count += 1
        
        return result

# --- 测试 ---
fib = Fibonacci(5)

# 方式 A: 使用 for 循环 (自动调用 __iter__ 和 __next__)
print("For 循环:")
for num in fib:
    print(num, end=" ") 
# 输出: 0 1 1 2 3

# 方式 B: 手动模拟 for 循环 (深入理解原理)
print("\n\n手动模拟:")
fib2 = Fibonacci(3)
it = iter(fib2)  # 调用 __iter__()
try:
    while True:
        val = next(it)  # 调用 __next__()
        print(val, end=" ")
except StopIteration:
    print("\n-> 遍历结束")

7.4.11 数值转换相关

class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = amount
        self.currency = currency

    def __int__(self):
        # 强制转为整数:向下取整,丢弃小数部分
        return int(self.amount)

    def __float__(self):
        # 强制转为浮点:保留精度
        return float(self.amount)

    def __complex__(self):
        # 转为复数:实部为金额,虚部为0 (或者你可以定义其他逻辑)
        return complex(self.amount, 0)

    def __repr__(self):
        return f"{self.amount} {self.currency}"

m = Money(19.95)

print(int(m))      # 19 (触发 __int__)
print(float(m))    # 19.95 (触发 __float__)
print(complex(m))  # (19.95+0j) (触发 __complex__)

# ✅ 实际用途:作为列表索引
items = ["apple", "banana", "cherry", "date"]
# 假设我们要买第 19.95 个物品?通常取整
index = int(m) % len(items) 
print(f"购买物品: {items[index]}") 

7.5 元类

元类(Metaclass) 是“创建类的类”。如果说类是对象的模板,那么元类就是类的模板。
默认情况下,Python 中所有的类都是由内置的 type 元类创建的。元类允许你在类被创建时(而不是实例化时)拦截并修改类的定义行为,例如自动注册子类、强制命名规范、自动添加方法或属性等在 Python 中,一切皆对象,类本身也是对象。
普通对象:由类创建(实例化)。
类:由元类创建。
默认元类:type

class MyClass:pass
#上面的定义相当于下面的定义,第一个参数类名,第二个参数父类元组,第三个参数父类字典
MyClass = type('MyClass', (), {})

总结
定义:元类是创建类的类,默认是 type。
入口:通过 class MyClass(metaclass=MyMeta): 指定。
核心方法:__new__ (修改类结构), __init__ (初始化类), __call__ (控制实例化)。
用途:框架开发、ORM 映射、API 自动注册、强制编码规范、单例模式等高级架构场景。
建议:除非你在编写框架或库,且类装饰器无法满足需求,否则尽量避免使用元类,以保持代码的可读性。

7.5.1 自定义元类

要创建自定义元类,通常需要继承 type 类,并重写其 __new__ 或 __init__ 方法。

__new__:在类创建之前调用,用于修改类的结构(如添加/删除属性)。它必须返回一个新的类对象。
__init__:在类创建之后调用,用于初始化类对象

class MyMeta(type):
    def __new__(mcs, name, bases, attrs):
        # mcs: 元类本身 (MyMeta)
        # name: 正在创建的类名 (字符串)
        # bases: 父类元组
        # attrs: 类属性字典
        
        print(f"正在创建类: {name}")
        
        # 示例:强制所有方法名必须是小写,否则报错
        for key, value in list(attrs.items()):
            if callable(value) and not key.startswith('__') and key != key.lower():
                raise TypeError(f"方法名 {key} 必须全小写")
      
        
        # 示例:自动添加一个属性
        attrs['created_by'] = 'MyMeta'
        
        return super().__new__(mcs, name, bases, attrs)
        

# 使用元类 (通过 metaclass 参数指定)
class MyClass(metaclass=MyMeta):
    def Hello(self):  # 这会触发错误,因为方法名不是全小写
        pass

7.5.2 元类实现单例

class SingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        print(cls.__name__)# 输出Database
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Database(metaclass=SingletonMeta):
    def __init__(self):
        self.connection = "Connected"

db1 = Database()
db2 = Database()
print(db1 is db2)  # 输出: True

7.5 3 自动注册插件

registry = {}

class PluginMeta(type):
    def __new__(mcs, name, bases, attrs):
        cls = super().__new__(mcs, name, bases, attrs)
        if name != 'BasePlugin':  # 不注册基类
            registry[name] = cls
        return cls

class BasePlugin(metaclass=PluginMeta):
    pass

class EmailPlugin(BasePlugin):
    pass

class SmsPlugin(BasePlugin):
    pass

print(registry) 
# 输出: {'EmailPlugin': <class '__main__.EmailPlugin'>, 'SmsPlugin': <class '__main__.SmsPlugin'>}

7.5.4 接口检查

class RequiredMethodsMeta(type):
    required_methods = ['process', 'save']
    
    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        # 跳过基类检查
        if any(base.__name__ == 'BaseHandler' for base in bases):
            return
            
        for method in cls.required_methods:
            if not hasattr(cls, method) or not callable(getattr(cls, method)):
                raise TypeError(f"类 {name} 必须实现方法: {method}")

class BaseHandler(metaclass=RequiredMethodsMeta):
    pass

# 正确
class GoodHandler(BaseHandler):
    def process(self): pass
    def save(self): pass

# 错误:定义时直接报错
# class BadHandler(BaseHandler):
#     def process(self): pass
#     # 缺少 save 方法

7.6 类型提示

       弱类型语言灵活(AI会说python是动态类型语言,不是弱类型,相对的是静态类型语言),代价是维护性不好。主要体现在可读,类型检查、代码提示等方面。所以python也想有强类型的优势,但是本身不是强类型,所以搞了个类型提示,用起来自然不太好使。

7.6.1 类型提示发展史

  1. Python 3.5 (2015年)

    • 类型提示诞生:通过 PEP 484 正式引入了类型提示语法,允许开发者为函数参数和返回值添加类型注解。
    • typing 模块出现:与类型提示一同引入的,还有 typing 模块。这个模块不仅提供了 ListDictOptional 等基础类型,也首次引入了泛型的核心概念,比如 TypeVar 和 Generic
  2. Python 3.9 (2020年)

    • 内置的容器类型(如 listdict)可以直接作为泛型使用,例如 list[str],无需再从 typing 模块导入 ListDict
  3. Python 3.10 (2021年)

    • 引入了 | 操作符来表示联合类型(如 int | str),使类型提示的语法更加简洁。
  4. Python 3.12 (2023年)

    • 通过 PEP 695 引入了全新的、更简洁的泛型语法,允许使用 class Box[T]: 这样的方式定义泛型类。

7.6.2 无类型缺点


class TestOne:
    def test(self):
        return 1

#这个函数可能是在某ff.py文件
#单看这个方法 声明  test5(obj) ,你不知道有没有返回值,如果有返回值是什么
#这个参数是什么。大体只知道是一个对象,不看代码体,你是不知道要传的对象需要具有什么限制
#即使看了代码体,你也只知道对象参数只需要有test方法就行。但是这个方法想要传参是TestOne,还是TestTwo的对象?限制不了
#很多方法的做作用,一般是看入参和返回值,很多时候不需要文档说明,静态类型的语言带有自己解释性,还能做强限制
def test5(testOneObj):
    testOneObj.test()


oneObj = TestOne()
test5(oneObj)

7.6.3 类型提示

就是做提示的作用,运行前不会检查,鸭式辨型

from typing import Protocol

class TestOne:
    def test(self) -> int:  # 提示 test 方法返回整数
        return 1

class TestTwo:
    def test(self) -> int:  # 提示 test 方法返回整数
        return 2
    
class TestThree:
    def testff(self) -> int:  return # 没有test方法,没有返回也不报错(None)
    
# 定义一个协议(接口),表示任何有 test 方法的对象都符合这个协议,更像个接口
class HasTestMethod(Protocol):
    def test(self) -> int: ...

def test5(testOneObj: TestOne) -> int:  # 提示参数必须是 TestOne 类型,返回值int
    return testOneObj.test()

# 使用协议作为类型提示,增加了代码的灵活性
def test6(testOneObj: HasTestMethod) -> None:
    testOneObj.test()

obj = TestOne()
print(test5(obj))
obj = TestTwo()
#test5 编写者是想传TestOne实例的,结果TestTwo的也行,当然test5在代码里面自己写代码校验类型,这是另外一回事了
print(test5(obj))
#
print(test6(obj))
obj = TestThree()
#这个也是不会检查错误,在运行时才会报错,不知道IDE,有没有相关设置,不要太依赖这个特性,毕竟用这个做开发的
#都是喜欢短、频繁、块,维护代价这玩意不重要。写多了的时候才觉得有必要,已经晚了,还不如摆烂好。
# 所以控制好规模和规范就好了,维护好说明文档,也能一定程度上解决
print(test6(obj))

7.7 泛型

       Python 的泛型语法经历了一次重大的演进,以 Python 3.12 为分界线,分为现代语法(推荐)和传统语法(兼容旧版本)。简单来说,泛型允许你编写“可复用”的代码,让函数或类能够处理多种数据类型,同时保留类型检查的严谨性(比如让 IDE 知道输入是 int,输出也一定是 int)

7.7.1 Python 3.12+ 现代语法 (推荐)

        PEP 695 引入了全新的语法糖,让 Python 的泛型写法终于像 Java、C++ 或 TypeScript 那样简洁自然。无需导入 TypeVar 或 Generic。直接在类或函数名后使用 [T] 定义类型参数。

泛型函数

# 直接在函数名后定义 [T]
def echo[T](x: T) -> T:
    return x

# 使用
val = echo(10)  # T 被推断为 int

泛型类

# 直接在类名后定义 [T]
class Box[T]:
    def __init__(self, value: T):
        self.value = value
    
    def get(self) -> T:
        return self.value

# 使用
box = Box[int](42)

7.7.2 Python 3.5 - 3.11 传统语法

泛型函数

from typing import TypeVar

# 1. 定义类型变量 T
T = TypeVar('T')

# 2. 在函数签名中使用
def echo(x: T) -> T:
    return x

泛型类

from typing import Generic, TypeVar

T = TypeVar('T')

# 1. 继承 Generic[T]
class Box(Generic[T]):
    def __init__(self, value: T):
        self.value = value
    
    def get(self) -> T:
        return self.value

# 使用
box = Box[int](42)

8 执行环境

8.1 可调用对象

“可调用对象”指的是任何可以像函数一样被“调用”(即使用 () 操作符执行)的对象。

常见的可调用对象包括

1 内置函数:如 len(), print(), max()。
2 用户定义函数:使用 def 或 lambda 定义的函数。
3 类:类本身是可调用的,调用时会触发 __new__ 和 __init__ 来创建实例。
4 类的实例:如果类定义了 __call__ 方法,其实例也可以像函数一样被调用。
5 方法:绑定到实例或类的方法。
6 其他实现了 __call__ 的对象

8.1.1 实例

# 1. 普通函数
def my_func(x):
    return x * 2

# 2. Lambda 表达式
my_lambda = lambda x: x * 2

# 3. 类 (调用类会创建实例)
class MyClass:
    def __init__(self, value):
        self.value = value

# 4. 实现了 __call__ 的实例 (函数式对象)
class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, x):
        return x * self.factor

    def test():pass
double = Multiplier(2)

# 测试调用
print(my_func(5))      # 输出: 10
print(my_lambda(5))    # 输出: 10
obj = MyClass(10)      # 调用类
print(double(5))       # 输出: 10 (实例像函数一样被调用)

# 检查是否可调用
print(callable(my_func))   # True
print(callable(double))    # True
print(callable(obj))       # False (除非 MyClass 定义了 __call__)

print(type(my_func)) # <class 'function'>
print(type(my_lambda)) #<class 'function'> 早期版本可能是 <type 'function'>
print(type(dict())) # <class 'dict'>
print(type([].append)) #<class 'builtin_function_or_method'>
print(type(Multiplier.test)) #<class 'function'>

8.2 代码对象

在 Python 中,“代码对象”(Code Object)是一个底层且核心的概念。是 types.CodeType 的实例。它包含了函数执行所需的所有静态信息,但不包含运行时的状态(如局部变量的值。
它是 编译后的字节码(Bytecode)的容器,是 Python 函数、类、模块甚至 lambda 表达式在内存中的真实表现形式。
当你编写 Python 代码时,解释器会经历以下步骤:
源码 (Source Code) .py 文件。
编译 (Compile) -> 生成 代码对象 (Code Object)。
执行 (Execute) -> Python 虚拟机 (PVM) 读取代码对象中的字节码并运行

8.2.1 代码对象包含信息

  • co_code: 实际的字节码指令序列(二进制数据)。
  • co_consts: 代码中使用的常量池(如数字、字符串、None)。
  • co_names: 代码中引用的全局变量名或属性名。
  • co_varnames: 局部变量名(包括参数)。
  • co_filename: 源代码文件名。
  • co_name: 代码块的名字(通常是函数名)。
  • co_firstlineno: 函数定义所在的起始行号。
  • co_argcount: 位置参数的数量
def my_func(x, y):
    z = x + y
    return z * 2

# 获取代码对象
code_obj = my_func.__code__

print(type(code_obj)) # 输出: <class 'code'>
# 查看关键属性
print(f"函数名: {code_obj.co_name}")
print(f"文件名: {code_obj.co_filename}")
print(f"局部变量: {code_obj.co_varnames}")
print(f"常量池: {code_obj.co_consts}") # None 是隐式的,2 是代码中的常数
print(f"字节码长度: {len(code_obj.co_code)} bytes")

output='''
<class 'code'>
函数名: my_func
文件名: F:\Users\admin2\AppData\Local\Temp\ipykernel_9620\990376298.py
局部变量: ('x', 'y', 'z')
常量池: (None, 2)
字节码长度: 20 bytes
'''

8.2.2 代码对象与函数对象区别

函数对象 (function): 是高级抽象。它包裹了代码对象,并添加了运行时上下文,如全局命名空间 (__globals__)、默认参数值、闭包变量 (__closure__)、注解等。
代码对象 (code): 是低级抽象。它只包含“怎么做”(指令),不包含“在哪里做”(环境)或“初始数据是什么”(默认值

函数对象 (func)
  ├── __code__  --->  [ 代码对象 (Code Object) ] (包含字节码 co_code)
  ├── __globals__ --->  { 'x': 1, ... } (全局作用域)
  ├── __defaults__ ---> (10, 20) (默认参数)
  └── __closure__ ---> (...) (闭包变量)

你可以用同一个代码对象创建多个不同的函数对象(虽然通常不这么做)

import types

def original(a):
    return a + 1

# 提取代码对象
code = original.__code__

# 用同一个代码对象创建一个新函数,但指定不同的全局环境
new_globals = {'__builtins__': __builtins__}
new_func = types.FunctionType(code, new_globals, "new_func_name")

print(new_func(5)) # 输出: 6
print(new_func.__name__) # 输出: new_func_name

8.2.2 反编译代码对象

import dis

def add_and_mul(x, y):
    return (x + y) * 2

# 直接反汇编函数(实际上是反汇编它的 __code__)
dis.dis(add_and_mul)

output='''
  3           RESUME                   0        #用于标记生成器、协程或普通函数的入口点,主要用于调试和异常处理追踪,替代了旧版本中的 GEN_START 或隐式逻辑 

  4           LOAD_FAST_LOAD_FAST      1 (x, y) #加载两个局部变量
              BINARY_OP                0 (+)    #执行加法操作。0 代表 + 运算符
              LOAD_CONST               1 (2)    #将常量 2 压入栈中
              BINARY_OP                5 (*)    #执行乘法
              RETURN_VALUE                      #返回结果
'''

8.2.3 代码对象不可变

代码对象一旦创建,就是不可变的。你不能直接修改某个函数的字节码。如果你想“修改”代码逻辑,必须创建一个新的代码对象(通常通过重新编译源码或使用 replace 方法生成新对象),然后替换函数的 __code__ 属性。

# 修改代码对象的一个属性(例如改变文件名记录,虽然很少见)
# 注意:不能直接修改 co_code,因为它是不可变的,但可以用 replace 创建副本
new_code = my_func.__code__.replace(co_name="modified_name")

# 将新代码对象赋回给函数
my_func.__code__ = new_code

print(my_func.__code__.co_name) # 输出: modified_name

8.3 compile函数

compile() 是 Python 的内置函数,用于将源代码字符串动态编译成代码对象(Code Object)
它是连接“源码文本”和“可执行代码”的桥梁。通常我们运行 .py 文件时,Python 解释器会在后台自动调用类似 compile 的过程;而当你需要在运行时动态生成并执行代码(例如实现脚本引擎、评估用户输入、元编程)时,就需要手动使用它。

8.3.1 方法签名

compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)

核心参数详解:
1 source (str | bytes | AST):
要编译的源代码字符串。
也可以是 ast.AST 对象(如果你先解析成了抽象语法树)。

2 filename (str):
代码来源的文件名。
作用:主要用于报错信息(Traceback)。如果代码出错,错误信息会显示这个文件名。
如果不是从文件读取,通常传 '<string>' 或 '<stdin>'。

3 mode (str) [最重要]:
指定代码的类型,必须是以下三者之一:
'exec': 用于编译一系列语句(如完整的脚本、函数定义、类定义)。返回值是一个可以执行的代码对象。
'eval': 用于编译单个表达式(如 1 + 2, x * y)。返回值是一个表达式对象,执行后返回计算结果。
'single': 用于编译单个交互式语句(类似 Python REPL 的行为)。如果结果是表达式且不为 None,会自动打印结果。

4 flags, dont_inherit, optimize:
高级选项,用于控制编译器行为(如开启优化、未来特性 from __future__ import ... 等),通常保持默认即可。

场景 A:编译并执行一段脚本 (mode='exec')

source_code = """
def greet(name):
    return f"Hello, {name}!"

result = greet("World")
print(result)
"""

# 1. 编译
code_obj = compile(source_code, filename='<dynamic_script>', mode='exec')

# 检查类型
print(type(code_obj))  # <class 'code'>

# 2. 执行 (需要提供一个命名空间字典)
global_ns = {}
exec(code_obj, global_ns)

# 现在 global_ns 中有了 greet 函数和 result 变量
print(global_ns['greet']("Python"))  # 输出: Hello, Python!

场景 B:计算表达式的值 (mode='eval')

当你只需要计算一个数学公式或逻辑表达式,而不需要定义函数或变量时使用

expr = "2 * (3 + 5) ** 2"

# 1. 编译为表达式
code_obj = compile(expr, filename='<expression>', mode='eval')

# 2. 求值 (使用 eval() 函数,而不是 exec())
result = eval(code_obj)
print(result)  # 输出: 128

# 也可以传入变量上下文
x = 10
expr_with_var = "x ** 2 + 5"
code_obj2 = compile(expr_with_var, '<var_expr>', 'eval')
print(eval(code_obj2, {"x": x}))  # 输出: 105

场景 C:模拟交互式命令行 (mode='single')

这种模式常用于实现自定义的 REPL(读取-求值-输出循环)。

# 在交互模式下,输入表达式会自动打印结果,输入赋值则不打印
code1 = compile("10 + 20", '<input>', 'single')
exec(code1) 
# 输出: 30 (自动打印了表达式的结果)

code2 = compile("a = 100", '<input>', 'single')
exec(code2)
# 无输出 (赋值语句不打印)

8.3.2 核心价值

1 性能优化:
如果你需要重复执行同一段动态生成的代码字符串,先 compile 再 exec/eval 比直接 exec/eval 字符串要快。因为编译过程(词法分析、语法分析、生成字节码)只做了一次,后续执行直接运行字节码。
2 安全性控制(部分):
虽然 eval/exec 本身有风险,但配合 compile 可以做些预处理。例如,你可以先解析成 AST,检查是否有危险操作(如 __import__, open),确认安全后再 compile 执行。
(注意:仅仅使用 compile 并不能阻止恶意代码执行,必须配合沙箱或白名单机制)
3 动态代码生成:
许多框架(如 Django ORM, SQLAlchemy, 模板引擎)会在运行时根据配置动态构建代码字符串,然后编译成代码对象以提高运行效率。

8.4 eval

是 Python 的内置函数,用于动态执行一个字符串形式的 Python 表达式,并返回表达式的计算结果。它的核心特点是:只接受单个表达式(不能包含语句,如 if, for, def 等),且执行结果有返回值

基本语法

eval(source, globals=None, locals=None)

  • source (必填): 一个字符串,或者编译后的代码对象(必须是表达式)。
  • globals (可选): 一个字典,指定全局命名空间。如果提供,必须是字典类型。
  • locals (可选): 一个映射对象(通常是字典),指定局部命名空间

8.4.1 用法举例

result = eval("1 + 2 * 3")
print(result)  # 输出: 7

x = 10
y = 5
# eval 可以访问当前作用域中的变量
result = eval("x + y * 2")
print(result)  # 输出: 20

def add(a, b):
    return a + b

# 可以在字符串中调用已定义的函数
result = eval("add(3, 4)")
print(result)  # 输出: 7

s = "[1, 2, 3]"
lst = eval(s)
print(lst)      # 输出: [1, 2, 3]
print(type(lst)) # 输出: <class 'list'>


# 只允许访问 x 和 y,不允许访问 z 或内置函数
allowed_globals = {"x": x, "y": y}

try:
    # 这里会报错,因为 z 不在 allowed_globals 中
    res = eval("x + y + z", allowed_globals)
except NameError as e:
    print(f"错误: {e}") 

# 正常执行
res = eval("x + y", allowed_globals)
print(res)  # 输出: 300

8.4.2 安全问题

永远不要对不可信的用户输入直接使用 eval()

如果用户输入的字符串包含恶意代码,eval() 会原样执行,可能导致:

  • 删除文件 (eval("os.system('rm -rf /')"))
  • 窃取敏感数据
  • 执行任意系统命令
  • 无限循环导致拒绝服务

1 ast.literal_eval(): 如果你只需要解析字符串形式的 Python 字面量(如数字、字符串、列表、字典、元组、布尔值、None),务必使用这个。它比 eval 安全得多,因为它只解析字面量,不执行代码。

2 专门的数学解析库: 如果需要复杂的数学计算,使用 sympy, numexpr 或自己写一个简单的解析器,而不是直接用 eval。
3 严格的沙箱: 如果必须用 eval 处理动态逻辑,必须配合极其严格的 globals/locals 限制(如上文的 {"__builtins__": {}}),但即便如此,仍有绕过风险。

完全沙箱化 eval 非常困难且容易出错。外部不可性输入风险极高。

8.5 exec

exec() 是 Python 的内置函数,用于动态执行存储在字符串或代码对象中的 Python 代码

基本语法

exec(object[, globals[, locals]])

  • object: 要执行的代码。可以是字符串,也可以是代码对象(由 compile() 生成)。
  • globals (可选): 一个字典,作为代码执行时的全局命名空间
  • locals (可选): 一个映射对象(通常是字典),作为代码执行时的局部命名空间

8.5.1 基本用法

用法 A:执行字符串代码(最简单

code = """
x = 10
y = 20
print(f"Sum: {x + y}")

def greet(name):
    return f"Hello, {name}"

message = greet("World")
"""

# 执行代码
exec(code) 
# 输出: Sum: 30

# 注意:exec 返回 None
result = exec(code)
print(result) # 输出: None

# 定义的变量和函数会进入当前的局部作用域
print(message) # 输出: Hello, World
print(greet("Python")) # 输出: Hello, Python

用法 B:控制命名空间(推荐做法)

code = """
a = 100
b = 200
result = a + b
"""

# 创建一个空字典作为全局命名空间
my_globals = {}

# 在 my_globals 中执行代码
exec(code, my_globals)

# 当前作用域没有 a, b, result
# print(a)  # 报错: NameError

# 但它们存在于 my_globals 字典中
print(my_globals['result']) # 输出: 300
print(my_globals['a']) # 输出: 300
print(my_globals.keys())    # 输出: dict_keys(['__builtins__', 'a', 'b', 'result'])

用法 C:配合 compile() 使用(高性能)

如果你需要重复执行同一段代码,先编译再 exec 效率更高

source = "for i in range(3): print(i)"

# 1. 编译成代码对象
code_obj = compile(source, '<string>', 'exec')

# 2. 多次执行代码对象
exec(code_obj) 
# 输出: 0, 1, 2
exec(code_obj) 
# 输出: 0, 1, 2

用法 D:动态定义函数或类

这是 exec 最强大的地方,可以在运行时根据字符串生成新的类或函数。

class_code = """
class DynamicClass:
    def __init__(self, value):
        self.value = value
    
    def show(self):
        return f"Value is {self.value}"
"""

namespace = {}
exec(class_code, namespace)

# 从命名空间中获取生成的类
DynamicClass = namespace['DynamicClass']

obj = DynamicClass(42)
print(obj.show()) # 输出: Value is 42

8.5.2 globals 和 locals 的陷阱

在函数内部执行时,如果 locals 是一个普通的字典,代码中定义的局部变量可能不会更新到这个字典中(取决于 Python 版本和优化级别)。
最佳实践:如果你需要读取执行后的变量,通常只传一个字典给 globals 参数,并让 locals 默认为与 globals 相同,或者干脆不传 locals

# 安全且通用的写法
namespace = {}
exec("y = 20", namespace) 
print(namespace['y']) # 可靠地输出 20

8.5.3 文件参数

# script.py
x = 100
y = 200
result = x + y
print(f"计算结果: {result}")

def say_hello():
    return "Hello from file!"

# main.py
# 1. 打开文件对象 (使用 'r' 模式读取)
with open('script.py', 'r') as f:
    # 2. 直接将文件对象传给 exec
    exec(f)

# 3. 执行后,script.py 中的变量和函数现在存在于当前作用域中
print(result)       # 输出: 300
print(say_hello())  # 输出: Hello from file!

可以使用f.seek,f.tell等文件方法

8.5.4 安全问题

有类似eval安全问题

8.5.5 与eval对比

特性 exec() eval()
输入类型 语句块 (Statements) 表达式 (Expression)
示例内容 if..., def..., x=1, import os 1+1, "hi", func(x)
返回值 总是 None 表达式的计算结果
主要用途 动态生成类/函数、运行脚本片段 动态计算数值、解析配置公式
安全性 极高危 (可执行任意操作) 高危 (但仍受限于表达式)

8.6 input

       由于 input() 总是返回字符串,如果你需要数字进行计算,必须手动转换类型,否则会报错或得到奇怪的结果(例如字符串拼接)

相当于raw_input和eval

# ❌ 错误示范:直接相加会变成字符串拼接
age_str = input("请输入年龄: ")  # 用户输入 18
#next_year = age_str + 1       # 报错: TypeError (不能把 str 和 int 相加)
# 或者如果是两个输入: "1" + "1" = "11" 而不是 2

# ✅ 正确示范:强制类型转换
age = int(input("请输入年龄: "))      # 转换为整数
height = float(input("请输入身高: ")) # 转换为浮点数

next_year_age = age + 1
print(f"明年你将是 {next_year_age} 岁")

8.7 os.system(不推荐)

用于在当前进程中启动一个子进程来执行系统命令(shell 命令)

import os

# Linux/Mac: 列出当前目录文件
# Windows: 可以使用 "dir"
ret = os.system("ls -l") 

if ret == 0:
    print("命令执行成功")
else:
    print(f"命令执行失败,退出码: {ret}")

8.8 os.fork(不推荐)

现代 Python 应用开发中,绝大多数情况下都不推荐直接使用它。
1 “一次调用,两次返回”:代码执行流突然分叉,需要写 if pid == 0: ... else: ...,逻辑容易混乱。
2 内存共享的陷阱:子进程复制了父进程的所有变量,修改数据时容易让人困惑(是改了自己的副本还是共同的?)。
3 资源泄露风险:忘记 wait() 会产生僵尸进程;文件描述符继承可能导致锁死或数据错乱。
4  多线程灾难:在多线程程序中 fork 极易导致死锁(只复制了当前线程,其他线程持有的锁在子进程中永远无法释放)。
5 跨平台不可用:Windows 直接报错,代码无法移植。

8.9 subprocess

官方唯一标准:它是 Python 标准库的一部分,无需安装,跨所有平台(Windows, Linux, macOS)。
安全性最高:支持以列表形式传递参数(['ls', '-l']),彻底杜绝 Shell 注入攻击(这是 os.system 的最大痛点)。
功能最全:
2 同时捕获 stdout 和 stderr。
2 设置超时时间 (timeout),防止命令卡死。
3 处理输入输出流 (stdin, stdout, PIPE)。
4 获取精确的退出码 (returncode)。
5 灵活性强:既可以用简单的 run() 一键执行,也可以用 Popen() 进行复杂的流式控制

8.9.1 基本执行

import subprocess

try:
    # check=True: 如果命令返回非零退出码,自动抛出 CalledProcessError
    # capture_output=True: 捕获 stdout 和 stderr
    # text=True: 返回字符串而不是字节 (bytes)
    result = subprocess.run(
        ["git", "status"], 
        check=True, 
        capture_output=True, 
        text=True
    )
    print("输出:\n", result.stdout)
except subprocess.CalledProcessError as e:
    print(f"命令执行失败: {e}")
    print("错误信息:", e.stderr)

8.9.2 超时设置

import subprocess

try:
    # timeout=5: 5秒没跑完就杀掉进程并抛出 TimeoutExpired
    result = subprocess.run(
        ["ping", "-c", "10", "google.com"], 
        timeout=5, 
        capture_output=True, 
        text=True
    )
except subprocess.TimeoutExpired:
    print("命令执行超时!")

8.9.3 实时查看输出

import subprocess

# 逐行读取输出
proc = subprocess.Popen(
    ["ping", "-c", "5", "8.8.8.8"], 
    stdout=subprocess.PIPE, 
    stderr=subprocess.STDOUT, 
    text=True, 
    bufsize=1 # 行缓冲
)

for line in proc.stdout:
    print(f"实时日志: {line}", end='')

proc.wait()

8.9.4 安全问题

# 错误写法
# 如果 user_input 是 "; rm -rf /",整个系统可能被清空!
cmd = f"ls -l {user_input}" 
subprocess.run(cmd, shell=True) # 极度危险!

# 正确写法
# 参数作为列表传递,shell 不会解析特殊字符
subprocess.run(["ls", "-l", user_input]) 

8.10 结束执行

8.10.1 sys.exit

这是官方推荐的用于“主动终止程序”的方法。

  • 机制:抛出 SystemExit 异常。如果这个异常没有被捕获,解释器就会退出。
  • 参数
    • 0 或 None:表示成功退出。
    • 非零整数:表示出错退出(Shell 中可以通过 $? 获取该状态码)。
    • 字符串/对象:会将内容打印到 stderr 并作为错误退出。
  • 优点
    • 允许清理:因为它本质是异常,所以会被 try...finally 捕获,确保资源被清理。
    • 可被拦截:在测试或特殊框架中,可以捕获 SystemExit 来阻止程序真的退出。
import sys
error_condition=1
try:
    if error_condition:
        print("发生错误,准备退出")
        sys.exit(1)  # 抛出异常
finally:
    print("这里一定会执行,进行清理工作")

8.10.2  os.exit(n)

这是底层级的“核按钮”,通常只在子进程中使用。

  • 机制:直接调用操作系统的 _exit 系统调用。不抛异常,不执行任何清理
  • 特点
    • ❌ 不执行 finally 块。
    • ❌ 不执行 atexit 钩子。
    • ❌ 不刷新 缓冲区(可能导致 print 的内容丢失)。
    • ❌ 不关闭 打开的文件。
  • 适用场景
    • 多进程 (os.fork) 的子进程中:防止子进程的清理逻辑干扰父进程(例如刷新父进程的文件缓冲区导致数据重复写入)。
    • 程序已经严重损坏(如死锁、内存破坏),无法执行正常的清理逻辑时
import os

pid = os.fork()
if pid == 0:
    # 子进程
    do_work()
    os._exit(0)  # 必须用这个,不能用 sys.exit
else:
    # 父进程
    os.wait()

注意os.fork不推荐使用

8.10.3 操作系统信号 (外部终止)

由外部发送信号给进程。

  • 方式
    • Ctrl + C:发送 SIGINT 信号(默认行为是退出)。
    • kill <pid>:发送 SIGTERM (默认) 或 SIGKILL (-9)。
  • Python 中的处理
    • 可以使用 signal 模块捕获这些信号进行优雅退出
import signal
import sys

def handler(sig, frame):
    print("\n收到中断信号,正在保存数据...")
    save_data()
    sys.exit(0)

signal.signal(signal.SIGINT, handler)
while True:
    pass

8.10.3 抛出未捕获异常

让程序因为错误而崩溃。

  • 机制:抛出异常且未被 try...except 捕获,解释器打印堆栈跟踪 (Traceback) 后退出。
  • 特点
    • 退出码通常为 1。
    • 会打印详细的错误信息。
    • finally 块依然会执行
  • 评价:这是错误处理的结果,而不是控制流程的手段。不要故意写 raise Exception("Exit") 来退出程序,这会让日志充满噪音。

8.11 python命令参数

基本格式:

python [解释器选项] 脚本文件.py [脚本参数]

8.11.1 基本格式(相当重要)

参数 全称 作用 典型场景
-m --module 以模块方式运行。告诉 Python 去 sys.path 里找模块名而不是文件。 python -m pip install ...
python -m http.server
python -m unittest test.py
-u --unbuffered 强制标准输出/错误不缓冲。确保 print 的内容立即显示,不存缓冲区。 Docker 日志、CI/CD 流水线中防止日志丢失或延迟。
-O --optimize 优化模式。移除 assert 断言,移除 __debug__ 为真的代码块。生成 .pyo (旧版) 或不生成 .pyc 生产环境部署,稍微提升启动速度,减小体积。
-OO (双重优化) 深度优化。除了 -O 的功能外,还会丢弃 __doc__ 文档字符串。 极度精简的生产环境。
-B --dont-write-bytecode 不生成 .pyc 文件。阻止 Python 在 __pycache__ 目录下创建编译后的字节码文件。 只读文件系统、Docker 构建层优化、不想产生垃圾文件时。
-i --interactive 交互模式。脚本执行完毕后,不退出,直接进入 Python 交互式命令行 (REPL)。 调试脚本:运行完脚本后,检查变量状态 (python -i script.py)。
-c --command 执行字符串命令。直接运行引号内的 Python 代码,不需要文件。 python -c "print('Hello')"
python -c "import sys; print(sys.version)"
-V / -version --version 打印版本号。显示 Python 版本并退出。 检查环境:python -V
-h / -help --help 显示帮助。列出所有可用的解释器选项。 忘记参数时查询:python -h

注意:大型的python项目都是以模块形式组织,所以执行时要加m,不加参数是以脚本方式执行,模块查找要出问题

8.11.2 进阶选项

参数 作用 典型场景
-X 设置特定的实现选项。 python -X faulthandler (崩溃时打印堆栈)
python -X dev (开启开发模式,检查资源泄漏)
-W 控制警告行为 (Warning control)。 python -W error (将警告视为错误)
python -W ignore::DeprecationWarning (忽略特定警告)
-s 不添加用户站点包目录 (site-packages)。 隔离环境,确保不使用用户全局安装的包。
-E 忽略所有环境变量 (如 PYTHONPATH, PYTHONHOME)。 确保脚本在纯净环境下运行,不受用户配置干扰。
-S 不自动导入 site 模块。 极特殊的启动定制,普通用户很少用。
-b bytesstr 混合操作发出警告 (-b) 或报错 (-bb)。 迁移 Python 2 到 3 的代码,检查类型混合问题。

8.12 multiprocessing

8.12.1 Process

multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
参数名 类型 默认值 说明
group None None 保留参数。必须为 None,用于兼容 threading 模块,无实际作用。
target callable None 目标函数。这是子进程启动后要执行的可调用对象(函数)。
⚠️ 如果为 None,则必须重写 Process 类的 run() 方法。
name str None 进程名称。用于标识进程(显示在日志或调试工具中)。
默认为 Process-N(N 是递增整数)。
args tuple () 位置参数元组。传递给 target 函数的位置参数。
即使只有一个参数,也必须加逗号,例如 args=(1,)
kwargs dict {} 关键字参数字典。传递给 target 函数的关键字参数。
例如 kwargs={'x': 10, 'y': 20}
daemon bool None 守护进程标志。
- True: 主进程退出时,子进程会被强制终止。
- False: 主进程会等待子进程结束。
- None (默认): 继承父进程的 daemon 状态(通常主进程为 False)。

相关属性

import multiprocessing
import time
import os

def worker():
    print(f"[子进程] 开始运行。PID: {os.getpid()}, 名称: {multiprocessing.current_process().name}")
    time.sleep(2)
    print(f"[子进程] 准备退出。")
    # 可以通过 sys.exit(5) 设置特定的退出码

if __name__ == '__main__':
    # 1. 创建进程
    p = multiprocessing.Process(target=worker, name="MyWorker-01")
    
    # 2. 查看启动前的属性
    print(f"启动前 -> Name: {p.name}, PID: {p.pid}, Exitcode: {p.exitcode}, Daemon: {p.daemon}")
    
    # 3. 设置守护进程 (必须在 start 之前)
    p.daemon = True  # 如果设为 True,主程序结束它会立刻被杀
    
    # 4. 启动进程
    p.start()
    
    # 5. 查看启动后的属性
    print(f"启动后 -> Name: {p.name}, PID: {p.pid}, Exitcode: {p.exitcode}")
    
    # 6. 等待进程结束 (如果设置了 daemon=True,注释掉这行,主程序会直接结束并杀掉子进程)
    p.join()
    
    # 7. 查看结束后的属性
    print(f"结束后 -> Name: {p.name}, PID: {p.pid}, Exitcode: {p.exitcode}")
    
    print("主程序结束。")
from multiprocessing import Process
import os

def worker(name, value):
    print(f"进程 {name} (PID: {os.getpid()}) 开始工作,接收值: {value}")
    # 模拟耗时任务
    import time
    time.sleep(1)
    print(f"进程 {name} 完成")

if __name__ == '__main__':
    # Windows/macOS 必须加这个保护,否则会导致无限递归创建进程
    processes = []
    
    for i in range(3):
        # target: 目标函数, args: 参数元组
        p = Process(target=worker, args=(f"Worker-{i}", i * 10))
        p.start()
        processes.append(p)
    
    # 等待所有进程结束
    for p in processes:
        p.join()
        
    print("✅ 所有子进程结束")
  • if __name__ == '__main__'::在 Windows 和 macOS (spawn 模式) 上必须加这个判断。因为子进程会重新导入主模块,如果没有这个保护,子进程会再次执行创建进程的代码,导致无限循环爆炸。
  • 参数传递:通过 args 或 kwargs 传递数据。数据会被序列化 (pickle) 后复制给子进程(内存隔离)。

继承 Process 类

如果你的任务逻辑很复杂,需要维护内部状态,或者需要重写 runterminate 等行为,可以继承 Process 类。

适用场景

  • 任务逻辑封装在类中。
  • 需要访问类的属性或方法。
  • 类似 threading.Thread 的子类用法。
from multiprocessing import Process
import time

class MyWorker(Process):
    def __init__(self, task_id):
        super().__init__()
        self.task_id = task_id
    
    def run(self):
        # 这里是子进程执行的入口
        print(f"任务 {self.task_id} 在进程 {self.pid} 中运行")
        time.sleep(1)
        print(f"任务 {self.task_id} 完成")

if __name__ == '__main__':
    workers = []
    for i in range(3):
        p = MyWorker(i)
        p.start()
        workers.append(p)
    
    for p in workers:
        p.join()

8.12.2 Pool

如果你有一堆相似的任务(例如处理 1000 张图片、计算 10000 个数据),手动创建 1000 个进程会耗尽系统资源。Pool 可以限制最大并发进程数,并自动分配任务。

适用场景

  • Map-Reduce 风格:对列表中的每个元素执行相同函数。
  • 需要控制最大并发数(如限制为 CPU 核心数)。
  • 希望自动管理进程的生命周期
multiprocessing.Pool(
    processes=None, 
    initializer=None, 
    initargs=(), 
    maxtasksperchild=None, 
    context=None
)

构造器参数说明

参数名 类型 默认值 详细说明
processes int None 进程池中的工作进程数量。
- 如果为 None (默认),则使用 os.cpu_count() 返回的 CPU 核心数。
- 建议:通常设置为 CPU 核心数。如果是 I/O 密集型任务,可以适当调大;如果是纯 CPU 密集型,不要超过核心数太多,否则上下文切换会降低性能。
initializer callable None 初始化函数。每个工作进程启动时都会执行一次此函数。
常用于:
1. 初始化全局变量(因为子进程是复制内存,无法共享父进程的全局变量)。
2. 建立数据库连接或打开文件句柄(避免在父进程打开后直接传给子进程导致锁问题)。
initargs tuple () 传递给 initializer 函数的参数元组。
maxtasksperchild int None 每个工作进程执行的任务上限。
- None (默认):进程会一直存活,直到池关闭。
- 设置为整数 N:当某个进程执行完 N 个任务后,它会被终止并替换为一个新进程。
用途:防止内存泄漏。如果任务函数中有轻微的内存泄漏,定期重启进程可以释放内存。
context context None 指定用于创建池的上下文(如 'fork', 'spawn', 'forkserver')。通常保持默认即可,除非你有特定的跨平台兼容性需求。

pool方法

方法 阻塞/非阻塞 返回值 说明
map(func, iterable) 阻塞 list iterable 中的数据分块发送给进程,按输入顺序返回结果列表。如果其中一个任务报错,整个调用会抛出异常。
imap(func, iterable) 非阻塞 (迭代器) iterator 返回一个迭代器。结果按完成顺序(或接近顺序)产生,无需等待所有任务完成即可开始处理结果。节省内存。
imap_unordered(func, iterable) 非阻塞 (迭代器) iterator 类似 imap,但结果完全无序,哪个先做完哪个先返回。性能通常略高于 imap
apply_async(func, args) 非阻塞 AsyncResult 提交单个任务。立即返回一个 AsyncResult 对象。可以通过 .get() 获取结果(可设超时),或注册回调函数 .apply(callback)
map_async(func, iterable) 非阻塞 AsyncResult map 的异步版本。返回 AsyncResult,稍后调用 .get() 获取完整列表。
close() - None 告诉池不再接受新任务。调用后不能再提交任务,但已提交的任务会继续执行。
join() 阻塞 None 等待所有工作进程结束。必须在 close()terminate() 之后调用。
terminate() - None 强制停止所有工作进程,不等待任务完成。慎用,可能导致资源未释放。

代码示例

from multiprocessing import Pool, cpu_count
import time

def square(x):
    time.sleep(0.5)
    return x * x

if __name__ == '__main__':
    data = [1, 2, 3, 4, 5, 6, 7, 8]
    
    # 默认进程数 = CPU 核心数
    # 也可以指定:Pool(processes=4)
    with Pool(processes=cpu_count()) as pool:
        # 方法 A: map (阻塞,按顺序返回结果)
        # results = pool.map(square, data)
        
        # 方法 B: imap (迭代器,结果出来一个yield一个,节省内存)
        # results = list(pool.imap(square, data))
        
        # 方法 C: apply_async (异步,最灵活,适合不同参数或回调)
        async_results = [pool.apply_async(square, args=(x,)) for x in data]
        
        # 获取结果 (get() 会阻塞直到该任务完成)
        results = [r.get() for r in async_results]
        
    print(f"计算结果: {results}")

关键注意事项

  1. if __name__ == '__main__': 保护
    在 Windows 和 macOS 上,必须将 Pool 的创建和使用代码放在 if __name__ == '__main__': 块内。否则,子进程导入模块时会递归创建新的进程池,导致无限循环崩溃。

  2. 可序列化对象 (Picklability)
    传递给 Pool 的 funcargs 以及返回值都必须能被 pickle 序列化。

    • ❌ 不能传递:lambda 函数、局部定义的函数、类方法(在某些旧版本或特定配置下)、打开的文件句柄、锁对象。
    • ✅ 推荐:在模块顶层定义的普通函数。
  3. 进程数量选择

    • CPU 密集型processes = os.cpu_count() 是最佳实践。过多会导致频繁的上下文切换,降低效率。
    • I/O 密集型:如果任务主要是等待网络或磁盘,可以设置 processes 大于 CPU 核心数(例如 2 倍或 4 倍),因为进程在等待时会释放 CPU。但在这种情况下,使用 threading 或 asyncio 可能更合适。
  4. 异常处理
    如果在子进程中发生异常,map 会在主进程中重新抛出该异常(通常是 RemoteTraceback 包装后的异常)。使用 apply_async 时,异常会在调用 .get() 时抛出。

9 正则表达式

     Python 的正则表达式功能由标准库 re 模块提供。它的语法风格主要基于 Perl 5,功能非常强大,用于字符串的匹配、查找、替换和分割。

9.1 元字符

这些字符在正则中有特殊含义,如果要匹配它们本身,需要使用反斜杠 \ 转义(例如匹配真实的 . 需写为 \.

字符 含义 示例 匹配内容
. 匹配除换行符以外的任意单个字符 a.c "abc", "axc", "a $ c"
^ 匹配字符串的开头 ^Hello "Hello world" (匹配开头)
$ 匹配字符串的结尾 world$ "Hello world" (匹配结尾)
* 匹配前一个字符 0 次或多次 ab* "a", "ab", "abb", "abbb"
+ 匹配前一个字符 1 次或多次 ab+ "ab", "abb" (不匹配 "a")
? 匹配前一个字符 0 次或 1 次 colou?r "color", "colour"
{m,n} 匹配前一个字符 m 到 n 次 a{2,4} "aa", "aaa", "aaaa"
[] 字符集,匹配括号内的任意一个字符 [aeiou] 任意元音字母
` ` 或 操作,匹配左边或右边的表达式 `cat
() 分组,将多个字符视为一个单元,也可用于捕获 (ab)+ "ab", "abab"
\ 转义符,取消特殊含义或表示特殊序列 \d, \. 数字, 真实的点号

9.2 预定义字符集

以 \ 开头的常用简写形式

序列 含义 等价写法
\d 匹配任意数字 [0-9]
\D 匹配任意非数字 [^0-9]
\w 匹配字母、数字或下划线 [a-zA-Z0-9_]
\W 匹配非字母、数字或下划线 [^a-zA-Z0-9_]
\s 匹配任意空白字符 (空格, tab, 换行等) [ \t\n\r\f\v]
\S 匹配任意非空白字符 [^ \t\n\r\f\v]
\b 匹配单词边界 (单词的开始或结束) \bword\b 匹配独立的 "word"
\A 仅匹配字符串绝对开头 (不受多行模式影响) 同^ -
\Z 仅匹配字符串绝对结尾 同$ -

9.3 匹配模式

贪婪与非贪婪匹配

默认情况下,量词(*, +, ?, {m,n})是贪婪的,即尽可能多地匹配字符。在量词后加 ? 可变为非贪婪(最小匹配)。
贪婪: <.*> 在字符串 "<div>hello</div>" 中会匹配整个字符串 "<div>hello</div>"。
非贪婪: <.*?> 在上述字符串中会先匹配 "<div>",再匹配 "</div>"。

9.4 常用函数

在使用前需导入:import re

函数 描述 返回值
re.match(pattern, string) 从字符串开头开始匹配。如果开头不符合,直接返回 None。 Match 对象 或 None
re.search(pattern, string) 扫描整个字符串,返回第一个匹配项。 Match 对象 或 None
re.findall(pattern, string) 找到字符串中所有匹配项,返回列表。 列表 ['match1', 'match2']
re.finditer(pattern, string) 找到所有匹配项,返回一个迭代器(节省内存)。 迭代器 (包含 Match 对象)
re.sub(pattern, repl, string) 将匹配到的部分替换为 repl 替换后的新字符串
re.split(pattern, string) 根据正则模式分割字符串。 列表
re.compile(pattern) 将正则字符串编译成对象,可重复使用,效率更高。 Pattern 对象

match像startWith,所以一般查找用search

9.4.1 match

从开始匹配,所以不需要^开头.如果字符串包含换行符,且你想让 match 匹配每一行的开头,需要配合 re.MULTILINE (re.M) 标志,但要注意 match 始终只匹配整个字符串的起始位置,即使加了 re.Mmatch 也不会去匹配第二行的开头。

    方法 描述 示例
    .group() 返回匹配到的完整字符串 result.group()
    .start() 返回匹配开始的索引(对于 match 通常是 0) result.start()
    .end() 返回匹配结束的索引 result.end()
    .span() 返回 (start, end) 元组 result.span()
    .groups() 返回所有捕获分组 () 的内容元组 见下文示例
    import re
    
    text = "Python is great"
    # 模式以 "Python" 开头,文本也是以 "Python" 开头 -> 匹配成功
    result = re.match(r"Python", text)
    
    if result:
        print("匹配成功:", result.group())  # 输出: Python
        print("start:", result.start()) #start: 0 必然是0
        print("end:", result.end()) # end: 6
        print("span:", result.span()) # (0,6)
    else:
        print("匹配失败")
    import re
    
    text = "2024-03-24"
    # 使用 () 创建分组:年、月、日
    pattern = r"(\d{4})-(\d{2})-(\d{2})"
    
    result = re.match(pattern, text)
    
    if result:
        print("完整匹配:", result.group())       # 2024-03-24
        print("第一组 (年):", result.group(1))   # 2024
        print("第二组 (月):", result.group(2))   # 03
        print("所有分组:", result.groups())      # ('2024', '03', '24')

    9.4.2 search

    re.search() 找到第一个符合条件的子串后就会立即停止,不会继续寻找后面的匹配项

    import re
    
    text = "Order ID: A-1001 was placed on 2026-03-24 user_admin."
    # 正则解释:
    # (A-\d+)      -> 第1组:订单号
    # .*?          -> 非贪婪匹配中间内容
    # (\d{4}-\d{2}-\d{2}) -> 第2组:日期
    # (\w+)        -> 第3组:用户名
    pattern = r"(A-\d+).*?(\d{4}-\d{2}-\d{2}).*?(\w+)"
    
    match_obj = re.search(pattern, text)
    
    if match_obj:
        # --- 基础获取 ---
        print("1. 完整匹配 (.group()):", match_obj.group()) 
        # 输出: Order ID: A-1001 was placed on 2026-03-24 by user_admin (注意:这里取决于正则是否覆盖了整句,通常只覆盖匹配部分)
        # 修正:上面的正则只匹配 "A-1001 ... user_admin" 部分
        # 实际输出: A-1001 was placed on 2026-03-24 by user_admin
        
        # --- 分组获取 ---
        print("2. 订单号 (.group(1)):", match_obj.group(1))   # A-1001
        print("3. 日期 (.group(2)):", match_obj.group(2))     # 2026-03-24
        print("4. 用户 (.group(3)):", match_obj.group(3))     # user_admin
        
        # --- 位置信息 ---
        print("5. 整体起始 (.start()):", match_obj.start())   # 10 (假设 "A" 在索引 10)
        print("6. 日期起始 (.start(2)):", match_obj.start(2)) # 日期开始的索引
        print("7. 整体范围 (.span()):", match_obj.span())     # (10, 55)
        
        # --- 批量获取 ---
        print("8. 所有组元组 (.groups()):", match_obj.groups()) 
        # 输出: ('A-1001', '2026-03-24', 'user_admin')
        
    else:
        print("未找到匹配")

    9.4.5 findall用法

    re.findall() 会扫描整个字符串,找到所有符合正则模式的子串,并将它们放入一个列表中返回。如果没有找到任何匹配,则返回空列表 []

    import re
    
    text = "Order A-100 on 2026-01-01; Order B-200 on 2026-02-02; Order C-300 on 2026-03-03"
    
    # 组1: 订单号 (A-\d+)
    # 组2: 日期 (\d{4}-\d{2}-\d{2})
    pattern = r"([A-Z]-\d+).*?(\d{4}-\d{2}-\d{2})"
    
    result = re.findall(pattern, text)
    
    print(result)
    # 输出: [('A-100', '2026-01-01'), ('B-200', '2026-02-02'), ('C-300', '2026-03-03')]
    # 类型: 列表中包含元组
    
    # 如何遍历?
    for order_id, date in result:
        print(f"订单 {order_id} 的日期是 {date}")
    
    output='''
    [('A-100', '2026-01-01'), ('B-200', '2026-02-02'), ('C-300', '2026-03-03')]
    订单 A-100 的日期是 2026-01-01
    订单 B-200 的日期是 2026-02-02
    订单 C-300 的日期是 2026-03-03
    '''

    返回值

    正则表达式模式 示例 返回值类型结构 示例输出
    无括号 r"\d+" List[str] ['1', '23']
    1 个括号 r"(\d+)" List[str] ['1', '23']
    2+ 个括号 r"(\d)(\d)" List[Tuple[str, ...]] [('1', ''), ('2', '3')]
    无匹配 (任何不匹配的正则) List (空) []

    9.4.6 sub

    re.sub(pattern, repl, string, count=0, flags=0)

    参数 含义 说明
    pattern 正则模式 要查找的模式(字符串或编译后的正则对象)。
    repl 替换内容 可以是字符串,也可以是函数。
    string 原字符串 需要被处理的原始文本。
    count 替换次数 默认为 0(表示替换所有匹配项)。如果设为 1,只替换第一个。
    flags 标志位 re.IGNORECASE (忽略大小写) 等。

    场景一:简单字符串替换

    将匹配到的内容直接替换为固定字符串。

    import re
    
    text = "Order A-100 and Order B-200"
    # 将所有 "Order [字母]-[数字]" 替换为 "订单"
    result = re.sub(r"Order [A-Z]-\d+", "订单", text)
    
    print(result)
    # 输出: 订单 and 订单

    场景二:使用反向引用(保留部分内容)

    在替换字符串中,可以使用 \1\2 来引用正则中括号捕获组的内容。

    • \1 代表第一个括号 () 匹配的内容。
    • \2 代表第二个括号 () 匹配的内容。
    import re
    
    text = "2026-03-24"
    # 将 "年-月-日" 格式转换为 "年/月/日"
    # (\d{4}) 是组1, (\d{2}) 是组2, (\d{2}) 是组3
    result = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1/\2/\3", text)
    print(text)
    print(result) # 输出: 2026/03/24

    场景三:使用函数进行动态替换

    当替换逻辑比较复杂(比如需要计算、判断、格式化)时,repl 参数可以传入一个函数

    • 该函数接收一个 Match 对象 作为参数。
    • 函数必须返回一个字符串,作为替换内容。
    import re
    
    text = "价格: 100元, 折扣价: 200元"
    
    def double_price(match):
        # match.group(0) 是完整匹配的内容 (如 "100元")
        # match.group(1) 是第一个括号的内容 (如 "100")
        num_str = match.group(1)
        num = int(num_str)
        new_num = num * 2
        return f"{new_num}元"
    
    # 正则只捕获数字部分,但匹配的是 "数字+元"
    # 这里的函数会对每一个匹配项执行一次
    result = re.sub(r"(\d+)元", double_price, text)
    print(result) # 输出: 价格: 200元, 折扣价: 400元

    场景四:限制替换次数 

    只替换前 N 个匹配项。

    import re
    
    text = "cat dog cat dog cat"
    # 只替换前 2 个 "cat" 为 "mouse"
    result = re.sub(r"cat", "mouse", text, count=2)
    
    print(result)
    # 输出: mouse dog mouse dog cat

    9.4.7 split 

    e.split() 是正则表达式中用于分割字符串的函数。它比标准的 string.split() 更强大,因为它允许你使用正则表达式作为分隔符,从而处理复杂的分割场景(例如:同时用逗号、分号或空格分割)

    re.split(pattern, string, maxsplit=0, flags=0)

    场景一:多分隔符分割

    当文本中包含多种分隔符(如逗号、分号、空格混用)时,标准 split 很难处理,而 re.split 可以轻松搞定。

    import re
    
    text = "apple,banana;orange grape,melon"
    # 分隔符是:逗号、分号 或 空格
    # 正则: [,;\s]+  表示匹配一个或多个逗号、分号或空白字符
    result = re.split(r"[,;\s]+", text)
    
    print(result)
    # 输出: ['apple', 'banana', 'orange', 'grape', 'melon']

    场景二:限制分割次数 

    只切前 N 刀,剩下的部分保持原样放在最后一个元素中。

    import re
    
    text = "A,B,C,D,E"
    # 只切 2 刀(遇到前两个逗号就停)
    result = re.split(r",", text, maxsplit=2)
    
    print(result)
    # 输出: ['A', 'B', 'C,D,E']

    场景三:保留分隔符

    如果在正则模式中使用了捕获组 (),那么分隔符本身也会被保留在结果列表中。

    import re
    
    text = "one,two;three four"
    # 注意括号:([,;\s]+) 把分隔符也捕获了
    result = re.split(r"([,;\s]+)", text)
    
    print(result)
    # 输出: ['one', ',', 'two', ';', 'three', ' ', 'four']
    # 解释:数据和分隔符交替出现

    场景四:处理空字符串

    如果分隔符出现在字符串开头、结尾,或者连续出现,默认会产生空字符串 ''

    import re
    
    text = ",apple,,banana,"
    # 简单分割
    result = re.split(r",", text)
    print(result) 
    # 输出: ['', 'apple', '', 'banana', ''] (包含空串)
    
    # 优化:过滤掉空串
    result_clean = [x for x in re.split(r",", text) if x]
    print(result_clean)
    # 输出: ['apple', 'banana']

    9.5 匹配标志

    作为函数的第三个参数传入,或用 (?...) 写在正则内部。

    • re.I 或 re.IGNORECASE: 忽略大小写。
    • re.M 或 re.MULTILINE: 多行模式。^ 和 $ 不仅匹配字符串首尾,还匹配每一行的首尾(针对 \n)。
    • re.S 或 re.DOTALL: 让 . 也能匹配换行符。
    • re.X 或 re.VERBOSE: 允许正则表达式中包含空白和注释,提高可读性

    10 多线程

           多线程目的是为了提高并发处理能力,线程比进程更轻量级。对于单cpu,多线程是通过分配时间片实现并发,对多cpu才能实现真正并发(同时执行)。

           在 Python 3(特指最主流的 CPython 实现)中,全局解释器锁(Global Interpreter Lock,简称 GIL) 是一个互斥锁,它确保在任何时刻,只有一个线程能够执行 Python 字节码。结合当前时间(2026年3月),关于 GIL 的最新状态和核心要点如下:

    1. 核心机制与影响

    • 工作原理:即使你的机器有多个 CPU 核心,对于同一个 Python 进程内的多个线程,GIL 会强制它们排队执行。同一时刻,只有一个线程在“跑”代码。
    • 主要影响
      • CPU 密集型任务(如复杂计算、图像处理):多线程无法利用多核优势,性能甚至可能不如单线程(因为多了锁的切换开销)。
      • I/O 密集型任务(如网络请求、文件读写):影响较小。因为线程在等待 I/O 时会主动释放 GIL,其他线程可以趁机执行。

    2. 2026年的最新现状(重要更新)

    根据搜索结果和 Python 社区的最新进展,截至 2026 年初:

    • 默认状态:在标准的 Python 3.13 及之前的版本中,GIL 依然是默认开启的,以保证最大的兼容性和稳定性。
    • “无 GIL”版本的进展
      • Python 3.13 引入了实验性的“自由线程”(Free-threading)构建选项(通过 --disable-gil 编译),允许用户在编译时选择移除 GIL。
      • Python 3.14(预计 2025 年底发布,2026 年广泛使用)标志着重大转折。官方正在推动将“无 GIL”模式从实验性转为正式支持。这意味着开发者可以选择安装一个没有 GIL 的 Python 版本,从而实现真正的多线程并行计算。
      • 注意:这并不意味着 GIL 被彻底“删除”了。为了兼容性,官方可能依然提供带 GIL 的标准版本,而“无 GIL”作为一个可选构建或特定模式存在。许多第三方 C 扩展库在 2026 年仍在适配线程安全的过程中。

    3. 为什么 GIL 长期存在?

    • 内存管理简化:CPython 使用引用计数进行内存管理,GIL 避免了多线程修改引用计数时的复杂锁竞争和数据损坏风险。
    • C 扩展兼容性:大量现有的 C 语言扩展库依赖 GIL 来保证线程安全,移除 GIL 需要整个生态系统的重构。

    4. 如何在 2026 年绕过 GIL 限制?

    如果你需要利用多核性能,目前有以下几种成熟方案:

    方案 适用场景 说明
    多进程 (multiprocessing) CPU 密集型 最稳妥的方案。每个进程有独立的解释器和内存空间,不受 GIL 限制,可跑满多核。
    无 GIL 构建版 CPU 密集型 & 多线程需求 使用 Python 3.13+/3.14+ 的 --disable-gil 编译版本。注意:需确认所有依赖库都支持线程安全。
    其他解释器 特定场景 使用 Jython (运行在 JVM 上) 或 IronPython (.NET),它们没有 GIL。或者关注 PyPy 的 STM 分支(软件事务内存)。
    C/C++/Rust 扩展 高性能计算 将计算密集型代码写成 C 扩展或 Rust (通过 PyO3),并在代码中显式释放 GIL (Py_BEGIN_ALLOW_THREADS)。
    异步 IO (asyncio) I/O 密集型 虽然不解决 CPU 并行问题,但在高并发网络服务中是比多线程更高效的选择。

    因为python在AI领域的应用处于领先地位,生态越来越复杂,研究有余,工程化能力不足,所以在想尽办法来弥补缺点。如果前期不考虑好,就会留下一堆烂摊子。需要工程化,如果有更好的选择,最好不要选择脚本语言。

    10.1 threading 模块

      threading 是一个比较推荐的模块,语法和java比较像,很多用脚本语言的都在吐槽java、c++之类的面向对象强类型语言不够灵活,但是所有都在往这方面靠,理由不言而喻。

          早期有个thread模块,但是一看api就很奇怪,不推荐使用了。threading 和Java中的并发工具包非常像

    主要功能组件:

    • Thread 类:用于创建和启动线程。
    • Lock / RLock:互斥锁,用于保护共享数据,防止竞态条件。
    • Event:事件对象,用于线程间通信(一个线程发信号,其他线程等待)。
    • Condition:条件变量,用于复杂的线程同步(如生产者-消费者模式)。
    • Semaphore / BoundedSemaphore:信号量,用于控制同时访问资源的线程数量。
    • Timer:定时线程,在指定时间后执行函数。
    • local:线程局部数据,每个线程拥有独立的数据副本。
    名称 类型 说明
    current_thread() 函数 最重要。返回当前正在执行的 Thread 对象。等价于旧版的 currentThread()
    main_thread() 函数 返回主线程对象 (MainThread)。即使你在子线程中调用,它也能找到主线程。
    enumerate() 函数 返回一个列表,包含当前所有存活的 Thread 对象。
    active_count() 函数 返回当前存活的线程数量 (等于 len(threading.enumerate()))。
    get_ident() 函数 返回当前线程的标识符 (等价于 current_thread().ident)。
    get_native_id() 函数 返回当前线程的原生 ID (等价于 current_thread().native_id)。
    stack_size([size]) 函数 获取或设置新创建线程的栈大小 (单位字节)。需在创建线程前设置。
    import threading
    
    def check_info():
        current = threading.current_thread()
        print(f"当前线程名: {current.name}")
        print(f"当前线程ID: {current.ident}")
        print(f"是不是主线程? {current is threading.main_thread()}")
    
    t = threading.Thread(target=check_info)
    t.start()
    t.join()
    
    # 查看系统中有多少线程
    print(f"活跃线程数: {threading.active_count()}")
    print(f"活跃线程列表: {[th.name for th in threading.enumerate()]}")

    10.2 Thread

    表示一个单独的控制线程。

    • 功能:创建、启动、管理线程。
    • 常用方法
      • __init__(target=None, name=None, args=(), kwargs={}, daemon=None): 初始化线程。
      • start(): 启动线程(调用 run() 方法)。
      • run(): 线程活动时调用的方法(通常被重写)。
      • join(timeout=None): 阻塞当前线程,直到该线程结束。
      • is_alive(): 检查线程是否仍在运行。
      • name: 线程名称属性(可读写)。
      • daemon: 守护线程属性(主程序退出时,守护线程会自动退出)。

    __init__参数详解

    1. target ( callable )
    含义:线程启动后要执行的可调用对象(通常是函数)。
    默认值:None。
    说明:
    如果提供了 target,当调用 start() 方法时,线程会自动执行 target(*args, **kwargs)。
    如果为 None,则必须子类化 Thread 并重写 run() 方法,否则线程启动后什么都不做。
    示例:target=my_function

    2. name ( str )
    含义:线程的名称。
    默认值:None(自动生成,格式为 Thread-N,N 是递增整数,如 Thread-1, Thread-2)。
    说明:
    主要用于调试和日志记录,方便区分不同的线程。
    可以通过 thread.name 属性在运行时读取或修改。
    主线程的名称默认为 MainThread。
    示例:name="Worker-01"

    3. args ( tuple )
    含义:传递给 target 函数的位置参数元组。
    默认值:() (空元组)。
    说明:
    即使只有一个参数,也必须写成元组形式(例如 (1,) 而不是 1)。
    这些参数会在 target 被调用时按顺序传入。
    示例:args=(10, 20) -> 对应 target(10, 20)

    4. kwargs ( dict )
    含义:传递给 target 函数的关键字参数字典。
    默认值:{} (空字典)。
    说明:
    用于传递命名参数。
    可以与 args 同时使用。
    示例:kwargs={"url": "http://...", "timeout": 5} -> 对应 target(url="...", timeout=5)

    5. daemon ( bool )
    含义:是否为守护线程。
    默认值:None。
    说明:
    None:继承创建该线程的当前线程的守护状态。如果是主线程创建的,默认为 False(非守护)。
    True:设置为守护线程。关键特性:当主程序(主线程)退出时,所有存活的守护线程会被强制立即终止,无论它们是否执行完毕。
    False:设置为非守护线程。主程序会等待所有非守护线程执行完毕后才会退出。
    注意:必须在调用 start() 之前设置此属性,否则会抛出 RuntimeError。
    适用场景:后台监控、心跳检测等不需要阻塞程序退出的任务通常设为 True;需要确保数据保存完成的任务应设为 False。

    import threading
    import time
    
    def worker(task_id, delay, prefix="Task"):
        print(f"[{prefix}-{task_id}] 开始工作...")
        time.sleep(delay)
        print(f"[{prefix}-{task_id}] 工作完成。")
    
    # 1. 基本用法
    t1 = threading.Thread(
        target=worker, 
        args=(1, 2),              # 位置参数: task_id=1, delay=2
        kwargs={"prefix": "A"},   # 关键字参数: prefix="A"
        name="Worker-A"           # 自定义名称
    )
    
    # 2. 守护线程用法 (程序退出时自动杀掉)
    t2 = threading.Thread(
        target=worker,
        args=(2, 10),             # 这个任务需要10秒,但因为是守护线程,主程序不会等它
        daemon=True               # 设置为守护线程
    )
    
    print(f"主线程: {threading.current_thread().name}")
    
    t1.start()
    t2.start()
    
    # 主线程只等待 t1 (非守护),不等待 t2 (守护)
    t1.join() 
    
    print("主程序结束。")
    # 此时 t2 可能还在睡梦中,但会被强制终止,不会打印 "工作完成"

    10.3 Timer

    threading.Timer 是 Thread 类的子类,用于在指定的秒数后执行某个函数。它非常适合用于延迟任务、超时处理或周期性检查(需手动重启)。

    timer = threading.Timer(interval, function, args=None, kwargs=None)
    • interval (float): 延迟执行的秒数。
    • function (callable): 要执行的函数。
    • args (tuple): 传递给函数的位置参数(默认为 ())。
    • kwargs (dict): 传递给函数的关键字参数(默认为 {})。
    方法 说明
    start() 启动定时器。调用后开始倒计时,时间一到自动执行函数(在新线程中)。
    cancel() 取消定时器。如果在倒计时结束前调用,函数将不会执行。这是 Timer 最重要的功能。
    is_alive() 检查定时器线程是否还在运行(包括等待时间和执行函数时间)。

    场景一:简单的延迟执行

    import threading
    
    def say_hello(name):
        print(f"你好, {name}! 这条消息延迟了 3 秒才出现。")
    
    # 创建定时器:3秒后执行 say_hello("Alice")
    t = threading.Timer(3.0, say_hello, args=["Alice"])
    t.start()
    print("定时器已启动,等待 3 秒...")
    
    # 主线程继续做其他事,不会被阻塞
    print("主线程正在忙别的...")

    场景二:取消定时器

    import threading
    import time
    
    def alarm():
        print("⏰ 警报响起!时间到!")
    
    print("设置 5 秒后的警报...")
    t = threading.Timer(5.0, alarm)
    t.start()
    
    print("哎呀,用户取消了操作!")
    time.sleep(1) # 模拟用户操作耗时
    
    # 在警报响起前取消它
    t.cancel()
    print("警报已取消。")
    
    # 等待足够长的时间确认警报不会响
    time.sleep(5)
    print("程序结束,警报确实没响。")

    10.4 Local

           python 中实现线程局部存储(Thread-Local Storage, TLS)的核心工具。
    它的核心特性是:创建一个对象后,每个线程访问该对象的属性时,看到的都是各自独立的副本,互不干扰。 即使多个线程操作同一个 local 实例,它们也不会看到彼此修改的数据。
    这在多线程编程中非常有用,常用于保存数据库连接、用户会话信息、事务上下文等“每个线程独有”的状态,从而避免使用复杂的锁机制。

    import threading
    import time
    import random
    
    # 创建一个线程局部存储对象
    my_data = threading.local()
    
    def worker(thread_name):
        # 1. 设置当前线程的专属数据
        # 注意:这里不需要初始化,直接赋值即可
        my_data.value = random.randint(1, 100)
        my_data.name = thread_name
        
        print(f"[{thread_name}] 初始值: {my_data.value}")
        
        # 模拟一些工作
        time.sleep(random.uniform(0.1, 0.5))
        
        # 2. 修改数据
        my_data.value += 10
        
        # 3. 读取数据
        # 关键点:这里读到的 value 是当前线程自己设置的,不会受到其他线程影响
        print(f"[{thread_name}] 修改后: {my_data.value} (名字: {my_data.name})")
    
    # 创建并启动多个线程
    threads = []
    for i in range(3):
        t = threading.Thread(target=worker, args=(f"Thread-{i}",))
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    print("\n--- 主线程尝试访问 ---")
    # 主线程访问时,因为没有设置过属性,会抛出 AttributeError
    try:
        print(f"主线程的值: {my_data.value}")
    except AttributeError as e:
        print(f"主线程报错 (预期行为): {e}")

    10.5 Lock / RLock

    threading.Lock 和 threading.RLock 是 Python 多线程编程中用于保护共享资源、防止竞态条件 (Race Condition) 的两个核心锁机制。虽然它们看起来很像,但关键区别在于“可重入性” 

    1 lock

    lock这是最基础的锁。它的规则非常简单粗暴:

    • 状态:只有两种状态 —— “锁定” 或 “未锁定”。
    • 获取:如果锁是空的,当前线程拿走锁;如果锁已被别人(或自己)拿走,当前线程阻塞等待
    • 释放:任何持有锁的线程都可以释放它(实际上通常由持有者释放,但底层不检查是谁)。
    • 致命限制不可重入。如果一个线程已经持有了锁,再次尝试获取同一个锁,会导致死锁 (Deadlock),程序永远卡住。

    ❌ 错误示例:死锁

    import threading
    
    lock = threading.Lock()
    
    def bad_function():
        print("1. 尝试第一次获取锁...")
        lock.acquire()
        print("   -> 锁已获取")
        
        print("2. 尝试第二次获取锁 (在同一个线程内)...")
        # 致命错误:因为锁已经被当前线程拿走了,这里会无限等待自己释放
        # 但代码卡在下面这行,永远无法执行到 release()
        lock.acquire() 
        print("   -> 这行永远不会打印")
        
        lock.release()
        lock.release()
    
    t = threading.Thread(target=bad_function)
    t.start()
    t.join()
    # 程序会挂起,必须强制终止

    ✅ 正确用法

    适用于简单的临界区保护,且逻辑中没有嵌套调用需要同一把锁的情况

    import threading
    
    lock = threading.Lock()
    counter = 0
    
    def safe_increment():
        global counter
        # 方式一:手动 acquire/release (推荐配合 try/finally)
        lock.acquire()
        try:
            temp = counter
            counter = temp + 1
        finally:
            lock.release()
    
    def safe_increment_with():
        global counter
        # 方式二:使用 with 语句 (更推荐,自动处理异常和释放)
        with lock:
            temp = counter
            counter = temp + 1
            print(f'{threading.current_thread().name}+{counter}')
            
    # 运行测试
    threads = [threading.Thread(target=safe_increment_with,name=i) for i in range(1000)]
    for t in threads: t.start()
    for t in threads: t.join()
    
    print(f"最终计数: {counter}") # 必定是 1000

    2 Rlock

    RLock = Reentrant Lock。
    它是 Lock 的升级版,专门解决“同一个线程需要多次获取同一把锁”的场景。
    核心特性:可重入。
    它内部记录了 “哪个线程” 持有了锁,以及 “获取次数”。
    如果是同一个线程再次请求锁,计数器 +1,直接通过,不会阻塞。
    如果是其他线程请求,则阻塞。
    释放规则:必须调用相同次数的 release(),计数器归零后,锁才真正释放给其他线程。

    当你有一个函数 A 加了锁,而 A 内部调用了函数 B,函数 B 也需要加同一把锁时,必须用 RLock

    import threading
    
    # 使用 RLock 允许递归调用
    r_lock = threading.RLock()
    
    def outer_function():
        print("[Outer] 尝试获取锁...")
        with r_lock:
            print("[Outer] 锁已获取 (计数=1)")
            inner_function() # 调用内部函数
            print("[Outer] 内部函数执行完毕")
    
    def inner_function():
        print("  [Inner] 尝试获取锁...")
        with r_lock:
            # 因为是同一个线程,RLock 允许再次获取,计数变为 2
            print("  [Inner] 锁已获取 (计数=2,没有死锁!)")
            # 做一些操作
            print("  [Inner] 准备释放锁 (计数变回 1)")
        # with 块结束,自动 release 一次
        print("  [Inner] 内部锁已释放")
    
    # 启动线程
    t = threading.Thread(target=outer_function)
    t.start()
    t.join()
    
    print("✅ 程序正常结束,没有死锁。")

    10.6 Condition

    threading.Condition(条件变量)是 Python 多线程中最强大但也最复杂的同步原语。

    如果说 Lock 是“独占厕所”,Event 是“红绿灯”,那么 Condition 就是“带休息室的会议室”

    • 线程可以进入会议室(获取锁)。
    • 如果条件不满足(比如人没到齐),线程可以释放锁并去休息室睡觉wait),把会议室让给别人。
    • 当有人改变了状态(比如人到了),可以叫醒休息室里的特定线程或所有线程(notify / notify_all)。
    • 被叫醒的线程会重新尝试获取锁,然后继续执行。

    它主要用于解决“生产者 - 消费者”模型或“等待特定状态变化”的场景,比 Event 更灵活,比单纯的 Lock 更高效。

    Condition 内部实际上包含了两部分:

    1. 一把锁 (Lock 或 RLock):用于保护共享数据。
    2. 一个等待池:存放那些因为条件不满足而暂时挂起的线程。

    你可以直接使用默认的锁,也可以传入自定义的锁(通常用 RLock 以支持重入)

    cond = threading.Condition() 
    # 或者
    lock = threading.RLock()
    cond = threading.Condition(lock)
    方法 说明 关键点
    acquire() / release() 获取/释放内部的锁。 通常配合 with cond: 使用。
    wait(timeout=None) 释放锁 -> 进入等待状态 -> 被唤醒后重新获取锁 -> 返回。 ⚠️ 必须在持有锁的情况下调用,否则报错。
    ⚠️ 唤醒后不一定条件就满足了(可能有虚假唤醒),通常需要放在 while 循环中检查条件。
    notify(n=1) 唤醒等待池中 最多 n 个 线程。 ⚠️ 必须在持有锁的情况下调用。
    ⚠️ 被唤醒的线程不会立即运行,它们要等当前线程释放锁后才能抢锁。
    notify_all() 唤醒等待池中 所有 线程。 适用于状态改变影响所有等待者的场景(如“系统关闭”、“资源耗尽”)。
    import threading
    import time
    import random
    
    # 共享资源
    buffer = []
    MAX_SIZE = 5
    
    # 创建条件变量
    cond = threading.Condition()
    
    def producer(name):
        for i in range(10):
            item = f"Item-{i}"
            
            with cond:
                # 1. 检查条件:如果缓冲区满了,就等待
                while len(buffer) >= MAX_SIZE:
                    print(f"[{name}] 缓冲区满 ({len(buffer)}/{MAX_SIZE}),等待消费者...")
                    cond.wait() # 自动释放锁,进入等待
                
                # 2. 生产数据
                buffer.append(item)
                print(f"[{name}] 生产了: {item} -> 缓冲区: {buffer}")
                
                # 3. 通知消费者:有新数据了!
                cond.notify() 
            
            # 模拟生产耗时
            time.sleep(random.uniform(0.1, 0.5))
    
    def consumer(name):
        for _ in range(10):
            with cond:
                # 1. 检查条件:如果缓冲区空了,就等待
                while len(buffer) == 0:
                    print(f"[{name}] 缓冲区空,等待生产者...")
                    cond.wait() # 自动释放锁,进入等待
                
                # 2. 消费数据
                item = buffer.pop(0)
                print(f"[{name}] 消费了: {item} -> 缓冲区: {buffer}")
                
                # 3. 通知生产者:有空位了!
                cond.notify()
            
            # 模拟消费耗时
            time.sleep(random.uniform(0.2, 0.6))
    
    # 启动线程
    p = threading.Thread(target=producer, args=("Producer",))
    c1 = threading.Thread(target=consumer, args=("Consumer-1",))
    c2 = threading.Thread(target=consumer, args=("Consumer-2",))
    
    p.start()
    c1.start()
    c2.start()
    

    10.7 Semaphore / BoundedSemaphore

    threading.Semaphore(信号量)和 threading.BoundedSemaphore 是 Python 多线程中用于控制同时访问特定资源的线程数量的同步原语

    信号量的本质是一个计数器

    • 初始化:创建一个信号量时,你设定一个初始值 value(例如 5)。
    • acquire() (P 操作)
      • 如果计数器 > 0:计数器减 1,线程继续执行。
      • 如果计数器 == 0:线程阻塞等待,直到有其他线程释放信号量。
    • release() (V 操作)
      • 计数器加 1。
      • 如果有线程在等待,唤醒其中一个
    特性 threading.Semaphore threading.BoundedSemaphore
    计数器上限 无限制。可以无限次调用 release(),计数器会一直增加。 有上限。计数器不能超过初始值。
    异常行为 如果你调用 release() 的次数多于 acquire(),计数器会虚高,导致后续过多的线程进入临界区,失去限流意义。 如果 release() 次数多于 acquire(),会抛出 ValueError
    安全性 较低。容易因代码逻辑错误(如多释放)导致并发失控。 较高。强制保证并发数不超过初始设定值。
    推荐度 ⭐⭐ (仅在特殊场景使用) ⭐⭐⭐⭐⭐ (默认首选)

    建议:除非你有非常特殊的理由需要计数器无限增长,否则永远使用 BoundedSemaphore。它能帮你发现“多释放”的逻辑错误。

    import threading
    import time
    import random
    
    # 创建一个最大值为 3 的信号量
    # 意味着同一时间最多允许 3 个线程进入临界区
    max_connections = 3
    sem = threading.BoundedSemaphore(max_connections)
    print(sem._value) #
    def access_database(thread_id):
        # 尝试获取信号量
        # 如果当前已有 3 个线程在内部,第 4 个线程会在这里阻塞
        sem.acquire()
        
        try: 
            print(f"[线程-{thread_id}] 获得许可 (剩余名额: {sem._value}),正在访问资源...")
            
            # 模拟耗时操作 (访问数据库/网络请求)
            time.sleep(random.uniform(1, 3))
        finally:
            # 【关键】无论是否出错,必须释放信号量,否则名额会被永久占用
            sem.release()
            print(f"[线程-{thread_id}] 完成操作,释放许可,剩余名额: {sem._value}")
    
    # 启动 10 个线程,但只有 3 个能同时运行
    threads = []
    for i in range(3):
        t = threading.Thread(target=access_database, args=(i,))
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    print("✅ 所有任务完成。")
    
    output='''
    3
    [线程-0] 获得许可 (剩余名额: 2),正在访问资源...
    [线程-1] 获得许可 (剩余名额: 1),正在访问资源...
    [线程-2] 获得许可 (剩余名额: 0),正在访问资源...
    [线程-0] 完成操作,释放许可,剩余名额: 0
    [线程-1] 完成操作,释放许可,剩余名额: 1
    [线程-2] 完成操作,释放许可,剩余名额: 2
    ✅ 所有任务完成。
    '''

    ❌ 陷阱 1:忘记释放 (资源泄露)

    如果线程在持有信号量时崩溃或抛出异常,且没有 finally 块释放,计数器就不会加回去。

    • 后果:可用名额永久减少。如果所有名额都被泄露,后续所有线程都会永久死锁。
    • 解决:务必使用 try...finally 或 with 语句。

    ❌ 陷阱 2:普通 Semaphore 的“超发”风险

    如果你用普通的 Semaphore,并且代码逻辑写错了(比如在一个循环里意外调用了两次 release):

    sem = threading.Semaphore(2)
    sem.acquire()
    sem.release()
    sem.release() # 错误!计数器变成了 3
    # 现在允许 3 个线程进入,突破了设定的 2 个限制!

    ✅ 最佳实践:使用 with 语句

    和 Lock 一样,Semaphore 也支持上下文管理器,这是最安全的写法

    # 推荐写法
    with sem:
        do_something()
    # 离开 with 块自动 release,即使中间报错

    高级用法:带超时的获取

    # 尝试获取信号量,最多等待 2 秒
    acquired = sem.acquire(timeout=2)
    
    if acquired:
        try:
            do_work()
        finally:
            sem.release()
    else:
        print("等待超时,放弃执行任务,处理降级逻辑...")

    10.8 Event

    threading.Event 是 Python 多线程编程中最简单、最常用的同步原语。

    它的核心作用相当于一个“信号灯”“旗帜”

    • 线程可以等待这个信号变成“真”(Green)。
    • 其他线程可以在任何时候将信号设置为“真”,从而唤醒所有正在等待的线程。
    • 信号也可以被重置为“假”(Red),让后续等待的线程继续阻塞。

    它非常适合用于:线程间通信、优雅停止线程、等待初始化完成、批量启动任务等场景

    方法 说明 返回值
    wait(timeout=None) 阻塞当前线程,直到事件被设置为 True
    - 如果事件已经是 True,立即返回。
    - 如果提供了 timeout (秒数),最多等待这么久,超时后返回 False
    - 如果事件被设置,返回 True;如果超时,返回 False
    bool
    set() 将事件内部标志设置为 True。所有正在 wait() 的线程会被立即唤醒。 None
    clear() 将事件内部标志重置为 False。后续调用 wait() 的线程将会再次阻塞。 None
    is_set() 检查内部标志是否为 True。相当于 flag == True 的判断。 bool
    import threading
    import time
    
    def worker(stop_event):
        print("👷 工作线程启动,开始监控...")
        while not stop_event.is_set():
            # 执行一些任务
            print("  ... 正在处理数据 ...")
            
            # 关键点:使用 wait 代替 sleep
            # 这样如果 stop_event 被设置,线程会立即醒来并退出循环,
            # 而不需要睡完剩下的时间。
            # 这里等待 1 秒,或者直到事件被触发
            if stop_event.wait(timeout=1.0):
                print("⚠️ 收到停止信号,准备退出...")
                break
                
        print("👋 工作线程已安全退出。")
    
    # 创建事件对象
    stop_event = threading.Event()
    
    # 启动线程,传入事件对象
    t = threading.Thread(target=worker, args=(stop_event,))
    t.start()
    
    # 主线程运行 3.5 秒
    time.sleep(3.5)
    
    print("\n🛑 主线程决定停止工作线程...")
    stop_event.set()  # 发送信号
    
    t.join() # 等待子线程彻底结束
    print("✅ 程序结束。")
    import threading
    import time
    
    # 事件:表示“初始化是否完成”
    init_done = threading.Event()
    
    data_store = {}
    
    def loader():
        print("📥 加载数据中... (耗时 3 秒)")
        time.sleep(3)
        data_store['config'] = "Loaded!"
        print("✅ 数据加载完成!")
        
        # 设置事件,通知所有等待者:我好了!
        init_done.set()
    
    def user_request(request_id):
        print(f"🙋 请求 {request_id} 到达,等待数据就绪...")
        
        # 阻塞在这里,直到 loader 线程调用 set()
        # 如果没有 timeout,会一直等下去
        init_done.wait() 
        
        print(f"🚀 请求 {request_id} 开始处理,读取数据: {data_store.get('config')}")
    
    # 启动加载线程
    t_loader = threading.Thread(target=loader)
    t_loader.start()
    
    # 模拟几个并发请求,它们都会卡在 wait()
    threads = []
    for i in range(3):
        t = threading.Thread(target=user_request, args=(i,))
        t.start()
        threads.append(t)
    
    #上面的代码实际上不需要,只有所有线程运行结束,主线程才会结束
    t_loader.join()
    for t in threads:
        t.join()

    10.9 总结

    场景 推荐工具 原因
    互斥访问 (只能 1 个) Lock / RLock 语义清晰,开销最小。虽然 Semaphore(1) 也能用,但没必要。
    限制并发数 (N 个) BoundedSemaphore(N) 首选。防止因代码错误导致并发数超标。
    特殊计数逻辑 Semaphore(N) 仅当你确实需要计数器超过初始值(极少见)时使用。
    事件通知 Event / Condition 如果只是通知“发生了某事”,而不是限制数量,请用这两个。

    10.10 queue 

    线程安全

    类名 行为模式 适用场景
    queue.Queue FIFO (先进先出) 通用场景,如日志处理、任务分发。
    queue.LifoQueue LIFO (后进先出/栈) 需要“最新任务优先处理”的场景,或深度优先搜索的多线程版。
    queue.PriorityQueue 优先级队列 任务调度。放入的数据必须是可比较的,通常放入元组 (优先级, 数据),优先级数字越小越先出。
    queue.SimpleQueue 简化版 FIFO (Python 3.7+) 去掉了 task_done()join() 功能,也不支持 timeout。由于内部实现更简单,性能略高。适用于只需要简单传递数据,不需要等待任务完成的场景。

    放入数据 q.put(item, block=True, timeout=None) 

    • block=True (默认): 如果队列已满(达到 maxsize),调用线程会阻塞,直到有空间。
    • timeout: 设置阻塞等待的最大秒数。超时抛出 queue.Full 异常。
    • block=False: 如果队列满,立即抛出 queue.Full,不等待

    11 异步编程

    11.1 核心概念

    要理解异步编程,需要掌握以下四个核心概念:

    1. async def (定义协程)
      使用 async def 定义的函数被称为协程函数。调用它并不会立即执行,而是返回一个协程对象。这个对象就像一个“任务计划书”,描述了要执行的操作。

    2. await (暂停与让出)
      await 关键字只能在 async def 函数内部使用。当执行到 await 时,当前协程会暂停,并将控制权交还给事件循环,让事件循环可以去执行其他任务。等待的操作完成后,事件循环会再回来恢复该协程的执行。await 后面必须跟一个“可等待对象”(如另一个协程、Task 或 Future)。

    3. 事件循环 (Event Loop)
      事件循环是异步编程的“总指挥”或“调度中心”。它负责管理和调度所有的协程任务。它会不断检查有哪些任务可以运行,哪些任务在等待,并在任务之间进行高效的切换。

    4. asyncio (标准库)
      asyncio 是 Python 用于编写异步代码的标准库。它提供了运行协程、创建任务、实现并发等所需的所有工具。

    await 是 Python 异步编程的核心关键字之一,它只能在 async def 定义的协程函数内部使用。它的核心作用是:暂停当前协程的执行,将控制权交还给事件循环,等待一个“可等待对象”完成,然后恢复执行。 这个过程是非阻塞的,意味着在等待期间,事件循环可以去执行其他任务,从而实现高效的并发。

    await 能等待什么?

    await 后面必须跟一个“可等待对象”(Awaitable),主要有以下三种:

    1. 协程对象 (Coroutine Object)
      通过调用 async def 定义的函数来创建。
    2. Task 对象
      通过 asyncio.create_task() 创建,它包装了一个协程并立即将其加入事件循环进行调度。
    3. Future 对象
      一个代表异步操作最终结果的底层对象,Task 是 Future 的子类

    11.2 基本示例

    示例1

    import asyncio
    import time
    
    async def say_after(delay, what):
        await asyncio.sleep(delay)
        print(what)
    
    async def main():
        print(f"程序开始时间: {time.strftime('%X')}")
        # 并发执行
        await asyncio.gather(
            say_after(2, 'Hello'),
            say_after(1, 'World')
        )
        print(f"程序结束时间: {time.strftime('%X')}")
    
    # jupyter直接 await main(),不要用 asyncio.run(main())
    # 因为jupyter已经有事件循环了
    await main()
    
    #输出
    output='''
    程序开始时间: 10:28:31
    World
    Hello
    程序结束时间: 10:28:33
    '''

    示例二

    import asyncio
    import time
    
    # 1. 修改函数,使其返回一个值
    async def say_after(delay, what):
        await asyncio.sleep(delay)
        # print(what) # 打印操作
        return f"{what} (延迟了{delay}秒)" # 返回结果
    
    async def main():
        print(f"程序开始时间: {time.strftime('%X')}")
        
        # 2. 使用变量接收 gather 的返回值
        # 注意:results 是一个列表,顺序与传入顺序一致
        results = await asyncio.gather(
            say_after(2, 'Hello'),
            say_after(1, 'World'),
            say_after(1, '!')
        )
        
        # 3. 打印结果
        print("--- 任务全部完成 ---")
        print(f"程序结束时间: {time.strftime('%X')}")
        print(f"获取到的结果列表: {results}")
        
        # 4. 你可以像操作普通列表一样操作它
        print(f"第一个结果: {results[0]}") # 对应 Hello
        print(f"第二个结果: {results[1]}") # 对应 World
    
    # 在 Jupyter 中运行
    await main()
    
    output='''
    程序开始时间: 10:45:20
    --- 任务全部完成 ---
    程序结束时间: 10:45:22
    获取到的结果列表: ['Hello (延迟了2秒)', 'World (延迟了1秒)', '! (延迟了1秒)']
    第一个结果: Hello (延迟了2秒)
    第二个结果: World (延迟了1秒)
    '''

    示例三

    import asyncio
    
    async def job(name, seconds):
        print(f"🔵 任务 {name} 开始,预计耗时 {seconds} 秒...")
        await asyncio.sleep(seconds)  # 模拟耗时操作
        print(f"✅ 任务 {name} 完成!")
        return f"{name}的结果"
    
    async def main():
        print("--- 1. 串行模式(排队做) ---")
        start_time = asyncio.get_event_loop().time()
        
        # 必须等 job("A") 做完,才会开始 job("B")
        result1 = await job("A", 2)
        result2 = await job("B", 2)
        
        print(f"串行总耗时: {asyncio.get_event_loop().time() - start_time:.2f} 秒\n")
    
        print("--- 2. 使用 create_task(并发做) ---")
        start_time = asyncio.get_event_loop().time()
        
        # create_task 会立即把任务扔到后台运行
        # 此时 task1 和 task2 同时开始倒计时
        task1 = asyncio.create_task(job("A", 2))
        task2 = asyncio.create_task(job("B", 2))
        
        print("💡 两个任务已在后台同时启动,主程序可以继续做别的事...")
        
        # await task1 会等待 task1 完成并获取结果
        # 注意:因为两个任务是同时开始的,这里只需要等待剩下的时间
        result1 = await task1
        result2 = await task2
        
        print(f"最终结果: {result1}, {result2}")
        print(f"并发总耗时: {asyncio.get_event_loop().time() - start_time:.2f} 秒")
    
    # 如果你在 Jupyter Notebook 中运行,请使用 await main()
    # 如果你在 .py 文件中运行,请使用 asyncio.run(main())
    await main() 
    1. await 串行模式
      • 任务 A 耗时 2 秒,任务 B 耗时 2 秒。
      • 总耗时约 4 秒。因为 B 必须等 A 做完。
    2. create_task 模式
      • create_task 被调用时,任务 A 和 B 同时开始计时。
      • 主程序打印提示语。
      • 然后 await 等待它们。
      • 总耗时约 2 秒。因为两个任务是重叠进行的

    11.3 异步 vs. 多线程

    特性 异步编程 (Asyncio) 多线程 (Threading)
    并发模型 单线程,协作式切换 多线程,操作系统抢占式切换
    资源开销 极低,可轻松创建上万协程 较高,线程数量受系统限制
    适用场景 I/O 密集型 (网络、文件、数据库) I/O 密集型,但库不支持异步时
    CPU 密集型 不适用 受 GIL 限制,效率不高,推荐用多进程

    12 工程架构

    这个可以参看博客

    后续如果有新章节,可能另外写一篇文章,这个内容太多卡

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               

    Logo

    AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

    更多推荐