Python(二十):第十九章 标准内置库详解

第十九章 标准内置库详解

1、时间处理模块详解

在 Python 开发中,处理日期和时间是常见的需求,无论是记录日志、数据打点、任务调度还是处理用户输入,都离不开时间模块。Python 标准库主要提供了 timedatetime 两个模块来处理时间

time 模块

time 模块提供了对C语言库中时间相关函数的底层访问接口,主要用于获取时间戳、进行时间格式转换以及控制程序执行流程(如暂停)。它处理的时间通常与操作系统的本地时间或UTC(协调世界时)相关。

time 模块常用函数

下表列出了 time 模块中一些核心且常用的函数:

函数描述
time.time()返回当前时间的时间戳(自1970年1月1日00:00:00 UTC到现在的浮点秒数)。
time.localtime([secs])将一个时间戳(默认为 time.time() 的返回值)转换为本地时间的 struct_time 对象。如果未提供 secs,则使用当前时间。
time.gmtime([secs])将一个时间戳(默认为 time.time() 的返回值)转换为 UTC(格林威治标准时间)的 struct_time 对象。如果未提供 secs,则使用当前时间。
time.mktime(t)time.localtime() 的反函数。接收一个 struct_time 对象(本地时间)作为参数,并返回其对应的时间戳(浮点秒数)。
time.asctime([t])接收一个 struct_time 对象(默认为 time.localtime() 的返回值)并返回一个24个字符的可读字符串,例如:'Mon May 12 11:18:35 2025'。不推荐在新代码中使用。
time.strftime(format[, t])struct_time 对象(默认为 time.localtime() 的返回值)按照指定的 format 字符串格式化为时间字符串。这是最常用的时间格式化函数。
time.strptime(string, format)将时间字符串 string 按照指定的 format 解析为 struct_time 对象。
time.sleep(secs)暂停当前程序的执行指定的秒数 secs(可以是浮点数,表示更精确的时间)。
time.perf_counter()返回一个具有最高可用精度的性能计数器的值(以秒为单位),用于测量短时间间隔。它的参考点是未定义的,因此只有连续调用的结果之间的差异才是有效的。
time.process_time()返回当前进程的CPU执行时间(用户时间 + 系统时间)的总和(以秒为单位)。不包括睡眠时间。

批注:核心记忆函数

  • time.time(): 获取时间戳,是很多时间操作的基础。
  • time.localtime(): 将时间戳转为本地时间元组,便于访问时间的各个部分。
  • time.strftime(): 强烈推荐记忆,用于将时间元RSA组格式化为任意字符串,非常灵活和常用。
  • time.strptime(): 用于将特定格式的字符串解析回时间元组。
  • time.sleep(): 控制程序执行节奏,非常实用。
  • time.mktime(): 在需要将本地时间元组转换回时间戳时使用。
struct_time 对象详解

time.localtime()time.gmtime() 函数返回的是 struct_time 对象,它是一个包含九个整数的元组(实际上是一个具有命名属性的元组,因此可以通过索引和属性名访问)。

索引属性名值范围含义
0tm_year例如, 2025年份
1tm_mon1 到 12月份
2tm_mday1 到 31日期
3tm_hour0 到 23小时
4tm_min0 到 59分钟
5tm_sec0 到 61 (闰秒)
6tm_wday0 到 6 (周一为0)星期几
7tm_yday1 到 366一年中的第几天
8tm_isdst0, 1, 或 -1是否为夏令时
strftimestrptime 常用格式化符号

strftime 用于将时间元组格式化为字符串,而 strptime 则相反,用于将字符串解析为时间元组。它们使用相同的格式化代码。

格式符含义示例 (基于 2025-05-12 14:30:45 周一)
%Y四位数的年份2025
%y两位数的年份 (00-99)25
%m两位数的月份 (01-12)05
%d两位数的日期 (01-31)12
%H24小时制的小时 (00-23)14
%I12小时制的小时 (01-12)02
%M两位数的分钟 (00-59)30
%S两位数的秒数 (00-61)45
%A完整的星期几名称 (本地化)Monday (英文环境)
%a简写的星期几名称 (本地化)Mon (英文环境)
%B完整的月份名称 (本地化)May (英文环境)
%b简写的月份名称 (本地化)May (英文环境)
%w星期几 (0-6,周日为0,周一为1,…周六为6)1 (如果周日是0)
%j一年中的第几天 (001-366)132
%pAM/PM (本地化)PM
%c本地化的日期和时间表示Mon May 12 14:30:45 2025
%x本地化的日期表示05/12/25 (美式)
%X本地化的时间表示14:30:45
%%一个字面的 ‘%’ 字符%

注意%w 的行为(周日是0还是周一是0)可能因平台而异。struct_time 中的 tm_wday 始终是周一为0。

time 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import time

## 1. 获取当前时间戳 (自纪元以来的秒数)
current_timestamp = time.time()
print(f"当前时间戳: {current_timestamp}") # 例如: 1715480315.123456

## 2. 将时间戳转换为本地时间的 struct_time 对象
local_time_struct = time.localtime(current_timestamp)
print(f"本地时间元组 (struct_time): {local_time_struct}")
## 访问 struct_time 的属性
print(f" 年份 (tm_year): {local_time_struct.tm_year}")
print(f" 月份 (tm_mon): {local_time_struct.tm_mon}")
print(f" 日期 (tm_mday): {local_time_struct.tm_mday}")
print(f" 小时 (tm_hour): {local_time_struct.tm_hour}")
print(f" 分钟 (tm_min): {local_time_struct.tm_min}")
print(f" 秒数 (tm_sec): {local_time_struct.tm_sec}")
print(f" 星期几 (tm_wday, 周一=0): {local_time_struct.tm_wday}")
print(f" 一年中的第几天 (tm_yday): {local_time_struct.tm_yday}")
print(f" 夏令时 (tm_isdst): {local_time_struct.tm_isdst}")

## 3. 将时间戳转换为 UTC 时间的 struct_time 对象
utc_time_struct = time.gmtime(current_timestamp)
print(f"UTC时间元组 (struct_time): {utc_time_struct}")
print(f" UTC 年份: {utc_time_struct.tm_year}, UTC 小时: {utc_time_struct.tm_hour}")

## 4. 将本地时间的 struct_time 对象转换回时间戳
timestamp_from_local_struct = time.mktime(local_time_struct)
print(f"从本地时间元组转换回的时间戳: {timestamp_from_local_struct}") # 应与 current_timestamp 非常接近

## 5. 使用 asctime() 获取可读字符串 (不推荐在新代码中使用)
readable_time_asctime = time.asctime(local_time_struct) # 等同于 time.asctime()
print(f"asctime() 格式的时间: {readable_time_asctime}") # 例如: 'Mon May 12 11:18:35 2025'

## 6. 使用 strftime() 将 struct_time 格式化为自定义字符串 (强烈推荐)
formatted_time_custom = time.strftime("%Y-%m-%d %H:%M:%S %A", local_time_struct)
print(f"strftime() 自定义格式化时间: {formatted_time_custom}") # 例如: '2025-05-12 11:18:35 Monday'

## 7. 使用 strptime() 将字符串解析为 struct_time 对象
time_string = "2024-07-20 10:30:00"
format_string = "%Y-%m-%d %H:%M:%S"
parsed_time_struct = time.strptime(time_string, format_string)
print(f"strptime() 解析 '{time_string}' 结果: {parsed_time_struct}")
print(f" 解析得到的年份: {parsed_time_struct.tm_year}")

## 8. 程序暂停
print("程序开始执行...")
time.sleep(1.5) # 暂停1.5秒
print("程序暂停1.5秒后继续执行。")

## 9. 性能计数器 (用于测量小段代码执行时间)
start_perf = time.perf_counter()
## 执行一些操作
for _ in range(100000):
pass
end_perf = time.perf_counter()
duration_perf = end_perf - start_perf
print(f"高性能计数器测量的操作耗时: {duration_perf:.6f} 秒")

## 10. 进程CPU时间
start_process = time.process_time()
## 执行一些CPU密集型操作
total = 0
for i in range(1000000):
total += i
end_process = time.process_time()
duration_process = end_process - start_process
print(f"CPU密集型操作的进程耗时: {duration_process:.6f} 秒")
time 模块开箱即用函数/场景
a. 获取特定格式的当前时间字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def get_current_time_str(format_str="%Y-%m-%d %H:%M:%S"):
"""
获取当前本地时间,并按指定格式返回字符串。

参数:
format_str (str): strftime 使用的时间格式化字符串。

返回:
str: 格式化后的当前时间字符串。
"""
return time.strftime(format_str, time.localtime())

## 示例使用
print(f"当前日期时间: {get_current_time_str()}") # 输出: 2025-05-12 11:18:35 (示例)
print(f"当前日期 (YYYYMMDD): {get_current_time_str('%Y%m%d')}") # 输出: 20250512 (示例)
print(f"当前时间 (HH-MM-SS): {get_current_time_str('%H-%M-%S')}") # 输出: 11-18-35 (示例)
b. 测量函数执行时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import time

def measure_execution_time(func, *args, **kwargs):
"""
测量并打印指定函数执行所需的时间。

参数:
func (callable): 需要测量执行时间的函数。
*args: 传递给 func 的位置参数。
**kwargs: 传递给 func 的关键字参数。

返回:
any: func 的返回值。
"""
start_time = time.perf_counter() # 使用 perf_counter 更精确
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"函数 '{func.__name__}' 执行耗时: {end_time - start_time:.6f} 秒")
return result

## 示例函数
def example_function(n):
time.sleep(n) # 模拟耗时操作
return f"操作完成,参数为 {n}"

## 测量示例函数的执行时间
measure_execution_time(example_function, 0.5)
measure_execution_time(sum, range(1000000))
c. 时间戳与格式化字符串之间的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time

def timestamp_to_str(ts, format_str="%Y-%m-%d %H:%M:%S"):
"""将时间戳转换为指定格式的本地时间字符串。"""
return time.strftime(format_str, time.localtime(ts))

def str_to_timestamp(time_str, format_str="%Y-%m-%d %H:%M:%S"):
"""将指定格式的时间字符串转换为本地时间戳。"""
struct_t = time.strptime(time_str, format_str)
return time.mktime(struct_t)

## 示例
ts_now = time.time()
print(f"当前时间戳: {ts_now}")

formatted_str = timestamp_to_str(ts_now)
print(f"时间戳 -> 字符串: {formatted_str}")

back_to_ts = str_to_timestamp(formatted_str)
print(f"字符串 -> 时间戳: {back_to_ts}")

datetime 模块

datetime 模块是 Python 中处理日期和时间的更高级、更面向对象的接口。它定义了几种类,用于处理日期、时间、日期时间、时间差以及时区信息。相比 time 模块,datetime 模块的功能更强大,使用更直观,尤其适合进行复杂的日期时间运算和时区处理。

datetime 模块核心类/方法
类/方法描述
datetime.date(year, month, day)表示一个理想化的日期对象,具有 year, month, day 属性。
date.today()(类方法) 返回当前的本地日期。
date.fromtimestamp(timestamp)(类方法) 根据给定的时间戳返回本地日期。
datetime.time(hour, minute, second, ...)表示一个理想化的时间对象,具有 hour, minute, second, microsecond, tzinfo 属性。
datetime.datetime(year, month, day, ...)表示日期和时间的组合,包含 datetime 对象的所有信息。
datetime.now([tz])(类方法) 返回当前的本地日期和时间。如果提供了可选参数 tz (一个 tzinfo 对象),则返回对应时区的日期时间。
datetime.utcnow()(类方法) 返回当前的UTC日期和时间,但返回的是一个 naive datetime 对象 (无时区信息)。不推荐使用,请用 datetime.now(timezone.utc) 替代。
datetime.fromtimestamp(timestamp, [tz])(类方法) 根据时间戳返回本地日期和时间。如果提供了 tz,则返回对应时区的日期时间。
datetime.strptime(date_string, format)(类方法) 将符合 format 格式的日期字符串 date_string 解析为 datetime 对象。
obj.strftime(format)(实例方法) 将 date, timedatetime 对象 obj 格式化为字符串。
datetime.timedelta(days, seconds, ...)表示两个日期或时间之间的时间差,可以用于日期时间的加减运算。
datetime.timezone(offset, [name])tzinfo 的一个具体子类,表示一个固定的UTC偏移量。例如 timezone.utc 代表UTC时区。

批注:核心记忆类与方法

  • datetime.datetime: 最核心的类,用于表示精确的日期和时间。
  • datetime.date: 当只需要日期部分时使用。
  • datetime.time: 当只需要时间部分时使用。
  • datetime.timedelta: 非常重要,用于进行日期时间的加减运算。
  • datetime.now(): 获取当前日期时间。
  • datetime.strptime(): 常用,将字符串解析为 datetime 对象。
  • obj.strftime(): 常用,将 datetime 对象格式化为字符串。
  • datetime.timezone.utc: 用于创建UTC时区感知的 datetime 对象。
datetime 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from datetime import *
from print_utils import *

# ======== 日期时间操作示例 ========
print_header("1. 获取当前日期时间")
dt_now = datetime.now() # 本地时区的当前日期时间
print_info(f"当前日期时间 (datetime.now()): {dt_now}")
print_info(f" 年份: {dt_now.year}, 月份: {dt_now.month}, 日期: {dt_now.day}")
print_info(f" 小时: {dt_now.hour}, 分钟: {dt_now.minute}, 秒数: {dt_now.second}, 微秒: {dt_now.microsecond}")

print_header("2. 获取当前UTC日期时间")
utc_now = datetime.now(timezone.utc) # 获取UTC时区的当前日期时间
print_info(f"当前UTC日期时间 (datetime.now(timezone.utc)): {utc_now}")

print_header("3. 创建特定的日期时间对象")
special_dt = datetime(2025, 1, 1, 12, 0, 0) # 2025年1月1日12:00:00
print_info(f"特定日期时间对象: {special_dt}")

print_header("4. 创建 date 对象 和 time 对象")
specific_date = date(2025, 10, 20)
print_info(f"特定日期 (date): {specific_date}")
print_info(f" 年份: {specific_date.year}, 月份: {specific_date.month}, 日期: {specific_date.day}")
print_info(f" 星期几 (weekday(), 周一=0): {specific_date.weekday()}") # 周日是6

specific_time = time(12, 0, 0)
print_info(f"特定时间 (time): {specific_time}")
print_info(f" 小时: {specific_time.hour}, 分钟: {specific_time.minute}, 秒数: {specific_time.second}")

print_header("5. 日期时间格式化 (strftime)")
formatted_dt = dt_now.strftime("%Y年%m月%d日 %H时%M分%S秒 星期%A")
print_info(f"格式化后的日期时间: {formatted_dt}")

print_header("6. 日期时间解析 (strptime)")
parsed_dt = datetime.strptime("2025-10-20 12:00:00", "%Y-%m-%d %H:%M:%S")
print_info(f"解析后的日期时间: {parsed_dt}")

print_header("7. 时间差 (timedelta)")
delta1 = timedelta(days=5, hours=3, minutes=30)
print_info(f"时间差: {delta1}")

print_header("8. 日期时间运算")
future_dt = dt_now + delta1 # 当前时间加五天 3小时 30分钟
print_info(f"未来日期时间: {future_dt}")

past_dt = dt_now - timedelta(weeks=2)
print_info(f"当前时间减去2周后: {past_dt}")

# 计算两个日期时间的差
specific_dt = datetime(2025, 10, 20, 12, 0, 0) # 2025年10月20日12:00:00
diff_dt = specific_dt - dt_now
print_info(f"两个datetime对象之间的差: {diff_dt}")
print_info(f" 相差天数: {diff_dt.days}")
print_info(f" 相差总秒数: {diff_dt.total_seconds()}")

print_header("9. datetime 与时间戳转换")
current_timestamp = dt_now.timestamp() # 将本地datetime对象转换为时间戳
print_info(f"当前datetime对象对应的时间戳: {current_timestamp}")

dt_from_ts_local = datetime.fromtimestamp(current_timestamp) # 从时间戳创建本地datetime对象
print_info(f"从时间戳转换回的本地datetime: {dt_from_ts_local}")

dt_from_ts_utc = datetime.fromtimestamp(current_timestamp, tz=timezone.utc) # 从时间戳创建UTC datetime对象
print_info(f"从时间戳转换回的UTC datetime: {dt_from_ts_utc}")

print_header("10. 替换日期时间的某些部分")
replaced_dt = dt_now.replace(year=2030, microsecond=0)
print_info(f"替换年份和微秒后的datetime: {replaced_dt}")

print_header("11. 时区处理")
# 创建一个表示东八区的 timezone 对象 (北京时间)
beijing_tz = timezone(timedelta(hours=8), name='Asia/Shanghai')
beijing_now = datetime.now(beijing_tz)
print_info(f"当前北京时间: {beijing_now}")
print_info(f"北京时间的时区信息: {beijing_now.tzinfo}")

# 将一个naive datetime对象转换为aware datetime对象
naive_dt = datetime(2025, 1, 1, 12, 0, 0)
aware_dt_beijing = naive_dt.replace(tzinfo=beijing_tz) # 假设naive_dt是北京时间
print_info(f"Naive datetime 赋予北京时区: {aware_dt_beijing}")

# 将一个aware datetime对象转换到另一个时区
utc_dt_example = aware_dt_beijing.astimezone(timezone.utc)
print_info(f"北京时间转换为UTC时间: {utc_dt_example}")

datetime 模块开箱即用函数/场景
a. 获取不同精度的当前时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from datetime import datetime, timezone

def get_formatted_current_datetime(fmt="%Y-%m-%d %H:%M:%S.%f", use_utc=False):
"""
获取格式化的当前日期时间字符串。

参数:
fmt (str): strftime 使用的时间格式化字符串。
'%f' 可以用于表示微秒。
use_utc (bool): 如果为True,则返回UTC时间,否则返回本地时间。

返回:
str: 格式化后的当前日期时间字符串。
"""
if use_utc:
now_dt = datetime.now(timezone.utc)
else:
now_dt = datetime.now()
return now_dt.strftime(fmt)

## 示例
print(f"本地当前精确时间: {get_formatted_current_datetime()}")
## 输出示例: 2025-05-12 11:18:35.123456
print(f"UTC当前日期 (YYYY-MM-DD): {get_formatted_current_datetime(use_utc=True)}")
## 输出示例: 2025-05-12 (假设当前UTC日期)
b. 计算未来或过去的日期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from datetime import datetime, timedelta

def calculate_future_past_date(days_offset=0, weeks_offset=0, hours_offset=0, base_date=None):
"""
根据偏移量计算未来或过去的日期时间。

参数:
days_offset (int): 天数偏移,正数表示未来,负数表示过去。
weeks_offset (int): 周数偏移。
hours_offset (int): 小时数偏移。
base_date (datetime, optional): 基准日期时间,默认为当前本地时间。

返回:
datetime: 计算后的日期时间对象。
"""
if base_date is None:
base_date = datetime.now()

offset = timedelta(days=days_offset, weeks=weeks_offset, hours=hours_offset)
return base_date + offset

## 示例
one_week_later = calculate_future_past_date(weeks_offset=1)
print(f"一周后的日期时间: {one_week_later.strftime('%Y-%m-%d %H:%M')}")

three_days_ago_noon = calculate_future_past_date(days_offset=-3, base_date=datetime(2025,5,15,12,0,0))
print(f"三天前的中午12点: {three_days_ago_noon.strftime('%Y-%m-%d %H:%M')}")
c. 安全地解析日期字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from datetime import datetime

def parse_date_string_safely(date_str, date_format="%Y-%m-%d"):
"""
尝试按指定格式解析日期字符串,如果失败则返回None。

参数:
date_str (str): 要解析的日期字符串。
date_format (str): datetime.strptime 使用的日期格式。

返回:
datetime.date or None: 解析成功则返回date对象,否则返回None。
"""
try:
dt_object = datetime.strptime(date_str, date_format)
return dt_object.date() # 通常我们只关心日期部分
except ValueError:
print(f"错误: 字符串 '{date_str}' 无法匹配格式 '{date_format}'")
return None

## 示例
valid_date_str = "2024-12-25"
invalid_date_str = "25/12/2024"

parsed_date1 = parse_date_string_safely(valid_date_str)
if parsed_date1:
print(f"成功解析 '{valid_date_str}': {parsed_date1}")

parsed_date2 = parse_date_string_safely(invalid_date_str) # 格式不匹配
if parsed_date2 is None:
print(f"解析 '{invalid_date_str}' 失败。")

parsed_date3 = parse_date_string_safely("31.01.2023", date_format="%d.%m.%Y")
if parsed_date3:
print(f"成功解析 '31.01.2023': {parsed_date3}")
d. 计算两个日期之间的天数/小时数/分钟数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from datetime import datetime

def get_difference_between_datetimes(dt1_str, dt2_str, fmt="%Y-%m-%d %H:%M:%S"):
"""
计算两个日期时间字符串之间的差异。

参数:
dt1_str (str): 第一个日期时间字符串。
dt2_str (str): 第二个日期时间字符串。
fmt (str): 日期时间字符串的格式。

返回:
dict or None: 包含天数、总小时数、总分钟数、总秒数的字典,如果解析失败返回None。
"""
try:
dt1 = datetime.strptime(dt1_str, fmt)
dt2 = datetime.strptime(dt2_str, fmt)
except ValueError:
print("日期时间字符串格式错误或无效。")
return None

# 确保 dt2 总是大于 dt1,如果不是,则交换它们或返回负值
if dt1 > dt2:
delta = dt1 - dt2
sign = -1 # 表示 dt1 在 dt2 之后
else:
delta = dt2 - dt1
sign = 1

return {
"days": sign * delta.days,
"total_hours": sign * (delta.total_seconds() / 3600),
"total_minutes": sign * (delta.total_seconds() / 60),
"total_seconds": sign * delta.total_seconds(),
"timedelta_object": delta if sign == 1 else -delta
}

## 示例
start_time = "2025-05-10 09:00:00"
end_time = "2025-05-12 11:30:45"

diff = get_difference_between_datetimes(start_time, end_time)
if diff:
print(f"从 '{start_time}' 到 '{end_time}':")
print(f" 相差天数: {diff['days']}")
print(f" 总小时数: {diff['total_hours']:.2f}")
print(f" 总分钟数: {diff['total_minutes']:.2f}")
print(f" 总秒数: {diff['total_seconds']:.0f}")
print(f" Timedelta对象: {diff['timedelta_object']}")

diff_reverse = get_difference_between_datetimes(end_time, start_time)
if diff_reverse:
print(f"\n从 '{end_time}' 到 '{start_time}':") # 注意这里是反向的
print(f" 相差天数: {diff_reverse['days']}") # 将会是负数

timedatetime 模块的选择与注意事项

  • 精度time.time() 返回的时间戳通常具有微秒级精度(取决于操作系统)。datetime 对象也支持微秒。
  • 易用性:对于复杂的日期时间运算、时区处理以及更清晰的API,datetime 模块通常是更好的选择。
  • 底层访问:如果你需要与C库或其他需要时间戳(秒数)的系统交互,time.time() 很直接。
  • 时区处理
    • time 模块对时区的支持有限,time.localtime() 依赖于系统本地时区,time.gmtime() 提供UTC时间。
    • datetime 模块通过 tzinfo 抽象基类及其子类(如 timezone)提供了更健壮的时区处理能力。处理跨时区应用时,强烈建议使用 datetime 并明确指定时区(通常将所有时间转换为UTC存储和处理,仅在显示时转换为本地时区)。
  • 字符串转换strftimestrptime 是这两个模块共有的重要功能,但它们作用的对象类型不同 (struct_time vs datetime 对象)。datetime 对象的 strftime 方法通常更易用。
  • 不可变性date, time, datetime 对象都是不可变的,这使得它们在用作字典键或在多线程环境中更安全。
  • ISO 8601 格式:在进行数据交换时,推荐使用 ISO 8601 标准格式(例如 YYYY-MM-DDTHH:MM:SS.ffffffYYYY-MM-DDTHH:MM:SSZ 表示UTC)。datetime 对象有 isoformat() 方法可以直接生成此格式。

在现代 Python 开发中,除非有特定需求要使用 time 模块的底层功能或性能计数器,否则**datetime 模块通常是进行日期和时间操作的首选**

2、随机数模块详解

在编程中,随机数扮演着重要的角色,可用于模拟、游戏、数据抽样、生成测试数据等多种场景。Python 的标准库通过 random 模块提供了生成伪随机数的工具。

random 模块介绍

random 模块实现了用于各种分布的伪随机数生成器。这些是伪随机数 (Pseudo-random numbers),意味着它们是通过一个确定性的算法生成的,但其序列在统计上看起来是随机的。如果不显式设置“种子”(seed),序列的起点通常是不可预测的。

random 模块常用函数

下表列出了 random 模块中一些核心且常用的函数:

函数描述
random.seed(a=None, version=2)初始化随机数生成器,让随机序列可以被复现。
random.getstate()获取随机数生成器当前的内部状态。
random.setstate(state)恢复随机数生成器到之前保存的状态。
random.random()返回一个 $0.0$ 到 $1.0$ 之间 (不包括 $1.0$) 的随机小数。
random.uniform(a, b)返回一个在 $a$ 和 $b$ 之间的随机小数。
random.randint(a, b)返回一个在 $a$ 和 $b$ 之间 (包括 $a$ 和 $b$) 的随机整数。
random.randrange(start, stop[, step])从一个按步长生成的整数序列中随机选一个数 (不包括 stop)。
random.choice(seq)从一个列表、元组或字符串等序列中随机选择一个元素。
random.choices(population, ..., k=1)从总体中进行 k 次可重复的随机抽样,可以设置权重。
random.sample(population, k)从总体中随机选择 k 个不重复的元素。
random.shuffle(x[, random])将一个列表中的元素顺序随机打乱 (直接修改原列表)。
random.gauss(mu, sigma)生成一个符合正态分布 (高斯分布) 的随机数,mu是平均值,sigma是标准差。
random.expovariate(lambd)生成一个符合指数分布的随机数。

批注:核心记忆函数

  • random.seed(): 需要结果可复现时使用。
  • random.random(): 生成基础随机小数。
  • random.randint(a, b): 非常常用,获取范围内随机整数。
  • random.choice(seq): 非常常用,从序列中随机挑一个。
  • random.sample(population, k): 选取多个不重复项。
  • random.shuffle(x): 打乱列表顺序。
  • random.choices(population, weights=..., k=...): 带权重或可重复抽样时使用。

random 模块代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import random
from print_utils import *

# =========================================================
# 1.设置随机数种子以获得可复现的结果
# =========================================================
print_header("随机数种子设置")
#random.seed(101) # 使用特定的种子 -> 为了下面的测试,这里需要注释掉
print_info(f"使用种子101生成的第一个随机数: {random.random()}")
print_info(f"使用种子101生成的第二个随机数: {random.random()}")

#random.seed(101) # 重置种子 -> 同上,这里需要注释掉
print_info(f"重置种子101后生成的第一个随机数: {random.random()}") # 输出会和上面第一个相同

# =========================================================
# 2.生成 [0.0 1.0] 之间的随机浮点数
# =========================================================
print_header("生成随机浮点数")
print_info(f"生成的随机浮点数: {random.random()}")

# =========================================================
# 3.生成指定范围内的随机浮点数(只有浮点数会变)
# =========================================================
print_header("生成指定范围内的随机浮点数")
rand_float_range = random.uniform(5.5, 15.5)
print_info(f"生成的随机浮点数 (5~15.5): {rand_float_range}")

# =========================================================
# 4. 生成指定范围内的随机整数
# =========================================================
print_header("生成随机范围的随机整数")
rand_int_range = random.randint(10, 20)
print_info(f"生成的随机整数 (10 到 20): {rand_int_range}")

# =========================================================
# 5. randrange指定步长的随机整数
# =========================================================
print_header("生成指定步长的随机整数")
rand_int_step = random.randrange(0, 100, 5) # 从0到100中选择一个5的倍数
print_info(f"生成的随机整数 (0 到 100, 步长5): {rand_int_step}")


# =========================================================
# 6.从序列中随机选择一个元素
# =========================================================
print_header("从序列中随机选择一个元素")
my_colors = ['红', '橙', '黄', '绿', '蓝', '紫']
random_color = random.choice(my_colors)
print(f"颜色列表: {my_colors}")
print(f"随机选择的颜色 (choice): {random_color}")

# =========================================================
# 7. 从序列中随机选择k个元素 (可重复,带权重 - choices)
# =========================================================
print_header("从序列中随机选择k个元素 (可重复)")
elements_pool = ['石头', '剪刀', '布']
element_weights = [0.8, 0.1, 0.1] # 石头有80%的概率被选中,剪刀和布各有10%的概率

# 选择1个 (默认k=1)
chosen_one_weighted = random.choices(elements_pool, weights=element_weights)
print_info(f"带权重选择1个元素 (choices): {chosen_one_weighted}")

# 选择5个可重复的元素
chosen_five_weighted = random.choices(elements_pool, weights=element_weights, k=5)
print_info(f"带权重选择5个可重复元素 (choices): {chosen_five_weighted}")


# =========================================================
# 8.随机打乱新序列元素的顺序
# =========================================================
print_header("随机打乱新序列元素的顺序")
game_players = ['玩家A', '玩家B', '玩家C', '玩家D']
print(f"原始玩家顺序: {game_players}")
random.shuffle(game_players) # 直接修改 game_players
print(f"打乱后的玩家顺序 (shuffle): {game_players}")

random 模块开箱即用函数/场景

a. 生成指定长度的随机字符串(用于非加密场景)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import random
import string

def generate_simple_random_string(length=10, use_letters=True, use_digits=True, use_punctuation=False):
"""
生成一个指定长度的简单随机字符串。

参数:
length (int): 生成字符串的长度。
use_letters (bool): 是否包含大小写字母。
use_digits (bool): 是否包含数字。
use_punctuation (bool): 是否包含标点符号。

返回:
str: 生成的随机字符串。
"""
char_pool = ""
if use_letters:
char_pool += string.ascii_letters
if use_digits:
char_pool += string.digits
if use_punctuation:
char_pool += string.punctuation

if not char_pool: # 如果用户什么都没选,默认只用小写字母
char_pool = string.ascii_lowercase

return ''.join(random.choice(char_pool) for _ in range(length))

# 示例
simple_id = generate_simple_random_string(8)
print(f"生成的8位随机ID (字母数字): {simple_id}")

complex_code = generate_simple_random_string(12, use_punctuation=True)
print(f"生成的12位复杂随机码: {complex_code}")

安全提示:此函数生成的随机字符串不应用于密码、会话令牌等安全敏感的场景。对于这些场景,请务必使用 Python 的 secrets 模块,它能提供密码学强度的随机性。

b. 模拟多次掷骰子并统计结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import random
from collections import Counter

def simulate_dice_rolls(num_rolls=100, num_sides=6):
"""
模拟多次掷骰子并返回每次的结果及统计。

参数:
num_rolls (int): 掷骰子的次数。
num_sides (int): 骰子的面数。

返回:
tuple: (包含所有掷骰结果的列表, 结果计数的Counter对象)
"""
rolls = [random.randint(1, num_sides) for _ in range(num_rolls)]
counts = Counter(rolls)
return rolls, counts

# 示例
rolls_100, counts_100 = simulate_dice_rolls(100)
print(f"\n模拟掷100次6面骰子:")
# print(f" 所有结果: {rolls_100}") # 列表可能太长,选择性打印
print(f" 结果统计: {counts_100}")
print(f" 最常出现的点数: {counts_100.most_common(1)}")
c. 从加权列表中随机抽取奖励
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import random

def get_weighted_reward(rewards_with_weights):
"""
根据权重从奖励列表中随机抽取一个奖励。

参数:
rewards_with_weights (dict): 一个字典,键是奖励,值是对应的权重 (整数或浮点数)。
例如: {'金币': 50, '装备': 30, '药水': 20}

返回:
any: 抽中的奖励。
"""
if not rewards_with_weights:
return None # 或者抛出异常

population = list(rewards_with_weights.keys())
weights = list(rewards_with_weights.values())

return random.choices(population, weights=weights, k=1)[0]

# 示例
daily_rewards = {
'100 金币': 60,
'小体力药剂': 25,
'随机材料包': 10,
'稀有装备碎片': 5
}

print("\n模拟每日签到奖励抽取5次:")
for day in range(1, 6):
reward = get_weighted_reward(daily_rewards)
print(f" 第 {day} 天签到获得: {reward}")
d. 生成一个范围内的随机日期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import random
from datetime import date, timedelta

def get_random_date(start_date_str, end_date_str, date_format="%Y-%m-%d"):
"""
在给定的开始日期和结束日期之间生成一个随机日期。

参数:
start_date_str (str): 开始日期字符串。
end_date_str (str): 结束日期字符串。
date_format (str): 日期字符串的格式。

返回:
datetime.date: 生成的随机日期对象,如果日期解析失败则返回None。
"""
try:
start_dt = date.strptime(start_date_str, date_format)
end_dt = date.strptime(end_date_str, date_format)
except ValueError:
print("日期格式错误或日期无效。")
return None

if start_dt > end_dt:
print("开始日期不能晚于结束日期。")
return None

time_between_dates = end_dt - start_dt
days_between_dates = time_between_dates.days
if days_between_dates < 0: return None # 再次检查以防万一

random_number_of_days = random.randrange(days_between_dates + 1) # 加1使结束日期也可能被选中
random_date = start_dt + timedelta(days=random_number_of_days)

return random_date

# 示例
start = "2024-01-01"
end = "2024-12-31"
random_birthday = get_random_date(start, end)
if random_birthday:
print(f"\n在 {start}{end} 之间随机生成的生日: {random_birthday.strftime('%Y年%m月%d日')}")

invalid_start = "2025-01-01"
get_random_date(invalid_start, end) # 会打印错误信息

3、操作系统交互与文件处理模块

在 Python 编程中,与操作系统进行交互(如管理文件和目录、执行系统命令)以及处理文件(如复制、移动、压缩)是非常常见的任务。Python 标准库为此提供了多个强大的模块,主要包括 osshutilzipfiletarfile

os 模块:与操作系统交互的基础(核心)

os 模块提供了一种可移植的方式来使用操作系统的功能,例如读取或写入文件、操作目录路径、获取环境变量等。它的一个重要子模块是 os.path,专门用于处理路径字符串。

osos.path 常用功能
方法/属性描述
os.getcwd()获取当前Python脚本的工作目录路径。
os.chdir(path)更改当前Python脚本的工作目录到 path
os.listdir(path='.')列出指定目录 path 下的所有文件和文件夹名称。
os.mkdir(path)创建一个名为 path 的新目录 (如果路径中的父目录不存在会失败)。
os.makedirs(path, exist_ok=False)递归创建多级目录,如果 exist_ok=True 则目录已存在时不会报错。
os.remove(path)删除指定路径 path 的文件。
os.rmdir(path)删除指定路径 path 的空目录 (如果目录非空则会失败)。
os.rename(src, dst)重命名文件或目录,从 src 名称改为 dst 名称。
os.environ一个表示环境变量的字典,可以用来获取或设置环境变量。
os.system(command)在系统的shell中执行 command 命令 (谨慎使用,有安全风险)。
os.path.join(path, *paths)强烈推荐:智能地拼接一个或多个路径部分,自动处理不同操作系统的分隔符。
os.path.exists(path)检查指定的路径 path 是否存在 (文件或目录)。
os.path.isfile(path)检查指定的路径 path 是否是一个文件。
os.path.isdir(path)检查指定的路径 path 是否是一个目录。
os.path.getsize(path)获取指定文件 path 的大小 (以字节为单位)。
os.path.basename(path)获取路径 path 中的文件名部分。
os.path.dirname(path)获取路径 path 中的目录部分。
os.path.abspath(path)获取路径 path 的绝对路径。
os.path.splitext(path)将路径 path 分割为 (文件名主体, 扩展名) 的元组。

批注:核心记忆功能 (osos.path)

  • os.getcwd(), os.chdir(): 管理当前工作目录。
  • os.listdir(): 查看目录内容。
  • os.mkdir(), os.makedirs(): 创建目录。
  • os.remove(), os.rmdir(): 删除文件和空目录。
  • os.path.join(): 极其重要,用于跨平台安全地构造路径。
  • os.path.exists(), os.path.isfile(), os.path.isdir(): 判断路径类型。
  • os.path.basename(), os.path.dirname(), os.path.splitext(): 解析路径字符串。
os 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from print_utils import *
import os

# ============== 目录操作 ==============
print_header("目录操作")
current_directory = os.getcwd()
print_info(f"当前工作目录: {current_directory}")

new_dir_name = "my_test_directory"
if not os.path.exists(new_dir_name):
os.mkdir(new_dir_name) # 创建单级目录
print_info(f"目录 '{new_dir_name}' 已创建。")

multi_level_dir = os.path.join("parent_dir", "child_dir") # 推荐使用 os.path.join
os.makedirs(multi_level_dir, exist_ok=True) # 递归创建,存在亦可
print_info(f"多级目录 '{multi_level_dir}' 已创建或已存在。")

print_info(f"当前目录内容: {os.listdir('.')[:5]}") # 列出当前目录内容 (最多显示5个)

# ============== 路径操作 (os.path) ==============
print_header("路径操作 (os.path)")
file_path_str = "/usr/local/bin/python_script.py" # 示例路径
print_info(f"处理路径: '{file_path_str}'")
print_info(f" 目录名: {os.path.dirname(file_path_str)}") # 输出: /usr/local/bin
print_info(f" 文件名: {os.path.basename(file_path_str)}") # 输出: python_script.py
print_info(f" 扩展名: {os.path.splitext(file_path_str)[1]}") # 输出: .py
print_info(f" 分割 (主体, 扩展名): {os.path.splitext(file_path_str)}") # 输出: ('/usr/local/bin/python_script', '.py')

path1 = "projects"
path2 = "my_project"
filename = "main.py"
full_project_path = os.path.join(current_directory, path1, path2, filename)
print_info(f"使用 os.path.join 拼接的路径: {full_project_path}")

# ============== 文件操作 ==============
print_header("文件操作")
test_file_name = "temp_file.txt"
with open(test_file_name, "w") as f:
f.write("Hello from os module example!")

if os.path.exists(test_file_name):
print_info(f"文件 '{test_file_name}' 存在。")
print_info(f" 是否是文件: {os.path.isfile(test_file_name)}")
print_info(f" 文件大小: {os.path.getsize(test_file_name)} 字节")

renamed_file_name = "renamed_temp_file.txt"
os.rename(test_file_name, renamed_file_name) # 重命名
print_info(f"文件已重命名为: '{renamed_file_name}'")

os.remove(renamed_file_name) # 删除文件
print_info(f"文件 '{renamed_file_name}' 已删除。")
else:
print_info(f"文件 '{test_file_name}' 不存在。")

# ============== 环境变量 ==============
print_header("环境变量")
python_path = os.environ.get('PYTHONPATH') # 获取环境变量
print_info(f"环境变量 PYTHONPATH: {python_path if python_path else '未设置'}")
# os.environ['MY_CUSTOM_VAR'] = 'my_value' # 设置环境变量 (仅对当前进程及其子进程有效)

# ============== 执行系统命令 ==============
print_header("执行系统命令")
# print_info("执行系统命令 'ls -l' (或 'dir' on Windows):")
# return_code = os.system("ls -l" if os.name != 'nt' else "dir") # os.name 可以判断操作系统
# print_info(f"命令执行完毕,返回码: {return_code}")
print_info("更好的方式是使用 subprocess 模块,它提供了更强的控制能力和安全性。")

# ============== 清理创建的目录 ==============
# 使用更加优雅的方式去递归删除:(shutil.rmtree)
import shutil
def remove_directory(directory_path):
if os.path.exists(directory_path):
shutil.rmtree(directory_path)
print_info(f"目录 '{directory_path}' 已成功删除。")
else:
print_info(f"目录 '{directory_path}' 不存在。")
remove_directory("parent_dir")

坑点与建议 (os 模块):

  • 路径拼接:务必使用 os.path.join() 来构造路径,它会自动处理不同操作系统(Windows \ vs Linux/macOS /)的路径分隔符,增强代码的可移植性。
  • os.system():尽量避免使用 os.system(),尤其当命令包含用户输入时,可能存在安全风险(命令注入)。对于需要执行外部命令的场景,subprocess 模块是更安全、更灵活的选择。
  • os.remove() vs os.rmdir()os.remove() 用于删除文件,os.rmdir() 用于删除空目录。如果要删除非空目录及其所有内容,请使用 shutil.rmtree()
  • 权限问题:文件和目录操作可能会因为权限不足而失败,应使用 try-except PermissionError 来捕获和处理这类异常。
  • pathlib 模块:对于更现代、面向对象的文件系统路径操作,Python 3.4+ 引入的 pathlib 模块是一个很好的选择,它提供了更直观的 APi,但从使用层面受众程度来说,使用os模块的程序员会占更多数

shutil 模块:高级文件操作

shutil (shell utilities) 模块提供了许多高级的文件和文件集合操作功能,例如复制、移动、删除整个目录树等,是对 os 模块中基本文件操作的补充。

shutil 常用函数
函数描述
shutil.copy(src, dst)复制文件 srcdst (如果 dst 是目录,则复制到该目录下)。
shutil.copy2(src, dst)类似 copy(),但同时会尝试复制文件的元数据 (如权限、时间戳)。
shutil.copyfile(src, dst)仅复制文件内容从 srcdst (不复制元数据)。
shutil.copytree(src, dst, dirs_exist_ok=False)递归地复制整个目录树从 srcdst
shutil.rmtree(path)危险操作:递归地删除整个目录树 path (包括非空目录)。
shutil.move(src, dst)递归地移动文件或目录 srcdst (类似 Unix 的 mv 命令)。
shutil.make_archive(base_name, format, root_dir)创建一个压缩归档文件 (如 zip 或 tar)。

批注:核心记忆函数 (shutil 模块)

  • shutil.copy2(): 复制文件时通常比 copy() 更好,因为它保留元数据。
  • shutil.copytree(): 复制整个文件夹。
  • shutil.rmtree(): 要特别小心使用,用于删除整个文件夹及其内容。
  • shutil.move(): 移动文件或文件夹。
  • shutil.make_archive(): 创建压缩包。
shutil 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from print_utils import *
import os
import shutil

# --- 准备测试环境 ---
src_dir = "shutil_source_dir"
dst_dir_base = "shutil_dest_base"
os.makedirs(src_dir, exist_ok=True)
os.makedirs(dst_dir_base, exist_ok=True)

src_file = os.path.join(src_dir, "original_file.txt")
with open(src_file, "w") as f:
f.write("源文件测试")

print_info(f"准备的源文件: {src_file}")

# 1. 复制文件
# shutil.copy(src, dst) 或 shutil.copy2(src, dst)
# 如果dst是一个目录,文件会被复制到该目录下并保持原名
# 如果dst是一个文件名,则文件会被复制并重命名为dst
copied_file_path = os.path.join(dst_dir_base, "copied_file.txt")
shutil.copy2(src_file, copied_file_path) # copy2 保留更多元数据
print_info(f"文件 '{src_file}' 已使用 copy2 复制到 '{copied_file_path}'")

# 2. 复制目录树
# shutil.copytree(src, dst)
# dst 必须是不存在的目录,copytree 会创建它
copied_tree_path = os.path.join(dst_dir_base, "copied_source_tree")
if os.path.exists(copied_tree_path): # 先清理,防止上次运行残留
shutil.rmtree(copied_tree_path)
shutil.copytree(src_dir, copied_tree_path)
print_info(f"目录树 '{src_dir}' 已复制到 '{copied_tree_path}'")

# 3. 移动文件或目录
# shutil.move(src, dst)
moved_file_destination_dir = os.path.join(dst_dir_base, "moved_files_here")
os.makedirs(moved_file_destination_dir, exist_ok=True)
moved_file_path = shutil.move(copied_file_path, moved_file_destination_dir) # 移动文件
print_info(f"文件 '{copied_file_path}' 已移动到 '{moved_file_path}'") # move返回目标路径

moved_tree_path_new_name = os.path.join(dst_dir_base, "moved_source_tree_new_name")
shutil.move(copied_tree_path, moved_tree_path_new_name) # 移动并重命名目录
print_info(f"目录 '{copied_tree_path}' 已移动并重命名为 '{moved_tree_path_new_name}'")

# 4. 删除目录树
# shutil.rmtree(path) - 非常危险,会无提示删除所有内容
dir_to_delete_completely = os.path.join(dst_dir_base, "temp_delete_me")
os.makedirs(os.path.join(dir_to_delete_completely, "subdir"), exist_ok=True) # 创建一些内容
with open(os.path.join(dir_to_delete_completely, "file.txt"), "w") as f: f.write("temp")
print_info(f"准备删除目录: {dir_to_delete_completely}")
shutil.rmtree(dir_to_delete_completely)
print_info(f"目录 '{dir_to_delete_completely}' 已使用 rmtree 删除。")

# 5. 创建压缩包
# shutil.make_archive(base_name, format, root_dir)
# base_name 是不带扩展名的归档文件名
# format 是归档格式,如 'zip', 'tar', 'gztar', 'bztar'
# root_dir 是要压缩的根目录
archive_output_name = os.path.join(dst_dir_base, "my_archive") # 生成 my_archive.zip
shutil.make_archive(archive_output_name, 'zip', src_dir)
print_info(f"目录 '{src_dir}' 已压缩为 '{archive_output_name}.zip'")

# --- 清理测试环境 ---
shutil.rmtree(src_dir)
shutil.rmtree(dst_dir_base)
print_info("\nshutil 测试环境已清理。")

pathlib 模块:面向对象的文件系统路径

pathlib 模块是 Python 3.4 版本引入的标准库,它将文件系统路径表示为具有方法和属性的实际对象,而不是简单的字符串。这种面向对象的方法使得路径操作更加清晰、易读,并且减少了在处理不同操作系统路径分隔符时可能出现的错误。

pathlib 常用类与方法

核心类是 Path,它同时代表文件和目录路径。

类/属性/方法描述
from pathlib import Path导入核心的 Path 类。
Path(path_str_or_Path_obj, ...)创建一个 Path 对象,可以传入字符串路径或已有的 Path 对象。
Path.cwd()(类方法) 返回一个表示当前工作目录的 Path 对象。
Path.home()(类方法) 返回一个表示用户主目录的 Path 对象。
path_obj / "another_part"使用 / 操作符轻松、跨平台地拼接路径部分。
path_obj.joinpath(*other_paths)另一种拼接路径部分的方法,可以接受多个参数。
path_obj.name(属性) 获取路径的最后一部分 (文件名或目录名)。
path_obj.stem(属性) 获取文件名中不包含最后一个后缀的部分。
path_obj.suffix(属性) 获取路径的最后一个文件扩展名 (例如 .txt)。
path_obj.suffixes(属性) 获取路径中所有文件扩展名的列表 (例如 ['.tar', '.gz'])。
path_obj.parent(属性) 获取路径的直接父目录。
path_obj.parents(属性) 一个序列,包含路径的所有父级目录,直到根目录。
path_obj.exists()检查路径是否存在 (无论是文件还是目录)。
path_obj.is_file()检查路径是否指向一个已存在的文件。
path_obj.is_dir()检查路径是否指向一个已存在的目录。
path_obj.is_symlink()检查路径是否指向一个符号链接。
path_obj.is_absolute()检查路径是否为绝对路径。
path_obj.resolve(strict=False)将路径解析为绝对路径,并处理任何符号链接 (如果 strict=True 且路径不存在则报错)。
path_obj.absolute()返回路径的绝对形式 (不解析符号链接)。
path_obj.mkdir(mode=0o777, parents=False, exist_ok=False)创建此路径表示的目录,parents=True 会创建所有必需的父目录,exist_ok=True 则目录已存在时不报错。
path_obj.rmdir()删除此路径表示的空目录。
path_obj.unlink(missing_ok=False)删除此路径表示的文件或符号链接 (如果 missing_ok=True 且文件不存在则不报错)。
path_obj.rename(target)将此路径重命名或移动到 target 路径。
path_obj.replace(target)类似 rename,但在目标已存在时会覆盖它 (如果操作系统支持原子操作)。
path_obj.touch(mode=0o666, exist_ok=True)创建此路径表示的文件 (类似 Unix touch 命令),如果文件已存在则更新其修改时间 (除非 exist_ok=False)。
path_obj.read_text(encoding=None, ...)读取文件内容并将其作为字符串返回。
path_obj.write_text(data, encoding=None, ...)将字符串 data 写入文件 (会覆盖已有内容)。
path_obj.read_bytes()读取文件内容并将其作为字节串返回。
path_obj.write_bytes(data)将字节串 data 写入文件 (会覆盖已有内容)。
path_obj.open(mode='r', ...)以指定模式打开文件,返回一个文件对象 (与内置 open() 类似)。
path_obj.iterdir()返回一个迭代器,产生此目录路径下的所有直接子项 (文件和目录) 的 Path 对象。
path_obj.glob(pattern)在此路径下查找匹配 pattern 的文件和目录,返回一个生成器。
path_obj.rglob(pattern)递归地在此路径及其所有子目录下查找匹配 pattern 的文件和目录,返回一个生成器。
path_obj.stat()返回一个包含此路径元数据信息的 os.stat_result 对象 (如大小、修改时间等)。

批注:核心记忆功能 (pathlib 模块)

  • Path(): 创建 Path 对象是所有操作的开始。
  • / 操作符: 非常常用且直观,用于安全地拼接路径。
  • .exists(), .is_file(), .is_dir(): 判断路径状态。
  • .name, .stem, .suffix, .parent: 方便地获取路径的各个组成部分。
  • .mkdir(parents=True, exist_ok=True): 创建目录的常用组合。
  • .read_text(), .write_text(), .read_bytes(), .write_bytes(): 简单快速地读写文件内容。
  • .iterdir(), .glob(), .rglob(): 遍历和搜索目录内容。
  • 使用 with path_obj.open(...) as f: 进行文件操作,确保文件正确关闭。
pathlib 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
from print_utils import *
from pathlib import Path
import os # 用于获取当前目录等辅助操作
import shutil # 用于演示后清理目录

# =============================================================
# 0. pathlib 模块演示准备
# =============================================================
print_header("pathlib 模块功能演示")
# 创建一个临时的演示工作目录
DEMO_ROOT_DIR_NAME: str = "pathlib_demo_space" # 演示目录名称
DEMO_ROOT_PATH: Path = Path.cwd() / DEMO_ROOT_DIR_NAME # 使用 / 拼接


def setup_demo_environment(base_path: Path) -> None:
"""
准备演示环境:创建目录结构和文件
"""
print_info(f"创建演示环境于 {base_path}")
if base_path.exists():
shutil.rmtree(base_path) # 递归删除清理旧环境
base_path.mkdir(parents=True, exist_ok=True) # Parents=True 确保父目录也创建 exist_ok=True 避免重复创建

(base_path / "documents").mkdir() # 创建 文件 目录
(base_path / "images").mkdir() # 创建 图片 目录
(base_path / "archives").mkdir() # 创建 压缩包 目录

(base_path / "main_script.py").write_text("# 主脚本") # 创建主脚本文件
(base_path / "readme.md").write_text("# 项目说明") # 创建项目说明文件
(base_path / "data.csv").write_text("name,age\nJohn,30\nJane,25") # 创建数据文件
(base_path / "documents" / "report.pdf").write_text("# 报告") # 创建文档文件
(base_path / "images" / "logo.png").write_text("# 图片") # 创建图片文件
(base_path / "archives" / "archive.zip").write_text("# 压缩包") # 创建压缩包文件

# 在documents下递归创建三层目录结构的数字txt文件
import random

# 创建三层目录结构的数字文件
for i in range(3): # 第一层
first_dir = base_path / "documents" / f"level_{i + 1}"
first_dir.mkdir(exist_ok=True)

for j in range(3): # 第二层
second_dir = first_dir / f"sublevel_{j + 1}"
second_dir.mkdir(exist_ok=True)

for k in range(5): # 第三层,每个第三层目录创建5个文件
file_num = random.randint(1, 100)
(second_dir / f"file_{file_num}.txt").write_text(f"# 第{i + 1}层第{j + 1}子层的数字文件 {file_num}")

print_success(f"演示环境准备完成于 {base_path}")


def cleanup_demo_environment(base_path: Path) -> None:
"""
清理演示环境
"""
print_info(f"清理演示环境于 {base_path}")
if base_path.exists():
shutil.rmtree(base_path) # 递归删除演示目录
print_success(f"演示环境清理完成,清理路径为 => {base_path}")


# =============================================================
# 1. Path 对象创建与基本属性
# =============================================================
def demo_path_creation_and_attributes(base_path: Path) -> None:
print_subheader("1. Path 对象创建与基本属性")

# 从字符串创建 Path 对象
p1: Path = Path("/usr/local/bin") # 绝对路径(示例)
p2: Path = base_path / "main_script.py" # 相对路径(示例)

print_info(f"Path 对象 p1: {p1}")
print_info(f"Path 对象 p2: {p2}")

# 基本属性
print_info(f"p1 是否为绝对路径: {p1.is_absolute()}")
print_info(f"p2 是否为绝对路径: {p2.is_absolute()}")

# 下面都以 p2 来做为演示
print_info(f"p2 的文件名: {p2.name}")
print_info(f"p2 的后缀: {p2.suffix}")
print_info(f"p2 的父目录: {p2.parent}")
print_info(f"p2 的祖先目录: {p2.parents}")
print_info(f"p2 的绝对路径: {p2.resolve()}")
print_info(f"p2 的规范化路径: {p2.as_posix()}") # 用于跨平台路径表示
print_info(f"p2 的网络路径: {p2.as_uri()}") # 用于网络路径表示
for i, parent_dir in enumerate(p2.parents):
print_info(f"p2 的第 {i + 1} 个祖先目录: {parent_dir}")

# 当前工作目录和用户主目录
cwd_path: Path = Path.cwd()
home_path: Path = Path.home()
print_info(f"当前工作目录 (Path.cwd()): {cwd_path}")
print_info(f"用户主目录 (Path.home()): {home_path}")


# =================================输出结果=================
r"""
1. Path 对象创建与基本属性
INFO: Path 对象 p1: \usr\local\bin
INFO: Path 对象 p2: D:\python\PythonStudy\pathlib_demo_space\main_script.py
INFO: p1 是否为绝对路径: False
INFO: p2 是否为绝对路径: True
INFO: p2 的文件名: main_script.py
INFO: p2 的后缀: .py
INFO: p2 的父目录: D:\python\PythonStudy\pathlib_demo_space
INFO: p2 的祖先目录: <WindowsPath.parents>
INFO: p2 的绝对路径: D:\python\PythonStudy\pathlib_demo_space\main_script.py
INFO: p2 的规范化路径: D:/python/PythonStudy/pathlib_demo_space/main_script.py
INFO: p2 的网络路径: file:///D:/python/PythonStudy/pathlib_demo_space/main_script.py
INFO: p2 的第 1 个祖先目录: D:\python\PythonStudy\pathlib_demo_space
INFO: p2 的第 2 个祖先目录: D:\python\PythonStudy
INFO: p2 的第 3 个祖先目录: D:\python
INFO: p2 的第 4 个祖先目录: D:\
INFO: 当前工作目录 (Path.cwd()): D:\python\PythonStudy
INFO: 用户主目录 (Path.home()): C:\Users\Prorise
"""


# ==============================输出结果====================================


# =============================================================
# 2. 路径拼接与解析
# =============================================================


def demo_path_joining_and_resolution(base_path: Path) -> None:
"""演示路径的拼接和解析。"""
print_subheader("2. 路径拼接与解析")

part1 = base_path
part2: str = "documents"
part3: str = "report_final.docx"

# 使用 / 操作符拼接
joined_path1 = Path = part1 / part2 / part3
print_info(f"使用 / 操作符拼接: {joined_path1}")

# 使用 joinPath () 方法拼接
joined_path2 = part1.joinpath(part2, part3)
print_info(f"使用 joinPath () 方法拼接: {joined_path2}")

# 获取绝对路径
# 前面我们创建了 base_path / "readme.md" 所以他应该是存在的
readme_path: Path = base_path / "readme.md"
if readme_path.exists():
print_info(f" 'readme.md' 的相对路径: {readme_path}")
print_info(f" 'readme.md' 的绝对路径 (absolute()): {readme_path.absolute()}")
print_info(f" 'readme.md' 的解析后绝对路径 (resolve()): {readme_path.resolve()}")
else:
print_warning(f" 'readme.md' 不存在,无法进行绝对路径解析。")


# 输出结果
r"""
INFO: 使用 / 操作符拼接: D:\python\PythonStudy\pathlib_demo_space\documents\report_final.docx
INFO: 使用 joinPath () 方法拼接: D:\python\PythonStudy\pathlib_demo_space\documents\report_final.docx
INFO: 'readme.md' 的相对路径: D:\python\PythonStudy\pathlib_demo_space\readme.md
INFO: 'readme.md' 的绝对路径 (absolute()): D:\python\PythonStudy\pathlib_demo_space\readme.md
INFO: 'readme.md' 的解析后绝对路径 (resolve()): D:\python\PythonStudy\pathlib_demo_space\readme.md
"""


# =============================================================
# 3. 检查路径类型和状态
# =============================================================
def demo_path_type_checks(base_path: Path) -> None:
"""演示检查路径的类型和状态。"""
print_subheader("3. 检查路径类型和状态")

file_p: Path = base_path / "readme.md"
dir_p: Path = base_path / "documents"
non_existent_p: Path = base_path / "non_existent_file.tmp"

print_info(f"路径 '{file_p.name}':")
print(f" - 是否存在 (exists()): {file_p.exists()}") # True
print(f" - 是否是文件 (is_file()): {file_p.is_file()}") # True
print(f" - 是否是目录 (is_dir()): {file_p.is_dir()}") # False

print_info(f"路径 '{dir_p.name}':")
print(f" - 是否存在 (exists()): {dir_p.exists()}") # True
print(f" - 是否是文件 (is_file()): {dir_p.is_file()}") # False
print(f" - 是否是目录 (is_dir()): {dir_p.is_dir()}") # True

print_info(f"路径 '{non_existent_p.name}':")
print(f" - 是否存在 (exists()): {non_existent_p.exists()}") # False


# =============================================================
# 4. 文件和目录操作
# =============================================================
def demo_file_directory_operations(base_path: Path) -> None:
"""演示文件的创建、读写和目录的创建、遍历。"""
print_subheader("4. 文件和目录操作")

# 创建新目录
new_sub_dir: Path = base_path / "new_app_data" / "config"
try:
# parents=True: 如果父目录不存在,则一并创建
# exist_ok=True: 如果目录已存在,不抛出错误
new_sub_dir.mkdir(parents=True, exist_ok=True)
print_success(f" 目录 '{new_sub_dir}' 已创建 (或已存在)。")

# 在新目录中创建并写入文件
config_file: Path = new_sub_dir / "settings.json"
sample_json_data: str = '{"theme": "dark", "version": "1.0"}'
config_file.write_text(sample_json_data, encoding="utf-8")
print_success(f" 文件 '{config_file.name}' 已创建并写入内容。")

# 读取文件内容
read_data: str = config_file.read_text(encoding="utf-8")
print_info(f" 从 '{config_file.name}' 读取的内容: {read_data}")

# 重命名文件
renamed_config_file: Path = new_sub_dir / "settings_v2.json"
config_file.rename(renamed_config_file)
print_success(f" 文件已重命名为: '{renamed_config_file.name}'")
print_info(f" 旧路径 '{config_file}' 是否还存在: {config_file.exists()}") # False
print_info(f" 新路径 '{renamed_config_file}' 是否存在: {renamed_config_file.exists()}") # True

# 删除文件
renamed_config_file.unlink(missing_ok=True) # missing_ok=True: 如果文件不存在不报错
print_warning(f" 文件 '{renamed_config_file.name}' 已删除。")

# 删除空目录
if new_sub_dir.is_dir(): new_sub_dir.rmdir() # 只能删除空目录
print_warning(f" 空目录 '{new_sub_dir}' 已删除。")
# 如果要删除包含父目录的整个空结构,需要逐级删除或使用 shutil.rmtree 建议直接使用 shutil.rmtree
if new_sub_dir.parent.is_dir() and not list(new_sub_dir.parent.iterdir()):
new_sub_dir.parent.rmdir()
print_warning(f" 空目录 '{new_sub_dir.parent}' 已删除。")


except Exception as e:
print_error(f" 文件/目录操作时发生错误: {e}")


# =============================================================
# 5. 目录遍历与文件搜索 (glob, rglob, iterdir)
# =============================================================
def demo_directory_iteration(base_path: Path) -> None:
"""演示遍历目录内容和使用 glob 搜索文件。"""
print_subheader("5. 目录遍历与文件搜索")

print_info(f"遍历 '{base_path / 'documents'}' 目录 (iterdir()):")
docs_dir: Path = base_path / "documents"
if docs_dir.is_dir():
for item in docs_dir.iterdir(): # 只遍历当前目录下的内容
item_type = "[目录]" if item.is_dir() else "[文件]"
print(f" {item_type} {item.name}")

# 使用 glob 搜索当前目录下的 .txt 文件
print_info(f"\n在 '{base_path / 'documents'}' 目录下搜索所有 .txt 文件 (glob('*.txt')):")
for txt_file in docs_dir.glob("*.txt"):
print(f" [文件] {txt_file.name}") # 这里按照道理来说他只能访问到level的目录,他的里面是下一级的子目录,所以是没有文件的

# 使用 rglob 递归搜索所有子目录中的 .txt 文件
print_info(f"\n在 '{base_path / 'documents'}' 及其所有子目录下搜索 .txt 文件 (rglob('*.txt')):")
for txt_file in docs_dir.rglob("*.txt"): # 而 rglob 可以递归搜索所有子目录中的文件
# 显示相对于 docs_dir 的路径
rel_path = txt_file.relative_to(docs_dir)
print(f" [文件] {rel_path}")


if __name__ == '__main__':
# 准备演示环境
setup_demo_environment(DEMO_ROOT_PATH)

# 清理演示环境
# cleanup_demo_environment(DEMO_ROOT_PATH)

# 演示Pathlib的所有操作属性
# demo_path_creation_and_attributes(DEMO_ROOT_PATH)

# 演示路径拼接与解析
# demo_path_joining_and_resolution(DEMO_ROOT_PATH)

# 演示检查路径的类型和状态
# demo_path_type_checks(DEMO_ROOT_PATH)

# 演示文件和目录操作
# demo_file_directory_operations(DEMO_ROOT_PATH)

# 演示目录遍历与文件搜索
demo_directory_iteration(DEMO_ROOT_PATH)

pathlib 模块开箱即用场景
a. 递归查找指定类型的文件并处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from print_utils import *
from pathlib import Path
from typing import List, Generator # 用于类型注解

def find_files_by_extension(directory: Path, extension: str) -> Generator[Path, None, None]:
"""
递归查找指定目录下具有特定扩展名的所有文件。

参数:
directory (Path): 要搜索的根目录。
extension (str): 文件扩展名 (例如: ".txt", ".jpg"),注意包含点。

返回:
Generator[Path, None, None]: 一个生成器,逐个产生找到的文件路径对象。
"""
print_info(f"在目录 '{directory}' 中递归查找所有 '{extension}' 文件...")
if not directory.is_dir():
print_error(f"错误: '{directory}' 不是一个有效的目录。")
return

# Path.rglob() 返回一个生成器
for file_path in directory.rglob(f"*{extension}"): # 例如 "*.txt"
if file_path.is_file(): # 确保是文件,因为 rglob 也可能匹配到带 .txt 后缀的目录名
yield file_path

if __name__ == "__main__": # 仅在此场景独立运行时执行
# 创建一些临时文件用于演示 find_files_by_extension
_demo_search_path = Path.cwd() / "pathlib_find_demo"
_demo_search_path.mkdir(exist_ok=True)
(_demo_search_path / "doc1.txt").write_text("text content 1")
(_demo_search_path / "image.png").touch()
(_demo_search_path / "subfolder").mkdir(exist_ok=True)
(_demo_search_path / "subfolder" / "doc2.txt").write_text("text content 2")
(_demo_search_path / "subfolder" / "another.log").touch()
(_demo_search_path / "archive.txt.gz").touch() # 注意这个后缀是 .gz

print_header("find_files_by_extension 函数演示")
found_files: List[Path] = list(find_files_by_extension(_demo_search_path, ".txt"))

if found_files:
print_success(f"找到以下 .txt 文件 (共 {len(found_files)} 个):")
for f_path in found_files:
print(f" - {f_path}")
else:
print_warning("未找到任何 .txt 文件。")

# 清理
import shutil
if _demo_search_path.exists():
shutil.rmtree(_demo_search_path)
print_info(f"已清理演示目录: {_demo_search_path}")
b. 安全地创建多级目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from print_utils import *
from pathlib import Path
from typing import Union # 用于类型注解

def ensure_directory_exists(dir_path: Union[str, Path]) -> Path:
"""
确保指定的目录路径存在,如果不存在则创建它 (包括所有必需的父目录)。

参数:
dir_path (Union[str, Path]): 要确保存在的目录路径 (可以是字符串或Path对象)。

返回:
Path: 对应的Path对象。

引发:
OSError: 如果路径存在但不是一个目录,或者创建失败且非权限问题。
"""
path_obj = Path(dir_path)
print_info(f"检查并确保目录 '{path_obj}' 存在...")
try:
path_obj.mkdir(parents=True, exist_ok=True)
# parents=True: 如果父目录不存在,一并创建。
# exist_ok=True: 如果目录已存在,不引发 FileExistsError。
print_success(f"目录 '{path_obj}' 现在已存在。")
return path_obj
except OSError as e: # 更广泛地捕获可能的OS错误,如权限问题
print_error(f"创建目录 '{path_obj}' 时发生错误: {e}")
raise # 将异常重新抛出,让调用者处理

if __name__ == "__main__": # 仅在此场景独立运行时执行
print_header("ensure_directory_exists 函数演示")

new_data_path_str = "project_alpha/raw_data/year_2025"

# 演示使用字符串路径
created_path_obj: Path = ensure_directory_exists(new_data_path_str)
print_info(f"返回的 Path 对象: {created_path_obj}")
print_info(f" 该路径是否是目录: {created_path_obj.is_dir()}")

# 演示使用已存在的路径 (应该不会报错)
ensure_directory_exists(created_path_obj)

# 清理 (如果需要)
import shutil
# 要删除 project_alpha,需要先删除其父目录 (如果它是唯一的)
# 或者直接删除 project_alpha (如果它是顶层创建的)
top_level_created_dir = Path("project_alpha")
if top_level_created_dir.exists():
shutil.rmtree(top_level_created_dir)
print_info(f"已清理演示目录: {top_level_created_dir}")

坑点与建议 (pathlib 模块):

  • 面向对象思维转变: 如果你习惯了 os.path 中基于字符串的函数式调用 (例如 os.path.join(path, name)), 切换到 pathlib 的面向对象方式 (例如 Path(path) / name) 可能需要一点时间适应,但适应后通常会觉得更自然和强大。
  • 性能考虑: 在绝大多数应用场景下,pathlib 的性能与 os.path 相当,甚至在某些情况下由于内部优化可能更快。只有在对路径操作进行极端密集的循环(例如,每秒处理数百万个路径)时,才可能需要进行性能分析比较。对于日常脚本和大多数应用程序,pathlib 带来的可读性和易用性提升远比微小的性能差异更重要。
  • 与旧代码的互操作性: Path 对象实现了 os.PathLike 接口,这意味着在很多接受字符串路径作为参数的内置函数和标准库函数中(包括 open(), os.chdir(), shutil 的很多函数等),你可以直接传递 Path 对象,Python 会自动将其转换为字符串路径。这使得逐步迁移到 pathlib 或在现有代码中引入它变得容易。
  • 绝对路径 vs. 相对路径: 要清楚你的 Path 对象代表的是绝对路径还是相对路径。resolve() 方法可以获取一个路径的绝对、规范化形式,并解析所有符号链接。absolute() 则简单地将相对路径转换为绝对路径,但不解析符号链接。
  • 文件系统操作的原子性: 像 Path.rename()Path.replace() 这样的操作,其原子性(即操作要么完全成功,要么完全不执行,不会留下中间状态)取决于底层操作系统和文件系统的支持。
  • 异常处理: 所有执行实际文件系统I/O操作(如创建、删除、读写文件/目录)的 pathlib 方法都可能引发 OSError 的各种子类异常 (如 FileNotFoundError, PermissionError, FileExistsError)。务必使用 try-except 块来妥善处理这些潜在的错误。

pathlib 提供了一种更现代、更 Pythonic 的方式来与文件系统路径交互。一旦熟悉了它的面向对象特性,很多路径相关的编程任务都会变得更加简单和愉快。

zipfile 模块:处理 ZIP 压缩文件

zipfile 模块用于创建、读取、写入、追加和列出 ZIP 文件的内容。通常与 with 语句一起使用以确保文件正确关闭。

zipfile 常用功能 (基于 ZipFile 对象)
方法/属性描述
ZipFile(file, mode='r', ...)打开或创建一个 ZIP 文件 (mode: 'r’读, 'w’写, 'a’追加)。
zf.namelist()返回 ZIP 文件中所有成员的名称列表。
zf.infolist()返回 ZIP 文件中所有成员的 ZipInfo 对象列表 (包含详细信息)。
zf.getinfo(name)获取指定成员 nameZipInfo 对象。
zf.extract(member, path=None)从 ZIP 文件中提取单个成员 member 到指定路径 path
zf.extractall(path=None, ...)从 ZIP 文件中提取所有成员到指定路径 path
zf.write(filename, arcname=None)将文件 filename 写入 ZIP 文件,可选的 arcname 是在压缩包中的名称。
zf.read(name)读取 ZIP 文件中成员 name 的内容并返回字节串。
zf.close()关闭 ZIP 文件 (推荐使用 with 语句自动管理)。

批注:核心记忆功能 (zipfile 模块)

  • 使用 with zipfile.ZipFile(...) as zf: 打开文件。
  • zf.write(): 向压缩包中添加文件。
  • zf.extractall(): 解压整个压缩包。
  • zf.namelist(): 查看压缩包内容。
zipfile 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import zipfile
import os

# --- 准备测试文件 ---
zip_test_dir = "zip_test_files"
os.makedirs(zip_test_dir, exist_ok=True)
file1_path = os.path.join(zip_test_dir, "doc1.txt")
file2_path = os.path.join(zip_test_dir, "image.jpg") # 模拟文件
with open(file1_path, "w") as f: f.write("This is document one.")
with open(file2_path, "w") as f: f.write("Fake image data.")

zip_filename = "my_documents.zip"

# 1. 创建 ZIP 文件并添加文件
print(f"创建 ZIP 文件: {zip_filename}")
with zipfile.ZipFile(zip_filename, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.write(file1_path, arcname="text_files/document1.txt") # arcname 指定在zip中的路径和名称
zf.write(file2_path, arcname="images/photo.jpg")
zf.writestr("manuals/readme.txt", "This is a readme file created directly in zip.") # 直接写入字符串内容
print("ZIP 文件创建完成。")

# 2. 读取 ZIP 文件内容
print(f"\n读取 ZIP 文件 '{zip_filename}' 的内容:")
with zipfile.ZipFile(zip_filename, mode='r') as zf:
print(" 文件列表 (namelist):")
for name in zf.namelist():
print(f" - {name}")

print("\n 文件详细信息 (infolist):")
for zipinfo in zf.infolist():
print(f" - 文件名: {zipinfo.filename}, 大小: {zipinfo.file_size} bytes, 修改日期: {zipinfo.date_time}")

# 读取特定文件内容
try:
readme_content = zf.read("manuals/readme.txt")
print(f"\n 'manuals/readme.txt' 的内容: {readme_content.decode('utf-8')}")
except KeyError:
print(" 'manuals/readme.txt' 未在压缩包中找到。")


# 3. 解压 ZIP 文件
extract_to_dir = "extracted_zip_contents"
os.makedirs(extract_to_dir, exist_ok=True) # 确保解压目录存在
print(f"\n解压 ZIP 文件 '{zip_filename}' 到 '{extract_to_dir}':")
with zipfile.ZipFile(zip_filename, mode='r') as zf:
zf.extractall(path=extract_to_dir)
# # 或者只解压特定文件
# zf.extract("images/photo.jpg", path=os.path.join(extract_to_dir, "specific_image"))
print(f"ZIP 文件解压完成。查看 '{extract_to_dir}' 目录。")

# --- 清理 ---
os.remove(zip_filename)
shutil.rmtree(zip_test_dir)
shutil.rmtree(extract_to_dir)
print("\nzipfile 测试环境已清理。")

tarfile 模块:处理 TAR 归档文件

tarfile 模块用于读取和写入 tar 归档文件,包括使用 gzip, bz2 和 lzma 压缩的 tar 文件。与 zipfile 类似,推荐使用 with 语句。

tarfile 常用功能 (基于 TarFile 对象)
方法/属性描述 (
tarfile.open(name, mode='r', ...)打开或创建一个 TAR 文件 (mode: 'r’读, 'w’写, 'a’追加, ‘r:gz’, 'w:bz2’等)。
tf.getnames()返回归档中所有成员的名称列表。
tf.getmembers()返回归档中所有成员的 TarInfo 对象列表 (包含详细信息)。
tf.getmember(name)获取指定成员 nameTarInfo 对象。
tf.extract(member, path="", ...)从归档中提取单个成员 member 到指定路径 path
tf.extractall(path=".", members=...)从归档中提取所有 (或指定 members) 到路径 path
tf.add(name, arcname=None, ...)将文件或目录 name 添加到归档,可选 arcname 是在归档中的名称。
tf.extractfile(member)返回一个类似文件的对象,用于读取成员 member 的内容 (不实际解压到磁盘)。
tf.close()关闭 TAR 文件 (推荐使用 with 语句自动管理)。

批注:核心记忆功能 (tarfile 模块)

  • 使用 with tarfile.open(...) as tf: 打开文件,注意 mode (如 ‘w:gz’ 创建gz压缩包)。
  • tf.add(): 向归档中添加文件或目录。
  • tf.extractall(): 解压整个归档。
  • tf.getnames(): 查看归档内容。
tarfile 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import tarfile
import os
import shutil # 用于清理

# --- 准备测试文件 ---
tar_test_dir = "tar_test_files"
os.makedirs(tar_test_dir, exist_ok=True)
os.makedirs(os.path.join(tar_test_dir, "subdir"), exist_ok=True) # 创建子目录

file_content1 = "Content for tar file 1."
file_content2 = "Content for tar file 2, in a subdirectory."
path_file1 = os.path.join(tar_test_dir, "data.txt")
path_file2 = os.path.join(tar_test_dir, "subdir", "notes.log")

with open(path_file1, "w") as f: f.write(file_content1)
with open(path_file2, "w") as f: f.write(file_content2)

tar_gz_filename = "my_archive.tar.gz" # 创建 .tar.gz 格式的压缩包

# 1. 创建 TAR 归档文件 (例如 .tar.gz)
print(f"创建 TAR.GZ 文件: {tar_gz_filename}")
# mode 'w:gz' 表示以 gzip 压缩方式写入
with tarfile.open(tar_gz_filename, mode='w:gz') as tf:
tf.add(tar_test_dir, arcname="archive_root") # 添加整个目录,arcname指定其在压缩包内的根名称
# # 或者分别添加文件
# tf.add(path_file1, arcname="text_files/data_document.txt")
# tf.add(path_file2, arcname="logs/important_notes.log")
print("TAR.GZ 文件创建完成。")

# 2. 读取 TAR 归档文件内容
print(f"\n读取 TAR 文件 '{tar_gz_filename}' 的内容:")
with tarfile.open(tar_gz_filename, mode='r:gz') as tf: # 'r:gz' 表示以 gzip 方式读取
print(" 文件列表 (getnames):")
for name in tf.getnames():
print(f" - {name}")

print("\n 文件详细信息 (getmembers):")
for tarinfo in tf.getmembers():
type_str = "DIR" if tarinfo.isdir() else ("FILE" if tarinfo.isfile() else "OTHER")
print(f" - 名称: {tarinfo.name}, 类型: {type_str}, 大小: {tarinfo.size}, 修改时间: {tarinfo.mtime}")

# 读取特定文件内容 (不解压到磁盘)
try:
member_to_read = "archive_root/subdir/notes.log" # 假设这是压缩包内的路径
if member_to_read in tf.getnames():
extracted_file_obj = tf.extractfile(member_to_read)
if extracted_file_obj:
content_bytes = extracted_file_obj.read()
print(f"\n '{member_to_read}' 的内容: {content_bytes.decode('utf-8')}")
extracted_file_obj.close()
else:
print(f" 文件 '{member_to_read}' 在归档中未找到。")

except KeyError as e:
print(f" 读取归档成员时出错: {e}")


# 3. 解压 TAR 归档文件
extract_tar_to_dir = "extracted_tar_contents"
os.makedirs(extract_tar_to_dir, exist_ok=True)
print(f"\n解压 TAR 文件 '{tar_gz_filename}' 到 '{extract_tar_to_dir}':")
with tarfile.open(tar_gz_filename, mode='r:gz') as tf:
tf.extractall(path=extract_tar_to_dir)
print(f"TAR 文件解压完成。查看 '{extract_tar_to_dir}' 目录。")

# --- 清理 ---
os.remove(tar_gz_filename)
shutil.rmtree(tar_test_dir)
shutil.rmtree(extract_tar_to_dir)
print("\ntarfile 测试环境已清理。")

4.系统交互与进程管理模块

在 Python 开发中,与程序运行的底层环境(如操作系统、解释器本身)进行交互,以及启动和管理外部的子进程,是非常常见的需求。sysargparsesubprocess 这三个标准库模块为此提供了强大的支持。

sys 模块:与 Python 解释器深度交互

sys 模块提供了访问和操作 Python 解释器本身以及其运行时环境的变量和函数。通过它,我们可以获取命令行参数、管理模块搜索路径、控制程序退出、了解当前平台信息等。

sys 模块常用属性和函数
属性/方法描述
sys.argv获取脚本启动时传入的命令行参数列表 (第一个是脚本名)。
sys.exit([status])用来终止当前 Python 程序的执行,可以指定一个退出状态码。
sys.path一个列表,指明了 Python 导入模块时会去哪些目录下查找。
sys.platform返回一个字符串,告诉你当前运行 Python 的操作系统平台是什么。
sys.version获取当前 Python 解释器的详细版本信息字符串。
sys.version_info以元组形式提供 Python 版本号的各部分 (主版本、次版本等)。
sys.executable获取当前正在运行的 Python 解释器程序文件的绝对路径。
sys.stdin代表标准输入流的对象,程序可以从中读取数据 (通常来自键盘)。
sys.stdout代表标准输出流的对象,print() 函数默认向这里输出 (通常是屏幕)。
sys.stderr代表标准错误流的对象,用于输出错误和警告信息 (通常是屏幕)。
sys.getrecursionlimit()查询 Python 解释器允许的最大递归调用层数。
sys.setrecursionlimit(limit)修改 Python 解释器允许的最大递归调用层数。
sys.modules一个字典,记录了当前已加载的所有模块及其对象。

批注:核心记忆功能 (sys 模块)

  • sys.argv: 开发命令行工具时,获取用户输入参数的起点。
  • sys.exit(): 控制脚本的退出行为和状态。
  • sys.path: 理解 Python 如何找到模块,偶尔需要动态修改它。
  • sys.platform, sys.version_info: 编写跨平台或版本兼容代码时的判断依据。
  • sys.stdin, sys.stdout, sys.stderr: 对于需要精细控制输入输出的脚本很有用。
sys 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from print_utils import * 
import sys
import os # 用于路径操作示例

# =============================================================
# 0. 模块说明与准备
# =============================================================
print_header("sys 模块功能演示")
print_info("以下示例展示 sys 模块的常用功能。")

# =============================================================
# 1. 命令行参数 (sys.argv)
# =============================================================
print_subheader("1. 命令行参数 (sys.argv)")
# 假设脚本运行时命令为: python your_script.py first_arg 123 --enable-feature
print_info(f"脚本本身的名称 (sys.argv[0]): {sys.argv[0]}")
if len(sys.argv) > 1:
print_info(f"所有命令行参数 (sys.argv): {sys.argv}")
print_info(f"第一个传递的参数 (sys.argv[1]): {sys.argv[1]}")
# 真实场景中,参数解析通常会交给 argparse 模块
else:
print_warning("未提供额外的命令行参数给脚本。")

# =============================================================
# 2. Python 版本和平台信息
# =============================================================
print_subheader("2. Python 版本和平台信息")
print_info(f"Python 完整版本字符串 (sys.version):\n{sys.version}")
print_info(f"Python 版本信息元组 (sys.version_info): {sys.version_info}")
print_info(f" 主版本号 (major): {sys.version_info.major}")
print_info(f" 次版本号 (minor): {sys.version_info.minor}")
print_info(f" 修订版本号 (micro): {sys.version_info.micro}")
print_info(f"当前操作系统平台 (sys.platform): {sys.platform}")
print_info(f"Python 解释器可执行文件路径 (sys.executable):\n {sys.executable}")

# =============================================================
# 3. 模块搜索路径 (sys.path)
# =============================================================
print_subheader("3. 模块搜索路径 (sys.path)")
print_info("Python 解释器查找模块时会依次检查以下路径 (仅显示前5条):")
for i, search_path in enumerate(sys.path[:5]):
print(f" 路径 {i}: {search_path}")

# 示例: 动态添加一个自定义模块目录到 sys.path
# 假设在脚本同级目录下有一个名为 'my_custom_libs' 的文件夹
custom_lib_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'my_custom_libs')
if custom_lib_path not in sys.path:
sys.path.insert(0, custom_lib_path) # 插入到最前面,优先搜索
print_success(f"已将 '{custom_lib_path}' 添加到 sys.path。现在可以尝试导入该目录下的模块。")
# 之后若要移除: sys.path.pop(0) 或者 sys.path.remove(custom_lib_path)
else:
print_info(f"路径 '{custom_lib_path}' 已存在于 sys.path 中。")


# =============================================================
# 4. 标准输入、输出、错误流 (sys.stdin, sys.stdout, sys.stderr)
# =============================================================
print_subheader("4. 标准输入、输出、错误流")
sys.stdout.write(f"{Colors.GREEN} ✔ 这是一条通过 sys.stdout.write 输出的成功消息。{Colors.END}\n")
sys.stderr.write(f"{Colors.FAIL} ❌ 这是一条通过 sys.stderr.write 输出的错误消息。{Colors.END}\n")
# print_info("尝试从 sys.stdin 读取一行 (程序会在此等待输入):")
# try:
# user_input = sys.stdin.readline().strip()
# print_success(f"从 stdin 读取到: '{user_input}'")
# except KeyboardInterrupt:
# print_warning("用户中断了输入。")

# =============================================================
# 5. 递归深度限制
# =============================================================
print_subheader("5. 递归深度限制")
current_recursion_limit = sys.getrecursionlimit()
print_info(f"当前的递归深度限制是: {current_recursion_limit}")
# 谨慎修改递归限制,过高可能导致栈溢出使程序崩溃
# sys.setrecursionlimit(current_recursion_limit + 100)
# print_info(f"修改后的递归深度限制是: {sys.getrecursionlimit()}")
# sys.setrecursionlimit(current_recursion_limit) # 恢复原状

# =============================================================
# 6. 程序退出 (sys.exit)
# =============================================================
print_subheader("6. 程序退出 (sys.exit)")
print_warning("以下 sys.exit() 调用将被注释掉,以允许脚本继续执行。")
# try:
# print_info("准备通过 sys.exit(0) 正常退出程序...")
# sys.exit(0) # 0 通常表示成功
# print_error("这行代码不应被执行,因为 sys.exit() 会引发 SystemExit。")
# except SystemExit as e:
# # SystemExit 默认不会被普通 except Exception 捕获
# print_success(f"程序已通过 SystemExit 退出,退出码: {e.code}")
# # 如果在这里不重新引发或再次调用 sys.exit,程序实际上可能不会完全终止(取决于上下文)

print_info("sys 模块演示结束。")

坑点与建议 (sys 模块):

  • sys.argv 的手动解析: 对于需要多个参数、不同类型参数、可选参数、帮助信息等的命令行工具,手动解析 sys.argv 会变得非常复杂和易错。强烈推荐使用 argparse 模块来处理命令行参数。
  • sys.path 的修改时机与影响: 动态修改 sys.path 主要用于临时解决模块导入问题或在特定环境下加载模块。过度依赖动态修改 sys.path 可能导致项目结构混乱和难以维护。优先考虑使用 Python 的包管理机制、虚拟环境 (如 venv, conda) 和设置 PYTHONPATH 环境变量。
  • sys.exit() 与异常处理: sys.exit() 通过引发 SystemExit 异常来终止程序。这意味着 try...finally 块中的 finally 子句在程序退出前仍会执行,这对于释放资源(如关闭文件、网络连接)非常重要。捕获 SystemExit 异常通常是不必要的,除非你有非常特殊的理由需要在程序退出前执行一些额外的清理逻辑,并且标准的 finally 块无法满足需求。
  • 递归深度的修改: Python 设置递归深度限制是为了防止因无限递归或过深递归耗尽调用栈空间,从而导致程序崩溃。只有在你完全理解递归算法的特性和潜在风险,并且确认需要更大深度时,才应谨慎地使用 sys.setrecursionlimit()。大多数情况下,如果遇到递归深度限制,应首先检查算法是否存在逻辑错误或考虑将其转换为迭代实现。
  • 标准流的编码: sys.stdin, sys.stdout, sys.stderr 的默认编码取决于操作系统和环境设置。在处理非 ASCII 字符时,尤其是在跨平台应用中,要注意编码问题,可能需要显式设置流的编码或使用 PYTHONIOENCODING 环境变量。

argparse 模块:强大的命令行参数解析(核心)

当你的 Python 脚本需要从命令行接收参数时,sys.argv 提供了原始的参数列表。但手动解析 sys.argv 会非常繁琐且容易出错,特别是当参数变多、类型各异、或需要可选参数和帮助信息时。这时,argparse 模块就派上了大用场。它使得创建用户友好的命令行界面变得简单。

argparse 模块核心功能
功能/方法描述
argparse.ArgumentParser(...)创建一个新的参数解析器对象,它是定义和解析参数的起点。
parser.add_argument(name_or_flags, ...)向解析器添加一个参数定义,指定其名称、类型、行为等。
parser.parse_args([args])解析命令行传入的参数 (默认来自 sys.argv[1:]),并返回一个包含参数值的对象。
parser.add_subparsers(...)添加子命令功能,使你的工具可以像 git <command> 一样拥有多个操作模式。
add_argument() 常用参数:
name_or_flags定义参数的名称 (如 'filename') 或标志 (如 '-f', '--file')。
type指定参数应该被自动转换成的目标类型 (如 int, float, str)。
default如果用户在命令行中没有提供该参数,则赋予该参数一个默认值。
help为该参数提供一段简短的描述,会显示在自动生成的帮助信息中。
action定义当参数出现时应执行的动作 (如 'store_true' 用于布尔开关, 'count' 用于计数)。
required=True/False指明一个可选参数 (--option) 是否必须被用户提供。
choices=[...]限制参数的值只能从一个给定的选项列表中选择。
nargs指定该参数可以接受的值的数量 (如 '+' 表示一个或多个)。

批注:核心记忆功能 (argparse 模块)

  • ArgumentParser(): 创建解析器的第一步,可以设置程序描述等。
  • add_argument(): 最核心的方法,用它来定义你的工具接受哪些位置参数和可选参数,以及它们的行为。
  • parse_args(): 调用此方法来实际解析 sys.argv 中的参数,并得到一个包含所有参数值的命名空间对象。
  • 熟悉 add_argument() 的各种选项 (type, default, help, action, nargs, choices) 是用好 argparse 的关键。
  • add_subparsers(): 当你的命令行工具功能较多,需要划分为不同的子命令时(例如 mytool command1 --optionAmytool command2 --optionB),此功能非常有用。
argparse 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from print_utils import *
import argparse
import sys # 用于 parse_args() 的默认行为和模拟输入
import os # 用于文件操作
import shutil # 用于高级文件操作


# =============================================================
# 0. argparse 模块演示准备 - 文件操作工具
# =============================================================

def list_directory(directory, all_files=False):
print(f"列出目录: {directory}")
if all_files:
print("显示所有文件和目录")
for root, dirs, files in os.walk(directory):
print(f"当前目录: {root}")
for file in files:
print(f" 文件: {file}")
for dir in dirs:
print(f" 目录: {dir}")
print("\n")
else:
print("显示目录内容")
for file in os.listdir(directory):
print(f" 文件: {file}")
print("\n")

def delete_file(path):
if os.path.exists(path):
if os.path.isfile(path):
os.remove(path)
print(f"文件已删除: {path}")
elif os.path.isdir(path):
shutil.rmtree(path)
print(f"目录已删除: {path}")



def setup_parser():
# =============================================================
# 1. 创建 ArgumentParser 对象
# =============================================================

# 创建一个解析器对象,用于定义和处理命令行参数
parser = argparse.ArgumentParser(
prog="FileManager", # 程序名称,会显示在帮助信息中
description="一个用于文件操作的命令行工具。", # 程序描述
epilog="--- File Manager Help Footer ---" # 帮助信息的结尾文本
)
# =============================================================
# 2. 添加子命令 (Subparsers) - 不同的文件操作
# =============================================================
# dest:指的是命令行参数的名称
subparsers = parser.add_subparsers(
dest="command",
help="选择要执行的操作",
required=True
)

# =============================================================
# 3. 添加子命令 - 列出目录内容
# =============================================================
parser_list = subparsers.add_parser(
"list",
help="列出目录内容"
)
parser_list.add_argument(
"directory",
help="要列出的目录路径",
default="."
)

parser_list.add_argument(
"--all",
help="显示所有文件和目录",
action="store_true"
)

# =============================================================
# 4. 添加子命令 - 删除文件
# =============================================================
parser_delete = subparsers.add_parser(
"delete",
help="删除文件"
)
parser_delete.add_argument(
"path",
help="要删除的文件路径"
)

return parser


def main():
parser = setup_parser()
args = parser.parse_args()


print(f"""
██████╗ ██████╗ ██████╗ ██████╗ ██╗███████╗███████╗
██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██║██╔════╝██╔════╝
██████╔╝██████╔╝██║ ██║██████╔╝██║███████╗█████╗
██╔═══╝ ██╔══██╗██║ ██║██╔══██╗██║╚════██║██╔══╝
██║ ██║ ██║╚██████╔╝██║ ██║██║███████║███████╗
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝
""")

if args.command == "list":
list_directory(args.directory, args.all)
elif args.command == "delete":
delete_file(args.path)
else:
parser.print_help()


if __name__ == "__main__":
main()

坑点与建议 (argparse 模块):

  • 用户友好性是目标:精心设计你的命令行参数和帮助信息。argparse 能够自动生成规范的帮助文档 (-h--help),这是 CLI 工具易用性的重要组成部分。利用 descriptionepilog 参数美化整体帮助信息,为每个参数提供清晰的 help 文本。
  • 参数类型 (type) 和 choices:充分利用 type 参数进行自动类型转换 (例如,type=int 会将用户输入的字符串 “10” 转换为整数 10)。如果类型转换失败,argparse 会自动向用户报告错误。使用 choices 参数可以限制参数的有效取值范围。
  • 布尔开关 (action='store_true', action='store_false'):对于表示“是/否”状态的选项(例如 --verbose, --quiet, --debug),使用 action="store_true" (如果参数出现,则对应属性值为 True,否则为 False) 或 action="store_false" (如果参数出现,则对应属性值为 False,否则为 True) 非常方便。
  • nargs 的灵活运用
    • nargs='?': 参数可选,如果提供了值则使用该值,否则使用 default 值 (如果未提供值但参数标志存在,则使用 const 值,需与 action 配合)。
    • nargs='*': 零个或多个参数值,收集到一个列表中。
    • nargs='+': 一个或多个参数值,收集到一个列表中 (至少需要一个)。
    • nargs=整数N: 固定需要 N 个参数值。
  • 子命令 (add_subparsers) 的强大之处:当你的工具功能复杂,包含多个独立的操作模式时(例如,像版本控制系统 git 那样,有 clone, commit, push, pull 等不同命令),子命令是组织这些功能的理想方式。每个子命令可以拥有自己的一套参数,使得整体界面更加清晰。务必为子命令设置 dest 参数,以便后续判断用户选择了哪个子命令。
  • 默认值 (default) 的合理设置:为可选参数提供明智的默认值可以减少用户需要输入的参数数量,提升使用体验。帮助信息中可以使用 %(default)s 来自动显示默认值。
  • 互斥参数组 (add_mutually_exclusive_group):当你有一组参数,其中用户在任何时候只能指定一个时(例如,--verbose--quiet 不能同时出现),可以使用互斥参数组来强制这种约束。
  • 参数顺序argparse 通常不关心可选参数 (--option) 在命令行中出现的顺序,但位置参数必须按照它们被 add_argument() 添加的顺序提供。
  • 错误处理argparse 会自动处理很多常见的用户输入错误(如缺少必需参数、参数类型错误、无效选项值等),并向用户显示友好的错误信息和用法提示。你通常不需要自己编写大量的参数验证代码。

argparse 是构建专业、健壮且用户友好的 Python 命令行应用程序的标准工具。投入时间学习和熟练运用它的各种特性,将极大提升你开发 CLI 工具的效率和质量。

subprocess 模块:管理子进程

subprocess 模块允许你创建新的子进程,连接到它们的输入/输出/错误管道,并获取它们的返回码。它是 Python 中执行外部命令和管理子进程的推荐方式,取代了像 os.system(), os.spawn* 等一些旧的模块和函数。

subprocess 常用功能
函数/类/参数描述
subprocess.run(args, ...)推荐:运行 args 指定的命令并等待其完成,返回一个 CompletedProcess 对象。
subprocess.Popen(args, ...)更底层的接口,创建一个子进程对象,可以进行非阻塞操作或更复杂的交互。
args (用于 runPopen)要执行的命令及其参数,通常是一个列表 (如 ['ls', '-l']) 或字符串 (当 shell=True 时)。
capture_output=True (用于 run)如果设为 True,则会捕获子进程的标准输出和标准错误流的内容。
text=True (用于 runPopen)如果设为 True,标准输入、输出和错误流会以文本模式处理 (自动进行编解码)。
check=True (用于 run)如果设为 True 且命令返回非零退出码 (表示错误),则会抛出 CalledProcessError 异常。
shell=True (用于 runPopen)谨慎使用:如果为 True,命令将通过系统的 shell 执行 (可能带来安全风险)。
stdin, stdout, stderr (用于 runPopen)指定子进程的标准输入、输出、错误流的来源或去向 (如 subprocess.PIPE 用于捕获)。
CompletedProcess 对象run() 函数成功完成后的返回值,包含 args, returncode, stdout, stderr 等信息。
Popen.communicate(input=None, timeout=None)与子进程交互:向其 stdin 发送数据,并从其 stdoutstderr 读取所有输出,直到结束。
Popen.wait(timeout=None)等待子进程执行完成,并返回其退出状态码。
Popen.poll()检查子进程是否已经结束,如果结束则返回退出码,否则立即返回 None (非阻塞)。
Popen.terminate() / Popen.kill() / Popen.send_signal(signal)用于向子进程发送信号以终止它 (terminate 较温和,kill 较强制)。

批注:核心记忆功能 (subprocess 模块)

  • subprocess.run(): 首选函数,适用于大多数执行外部命令并等待其完成的场景。它是对 Popen 的高级封装。
  • args 参数的正确形式:当 shell=False (默认且推荐) 时,args 应为列表;当 shell=True 时,args 为字符串。
  • capture_output=Truetext=True: 获取命令输出时非常方便,可以得到解码后的文本。
  • check=True: 使得在命令执行失败时能自动抛出异常,简化错误处理。
  • subprocess.PIPE: 当你需要捕获子进程的输出或向其输入数据时,将其用于 stdout, stderr, stdin 参数。
  • Popen 类:当需要更精细的控制,例如非阻塞 I/O、与进程进行持续的双向交互,或者需要并行运行多个子进程时使用。
subprocess 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
from print_utils import *
import subprocess
import sys # 用于演示平台差异和获取解释器路径
import os # 用于 Popen 示例中的 cwd

# =============================================================
# 0. subprocess 模块演示准备
# =============================================================
print_header("subprocess 模块功能演示")

# =============================================================
# 1. 基本命令执行与等待 (subprocess.run)
# =============================================================
print_subheader("1. 基本命令执行与等待 (subprocess.run)")
# 根据操作系统选择一个简单命令
list_files_command = ['ls', '-la'] if sys.platform != 'win32' else ['dir', '/A']
print_info(f"尝试执行命令: {' '.join(list_files_command)}")

# subprocess.run() 会等待命令完成
# capture_output=True 来捕获标准输出和标准错误
# text=True 使输出以文本形式解码 (通常是 UTF-8 或系统默认编码)
# check=False 表示即使命令返回非0退出码,也不会抛出异常 (我们手动检查)
try:
completed_process_simple = subprocess.run(
list_files_command,
capture_output=True,
text=True, # Python 3.7+,使 stdout/stderr 为字符串
check=False # 手动检查 returncode
)

print_info(f" 命令实际执行: {' '.join(completed_process_simple.args)}")
print_info(f" 命令退出码: {completed_process_simple.returncode}")

if completed_process_simple.returncode == 0:
print_success(" 命令成功执行。")
print_info(" 标准输出 (前5行):")
for line in completed_process_simple.stdout.splitlines()[:5]:
print(f" {line}")
else:
print_warning(f" 命令执行可能存在问题 (退出码非0)。")
if completed_process_simple.stderr:
print_error(" 标准错误输出:")
for line in completed_process_simple.stderr.splitlines()[:5]:
print(f" {line}")
except FileNotFoundError:
print_error(f" 命令 '{list_files_command[0]}' 未找到。请确保它在系统 PATH 中。")
except Exception as e:
print_error(f" 执行命令时发生意外错误: {e}")


# =============================================================
# 2. 自动错误检查 (check=True)
# =============================================================
print_subheader("2. 自动错误检查 (check=True)")
print_info("尝试执行一个不存在的命令,并使用 check=True:")
try:
subprocess.run(
["command_that_does_not_exist_12345"],
check=True, # 如果命令返回非0退出码,会引发 CalledProcessError
capture_output=True, text=True # 捕获输出以便在异常中查看
)
print_success(" (理论上这行不会执行,因为上面命令会失败)")
except FileNotFoundError as e:
print_warning(f" 命令未找到 (FileNotFoundError): {e}")
except subprocess.CalledProcessError as e:
print_warning(f" 命令执行失败 (CalledProcessError): {e}")
print_info(f" 失败命令的返回码: {e.returncode}")
print_info(f" 失败命令的输出 (stdout): {e.stdout.strip() if e.stdout else '无'}")
print_info(f" 失败命令的错误 (stderr): {e.stderr.strip() if e.stderr else '无'}")

# =============================================================
# 3. 向子进程传递输入 (input 参数)
# =============================================================
print_subheader("3. 向子进程传递输入 (input 参数)")
# Linux/macOS: 使用 'grep' 查找包含 'Python' 的行
# Windows: 使用 'findstr' 查找包含 'Python' 的行
search_tool_command = ['grep', 'Python'] if sys.platform != 'win32' else ['findstr', 'Python']
text_to_search = "Hello World\nWelcome to Python scripting\nAnother line with Python here\nEnd of text"

print_info(f"将向 {' '.join(search_tool_command)} 命令输入以下文本:\n'''\n{text_to_search}\n'''")
try:
search_result = subprocess.run(
search_tool_command,
input=text_to_search, # 将此字符串作为子进程的标准输入
capture_output=True,
text=True,
check=False # grep/findstr 找不到时退出码可能非0
)
if search_result.returncode == 0: # 0 通常表示找到匹配
print_success(" 找到匹配的行:")
print(search_result.stdout)
elif search_result.returncode == 1 and not search_result.stderr: # grep/findstr 未找到匹配的典型退出码
print_warning(" 未找到任何匹配 'Python' 的行。")
else: # 其他错误
print_error(f" 命令执行出错或未找到,退出码: {search_result.returncode}")
if search_result.stderr:
print_error(f" 标准错误: {search_result.stderr.strip()}")

except FileNotFoundError:
print_error(f" 命令 '{search_tool_command[0]}' 未找到。")


# =============================================================
# 4. 使用 shell=True (需要格外小心!)
# =============================================================
print_subheader("4. 使用 shell=True (需要格外小心!)")
# shell=True 允许你执行包含 shell 特性(如管道、通配符、环境变量展开)的命令字符串。
# 但如果命令字符串包含任何来自不可信来源的内容,则极易受到命令注入攻击。
# 示例: 安全地获取当前目录下的文件列表 (不推荐,仅为演示 shell 特性)
# 更安全的方式是使用 Python 的 os.listdir() 和 glob 模块
shell_list_command = "ls -a" if sys.platform != 'win32' else "dir /A"
print_warning(f"演示执行 shell 命令: '{shell_list_command}' (通常应避免 shell=True)")
try:
shell_result = subprocess.run(shell_list_command, shell=True, capture_output=True, text=True, check=True)
print_info(" Shell 命令输出 (前5行):")
for line in shell_result.stdout.splitlines()[:5]:
print(f" {line}")
except subprocess.CalledProcessError as e:
print_error(f" Shell 命令执行失败: {e}")
except FileNotFoundError:
print_error(f" Shell 或命令未找到。")


# =============================================================
# 5. 使用 Popen 进行更精细的异步或交互式控制
# =============================================================
print_subheader("5. 使用 Popen 进行更精细控制")
print_info("启动一个 Python 子进程,并通过管道与其交互:")

# 子进程执行的 Python 代码:从标准输入读取一行,转换为大写,然后打印到标准输出
python_child_code = "import sys; line = sys.stdin.readline().strip(); print(f'Child received and uppercased: {line.upper()}')"
try:
# Popen 不会阻塞,子进程会立即开始执行
process = subprocess.Popen(
[sys.executable, "-c", python_child_code], # [解释器, '-c', '代码字符串']
stdin=subprocess.PIPE, # 创建到子进程 stdin 的管道
stdout=subprocess.PIPE, # 创建从子进程 stdout 的管道
stderr=subprocess.PIPE, # 创建从子进程 stderr 的管道
text=True, # 使用文本模式进行 I/O
cwd=os.getcwd() # 可以指定子进程的工作目录
)
print_info(f" Popen 子进程已启动 (PID: {process.pid})。")

# 通过 communicate() 与子进程交互:
# - 向子进程的 stdin 发送数据 (input 参数)
# - 从子进程的 stdout 和 stderr 读取所有输出,直到它们关闭
# - 等待子进程结束
# communicate() 是一次性交互,适合简单场景。
input_to_child = "hello from parent process"
print_info(f" 向子进程发送: '{input_to_child}'")
stdout_data, stderr_data = process.communicate(input=input_to_child, timeout=5) # timeout 防止无限等待

print_info(f" 子进程退出码: {process.returncode}")
if stdout_data:
print_success(f" 子进程标准输出:\n {stdout_data.strip()}")
if stderr_data:
print_warning(f" 子进程标准错误:\n {stderr_data.strip()}")

except subprocess.TimeoutExpired:
print_error(" 与 Popen 子进程交互超时!正在尝试终止...")
process.kill() # 强制终止
stdout_data, stderr_data = process.communicate() # 清理管道
print_warning(" 子进程已被终止。")
except Exception as e:
print_error(f" Popen 交互过程中发生错误: {e}")

print_header("subprocess 模块演示结束。")

5.日志与配置管理模块

在开发应用程序时,通过配置文件管理程序行为以及记录程序运行状态和错误信息(即日志)是至关重要的实践。Python 标准库为此提供了 configparserlogging 两个核心模块。

configparser 模块:读写 INI 配置文件

configparser 模块用于处理结构化的配置文件,其格式类似于 Windows INI 文件。这种文件通常以 .ini.cfg 为扩展名,由一个或多个“节 (section)”组成,每个节内包含若干“键=值 (key=value)”对。它非常适合存储应用程序的设置和参数。

示例配置文件 (example.ini) 内容可能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 这是一个注释行
; 这也是一个注释行

[DEFAULT] ; DEFAULT 节比较特殊,它的值会作为其他节的默认值
app_name = MyApplication
debug_mode = false
timeout = 30

[database]
db_type = mysql
host = 127.0.0.1
port = 3306
username = db_user
password = secret_password # 在实际应用中,密码不应明文存储在配置文件中
db_name = my_database
connection_retries = 3

[user_settings]
theme = dark
language = en-us
notifications_enabled = true
items_per_page = 25
configparser 常用功能
方法/类描述
configparser.ConfigParser(...)创建一个配置文件解析器对象,用于后续的读写操作。
config.read(filenames, encoding=None)从指定的一个或多个文件 filenames 中读取配置信息。
config.sections()返回配置文件中所有节的名称列表 (不包括 DEFAULT 节)。
config.has_section(section_name)检查配置文件中是否存在名为 section_name 的节。
config.options(section_name)返回指定节 section_name 下所有选项 (键) 的名称列表。
config.has_option(section, option)检查指定的 section 中是否存在名为 option 的选项。
config.items(section_name)返回指定节 section_name 下所有选项的 (键, 值) 对列表。
config.get(section, option, ...)获取指定节 section 中选项 option 的值 (始终作为字符串返回)。
config.getint(section, option, ...)获取选项值并将其转换为整数。
config.getfloat(section, option, ...)获取选项值并将其转换为浮点数。
config.getboolean(section, option, ...)获取选项值并将其转换为布尔值 (如 ‘true’, ‘yes’, ‘1’ 会转为 True)。
config.add_section(section_name)在配置中添加一个新的节 section_name
config.set(section, option, value)设置指定节 section 中选项 option 的值为 value (注意 value 必须是字符串)。
config.remove_option(section, option)从指定节 section 中移除选项 option
config.remove_section(section)移除整个节 section 及其所有选项。
config.write(fileobject, ...)将当前的配置数据写入到一个已打开的文件对象 fileobject 中。

批注:核心记忆功能 (configparser 模块)

  • ConfigParser(): 创建实例是第一步。
  • read(filename): 加载配置文件内容。
  • sections(), options(section), items(section): 遍历配置结构。
  • get(section, option): 获取配置值 (字符串)。
  • getint(), getfloat(), getboolean(): 常用,获取特定类型的值。
  • set(section, option, value): 修改或添加配置项。
  • write(file_object): 保存更改到文件。
configparser 模块代码示例 (读取、修改、创建)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
from print_utils import *
import configparser
import os

# =============================================================
# 0. configparser 模块演示准备
# =============================================================
print_header("configparser 模块功能演示")

# 创建一个临时的示例配置文件
temp_config_filename = "userConfig.ini"
temp_config_content = """
[DEFAULT]
name = John Doe
age = 30

[DATABASE]
host = localhost
port = 3306
user = root
password = root
is_connected = True

[USER]
username = admin
password = admin
"""

# 注意 DEFAULT较为特殊,他会给所有的配置名都加上它所定义的键 - 值
with open(temp_config_filename, "w", encoding="utf-8") as f:
f.write(temp_config_content.strip())
print_info(f"已创建临时配置文件: {temp_config_filename}")
# 实例化 ConfigParser
config = configparser.ConfigParser()


def read_config_file(temp_config_filename: str = "userConfig.ini") -> None:
# =============================================================
# 1. 读取配置文件
# =============================================================
print_subheader("1. 读取配置文件")
# read() 方法可以读取一个或多个文件,如果文件不存在,则会忽略
# 返回成功读取的文件列表
files_read = config.read(temp_config_filename, encoding="utf-8")

if not files_read:
print_error(f"错误: 无法读取配置文件 '{temp_config_filename}'")
else:
print_success(f"成功读取配置文件: {files_read}")


def check_config_sections() -> None:
# 查看所有 section (不包括 DEFAULT)
print_info(f"所有节 (不包括 DEFAULT): {config.sections()}") # ['DATABASE', 'USER']

# 查看 DEFAULT 节中的默认值 (可以直接通过 config['DEFAULT'] 访问,或通过 defaults() 方法)
if "DEFAULT" in config: # 也可以用 config.defaults() 访问
print_info("DEFAULT 节中的默认值:")
for key, value in config.defaults().items():
print(f" {key} = {value}")

# 查看特定节 (例如 "DATABASE") 中的所有键值对
database_config = "DATABASE"
if config.has_section(database_config):
print_info(f"'{database_config}' 节的选项 (options): {config.options(database_config)}")

# 获取特定节中特定选项的值
host_value = config.get(database_config, "host") # 返回字符串
port_value_int = config.getint(database_config, "port") # 返回整数
is_connected_value_bool = config.getboolean(database_config, "is_connected") # 返回布尔值

print_info(f" {database_config} 节中的 'host' 值: {host_value} -> {type(host_value)}")
print_info(f" {database_config} 节中的 'port' 值: {port_value_int} -> {type(port_value_int)}")
print_info(
f" {database_config} 节中的 'is_connected' 值: {is_connected_value_bool} -> {type(is_connected_value_bool)}")

# 访问 DEFAULT 节中的值 (如果子节中没有定义,会从 DEFAULT 继承)
default_name = config.get("DEFAULT", "name")
default_age = config.getint("DEFAULT", "age")
print_info(f"DEFAULT 节中的 'name' 值: {default_name} -> {type(default_name)}")
print_info(f"DEFAULT 节中的 'age' 值: {default_age} -> {type(default_age)}")


# =============================================================
# 2. 修改配置文件内容 (在内存中)
# =============================================================
def modify_config_file() -> None:
print_subheader("2. 修改配置文件内容 (在内存中)")
# 修改特定节中的值
if config.has_section("DATABASE"):
config.set("DATABASE", "port", "3307") # 修改端口号
config.set("DATABASE", "is_connected", "False") # 修改连接状态
print_success("已修改 'DATABASE' 节中的值")

# 添加新的选项
config.set("DATABASE", "database", "mydb") # 添加新的数据库名称
print_success("已添加新的选项 'database'")

# 添加新的节
config.add_section("LOGGING")
config.set("LOGGING", "level", "INFO")
print_success("已添加新的节 'LOGGING'")

# 移除一个选项
if config.has_option("DATABASE", "password"):
config.remove_option("DATABASE", "password")
print_success("已移除 'password' 选项")


# =============================================================
# 3. 将修改后的配置写回文件
# =============================================================
def write_config_file() -> None:
print_subheader("3. 将修改后的配置写回文件")
# 写回文件 (注意: 写回文件时,会覆盖原文件内容)
with open(temp_config_filename, "w", encoding="utf-8") as configfile:
config.write(configfile)
print_success(f"已将修改后的配置写回文件: {temp_config_filename}")


# =============================================================
# 4. 以编程方式创建全新的配置文件
# =============================================================
def create_new_config_file() -> None:
print_subheader("4. 以编程方式创建全新的配置文件")
new_config = configparser.ConfigParser()
# 添加新的节和选项
new_config["DEFAULT"] = {
"name": "Alice",
"age": 25
}

# 添加其他节和选项
new_config['WebApp'] = {} # 必须先创建节
web_app_section = new_config['WebApp']
web_app_section['base_url'] = 'https://myapp.com'
web_app_section['api_key'] = 'YOUR_API_KEY_HERE' # 值必须是字符串
web_app_section['debug_port'] = str(9000) # 如果是数字,需要转为字符串

new_config.add_section('FTPCredentials')
new_config.set('FTPCredentials', 'ftp_host', 'ftp.example.com')
new_config.set('FTPCredentials', 'ftp_user', 'ftp_user_01')
new_config.set('FTPCredentials', 'use_passive_mode', 'True') # 布尔值也应是字符串
print_info("为新配置添加了 'WebApp' 和 'FTPCredentials' 节及选项。")

# 将新创建的配置写入文件
created_config_filename = "temp_created_config.ini"
try:
with open(created_config_filename, 'w', encoding="utf-8") as f_new_config:
new_config.write(f_new_config)
print_success(f"新创建的配置已写入到: {created_config_filename}")
except IOError as e:
print_error(f"写入新创建的配置文件失败: {e}")






if __name__ == '__main__':
read_config_file() # 读取配置文件
check_config_sections() # 检查配置文件中的节和选项
modify_config_file() # 修改配置文件内容
write_config_file() # 将修改后的配置写回文件

logging 模块:Python的日志记录系统(核心)

logging 模块是 Python 内置的、功能强大且灵活的日志记录框架。良好的日志记录对于应用程序的调试、监控、问题排查和审计至关重要。它允许开发者根据日志的重要性(级别)将其发送到不同的目的地(如控制台、文件、网络等),并可以自定义日志的格式。

logging 模块基本使用与日志级别

日志级别从低到高表示日志信息的重要性:

日志级别 (Level)数值 (Value)描述
DEBUG10最详细的诊断信息,通常只在调试问题时关心。
INFO20确认程序按预期运行的常规信息。
WARNING30表明发生了意外情况,或在不久的将来可能出现问题,但程序仍能按预期工作。
ERROR40由于更严重的问题,程序未能执行某些功能。
CRITICAL50严重错误,表明程序本身可能无法继续运行。

Python 的 logging 模块默认的日志级别是 WARNING。这意味着,如果你不进行任何配置,只有 WARNING, ERROR, CRITICAL 级别的日志消息会被处理和显示。

logging.basicConfig(**kwargs) 是一个快速配置日志系统的便捷函数,通常在脚本的早期调用。如果root logger已经配置过handlers,它将不起作用。

basicConfig 常用参数描述
level设置日志记录器处理消息的最低级别 (如 logging.DEBUG, logging.INFO)。
format定义日志消息的输出格式字符串。
datefmt如果 format 中包含时间 (%(asctime)s),此参数定义时间的显示格式。
filename指定日志输出到的文件名;如果省略,则输出到控制台 (sys.stderr)。
filemode如果指定了 filename,此参数定义文件的打开模式 (如 'a' 追加, 'w' 覆盖)。

批注:核心记忆功能 (logging 模块 - 基础)

  • 日志级别:理解五个基本级别及其含义。
  • logging.basicConfig(): 进行简单、快速的日志配置的首选方法,尤其适用于小型脚本或应用的初期。
  • logging.debug(), logging.info(), logging.warning(), logging.error(), logging.critical(): 发出不同级别日志消息的函数。
  • 默认级别是 WARNING: 如果不配置,DEBUGINFO 消息不会显示。
logging 模块基础配置代码示例 (basicConfig)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from print_utils import * 
import logging
import os


# =============================================================
# 0. logging 模块基础配置演示
# =============================================================
print_header("logging 模块基础配置 (basicConfig) 演示")

# =============================================================
# 1. 最简单的日志记录 (默认配置)
# =============================================================
def print_logging_basic():
print_subheader("1. 最简单的日志记录 (默认配置)")
# 此时没有调用 basicConfig,日志级别默认为 WARNING,输出到 stderr
logging.debug("这是一条 DEBUG 消息 (默认情况下不显示)。")
logging.info("这是一条 INFO 消息 (默认情况下不显示)。")
logging.warning("这是一条 WARNING 消息 (默认显示)。") # 会以 WARNING:root:消息 的格式输出
logging.error("这是一条 ERROR 消息 (默认显示)。")
logging.critical("这是一条 CRITICAL 消息 (默认显示)。")
print_info("注意:上面 DEBUG 和 INFO 级别的日志在默认情况下不会打印。")

# =============================================================
# 2. 让所有日志级别都能打印出来
# =============================================================
def print_logging_basic_level():
print_subheader("2. 让所有日志级别都能打印出来")
# 配置日志级别为 DEBUG,输出到 stderr
logging.basicConfig(level=logging.DEBUG)
logging.debug("这是一条 DEBUG 消息 (现在显示)。")
logging.info("这是一条 INFO 消息 (现在显示)。")

if __name__ == '__main__':
# print_logging_basic()
print_logging_basic_level()
logging 模块核心组件 (Loggers, Handlers, Formatters)

对于更复杂的日志需求,basicConfig() 可能不够用。这时需要了解 logging 模块的三个核心组件:

  1. Loggers (日志记录器):
  • 它是与应用程序代码直接打交道的对象。
    • 你可以通过 logging.getLogger(name) 获取 Logger 实例。通常使用模块名 __name__作为 logger 名称 (例如 logger = logging.getLogger(__name__)),这样可以方便地根据日志来源模块来配置和过滤日志。
    • Loggers 有层级关系,类似 Python 的包名。例如,名为 app.module1 的 logger 是名为 app 的 logger 的子级。
    • Loggers 可以设置自己的日志级别。如果一个 logger 的级别未设置,它会继承其父级 logger 的级别,最终到 root logger。
    • 日志消息会沿着 logger 层级向上传播 (propagate) 到父级 logger 的 handlers,除非将 logger 的 propagate 属性设为 False
  1. Handlers (处理器):
  • 决定日志消息的最终去向。一个 logger 可以关联多个 handler。
    • 常见的 Handlers 类型包括:
      • StreamHandler: 将日志发送到流,如 sys.stdoutsys.stderr (控制台)。
      • FileHandler: 将日志写入磁盘文件。
      • RotatingFileHandler: 支持日志文件按大小轮转(达到一定大小时创建新文件)。
      • TimedRotatingFileHandler: 支持日志文件按时间间隔轮转(如每天、每小时)。
      • NullHandler: 什么也不做,常用于库模块中,让应用开发者决定如何处理库的日志。
      • 还有用于网络发送 (如 HTTPHandler, SMTPHandler)、系统日志等的 handlers。
    • Handlers 也可以设置自己的日志级别,只处理高于或等于该级别的日志消息。
  1. Formatters (格式化器):
  • 定义最终输出的日志消息的布局和格式。
    • 创建一个 Formatter 对象时,可以指定一个格式字符串,其中包含各种预定义的属性(如 %(asctime)s 时间, %(levelname)s 级别名, %(message)s 日志内容, %(name)s logger名, %(module)s 模块名, %(lineno)d 行号等)。
    • 创建好的 Formatter 对象需要附加到一个或多个 Handler 上。

批注:核心记忆功能 (logging 模块 - 组件)

  • logging.getLogger(name): 获取 Logger 实例,是进行高级日志配置的入口。
  • Handlers 决定日志去哪儿 (控制台、文件、网络等),并可设置级别。
  • Formatters 决定日志长什么样 (时间格式、包含哪些信息等)。
  • Logger -> Handler -> Formatter 是日志消息处理的基本流程。
logging 模块高级配置代码示例 (使用字典配置 dictConfig)

使用字典来配置日志是推荐的方式,因为它清晰、灵活,并且可以从 JSON 或 YAML 等配置文件加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from print_utils import *  # 假设 print_utils.py 可用
import logging
import logging.config
import os
import sys


def setup_advanced_logging():
"""
设置并配置高级日志系统
"""
print_header("logging 模块高级配置 (dictConfig) 演示")

# 创建日志目录
log_directory = "app_logs_adv"
os.makedirs(log_directory, exist_ok=True)
application_log_file = os.path.join(log_directory, "advanced_app.log")
error_log_file = os.path.join(log_directory, "error_only.log")

# 定义日志配置
print_subheader("1. 定义日志配置字典")
logging_config = {
'version': 1, # 配置版本,目前只能是 1
'disable_existing_loggers': False, # 是否禁用现有的日志记录器
# 日志格式化器 (Formatters)
'formatters': {
'verbose': {
'format': '%(asctime)s - %(name)s - [%(levelname)s] - (%(module)s.%(funcName)s:%(lineno)d) - %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'
},
'simple': {
'format': '[%(levelname)s] %(asctime)s: %(message)s',
'datefmt': '%H:%M:%S'
},
},z

# 过滤器 (Filters) - 这里不详细演示,可以用于更细致的日志过滤
'filters': {},

# 处理器 (Handlers)
'handlers': {
'console_stdout': { # 输出到控制台 (标准输出)
'class': 'logging.StreamHandler',
'level': 'DEBUG', # 此 handler 处理 DEBUG 及以上级别
'formatter': 'simple',
'stream': sys.stdout, # 明确指定流
},
'app_file_handler': { # 输出到应用日志文件,带轮转
'class': 'logging.handlers.RotatingFileHandler',
'level': 'INFO', # 此 handler 处理 INFO 及以上级别
'formatter': 'verbose',
'filename': application_log_file,
'maxBytes': 1024 * 1024 * 5, # 5 MB
'backupCount': 3, # 保留3个备份文件
'encoding': 'utf-8',
},
'error_file_handler': { # 单独记录 ERROR 及以上级别到错误日志文件
'class': 'logging.FileHandler', # 简单文件 handler
'level': 'ERROR',
'formatter': 'verbose',
'filename': error_log_file,
'encoding': 'utf-8',
'mode': 'a' # 追加模式
}
},

# 日志记录器 (Loggers)
'loggers': {
'my_app_module': { # 假设我们有一个名为 'my_app_module' 的 logger
'handlers': ['console_stdout', 'app_file_handler', 'error_file_handler'],
'level': 'DEBUG', # 此 logger 捕获 DEBUG 及以上级别
'propagate': False, # 不再向父级 (root) logger 传播,避免重复记录
},
'another_module': {
'handlers': ['console_stdout'],
'level': 'INFO',
'propagate': False,
},
# 可以配置 root logger (所有未明确指定 logger 的日志最终会到这里,如果 propagate 为 True)
# 'root': {
# 'handlers': ['console_stdout'], # 默认情况下 root logger 也有一个 handler
# 'level': 'WARNING',
# }
}
}
print_info("日志配置字典已定义。")

# 应用配置
print_subheader("2. 应用字典配置")
logging.config.dictConfig(logging_config)
print_success("已使用 dictConfig 应用日志配置。")

return logging_config


def demonstrate_logging_usage():
"""
演示如何使用配置好的日志系统
"""
print_subheader("3. 获取并使用 Logger 实例")

# 演示应用模块日志记录
app_logger = logging.getLogger('my_app_module')
print_info("获取 logger: 'my_app_module'")
app_logger.debug("这是来自 'my_app_module' 的 DEBUG 日志。 (应出现在控制台)")
app_logger.info("这是来自 'my_app_module' 的 INFO 日志。 (应出现在控制台和 app_file)")
app_logger.warning("这是来自 'my_app_module' 的 WARNING 日志。 (处理同 INFO)")
app_logger.error("这是来自 'my_app_module' 的 ERROR 日志。 (应出现在控制台、app_file 和 error_file)")
app_logger.critical("这是来自 'my_app_module' 的 CRITICAL 日志。 (处理同 ERROR)")

# 演示其他模块日志记录
other_logger = logging.getLogger('another_module')
print_info("\n获取 logger: 'another_module'")
other_logger.debug("这是来自 'another_module' 的 DEBUG 日志。 (不应出现,因为 other_logger 级别是 INFO)")
other_logger.info("这是来自 'another_module' 的 INFO 日志。 (应出现在控制台)")
other_logger.error("这是来自 'another_module' 的 ERROR 日志。 (应出现在控制台)")

# 演示未配置的日志记录器
unconfigured_logger = logging.getLogger('some.other.unconfigured.module')
print_info("\n获取 logger: 'some.other.unconfigured.module' (将使用 root logger 配置)")
# 如果 root logger 未配置 handlers,可能无输出或输出到默认 stderr (WARNING级别)
# 如果 root logger 在 logging_config 中配置了,则按其配置
unconfigured_logger.warning("这是来自 'unconfigured' logger 的 WARNING。(行为取决于 root logger)")


# 执行日志系统设置和演示
setup_advanced_logging()
demonstrate_logging_usage()

logging 模块使用文件配置 (.ini/.cfg)

除了字典配置,logging 模块也支持从类似 INI 格式的配置文件加载配置,这通过 logging.config.fileConfig() 函数实现。

示例 log.cfg 文件内容 (简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[loggers]
keys=root,module_logger

[handlers]
keys=consoleHandler,fileHandler

[formatters]
keys=simpleFormatter,detailFormatter

[logger_root]
level=WARNING
handlers=consoleHandler

[logger_module_logger]
level=DEBUG
handlers=consoleHandler,fileHandler
qualname=my_app.moduleX # 用于getLogger('my_app.moduleX')
propagate=0 # 不向root传递

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[handler_fileHandler]
class=FileHandler
level=INFO
formatter=detailFormatter
args=('app_from_fileconfig.log', 'a', 'utf-8') # 文件名, 模式, 编码

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=%H:%M:%S

[formatter_detailFormatter]
format=%(asctime)s [%(levelname)-8s] %(name)s (%(filename)s:%(lineno)d) %(message)s
datefmt=%Y-%m-%d %H:%M:%S

使用 fileConfig 加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
from print_utils import *
import logging
import logging.config
import os

# =============================================================
# 0. logging 模块 fileConfig 演示
# =============================================================
print_header("logging 模块 fileConfig 演示")

# 创建一个临时的 log.cfg 文件
temp_log_cfg_content = """
[loggers]
keys=root,sampleLogger

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=WARNING
handlers=consoleHandler

[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleApp
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=[%(levelname)s] %(name)s: %(message)s
datefmt=
"""
cfg_filename = "temp_log_for_fileconfig.cfg"
with open(cfg_filename, "w", encoding="utf-8") as f_cfg:
f_cfg.write(temp_log_cfg_content)
print_info(f"已创建临时日志配置文件: {cfg_filename}")


# =============================================================
# 1. 使用 fileConfig 加载配置
# =============================================================
print_subheader("1. 使用 fileConfig 加载配置")
try:
# 默认情况下,fileConfig 可能对 UTF-8 BOM 或某些编码处理不佳
# disable_existing_loggers=False 确保不会禁用通过 dictConfig 等方式已配置的 logger
logging.config.fileConfig(cfg_filename, disable_existing_loggers=False, encoding='utf-8') # Python 3.10+ 支持 encoding 参数
print_success(f"已从 '{cfg_filename}' 加载日志配置。")

# =============================================================
# 2. 使用配置好的 Logger
# =============================================================
print_subheader("2. 使用配置好的 Logger")
logger_from_file = logging.getLogger('sampleApp') # 'sampleApp' 是 qualname 定义的
logger_from_file.debug("这是一条来自 fileConfig 配置的 DEBUG 日志 (sampleApp)。")
logger_from_file.info("这是一条来自 fileConfig 配置的 INFO 日志 (sampleApp)。")

root_logger_test = logging.getLogger("non_existent_so_root")
root_logger_test.warning("这是一条来自 root logger 的 WARNING 日志 (通过 fileConfig 配置)。")
root_logger_test.debug("这是一条来自 root logger 的 DEBUG 日志 (root 级别是 WARNING,所以不显示)。")

except Exception as e:
print_error(f"使用 fileConfig 加载配置时发生错误: {e}")
print_warning("注意:fileConfig 对编码的处理可能不如 dictConfig 灵活。")
print_warning("如果遇到编码问题,可能需要确保配置文件不含 BOM,或在旧版 Python 中修改 configparser 源码(不推荐)。")

# 清理
if os.path.exists(cfg_filename):
os.remove(cfg_filename)
print_info(f"已删除临时文件: {cfg_filename}")

print_header("fileConfig 演示结束。")

fileConfig 的编码问题:
fileConfig 在 Windows 下处理 UTF-8 文件(尤其是有 BOM 的)时可能存在问题,需要修改 configparser 源码的 read 方法来指定编码。这是一个历史问题。
好消息是:从 Python 3.10 开始,logging.config.fileConfig() 函数增加了 encoding 参数,可以直接指定文件编码,例如 logging.config.fileConfig('log.conf', encoding='utf-8')。对于旧版 Python,如果遇到此问题,更推荐的做法是:

  1. 确保配置文件本身保存为不带 BOM 的 UTF-8。
  2. 或者,考虑将配置迁移到使用 dictConfig,因为 dictConfig 从 Python 字典加载配置,字典可以从任何来源(如 JSON, YAML 文件)以正确的编码读取,从而避免 fileConfig 的编码限制。
logging 模块开箱即用场景
a. 为脚本快速设置日志记录到文件和控制台
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from print_utils import *
import logging
import sys
import os

def setup_script_logging(log_file_name="script.log", console_level=logging.INFO, file_level=logging.DEBUG):
"""
为脚本设置一个简单的日志记录器,同时输出到控制台和文件。
"""
print_info(f"尝试设置日志,输出到控制台 (级别: {logging.getLevelName(console_level)}) 和文件 '{log_file_name}' (级别: {logging.getLevelName(file_level)})")

# 获取根日志记录器或创建一个特定的记录器
# logger = logging.getLogger() # 获取 root logger
logger = logging.getLogger("MyScriptLogger") # 或者使用一个特定的 logger 名称
logger.setLevel(logging.DEBUG) # 设置 logger 的最低捕获级别为 DEBUG,具体输出由 handler 控制

# 防止重复添加 handler (如果此函数可能被多次调用)
if logger.hasHandlers():
print_warning("Logger 已有 handlers,不再重复添加。")
# 或者根据需要移除旧的 handlers:
# for handler in logger.handlers[:]:
# logger.removeHandler(handler)
# return logger # 如果已有,直接返回

# 创建控制台处理器 (StreamHandler)
console_handler = logging.StreamHandler(sys.stdout) # 输出到标准输出
console_handler.setLevel(console_level)
console_formatter = logging.Formatter('%(asctime)s [%(levelname)-7s] %(message)s', datefmt='%H:%M:%S')
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)

# 创建文件处理器 (FileHandler)
try:
# 确保日志目录存在 (如果 log_file_name 包含路径)
log_dir = os.path.dirname(log_file_name)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
print_info(f"创建日志目录: {log_dir}")

file_handler = logging.FileHandler(log_file_name, mode='a', encoding='utf-8') # 'a' for append
file_handler.setLevel(file_level)
file_formatter = logging.Formatter('%(asctime)s - %(name)s - [%(levelname)-8s] (%(filename)s:%(lineno)d) - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
print_success(f"日志将记录到文件: {os.path.abspath(log_file_name)}")
except Exception as e:
print_error(f"设置文件日志处理器失败: {e}")
# 即使文件处理器失败,控制台处理器仍然可能工作

return logger

if __name__ == "__main__": # 仅在此脚本直接运行时执行
print_header("setup_script_logging 函数演示")

# 第一次设置
my_logger = setup_script_logging(log_file_name="logs/my_app_activity.log")

my_logger.debug("这是一条详细的调试信息,用于诊断。")
my_logger.info("应用程序已开始正常运行。")
my_logger.warning("一个非关键性问题发生,但不影响核心功能。")
my_logger.error("处理请求时发生错误,操作失败。")

# 模拟另一个模块使用同一个 logger name
another_part_logger = logging.getLogger("MyScriptLogger")
another_part_logger.info("来自应用另一部分的信息。")

# 尝试再次调用 setup (理想情况下,配置只做一次)
print_info("\n尝试再次调用 setup_script_logging (应提示已有 handlers):")
setup_script_logging() # 应该会提示已有 handlers 而不重复添加 (如果上面 hasHandlers 逻辑生效)

my_logger.info("第二次调用 setup 后的日志。")

print_header("日志记录演示结束。请检查控制台输出和 'logs/my_app_activity.log' 文件。")

Loguru 模块:更简单的日志记录

Loguru 是一个golang语言非常受欢迎的第三方日志库,在Python中也有类似的日志库,其设计目标是让日志记录变得尽可能简单和愉悦,同时又不失强大和灵活。它提供了许多开箱即用的功能,大大减少了配置日志所需的样板代码,尽管我们是介绍标准内置库,但原生logging的配置流程实在是太过繁琐

Loguru 核心特性与常用功能
方法/特性描述
from loguru import logger导入 Loguru 的核心 logger 对象,大多数操作都通过它进行。
logger.add(sink, ...)核心方法:添加一个新的“接收器”(sink)来处理日志消息,如文件、函数或流。
logger.debug("...")logger.critical("...")与标准库类似的日志记录方法,用于发出不同级别的日志消息。
logger.exception("...")记录一条 ERROR 级别的消息,并自动附加当前的异常信息和堆栈跟踪。
logger.catch(exception=Exception, ...)一个装饰器,可以优雅地捕获函数中发生的异常并自动记录它们。
logger.bind(**kwargs)将自定义的键值对数据绑定到后续的日志记录中,方便实现结构化日志。
logger.remove(handler_id=None)移除之前通过 add() 添加的日志处理器 (sink)。
add() 常用参数:
sink日志的输出目标 (文件名字符串、文件对象、可调用对象等)。
level="DEBUG"此 sink 处理的最低日志级别。
format="..."定义此 sink 输出日志的格式字符串 (Loguru 有自己丰富的格式化标记)。
rotation="..."设置日志文件轮转条件 (如 "500 MB", "1 week", "00:00" 每天午夜)。
retention="..."设置旧日志文件的保留策略 (如 "10 days", 5 保留最新的5个文件)。
compression="..."设置日志文件压缩格式 (如 "zip", "gz")。
serialize=True将日志消息序列化为 JSON 格式输出,用于结构化日志。
enqueue=True异步记录日志,将日志消息放入队列中处理,可提高性能。

批注:核心记忆功能 (Loguru 模块)

  • logger.add(): 最重要的方法,几乎所有的日志输出配置都通过它完成,极其灵活。
  • logger.debug(), info(), warning(), error(), critical(): 日常使用的日志记录方法。
  • logger.exception(): 记录错误和异常信息时非常方便。
  • format 参数 (在 add() 中): 灵活定义日志输出格式。
  • rotation, retention, compression 参数 (在 add() 中): 强大的日志文件管理功能。
Loguru 模块代码示例

重要提示: Loguru 是一个第三方库,您需要先安装它:
pip install loguru

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# =========标准库======================
import sys # 用于配置 logger 输出到 stdout
import os # 用于文件路径操作
import time # 用于演示 rotation

# =========三方库======================
import loguru
from loguru import logger # 导入 Loguru
# =========个人库========================
from print_utils import *

# =============================================================
# 0. Loguru 模块演示准备
# =============================================================
print_header("Loguru 模块功能演示")


def demo_loguru():
print_info("Loguru 默认会将日志输出到 stderr,并带有颜色和良好格式。")
# 2025-05-17 16:49:28.178 | INFO | __main__:demo_loguru:21 - 这是默认的日志信息
# 他的格式为:日期 时间 日志级别 模块名:函数名:行号 - 日志内容

# 先演示一下默认的日志
logger.info("这是默认的日志信息")
logger.warning("这是警告信息")
logger.error("这是错误信息")
logger.debug("这是调试信息")
logger.success("这是成功信息")
logger.critical("这是严重错误信息")
logger.exception("这是异常信息")
logger.log("INFO", "这是自定义日志级别")


# =============================================================
# 1. 添加基本控制台输出 (stdout)
# =============================================================
def append_stdout():
print_subheader("1. 添加基本控制台输出 (stdout)")
# 移除 Loguru 的默认处理器 (通常是一个输出到 stderr 的处理器)
# 这样我们可以完全控制日志的输出目的地和格式
logger.remove()
logger.add(
sys.stdout, # Sink:标准输出流
level="DEBUG", # 处理 DEBUG 及以上级别的日志
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
colorize=True, # 启用颜色
)
print_success("已添加一个输出到stdout的Loguru处理器")
logger.info("这是添加了处理器后的日志信息")
logger.debug("这条消息不会被输出到stdout,因为级别低于DEBUG")
logger.warning("这是一个警告信息")
logger.error("这是一个错误信息")
logger.success("这是一个成功信息")
logger.critical("这是一个严重错误信息")


# =============================================================
# 2. 添加文件输出,并配置轮转和保留
# =============================================================
def append_file():
print_subheader("2. 添加文件输出,并配置轮转和保留")
# 移除所有处理器
logger.remove()
log_file_path = "logs/loguru_app_{time:YYYY-MM-DD}.log" # 文件名中可以包含时间格式
error_log_file_path = "logs/loguru_error_{time}.log" # 错误日志
# 确保 logs 目录存在
os.makedirs("logs", exist_ok=True)

logger.add(
log_file_path,
level="DEBUG",
rotation="10 KB", # 当文件达到 10KB 时轮转 (创建新文件)
# 也可以是 "12:00" (每天中午12点轮转), "1 week" (每周轮转)
retention=5, # 最多保留5个轮转后的日志文件
# 也可以是 "1 month" (保留1个月的日志)
compression="zip", # 将轮转后的旧日志文件压缩为 .zip 格式
format="{time:HH:mm:ss} | {level} | {module}.{function} | {message}", # 以时间 级别 模块.函数 消息格式输出
encoding="utf-8"
)
print_success(f"已添加一个输出到 '{log_file_path}' 的文件处理器 (INFO级别, 10KB轮转, 保留5个, zip压缩)。")

logger.add(
error_log_file_path,
level="ERROR", # 此文件只记录 ERROR 及以上级别
format="<red>{time}</red> - {level} - {name}:{function}:{line} - {message}",
rotation="1 MB",
retention="30 days",
encoding="utf-8"
)
print_success(f"已添加一个输出到 '{error_log_file_path}' 的错误日志处理器 (ERROR级别)。")

# 测试轮转
[logger.info(f"这是第 {i} 条日志信息") for i in range(1, 100000)]


# =============================================================
# 4. 使用 @logger.catch 装饰器捕获函数异常
# =============================================================
@logger.catch
def divide(a, b):
return a / b

def demo_catch():
print_subheader("4. 使用 @logger.catch 装饰器捕获函数异常")
divide(1, 0) # 这将触发异常,并被 @logger.catch 捕获
divide(1, 2) # 这不会触发异常

# =============================================================
# 5. 使用 logger.bind() 实现结构化日志 (或添加上下文)
# =============================================================
def demo_bind():
print_subheader("5. 使用 logger.bind() 实现结构化日志 (或添加上下文)")
# 先移除之前的 stdout handler,重新配置一个支持额外参数的
logger.remove() # 移除所有已配置的 handlers
logger.add(
sys.stdout,
level="INFO",
format="{time:HH:mm:ss} | {level: <7} | {extra} | {message}", # {extra} 会显示 bind 的内容
colorize=True
)
print_success("已重新配置 stdout handler 以显示 {extra}。")

user_logger = logger.bind(user_id=123, session_id="abcdef123")
user_logger.info("用户登录成功。") # 这条日志会包含 user_id 和 session_id
user_logger.warning("用户尝试访问未授权资源。", resource_path="/admin") # 也可以在记录时临时绑定

logger.bind(ip="192.168.1.100").info("来自特定IP的请求。")




# =============================================================
# 6. 序列化为 JSON (结构化日志)
# =============================================================
def demo_json():
print_subheader("6. 序列化为 JSON (结构化日志)")
json_log_file = "logs/structured_logs.json"
# 移除所有处理器
logger.remove()
logger.add(
json_log_file,
level="INFO",
serialize=True, # 关键:将日志记录序列化为JSON
format="{time:HH:mm:ss} | {level: <7} | {extra} | {message}", # {extra} 会显示 bind 的内容
rotation="1 MB",
encoding="utf-8"
)
print_success(f"已添加一个输出到 '{json_log_file}' 的 JSON 格式文件处理器。")

logger.bind(transaction_id="txn_789").info(
"订单已处理",
order_id=456,
customer_email="customer@example.com",
amount=99.99,
items_count=3
)
# 打开 structured_logs.json 文件会看到 JSON 格式的日志

if __name__ == '__main__':
# demo_loguru()
# append_stdout()
# append_file()
# demo_catch()
# demo_bind()
demo_json()
Loguru 模块开箱即用场景
a. 快速为脚本设置控制台和文件日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from print_utils import *
from loguru import logger
import sys
import os

def setup_quick_loguru_logging(
console_level="INFO",
log_file="app_activity_{time:YYYY-MM-DD}.log",
file_level="DEBUG",
log_dir="my_app_logs" # 新增日志目录参数
):
"""
快速配置 Loguru,将日志同时输出到控制台和带轮转的文件。
"""
print_info("正在配置 Loguru 日志系统...")

# 确保日志目录存在
os.makedirs(log_dir, exist_ok=True)
full_log_file_path = os.path.join(log_dir, log_file)

logger.remove() # 清除所有旧的 handlers,确保配置纯净

# 配置控制台输出
logger.add(
sys.stderr, # 通常日志输出到 stderr,print 到 stdout
level=console_level.upper(),
format="<level>{level: <8}</level> | <cyan>{time:HH:mm:ss}</cyan> | <level>{message}</level>",
colorize=True
)
print_success(f"Loguru 控制台日志已配置 (级别: {console_level.upper()})。")

# 配置文件输出
logger.add(
full_log_file_path,
level=file_level.upper(),
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message}",
rotation="10 MB", # 每 10MB 轮转
retention="7 days", # 保留最近7天的日志
compression="zip", # 压缩旧日志
encoding="utf-8",
enqueue=True # 异步写入,提高性能
)
print_success(f"Loguru 文件日志已配置 (级别: {file_level.upper()}),路径: '{os.path.abspath(full_log_file_path)}'")

logger.info("Loguru 日志系统初始化完成。")

if __name__ == "__main__":
print_header("快速设置 Loguru 日志场景演示")
setup_quick_loguru_logging(log_dir="project_X_logs") # 指定日志目录

logger.debug("这是一条 DEBUG 级别的调试信息,只有文件日志会记录。")
logger.info("用户 'Alice' 登录了系统。")
logger.warning("配置项 'API_KEY' 未找到,使用默认值。")

try:
items = []
items[0] # 故意引发 IndexError
except IndexError:
logger.exception("处理项目列表时发生错误!")

logger.info("脚本演示部分执行完毕。")
print_header("请检查控制台输出和 'project_X_logs' 目录下的日志文件。")

自此,Python最常用的标准库就介绍完了=>(Python是全世界三方库生态最好的语言),所以掌握一定的标准库即可,剩下的库作为了解或是做底层开发会合适

6.较为不常用实用库

Python 标准库中包含了许多小而精的模块,它们为日常开发中的特定任务提供了便捷的解决方案。本节将介绍几个常用的工具模块:contextlib 用于简化上下文管理,tempfile 用于处理临时文件和目录,glob 用于文件路径的模式匹配,以及 uuid 用于生成通用唯一标识符。

contextlib 模块:简化上下文管理器

上下文管理器是 Python 中一种重要的编程构造,主要通过 with 语句来使用。它能够帮助我们自动获取和释放资源,或者在代码块执行前后执行特定操作(如加锁/解锁,打开/关闭文件)。contextlib 模块提供了一些工具来更方便地创建和使用上下文管理器。

contextlib 常用功能

这个模块最常用的只有**@contextlib.contextmanage**这个装饰器,其他的方法并不常用,简单了解即可

函数/装饰器描述
@contextlib.contextmanager一个装饰器,能让你用简单的生成器函数快速创建支持 with 语句的上下文管理器。
contextlib.closing(thing)为那些有 close() 方法但本身不是上下文管理器的对象创建一个上下文管理器,确保其 close() 被调用。
contextlib.suppress(*exceptions)创建一个上下文管理器,在其管理的 with 代码块中可以忽略指定的几种异常类型。
contextlib.ExitStack()一个上下文管理器,可以动态地注册和管理多个其他的上下文管理器,确保它们都能被正确清理。
contextlib.nullcontext(enter_result=None)一个什么也不做的上下文管理器,有时用于在条件分支中提供一个“空操作”的 with 目标。

批注:核心记忆功能 (contextlib 模块)

  • @contextlib.contextmanager: 非常常用,是将自定义资源管理逻辑封装为上下文管理器的最便捷方式。
  • contextlib.closing(): 当遇到只有 close() 方法的老旧对象时很有用。
  • contextlib.suppress(): 优雅地处理某些预期内且不需要特别处理的异常。
  • contextlib.ExitStack(): 管理多个或动态数量的资源时非常强大。
contextlib 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from print_utils import *
import contextlib
import time
import os
from typing import List, Any # 用于类型提示

# =============================================================
# 0. contextlib 模块演示准备
# =============================================================
print_header("contextlib 模块功能演示")

# =============================================================
# 1. 使用 @contextlib.contextmanager 创建自定义上下文管理器
# =============================================================
print_subheader("1. 使用 @contextlib.contextmanager 创建计时器")

"""
yield 之前的部分 ≈ __enter__ 方法:
当 with 语句开始执行时, @contextlib.contextmanager 装饰的生成器函数中,yield 关键字之前的所有代码会被执行。这部分代码通常用来做一些准备工作,比如打开文件、连接数据库、获取资源等。如果 yield 语句产生了一个值 (例如 yield resource),这个值会赋给 with ... as var 中的 var。

yield 执行的时刻:
程序执行到 yield 时,会将控制权交还给 with 语句块内部的代码,让 with 块中的代码开始执行。

yield 之后的部分 (通常在 finally 块中) ≈ __exit__ 方法:
当 with 语句块中的代码执行完毕(无论是正常结束还是发生异常),程序会回到生成器函数中,从 yield 语句之后的地方继续执行。这部分代码(通常放在 finally 块中以确保其总能执行)用来做清理工作,比如关闭文件、释放资源、回滚事务等。finally 块保证了即使 with 块中发生错误,清理操作也能进行。

简单来说,@contextlib.contextmanager 巧妙地利用了生成器的特性,将一个普通的生成器函数变成了符合上下文管理器协议的对象,使得 yield 前的代码在进入 with 块时运行,yield 后的代码在退出 with 块时运行。这种方式比手动定义一个包含 __enter__ 和 __exit__ 方法的类要简洁得多。

"""

@contextlib.contextmanager
def simple_timer(label: str = "默认计时器"):
start = time.perf_counter() # 获取当前精确的时间戳
try:
yield # yield 之前是 __enter__ 部分,yield 之后是 __exit__ 部分
finally:
end = time.perf_counter()
print(f"{label} 耗时: {end - start:.6f} 秒")
print_info(f"计时器 {label} 结束。耗时: {end - start:.4f} 秒")



# 使用自定义的计时器
with simple_timer("文件读取"):
print_info("模拟读取文件内容...")
time.sleep(2) # 模拟读取文件的耗时


with simple_timer("数据处理"):
print_info("模拟数据处理...")
time.sleep(1) # 模拟数据处理的耗时

tempfile 模块:处理临时文件和目录

在程序运行过程中,有时需要创建一些临时的文件或目录来存储中间数据,这些数据在程序结束或不再需要时应该被自动清理。tempfile 模块为此提供了安全且便捷的解决方案。

tempfile 常用功能
函数/类描述
tempfile.TemporaryFile(mode='w+b', ...)创建一个匿名的临时文件对象 (没有可见的文件名),文件关闭时自动删除。
tempfile.NamedTemporaryFile(mode='w+b', delete=True, ...)创建一个带名字的临时文件,文件关闭时通常也会自动删除 (除非 delete=False)。
tempfile.SpooledTemporaryFile(max_size=0, ...)创建一个在内存中操作的临时文件,当内容超过 max_size 字节时,会自动溢出到磁盘。
tempfile.TemporaryDirectory(suffix=None, ...)创建一个临时目录,当上下文管理器退出或目录对象被垃圾回收时,目录及其内容会被自动删除。
tempfile.gettempdir()获取系统用于存放临时文件的默认目录路径。
tempfile.gettempprefix()获取生成临时文件或目录名称时使用的默认前缀。

批注:核心记忆功能 (tempfile 模块)

  • NamedTemporaryFile(): 当你需要一个临时文件并且需要知道它的文件名时使用,通常配合 with 语句。
  • TemporaryDirectory(): 当你需要一个临时的目录来存放多个文件,并且希望它能自动清理时使用,通常配合 with 语句。
  • TemporaryFile(): 用于完全匿名的临时文件,不需要文件名。
tempfile 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from print_utils import *
import tempfile
import os
import shutil # 用于演示 mkdtemp 的清理

# =============================================================
# 0. tempfile 模块演示准备
# =============================================================
print_header("tempfile 模块功能演示")

# =============================================================
# 1. 获取系统临时目录信息
# =============================================================
print_subheader("1. 获取系统临时目录信息")
system_temp_dir = tempfile.gettempdir()
print_info(f"系统默认的临时目录路径: {system_temp_dir}") # C:\Users\Prorise\AppData\Local\Temp
temp_prefix = tempfile.gettempprefix()
print_info(f"生成临时文件/目录的默认前缀: {temp_prefix}") # 生成临时文件/目录的默认前缀: tmp

# =============================================================
# 2. 创建匿名的临时文件 (TemporaryFile)
# =============================================================
print_subheader("2. 创建匿名的临时文件 (TemporaryFile)")


# TemporaryFile 创建的文件在文件系统上通常没有可见的目录项(文件名)
# 他是一个真正的临时文件,关闭后即删除
def create_temporary_file():
try:
with tempfile.TemporaryFile(mode="w+t", encoding="utf-8") as tf:
print_info(f"临时文件创建成功: {tf.name}")
tf.write("这是一个临时文件的内容")
tf.write("\n这是第二行内容")
tf.seek(0) # 回到文件开头才能读取
content = tf.read()
print_info(f"临时文件内容: {content}")
print_success("匿名临时文件已在 with 块结束时自动关闭并删除")
except Exception as e:
print_error(f"创建临时文件时发生错误: {e}")


# =============================================================
# 3. 创建带名字的临时文件 (NamedTemporaryFile)
# =============================================================
# NamedTemporaryFile 创建的文件在文件系统中有可见的文件名
# 默认情况下 (delete=True),文件关闭时也会被自动删除
def create_named_temporary_file():
try:
with tempfile.NamedTemporaryFile(mode='w+t', encoding='utf-8', prefix='my_app_temp_', suffix='.data',
delete=True) as nf:
print_info(f"带名字的临时文件创建成功: {nf.name}")
nf.write("这是一个带名字的临时文件的内容")
nf.write("\n这是第二行内容")
nf.seek(0) # 回到文件开头才能读取
content = nf.read()
print_info(f"临时文件内容: {content}")
print_success("带名字的临时文件已在 with 块结束时自动关闭")
except Exception as e:
print_error(f"创建带名字的临时文件时发生错误: {e}")


# =============================================================
# 4. 创建临时目录 (TemporaryDirectory)
# =============================================================
def create_temporary_directory():
print_subheader("4.创建临时目录 (TemporaryDirectory)")
try:
with tempfile.TemporaryDirectory() as temp_dir:
print_info(f"临时目录创建成功: {temp_dir}")
# 在临时目录中创建文件
# 可以在这个临时目录中创建文件和子目录
temp_file_in_dir = os.path.join(temp_dir, "temp_file.txt")
with open(temp_file_in_dir, "w") as f_in_dir:
f_in_dir.write("这是文件在临时目录中的内容")
print_info(f"临时文件创建成功: {temp_file_in_dir}")
# 临时目录会在 with 块结束时自动删除
print_success("临时目录已在 with 块结束时自动删除")
except Exception as e:
print_error(f"创建临时目录时发生错误: {e}")


if __name__ == '__main__':
create_temporary_file() # INFO: 临时文件创建成功: C:\Users\Prorise\AppData\Local\Temp\tmpfrqy2tno
create_named_temporary_file() # INFO: 带名字的临时文件创建成功: C:\Users\Prorise\AppData\Local\Temp\my_app_temp_k0g52b1n.data
create_temporary_directory() # INFO: 临时目录创建成功: C:\Users\Prorise\AppData\Local\Temp\tmpwivr5y11
from print_utils import *
import tempfile
import os
import shutil # 用于演示 mkdtemp 的清理

# =============================================================
# 0. tempfile 模块演示准备
# =============================================================
print_header("tempfile 模块功能演示")

# =============================================================
# 1. 获取系统临时目录信息
# =============================================================
print_subheader("1. 获取系统临时目录信息")
system_temp_dir = tempfile.gettempdir()
print_info(f"系统默认的临时目录路径: {system_temp_dir}") # C:\Users\Prorise\AppData\Local\Temp
temp_prefix = tempfile.gettempprefix()
print_info(f"生成临时文件/目录的默认前缀: {temp_prefix}") # 生成临时文件/目录的默认前缀: tmp

# =============================================================
# 2. 创建匿名的临时文件 (TemporaryFile)
# =============================================================
print_subheader("2. 创建匿名的临时文件 (TemporaryFile)")


# TemporaryFile 创建的文件在文件系统上通常没有可见的目录项(文件名)
# 他是一个真正的临时文件,关闭后即删除
def create_temporary_file():
try:
with tempfile.TemporaryFile(mode="w+t", encoding="utf-8") as tf:
print_info(f"临时文件创建成功: {tf.name}")
tf.write("这是一个临时文件的内容")
tf.write("\n这是第二行内容")
tf.seek(0) # 回到文件开头才能读取
content = tf.read()
print_info(f"临时文件内容: {content}")
print_success("匿名临时文件已在 with 块结束时自动关闭并删除")
except Exception as e:
print_error(f"创建临时文件时发生错误: {e}")


# =============================================================
# 3. 创建带名字的临时文件 (NamedTemporaryFile)
# =============================================================
# NamedTemporaryFile 创建的文件在文件系统中有可见的文件名
# 默认情况下 (delete=True),文件关闭时也会被自动删除
def create_named_temporary_file():
try:
with tempfile.NamedTemporaryFile(mode='w+t', encoding='utf-8', prefix='my_app_temp_', suffix='.data',
delete=True) as nf:
print_info(f"带名字的临时文件创建成功: {nf.name}")
nf.write("这是一个带名字的临时文件的内容")
nf.write("\n这是第二行内容")
nf.seek(0) # 回到文件开头才能读取
content = nf.read()
print_info(f"临时文件内容: {content}")
print_success("带名字的临时文件已在 with 块结束时自动关闭")
except Exception as e:
print_error(f"创建带名字的临时文件时发生错误: {e}")


# =============================================================
# 4. 创建临时目录 (TemporaryDirectory)
# =============================================================
def create_temporary_directory():
print_subheader("4.创建临时目录 (TemporaryDirectory)")
try:
with tempfile.TemporaryDirectory() as temp_dir:
print_info(f"临时目录创建成功: {temp_dir}")
# 在临时目录中创建文件
# 可以在这个临时目录中创建文件和子目录
temp_file_in_dir = os.path.join(temp_dir, "temp_file.txt")
with open(temp_file_in_dir, "w") as f_in_dir:
f_in_dir.write("这是文件在临时目录中的内容")
print_info(f"临时文件创建成功: {temp_file_in_dir}")
# 临时目录会在 with 块结束时自动删除
print_success("临时目录已在 with 块结束时自动删除")
except Exception as e:
print_error(f"创建临时目录时发生错误: {e}")


if __name__ == '__main__':
create_temporary_file() # INFO: 临时文件创建成功: C:\Users\Prorise\AppData\Local\Temp\tmpfrqy2tno
create_named_temporary_file() # INFO: 带名字的临时文件创建成功: C:\Users\Prorise\AppData\Local\Temp\my_app_temp_k0g52b1n.data
create_temporary_directory() # INFO: 临时目录创建成功: C:\Users\Prorise\AppData\Local\Temp\tmpwivr5y11

坑点与建议 (tempfile 模块):

  • 自动清理: TemporaryFile, NamedTemporaryFile (当 delete=True, 默认行为), 和 TemporaryDirectory 在与 with 语句一起使用时,会在退出 with 块时自动关闭并删除对应的文件或目录。这是它们主要的便利之处。
  • NamedTemporaryFiledelete=True 在 Windows 上的行为: 在 Windows 上,当 NamedTemporaryFiledelete=True (默认) 打开时,文件在创建后不能被其他进程(甚至同一进程中的其他代码)通过其文件名再次打开,直到该 NamedTemporaryFile 对象被关闭。如果你需要在文件保持打开状态时让其他部分通过名字访问它,或者希望在关闭后文件仍然保留以便后续处理,应设置 delete=False,但这种情况下你需要自己负责在不再需要时手动删除文件 (例如使用 os.remove())。
  • 文件名安全性: tempfile 模块在生成临时文件名时会采取措施避免可预测的文件名,有助于防止一些安全问题。通常不建议使用像 mktemp() 这样只返回一个文件名但不实际创建文件的旧函数,因为它在文件名生成和文件创建之间存在竞争条件风险。优先使用 TemporaryFile, NamedTemporaryFile, TemporaryDirectory 这些会实际创建并管理临时实体的工具。
  • 权限: 创建临时文件或目录仍然受限于当前用户的操作系统权限。如果程序没有在临时目录中写入的权限,操作将会失败。
  • 资源泄露: 如果不使用 with 语句,而是手动创建 TemporaryFileTemporaryDirectory 对象,你需要确保调用它们的 close() 方法(对于文件)或 cleanup() 方法(对于目录,尽管目录对象被垃圾回收时也会尝试清理)来释放资源和删除实体。with 语句是推荐的最佳实践。

glob 模块:文件路径名模式匹配

glob 模块用于查找所有匹配特定模式的文件路径名,它使用的模式规则类似 Unix shell 中的通配符。这对于需要批量处理符合某种命名规则的文件非常有用。

glob 常用功能
函数描述
glob.glob(pathname, *, recursive=False)返回一个包含所有匹配 pathname 模式的文件和目录路径的列表。
glob.iglob(pathname, *, recursive=False)返回一个迭代器,逐个产生匹配 pathname 模式的文件和目录路径 (更节省内存)。
glob.escape(pathname)pathname 中的特殊字符 (如 *, ?, [) 进行转义,使其能被字面匹配。

glob 模式中的特殊字符:

  • *: 匹配任意多个(包括零个)任意字符。
  • ?: 匹配单个任意字符。
  • [...]: 匹配方括号中出现的任意一个字符 (例如 [abc] 匹配 ‘a’, ‘b’, 或 ‘c’)。可以使用连字符表示范围 (例如 [0-9] 匹配任意数字)。
  • [!...]: 匹配任意不在方括号中出现的字符。
  • ** (当 recursive=True 时): 匹配任意文件和任意层级的目录 (递归匹配)。

批注:核心记忆功能 (glob 模块)

  • glob.glob(pattern): 最常用的功能,直接获取所有匹配路径的列表。
  • 理解 *, ?, [...] 通配符的含义。
  • recursive=True 配合 ** 进行递归搜索。
glob 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
from print_utils import * 
import glob
import os
import shutil # 用于创建和清理测试目录/文件

# =============================================================
# 0. glob 模块演示准备
# =============================================================
print_header("glob 模块功能演示")
test_glob_dir = "glob_playground"

def setup_glob_test_environment():
print_info(f"创建测试环境 '{test_glob_dir}'...")
if os.path.exists(test_glob_dir):
shutil.rmtree(test_glob_dir) # 清理旧的测试目录
os.makedirs(os.path.join(test_glob_dir, "subdir1", "subsubdir"), exist_ok=True)
os.makedirs(os.path.join(test_glob_dir, "subdir2"), exist_ok=True)

# 创建一些测试文件
files_to_create = [
os.path.join(test_glob_dir, "report.txt"),
os.path.join(test_glob_dir, "report_final.txt"),
os.path.join(test_glob_dir, "image01.jpg"),
os.path.join(test_glob_dir, "image02.png"),
os.path.join(test_glob_dir, "data_2025.csv"),
os.path.join(test_glob_dir, "config.ini"),
os.path.join(test_glob_dir, "subdir1", "notes.txt"),
os.path.join(test_glob_dir, "subdir1", "archive.zip"),
os.path.join(test_glob_dir, "subdir1", "subsubdir", "deep_log.txt"),
os.path.join(test_glob_dir, "subdir2", "backup_2024.dat"),
]
for f_path in files_to_create:
with open(f_path, "w") as f:
f.write(f"Content of {os.path.basename(f_path)}")
print_success("测试文件和目录已创建。")

def cleanup_glob_test_environment():
if os.path.exists(test_glob_dir):
shutil.rmtree(test_glob_dir)
print_info(f"已清理测试环境 '{test_glob_dir}'。")

setup_glob_test_environment()

# =============================================================
# 1. 基本通配符匹配 (glob.glob)
# =============================================================
print_subheader("1. 基本通配符匹配 (glob.glob)")

# 匹配所有 .txt 文件
txt_files_pattern = os.path.join(test_glob_dir, "*.txt")
print_info(f"查找模式: '{txt_files_pattern}'")
found_txt_files = glob.glob(txt_files_pattern)
print_info(f" 找到的 .txt 文件: {found_txt_files}")

# 匹配所有以 'image' 开头的文件
image_files_pattern = os.path.join(test_glob_dir, "image*")
print_info(f"查找模式: '{image_files_pattern}'")
found_image_files = glob.glob(image_files_pattern)
print_info(f" 找到的以 'image' 开头的文件: {found_image_files}")

# 匹配单个字符 'image0?.jpg' 或 'image0?.png'
specific_image_pattern = os.path.join(test_glob_dir, "image0?.jp*g") # ? 匹配一个字符, * 匹配多个
print_info(f"查找模式: '{specific_image_pattern}'")
found_specific_images = glob.glob(specific_image_pattern)
print_info(f" 找到的 'image0X.jp(e)g' 文件: {found_specific_images}")

# 匹配字符集 [abc] 或 [0-9]
data_files_pattern = os.path.join(test_glob_dir, "data_[0-9][0-9][0-9][0-9].csv")
print_info(f"查找模式: '{data_files_pattern}'")
found_data_files = glob.glob(data_files_pattern)
print_info(f" 找到的 'data_YYYY.csv' 文件: {found_data_files}")

# =============================================================
# 2. 递归匹配 (recursive=True 和 ** 通配符)
# =============================================================
print_subheader("2. 递归匹配 (recursive=True 和 **)")

# 递归查找所有子目录中的 .txt 文件
# 注意: '**' 必须单独作为路径的一部分才能正确递归
recursive_txt_pattern = os.path.join(test_glob_dir, "**", "*.txt")
print_info(f"递归查找模式: '{recursive_txt_pattern}'")
all_txt_recursively = glob.glob(recursive_txt_pattern, recursive=True)
print_info(f" 递归找到的所有 .txt 文件: {all_txt_recursively}")

# =============================================================
# 3. 使用迭代器进行匹配 (glob.iglob)
# =============================================================
print_subheader("3. 使用迭代器进行匹配 (glob.iglob)")
print_info(f"使用 iglob 迭代查找 '{recursive_txt_pattern}' (适合大量匹配结果):")
# iglob 返回一个迭代器,当你处理大量文件时可以节省内存
count = 0
for filepath in glob.iglob(recursive_txt_pattern, recursive=True):
print(f" - (iglob) 找到: {filepath}")
count += 1
print_success(f" iglob 共找到 {count} 个匹配项。")

# =============================================================
# 4. 转义特殊字符 (glob.escape)
# =============================================================
print_subheader("4. 转义特殊字符 (glob.escape)")
# 假设我们有一个文件名包含特殊字符,例如 'file_with[star]*.txt'
# 如果想字面匹配它,而不是让 '*' 和 '[]' 作为通配符,就需要转义
literal_filename_with_star = "file_with[star]*.txt"
# os.path.join(test_glob_dir, literal_filename_with_star) # 如果实际创建此文件

escaped_pattern_part = glob.escape(literal_filename_with_star)
print_info(f" 原始文件名含特殊字符: '{literal_filename_with_star}'")
print_info(f" 转义后的部分: '{escaped_pattern_part}'")
# 完整的查找模式会是: os.path.join(test_glob_dir, escaped_pattern_part)
# 假设我们真的创建了这个文件:
# with open(os.path.join(test_glob_dir, literal_filename_with_star), 'w') as f: f.write("test")
# found_escaped = glob.glob(os.path.join(test_glob_dir, escaped_pattern_part))
# print_info(f" 查找转义后的模式结果: {found_escaped}")


cleanup_glob_test_environment()
print_header("glob 模块演示结束。")

坑点与建议 (glob 模块):

  • **recursive=True: 要想让 ** 通配符实现递归匹配(即匹配任意层级的子目录),必须同时在 glob.glob()glob.iglob() 函数中将 recursive 参数设置为 True
  • 路径分隔符: glob 模块会自动处理当前操作系统的路径分隔符(Windows 上的 \ 和 Linux/macOS 上的 /)。你在模式中可以使用 /,它在 Windows 上也能正常工作。
  • 结果顺序: glob 函数返回的路径列表的顺序是不确定的,它依赖于操作系统底层 listdir() 函数返回的顺序。如果需要排序的结果,你需要自己对返回的列表进行排序(例如 sorted(glob.glob(...)))。
  • 性能: 对于非常大的目录结构或非常多的文件,glob.glob() 会一次性将所有匹配项加载到内存中,可能消耗较多内存。在这种情况下,使用 glob.iglob() 返回一个迭代器会更节省内存,因为它逐个产生匹配项。
  • 隐藏文件 (以 .开头的文件): 在 Unix-like 系统上,像 * 这样的通配符默认不会匹配以点 . 开头的隐藏文件或目录名。如果需要匹配它们,模式需要显式地包含点,例如 .*.[^.]*
  • pathlib 模块的替代方案: Python 3.4+ 引入的 pathlib 模块提供了面向对象的路径操作。Path 对象有自己的 glob()rglob() (recursive glob) 方法,使用起来可能更直观和 Pythonic。例如:
    1
    2
    3
    4
    from pathlib import Path
    p = Path("./my_dir")
    txt_files = list(p.glob("*.txt")) # 非递归
    all_py_files = list(p.rglob("*.py")) # 递归
  • 无匹配项: 如果没有文件或目录匹配给定的模式,glob.glob() 会返回一个空列表,glob.iglob() 会返回一个空的迭代器。程序不会因此报错。

uuid 模块:生成通用唯一标识符

UUID (Universally Unique Identifier) 是一种128位长的数字,设计用来在空间和时间上保证唯一性,而不需要一个中央协调机构。uuid 模块提供了生成各种版本 UUID 的功能。这些 ID 在分布式系统、数据库主键、临时文件名、会话 ID 等场景中非常有用。

uuid 常用功能与版本
函数/属性描述
uuid.uuid1([node, clock_seq])基于当前时间和计算机的 MAC 地址生成 UUID (版本1)。可能涉及隐私。
uuid.uuid3(namespace, name)基于一个命名空间 UUID 和一个名称的 MD5 哈希值生成 UUID (版本3),结果是确定的。
uuid.uuid4()推荐常用:生成一个完全随机的 UUID (版本4)。
uuid.uuid5(namespace, name)基于一个命名空间 UUID 和一个名称的 SHA-1 哈希值生成 UUID (版本5),结果是确定的。
UUID(hex=..., bytes=..., int=...)(类构造器) 从不同表示形式(十六进制字符串、字节串、整数)创建一个 UUID 对象。
my_uuid.hex(UUID对象属性) 获取 UUID 的32个字符的十六进制字符串表示 (无连字符)。
str(my_uuid)(UUID对象转字符串) 获取 UUID 的标准36个字符的十六进制字符串表示 (带连字符)。
my_uuid.bytes(UUID对象属性) 获取 UUID 的16字节表示。
my_uuid.int(UUID对象属性) 获取 UUID 的128位整数表示。
my_uuid.version(UUID对象属性) 获取 UUID 的版本号 (1, 3, 4, 或 5)。

预定义的命名空间 (用于 uuid3uuid5):

  • uuid.NAMESPACE_DNS: 当 name 是一个完全限定域名时使用。
  • uuid.NAMESPACE_URL: 当 name 是一个 URL 时使用。
  • uuid.NAMESPACE_OID: 当 name 是一个 ISO OID 时使用。
  • uuid.NAMESPACE_X500: 当 name 是一个 X.500 DN 时使用。

批注:核心记忆功能 (uuid 模块)

  • uuid.uuid4(): 最常用,用于生成随机的、无法预测的唯一 ID。
  • str(generated_uuid): 将 UUID 对象转换为标准的带连字符的字符串形式,便于存储和显示。
  • generated_uuid.hex: 获取不带连字符的紧凑十六进制字符串。
uuid 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from print_utils import * 
import uuid

# =============================================================
# 0. uuid 模块演示准备
# =============================================================
print_header("uuid 模块功能演示")

# =============================================================
# 1. 生成 UUID 版本 1 (基于时间和 MAC 地址)
# =============================================================
print_subheader("1. 生成 UUID 版本 1 (基于时间和 MAC 地址)")
try:
uuid_v1 = uuid.uuid1()
print_info(f"UUID v1: {uuid_v1}")
print_info(f" - 字符串表示: {str(uuid_v1)}")
print_info(f" - 十六进制 (hex): {uuid_v1.hex}")
print_info(f" - 版本号: {uuid_v1.version}")
# print_info(f" - 节点 (可能含MAC地址): {uuid_v1.node:x}") # 以十六进制显示节点
except Exception as e:
print_warning(f"生成 UUID v1 时可能出现问题 (例如,无法获取 MAC 地址): {e}")


# =============================================================
# 2. 生成 UUID 版本 4 (随机生成 - 推荐常用)
# =============================================================
print_subheader("2. 生成 UUID 版本 4 (随机生成)")
uuid_v4_1 = uuid.uuid4()
uuid_v4_2 = uuid.uuid4()
print_info(f"UUID v4 (示例1): {uuid_v4_1}")
print_info(f" - 字符串表示: {str(uuid_v4_1)}")
print_info(f" - 十六进制 (hex): {uuid_v4_1.hex}")
print_info(f" - 版本号: {uuid_v4_1.version}")
print_info(f"UUID v4 (示例2): {uuid_v4_2} (每次调用都不同)")


# =============================================================
# 3. 生成 UUID 版本 3 (基于命名空间和名称的 MD5 哈希)
# =============================================================
print_subheader("3. 生成 UUID 版本 3 (MD5 哈希)")
# uuid3 和 uuid5 是确定性的:相同的命名空间和名称总是产生相同的 UUID
namespace_for_v3 = uuid.NAMESPACE_DNS # 使用预定义的 DNS 命名空间
name_for_v3 = "example.com"
uuid_v3 = uuid.uuid3(namespace_for_v3, name_for_v3)
print_info(f"UUID v3 (NAMESPACE_DNS, '{name_for_v3}'): {uuid_v3}")
print_info(f" - 版本号: {uuid_v3.version}")

# 再次用相同参数生成,结果应该一样
uuid_v3_again = uuid.uuid3(namespace_for_v3, name_for_v3)
print_info(f" 再次生成 (应相同): {uuid_v3_again == uuid_v3}")


# =============================================================
# 4. 生成 UUID 版本 5 (基于命名空间和名称的 SHA-1 哈希)
# =============================================================
print_subheader("4. 生成 UUID 版本 5 (SHA-1 哈希)")
namespace_for_v5 = uuid.NAMESPACE_URL # 使用预定义的 URL 命名空间
name_for_v5 = "https://www.python.org/"
uuid_v5 = uuid.uuid5(namespace_for_v5, name_for_v5)
print_info(f"UUID v5 (NAMESPACE_URL, '{name_for_v5}'): {uuid_v5}")
print_info(f" - 版本号: {uuid_v5.version}")


# =============================================================
# 5. 从字符串或其他形式创建 UUID 对象
# =============================================================
print_subheader("5. 从字符串或其他形式创建 UUID 对象")
uuid_string = "a1b2c3d4-e5f6-7890-1234-567890abcdef" # 必须是标准格式
try:
uuid_from_string = uuid.UUID(uuid_string)
print_info(f"从字符串 '{uuid_string}' 创建的 UUID 对象: {uuid_from_string}")
print_info(f" 其整数表示 (uuid_from_string.int): {uuid_from_string.int}")
print_info(f" 其字节表示 (uuid_from_string.bytes): {uuid_from_string.bytes}")
except ValueError as e:
print_error(f"从字符串创建 UUID 失败: {e}")

print_header("uuid 模块演示结束。")

坑点与建议 (uuid 模块):

  • 版本选择:
    • uuid.uuid4() (版本4): 通常是最佳选择。它生成基于高质量随机数的 UUID,碰撞概率极低,且不包含任何可识别的硬件或时间信息,因此对隐私更友好。
    • uuid.uuid1() (版本1): 基于当前时间和计算机的 MAC 地址生成。虽然能保证在同一台机器上按时间顺序生成(大致上),但它可能暴露生成 UUID 的机器的 MAC 地址和生成时间,存在一定的隐私和安全风险。在需要基于时间排序且能容忍这些风险的特定场景下使用。
    • uuid.uuid3()uuid.uuid5(): 这两个版本是确定性的,即对于相同的命名空间 UUID 和相同的名称字符串,它们总是生成相同的 UUID。uuid3 使用 MD5 哈希,uuid5 使用 SHA-1 哈希。它们适用于需要基于某个已知名称(如 URL、DNS名、OID等)生成一个唯一的、可重现的标识符的场景。由于 MD5 和 SHA-1 在密码学上已不被认为是强哈希算法,这些 UUID 主要用于标识,而非安全目的。
  • 碰撞概率: UUID 的设计目标是全球唯一。对于版本4的随机 UUID,其可能性的数量非常巨大( $2^{122}$ 种,因为有几位用于版本和变体信息),因此两个独立生成的 uuid4 相同的概率微乎其微,在实际应用中基本可以忽略。
  • 存储与表示:
    • str(my_uuid_obj): 返回标准的36字符十六进制表示,包含连字符 (例如 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx)。这是最常见的交换和显示格式。
    • my_uuid_obj.hex: 返回32字符的紧凑十六进制表示,不含连字符。
    • my_uuid_obj.bytes: 返回16字节的字节串表示。
    • my_uuid_obj.int: 返回128位的整数表示。根据存储系统(数据库、文件名等)的要求和效率选择合适的表示形式。例如,某些数据库可能对存储字节串或整数形式的 UUID 更高效。
  • 命名空间的使用 (uuid3, uuid5): 当使用 uuid3uuid5 时,选择合适的预定义命名空间(NAMESPACE_DNS, NAMESPACE_URL, NAMESPACE_OID, NAMESPACE_X500)或者自己生成一个固定的 UUID 作为自定义命名空间,对于确保生成的 ID 的上下文和唯一性非常重要。

7.底层工具库

本小节将探讨 Python 标准库中一系列强大的工具,它们可以帮助我们更有效地处理迭代、函数式编程、数据复制、美化输出、定义枚举、实现抽象基类以及运用类型提示来构建更健壮的应用程序。

typing 模块:类型提示支持

typing 模块是 Python 中用于支持类型提示 (Type Hints) 的核心工具。类型提示允许开发者为变量、函数参数和函数返回值指定预期的类型。虽然 Python 本质上是动态类型语言(类型在运行时检查),但类型提示有诸多好处:

  • 提高代码可读性和可理解性:明确了函数期望什么类型的输入,以及会返回什么类型的结果。
  • 辅助静态分析工具:像 MyPy, Pyright, Pytype 这样的静态类型检查器可以使用类型提示来检测代码中潜在的类型错误,而无需实际运行代码。
  • 改善IDE支持:IDE 可以利用类型提示提供更准确的代码补全、重构和错误高亮。
  • 促进更好的API设计:思考类型有助于设计出更清晰、更健壮的函数和类接口。

类型提示在运行时通常不会强制执行(除非使用特定库如 pydantic),它们更多的是给开发者和工具看的元数据。

typing 常用类型与构造器
类型/构造器描述
List[T] (或 Python 3.9+ list[T])表示元素类型为 T 的列表。例如 List[int] 表示整数列表。
Dict[K, V] (或 Python 3.9+ dict[K, V])表示键类型为 K、值类型为 V 的字典。例如 Dict[str, int]
Tuple[T1, T2, ...] (或 Python 3.9+ tuple[T1, ...])表示一个元组,其中每个元素都有特定的类型。例如 Tuple[str, int, bool]
Tuple[T, ...] (或 Python 3.9+ tuple[T, ...])表示一个所有元素类型均为 T 的可变长度元组。
Set[T] (或 Python 3.9+ set[T])表示元素类型为 T 的集合。例如 Set[str]
Optional[T]表示一个值可以是类型 T 或者 None。等价于 Union[T, None]
Union[T1, T2, ...] (或 Python 3.10+ `T1T2
Any表示一个未受约束的类型,可以是任何类型 (应谨慎使用,因为它会削弱类型检查)。
Callable[[Arg1Type, Arg2Type], ReturnType]表示一个可调用对象 (如函数),指定其参数类型和返回类型。
TypeVar('T')用于定义泛型类型变量,常用于编写泛型函数和类。
Generic[T]用于创建泛型类的基类。
NewType('TypeName', BaseType)创建一个概念上不同但运行时与 BaseType 相同的“新类型”,主要用于静态类型检查。
Type[C]表示类 C 本身 (或其子类),而不是 C 的实例。
Iterable[T], Iterator[T]分别表示可迭代对象和迭代器,其元素类型为 T
Mapping[K, V], Sequence[T]表示更通用的映射类型 (如字典) 和序列类型 (如列表、元组)。

批注:核心记忆功能 (typing 模块)

  • 基本集合类型:List, Dict, Tuple, Set 的泛型版本。
  • Optional[T]: 处理可能为 None 的值。
  • Union[T1, T2]: 当一个值可能有几种不同类型时使用 (或 Python 3.10+ 的 T1 | T2 语法)。
  • Callable: 为回调函数或作为参数传递的函数指定签名。
  • Any: 在确实无法或不需要指定具体类型时使用,但应尽量避免滥用。
  • Python 3.9+ 的改进: 许多标准集合类型 (如 list, dict, tuple, set)可以直接用作泛型类型,无需从 typing 模块导入对应的大写版本。例如,用 list[int] 代替 List[int]
  • Python 3.10+ 的改进: 引入了 | 操作符作为 Union 的更简洁语法,例如 int | str 代替 Union[int, str]
typing 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from print_utils import *
## 对于 Python 3.9+,许多大写的类型 (List, Dict等) 可以用小写内置类型替代
## 例如,用 list[int] 代替 List[int]
## 为了兼容性,这里仍然使用 typing 中的大写版本,除非特别说明
from typing import (
List, Dict, Tuple, Set, Optional, Union, Any, Callable,
TypeVar, Generic, NewType, Type, Sequence
)
import os # 用于示例

## =============================================================
## 0. typing 模块演示准备
## =============================================================
print_header("typing 模块 (类型提示) 功能演示")

## =============================================================
## 1. 基本类型提示 (函数参数和返回值)
## =============================================================
print_subheader("1. 基本类型提示")

def greet(name: str, age: int, is_vip: bool = False) -> str:
"""根据用户信息生成问候语。"""
title: str = "VIP " if is_vip else ""
return f"Hello, {title}{name}! You are {age} years old."

message: str = greet("Alice", 30, is_vip=True)
print_success(f"greet() 返回: {message}")
## message = greet("Bob", "forty") # MyPy 这类静态检查器会警告 "forty" 不是 int

def process_data(data: List[int], factor: float = 1.0) -> List[float]:
"""处理一个整数列表,返回一个浮点数列表。"""
return [item * factor for item in data]

int_list: List[int] = [1, 2, 3, 4, 5]
float_list: List[float] = process_data(int_list, 0.5)
print_success(f"process_data({int_list}, 0.5) 返回: {float_list}")

## =============================================================
## 2. Optional, Union, Any
## =============================================================
print_subheader("2. Optional, Union, Any")

def find_user(user_id: int) -> Optional[Dict[str, Any]]:
"""根据用户ID查找用户,如果找不到则返回 None。"""
users_db: Dict[int, Dict[str, Any]] = {
101: {"name": "Alice", "email": "alice@example.com"},
102: {"name": "Bob", "active": False}
}
return users_db.get(user_id) # .get() 找不到键时默认返回 None

user_profile: Optional[Dict[str, Any]] = find_user(101)
if user_profile:
print_success(f"找到用户 101: {user_profile}")
else:
print_warning("用户 101 未找到。")

user_profile_none: Optional[Dict[str, Any]] = find_user(999)
print_info(f"查找用户 999: {user_profile_none}") # None

def parse_value(value: Union[str, int, float]) -> str: # Python 3.10+ 可以写 str | int | float
"""根据不同类型的值返回描述。"""
if isinstance(value, str):
return f"String value: '{value}'"
elif isinstance(value, int):
return f"Integer value: {value}"
elif isinstance(value, float):
return f"Float value: {value:.2f}"
return "Unknown type" # 理论上不会到这里,因为 Union 限制了类型

print_info(f"parse_value('hello'): {parse_value('hello')}")
print_info(f"parse_value(123): {parse_value(123)}")
print_info(f"parse_value(3.14159): {parse_value(3.14159)}")

def process_anything(data: Any, operation: str = "print") -> None:
"""处理任何类型的数据 (应谨慎使用 Any)。"""
print_info(f"Processing '{operation}' on data of type {type(data).__name__}: {data}")

process_anything([1, 2, 3], "summing (simulated)")
process_anything({"key": "value"}, "logging (simulated)")

## =============================================================
## 3. Callable (可调用对象)
## =============================================================
print_subheader("3. Callable (可调用对象)")

def apply_operation(x: int, y: int, operation: Callable[[int, int], int]) -> int:
"""应用一个二元整数操作并返回结果。"""
return operation(x, y)

def add(a: int, b: int) -> int: return a + b
def multiply(a: int, b: int) -> int: return a * b

result_add: int = apply_operation(10, 5, add)
result_multiply: int = apply_operation(10, 5, multiply)
## result_wrong = apply_operation(10, 5, lambda s: s.upper()) # MyPy 会报错,lambda签名不匹配

print_success(f"apply_operation(10, 5, add) = {result_add}")
print_success(f"apply_operation(10, 5, multiply) = {result_multiply}")

## =============================================================
## 4. TypeVar 和 Generic (泛型)
## =============================================================
print_subheader("4. TypeVar 和 Generic (泛型)")

T = TypeVar('T') # 定义一个类型变量 T (可以是任何类型)
K = TypeVar('K')
V = TypeVar('V')

def get_first_element(items: Sequence[T]) -> Optional[T]: # Sequence[T] 比 List[T] 更通用
"""获取序列的第一个元素,如果序列为空则返回 None。"""
return items[0] if items else None

print_info(f"get_first_element([10, 20, 30]): {get_first_element([10, 20, 30])}")
print_info(f"get_first_element(['a', 'b']): {get_first_element(['a', 'b'])}")
print_info(f"get_first_element([]): {get_first_element([])}")

class Container(Generic[T]): # 定义一个泛型类 Container,它可以容纳类型 T 的项目
def __init__(self, item: T):
self._item: T = item

def get_item(self) -> T:
return self._item

def __repr__(self) -> str:
return f"Container[{type(self._item).__name__}](item={self._item!r})"

int_container = Container[int](123) # 明确指定 T 是 int
str_container = Container("hello") # 类型检查器通常能推断出 T 是 str

print_success(f"int_container: {int_container}, item: {int_container.get_item()}")
print_success(f"str_container: {str_container}, item: {str_container.get_item()}")

## =============================================================
## 5. NewType 和 Type (简介)
## =============================================================
print_subheader("5. NewType 和 Type (简介)")

UserId = NewType('UserId', int) # 创建一个概念上不同的类型 UserId,但运行时它仍然是 int
def get_user_by_id(user_id: UserId) -> str:
# # MyPy 会将 user_id 视为 UserId 类型,有助于防止意外传递普通 int
return f"User data for ID {user_id}"

my_user_id: UserId = UserId(101) # 创建 UserId 实例
## another_id: int = 102
## print_info(get_user_by_id(another_id)) # MyPy 可能会警告类型不匹配

print_info(f"get_user_by_id(my_user_id): {get_user_by_id(my_user_id)}")
print_info(f" 类型 of my_user_id: {type(my_user_id).__name__} (运行时仍是 int)")


def create_instance(cls: Type[Container[str]], value: str) -> Container[str]:
"""创建一个指定类 (必须是 Container[str] 或其子类) 的实例。"""
return cls(value)

## MyContainer = Container # 可以是 Container 本身
## instance = create_instance(MyContainer, "some_string")
## print_success(f"create_instance(Container, 'some_string'): {instance}")

if __name__ == '__main__':
pass # 演示已在各节内进行
print_header("typing 模块演示结束。")

坑点与建议 (typing 模块):

  • 运行时无强制性 (通常):类型提示主要是给静态类型检查器(如 MyPy)和 IDE 看的,Python 解释器本身在运行时通常不会因为类型不匹配而报错(除非你使用了像 pydantic 这样会进行运行时验证的库)。它们的目的是在开发阶段帮助发现潜在错误。
  • 逐渐采用 (Gradual Typing):你不需要一次性为整个项目添加类型提示。可以从新的代码开始,或者从项目的关键部分开始,逐步引入类型提示。
  • Any 的使用: Any 类型表示“可以是任何类型”,它会有效地关闭对该变量或表达式的类型检查。应尽量避免使用 Any,只在确实无法确定类型或为了兼容旧代码时才用。过度使用 Any 会降低类型提示带来的好处。
  • Python 版本与类型提示语法:
    • Python 3.9+: 许多标准内置集合类型 (如 list, dict, tuple, set) 可以直接用作泛型类型注解,例如 list[int] 而不是 from typing import List; List[int]。这使得代码更简洁。
    • Python 3.10+: 引入了使用 | 操作符来表示联合类型 (Union) 的语法,例如 int | str 代替 Union[int, str]。还引入了 ParamSpecConcatenate 用于更精确地注解高阶函数和装饰器。
    • from __future__ import annotations: 在 Python 3.7+ 的文件中,可以在文件顶部添加 from __future__ import annotations。这使得所有类型提示都被视为字符串字面量,从而可以使用在当前 Python 版本中可能尚不支持的未来类型提示语法(例如,在 Python 3.8 中使用 list[int] 而不是 List[int],如果配合这个导入)。
  • 泛型 (TypeVar, Generic): 当你编写的函数或类可以处理多种不同类型的数据,并且希望保持这些类型之间的一致性时(例如,一个函数接受一个列表并返回该列表中的一个元素,元素的类型应该相同),泛型是非常有用的。
  • 循环导入与类型提示: 有时在进行类型提示时,如果两个模块相互导入对方定义的类型,可能会导致循环导入问题。一种常见的解决方法是将其中一个导入放在 if TYPE_CHECKING: 块中(需要 from typing import TYPE_CHECKING),或者使用字符串字面量作为类型提示 (例如 my_var: 'MyOtherClass')。
  • 类型别名 (Type Aliases):对于复杂的类型签名,可以使用类型别名来提高可读性。例如:
    Vector = List[float]
    UserRecord = Dict[str, Union[int, str, bool]]
    def process_vector(v: Vector) -> None: ...

itertools 提供了高效的迭代工具,functools 增强了函数式编程能力,copy 解决了对象复制的深浅问题,pprint 让复杂数据显示更友好,enum 带来了更安全和可读的常量定义方式,abc 模块是实现面向对象中接口和抽象的基石,而 typing 模块则为 Python 代码的健壮性和可维护性提供了强大的支持。掌握这些模块能让你的 Python 编程技能更上一层楼。

itertools 模块:高效的迭代器构建工具

itertools 模块提供了一系列用于创建和使用迭代器的函数。这些函数实现了很多常见的迭代算法,运行速度快,内存效率高,并且能够以简洁的方式组合使用。当你需要处理复杂的迭代逻辑时,这个模块往往能提供优雅的解决方案。

itertools 常用功能
函数描述
itertools.count(start=0, step=1)创建一个从 start 开始,按 step 无限递增 (或递减) 的数字迭代器。
itertools.cycle(iterable)创建一个无限循环遍历 iterable 中元素的迭代器。
itertools.repeat(object[, times])创建一个重复生成 object 的迭代器,可以指定重复 times 次,或无限重复。
itertools.chain(*iterables)将多个可迭代对象连接起来,形成一个更长的迭代器。
itertools.islice(iterable, stop)itertools.islice(iterable, start, stop[, step])从可迭代对象中返回指定切片的元素,类似列表切片但作用于迭代器。
itertools.product(*iterables, repeat=1)计算多个可迭代对象的笛卡尔积,相当于嵌套的 for 循环。
itertools.permutations(iterable, r=None)iterable 中生成长度为 r 的所有可能排列 (元素不重复,顺序有关)。
itertools.combinations(iterable, r)iterable 中生成长度为 r 的所有可能组合 (元素不重复,顺序无关)。
itertools.combinations_with_replacement(iterable, r)允许元素重复的组合。
itertools.groupby(iterable, key=None)根据 key 函数对连续的元素进行分组,返回 (键, 组内元素迭代器) 对。

批注:核心记忆功能 (itertools 模块)

  • count(), cycle(), repeat(): 创建特定模式的无限或有限迭代器。
  • chain(): 轻松合并多个序列。
  • product(), permutations(), combinations(): 处理排列组合问题。
  • groupby(): 对已排序或按键分组的数据进行聚合处理。
  • islice(): 安全地从无限迭代器或普通迭代器中取一部分。
itertools 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
from print_utils import *
import itertools
from typing import Iterable, Any, Tuple, Iterator, List # 用于类型注解

## =============================================================
## 0. itertools 模块演示准备
## =============================================================
print_header("itertools 模块功能演示")

## =============================================================
## 1. 无限迭代器 (count, cycle, repeat) 与 islice
## =============================================================
print_subheader("1. 无限迭代器与 islice")


def demo_infinite_iterators() -> None:
"""演示 count, cycle, repeat 及如何使用 islice 获取部分元素。"""
print_info("itertools.count(start, step):")
# # count(10, 2) 会生成 10, 12, 14, ...
# # islice 用于从迭代器中获取指定数量的元素,避免无限循环
limited_count: List[int] = list(itertools.islice(itertools.count(10, 2), 5))
print_success(f" 从 count(10, 2) 中获取前5个元素: {limited_count}") # 输出: [10, 12, 14, 16, 18]

print_info("\nitertools.cycle(iterable):")
# # cycle(['A', 'B', 'C']) 会生成 A, B, C, A, B, C, ...
limited_cycle: List[str] = list(itertools.islice(itertools.cycle(['A', 'B', 'C']), 8))
print_success(
f" 从 cycle(['A', 'B', 'C']) 中获取前8个元素: {limited_cycle}") # 输出: ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B']

print_info("\nitertools.repeat(object, times):")
# # repeat('Hi', 3) 会生成 Hi, Hi, Hi
limited_repeat: List[str] = list(itertools.islice(itertools.repeat("Hi", 3), 3))
print_success(f" 从 repeat('Hi', 3) 中获取前3个元素: {limited_repeat}") # 输出: ['Hi', 'Hi', 'Hi']


## =============================================================
## 2. 连接与组合迭代器 (chain, product, permutations, combinations)
## =============================================================
print_subheader("2. 连接与组合迭代器")


def demo_combining_iterators() -> None:
"""演示 chain, product, permutations, combinations 的用法。"""
list1: List[str] = ['a', 'b']
tuple1: Tuple[int, ...] = (1, 2)
string1: str = "XY"

print_info("itertools.chain(*iterables):")
# chain 可以将多个可迭代对象连接在一起
chained_elements: List[any] = list(itertools.chain(list1, tuple1, string1))
print_success(f" 连接后的元素: {chained_elements}") # 输出: ['a', 'b', 1, 2, 'X', 'Y']

print_info("\nitertools.product(*iterables, repeat=1):")
# product 生成多个可迭代对象的笛卡尔积
cartesian_product: List[Tuple[str, str]] = list(itertools.product('AB', '12'))
print_success(
f" product('AB', '12') 结果: {cartesian_product}") # 输出: [('A', '1'), ('A', '2'), ('B', '1'), ('B', '2')]

print_info("\nitertools.permutations(iterable, r):")
# permutations 生成可迭代对象的所有可能排列
perms: List[Tuple[str, ...]] = list(itertools.permutations("ABC", 2))
print_success(
f" permutations('ABC', 2) 结果: {perms}") # 输出: [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

print_info("\nitertools.combinations(iterable, r):")
# combinations 生成可迭代对象的所有可能组合(不考虑顺序)
combos: List[Tuple[str, ...]] = list(itertools.combinations("ABC", 2))
print_success(f" combinations('ABC', 2) 结果: {combos}") # 输出: [('A', 'B'), ('A', 'C'), ('B', 'C')]


## =============================================================
## 3. 分组迭代器 (groupby)
## =============================================================
print_subheader("3. 分组迭代器 (groupby)")


def demo_groupby_iterator() -> None:
"""演示 groupby。重要:groupby 需要数据预先按分组键排序。"""
data_to_group: List[Tuple[str, str]] = [
('A', 'Apple'), ('A', 'Ant'),
('B', 'Ball'), ('B', 'Banana'), ('B', 'Bat'),
('C', 'Cat'), ('C', 'Cow'), ('C', 'Car'), ("C", "Com")
]
print_info(f"待分组数据 (已按首字母排序): {data_to_group}")

# 按首字母分组
# 其中 key_char 是分组键,group_iter 是一个迭代器,包含该组的所有元素
# keyfunc = lambda x: x[0] # 可以省略,因为元组默认按第一个元素比较
for key_char, group_iter in itertools.groupby(data_to_group, key=lambda x: x[0]):
group_items: List[Tuple[str, str]] = list(group_iter) # group_iter 是一个迭代器,需要转换为列表
print_success(f" 以 {key_char} 开头的分组: {group_items}")


print_info("\n对未排序数据使用 groupby (可能不符合预期):")
unsorted_data: List[int] = [1, 1, 2, 3, 3, 3, 1, 2, 2]
print_info(f" 未排序数据: {unsorted_data}")
for key_num, group_iter_num in itertools.groupby(unsorted_data): # 未排序
print(f" 键 '{key_num}': {list(group_iter_num)}")
print_warning(" 注意:groupby 对未排序数据是按连续相同元素进行分组的。")


if __name__ == '__main__':
# demo_infinite_iterators()
# demo_combining_iterators()
demo_groupby_iterator()

坑点与建议 (itertools 模块):

  • 无限迭代器: 像 count(), cycle(), repeat() (不带 times 参数) 这样的函数会产生无限序列。在直接使用它们进行迭代时,务必配合 itertools.islice() 或其他机制来限制迭代次数,否则会导致无限循环。
  • 迭代器消耗: itertools 中的函数通常返回迭代器。迭代器是一次性的,遍历过后就会耗尽。如果需要多次使用结果,应先将其转换为列表或元组 (例如 list(my_iterator))。
  • groupby() 的前提: itertools.groupby() 函数在对数据进行分组前,要求数据必须已经按照分组的键排好序。如果数据未排序,它只会将连续出现的相同键的元素分为一组,可能与预期不符。
  • 内存效率: itertools 的函数通常设计为内存高效的,因为它们是基于迭代器操作的,一次只处理一个元素,而不是一次性加载所有数据到内存。这使得它们非常适合处理大型数据集。
  • 组合性: itertools 函数的强大之处在于它们的组合性。你可以将多个 itertools 函数串联起来,用非常少的代码构建出复杂的迭代逻辑。
functools 模块:高阶函数与可调用对象工具

functools 模块包含了一些用于高阶函数(操作或返回其他函数的函数)的工具。这些工具可以帮助你修改或扩展函数及其他可调用对象的行为,而无需完全重写它们。

functools 常用功能
函数/装饰器描述
functools.partial(func, *args, **keywords)创建一个新的函数,该函数在调用时会自动传入预设的 argskeywords 参数。
functools.reduce(function, iterable[, initializer])将一个二元函数 function 累积地应用到 iterable 的项上,从而将序列规约为单个值。
functools.wraps(wrapped_func)一个装饰器,用于更新一个包装函数 (wrapper) 的元信息 (如名称、文档字符串) 以匹配被包装函数 wrapped_func
@functools.lru_cache(maxsize=128, typed=False)一个装饰器,为函数调用结果提供最近最少使用 (LRU) 缓存,可以显著提高重复调用相同参数的耗时函数的性能。
@functools.total_ordering一个类装饰器,如果你定义了 __eq__() 和至少一个其他比较方法 (__lt__, __le__, __gt__, __ge__),它会自动为你补全所有其他的比较方法。
@functools.singledispatch一个装饰器,将函数转换为单分派泛函数 (generic function),可以根据第一个参数的类型注册不同的实现。

批注:核心记忆功能 (functools 模块)

  • partial(): 当你需要固定函数的一些参数,生成一个更特化的函数版本时非常有用。
  • reduce(): 用于执行累积计算,如求和、求积、或更复杂的聚合。
  • wraps(): 编写自定义装饰器时务必使用,以保持被装饰函数的元数据。
  • lru_cache(): 对于计算成本高且参数可能重复调用的函数,这是一个简单有效的性能优化手段。
functools 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
from print_utils import *
import functools
import time
from typing import Callable, Any, List # 用于类型注解

## =============================================================
## 0. functools 模块演示准备
## =============================================================
def show_functools_intro():
print_header("functools 模块功能演示")

## =============================================================
## 1. functools.partial: 固定函数参数
## =============================================================
def demo_partial():
print_subheader("1. functools.partial: 固定函数参数")

def power(base: float, exponent: float) -> float:
"""计算 base 的 exponent 次方。"""
return base ** exponent

## 创建一个总是计算平方的函数
square: Callable[[float], float] = functools.partial(power, exponent=2)
## 创建一个总是计算立方的函数
cube: Callable[[float], float] = functools.partial(power, exponent=3)
## 创建一个总是计算以2为底的幂的函数
power_of_2: Callable[[float], float] = functools.partial(power, 2) # 第一个未命名参数被固定为 base

print_info(f"power(3, 2) = {power(3, 2)}")
print_success(f"square(3) (partial(power, exponent=2)): {square(3)}")
print_success(f"cube(3) (partial(power, exponent=3)): {cube(3)}")
print_success(f"power_of_2(5) (partial(power, 2)): {power_of_2(5)}") # 2**5


## =============================================================
## 2. functools.reduce: 累积计算
## =============================================================
def demo_reduce():
print_subheader("2. functools.reduce: 累积计算")
## 计算阶乘: 5! = 5 * 4 * 3 * 2 * 1
numbers_for_factorial: List[int] = [1, 2, 3, 4, 5]
factorial_5: int = functools.reduce(lambda x, y: x * y, numbers_for_factorial)
print_success(f"reduce(lambda x, y: x * y, {numbers_for_factorial}) = {factorial_5}")


## =============================================================
## 3. @functools.lru_cache: 函数结果缓存
## =============================================================
def demo_lru_cache():
print_subheader("3. @functools.lru_cache: 函数结果缓存")

@functools.lru_cache(maxsize=128) # maxsize=None 表示不限制缓存大小
def expensive_fibonacci(n: int) -> int:
"""一个计算斐波那契数的耗时函数 (递归实现,不加缓存效率极低)。"""
print_info(f" 计算({n})...") # 打印这条可以看出函数是否实际执行
if n < 2:
return n
time.sleep(0.5) # 模拟计算耗时
return expensive_fibonacci(n - 1) + expensive_fibonacci(n - 2)

## 第一次调用 (计算并缓存)
print_info("第一次调用 expensive_fibonacci(10):")
start_time = time.perf_counter()
fib_10_first_call = expensive_fibonacci(10)
duration1 = time.perf_counter() - start_time
print_success(f" fibonacci(10) = {fib_10_first_call}, 耗时: {duration1:.6f} 秒")
print_info(f" 缓存信息: {expensive_fibonacci.cache_info()}")

## 第二次调用 (应从缓存中获取,非常快)
print_info("\n第二次调用 expensive_fibonacci(10) (应从缓存读取):")
start_time_cached = time.perf_counter()
fib_10_second_call = expensive_fibonacci(10)
duration2 = time.perf_counter() - start_time_cached
print_success(f" fibonacci(10) = {fib_10_second_call}, 耗时: {duration2:.6f} 秒 (缓存)")

## 清除缓存
expensive_fibonacci.cache_clear()
print_info(f"\n缓存已清除。缓存信息: {expensive_fibonacci.cache_info()}")

## =============================================================
## 4. @functools.wraps: 编写行为正确的装饰器
## =============================================================
def demo_wraps():
print_subheader("4. @functools.wraps: 编写行为正确的装饰器")

def simple_logging_decorator(original_function: Callable) -> Callable:
"""一个简单的日志装饰器,演示 @functools.wraps 的作用。"""
# # 如果没有 @functools.wraps(original_function),
# # decorated_function 的 __name__, __doc__ 等元信息会变成 wrapper 的。
@functools.wraps(original_function)
def wrapper(*args: Any, **kwargs: Any) -> Any:
"""wrapper 函数的文档字符串。"""
print_info(f" 装饰器: 调用函数 '{original_function.__name__}' 之前...")
result = original_function(*args, **kwargs)
print_info(f" 装饰器: 调用函数 '{original_function.__name__}' 之后,结果: {result}")
return result
return wrapper


@simple_logging_decorator
def greet_person(name: str) -> str:
"""向一个人问好并返回问候语。"""
return f"Hello, {name}!"

print_info("调用被装饰的 greet_person('Alice'):")
greeting_output = greet_person("Alice")

print_info("\n检查被装饰函数的元信息:")
print_success(f" 函数名 (greet_person.__name__): '{greet_person.__name__}' (应为 'greet_person')")
print_success(f" 文档字符串 (greet_person.__doc__):\n '''{greet_person.__doc__.strip()}''' (应为 greet_person 的文档)")

def main():
"""主函数,用于调用各个演示函数"""
show_functools_intro()

# 用户选择要运行的演示
print("\n请选择要运行的演示:")
print("1. functools.partial 演示")
print("2. functools.reduce 演示")
print("3. functools.lru_cache 演示")
print("4. functools.wraps 演示")
print("5. 运行所有演示")
print("0. 退出")

choice = input("\n请输入选项编号: ")

if choice == '1':
demo_partial()
elif choice == '2':
demo_reduce()
elif choice == '3':
demo_lru_cache()
elif choice == '4':
demo_wraps()
elif choice == '5':
demo_partial()
demo_reduce()
demo_lru_cache()
demo_wraps()
elif choice == '0':
print("退出程序")
else:
print("无效选项,请重新运行程序")

if __name__ == "__main__":
main()

坑点与建议 (functools 模块):

  • partial() 的参数顺序: partial() 会按顺序填充位置参数,然后是关键字参数。如果 func 本身有一些默认参数,partial 可以覆盖它们,或者提供那些未被 partial 固定的参数。
  • reduce() 的可读性: 虽然 reduce() 很强大,但对于简单的求和、求积等操作,直接使用内置的 sum() 或一个显式的循环通常更具可读性。reduce() 更适用于一些自定义的复杂累积逻辑。确保 reduce 中使n用的二元函数易于理解。
  • lru_cache() 的适用场景:
    • 适用于那些输入参数组合相对有限,且单次计算成本较高的纯函数(即对于相同的输入总是返回相同输出,且没有副作用的函数)。
    • 不适用于参数是可变对象(如列表、字典)且函数行为依赖于对象内部状态变化的情况,因为缓存是基于参数值的哈希。如果可变对象被修改但其哈希不变(或哈希变化但对象视为等价),可能导致缓存命中错误。typed=True 可以让缓存区分不同类型的参数(如 33.0)。
    • maxsize 参数很重要,设置太小可能导致缓存命中率低,None 则不限制大小,可能消耗过多内存。
  • wraps() 的必要性: 在编写任何自定义装饰器时,强烈建议使用 @functools.wraps(original_function) 来装饰内部的 wrapper 函数。这能确保被装饰后的函数保留其原始函数的名称 (__name__)、文档字符串 (__doc__)、参数列表、模块名等重要元信息,这对于调试、文档生成和代码可维护性至关重要。
copy 模块:对象的浅拷贝与深拷贝

在 Python 中,赋值操作 (=) 对于可变对象(如列表、字典)通常只是创建了一个新的引用,指向同一个对象。这意味着修改一个引用会影响到所有指向该对象的引用。copy 模块提供了创建对象副本的方法,分为浅拷贝和深拷贝,用于在不同程度上复制对象及其内容。

copy 常用功能
函数描述
copy.copy(x)创建对象 x浅拷贝 (shallow copy)
copy.deepcopy(x)创建对象 x深拷贝 (deep copy)

浅拷贝 (Shallow Copy) vs. 深拷贝 (Deep Copy):

  • 浅拷贝 (copy.copy()):

    1. 创建一个新的复合对象 (例如一个新的列表实例)。
    2. 然后,对于原始对象中的元素,如果是不可变类型(如数字、字符串、元组),则直接复制其值。
    3. 如果是可变类型(如列表、字典、自定义对象),则复制其引用。也就是说,新列表中的可变元素和旧列表中的对应可变元素将指向内存中同一个对象。

image-20250518152854653

影响: 修改新拷贝中嵌套的可变对象的内容,会影响到原始对象中对应的嵌套可变对象。

image-20250518153103717

  • 深拷贝 (copy.deepcopy()):

    1. 创建一个新的复合对象。
    2. 然后,递归地复制原始对象中的所有内容。这意味着,如果遇到嵌套的可变对象,它会为这些嵌套的可变对象也创建新的副本,而不是仅仅复制引用。
    • 影响: 修改新拷贝中嵌套的可变对象的内容,不会影响到原始对象,因为它们是完全独立的副本。
  • deepcopy 能够正确处理循环引用(对象内部直接或间接引用自身)。

image-20250518153447234

批注:核心记忆功能 (copy 模块)

  • 理解浅拷贝深拷贝的根本区别:浅拷贝只复制顶层容器,内部元素如果是可变对象则共享引用;深拷贝会递归复制所有嵌套的可变对象,创建完全独立的副本。
  • copy.copy(): 用于浅拷贝。
  • copy.deepcopy(): 常用且重要,当你需要一个与原始对象完全独立的副本,尤其是当对象包含嵌套的可变结构时,应使用深拷贝。
pprint 模块:数据结构的美化输出

当需要打印复杂的 Python 数据结构(如深度嵌套的列表、字典)时,内置的 print() 函数的输出可能挤在一行,难以阅读和理解。pprint 模块 (pretty-print) 提供了“美化打印”的功能,可以将这些数据结构以更易读的格式输出,例如自动换行、缩进和排序(可选)。

pprint 常用功能
函数/类描述
pprint.pprint(object, stream=None, ...)将对象 object 美化输出到指定的流 stream (默认为 sys.stdout)。
pprint.pformat(object, indent=1, width=80, ...)返回对象 object 美化后的字符串表示,而不是直接打印。
pprint.PrettyPrinter(indent=1, width=80, depth=None, stream=None, *, compact=False, sort_dicts=True)创建一个 PrettyPrinter 对象,可以更细致地控制美化输出的参数,并可复用。

PrettyPrinterpprint/pformat 的常用参数:

  • indent: 每级缩进的空格数。
  • width: 输出内容的目标宽度,超过此宽度会尝试换行。
  • depth: 控制嵌套数据结构的打印深度,超出此深度的部分会用 ... 表示。
  • stream: 输出流。
  • compact: 如果为 True,则输出会更紧凑,尽可能将多个短元素放在同一行。
  • sort_dicts: (Python 3.8+) 如果为 True (默认),则字典会按键排序后输出,保证输出顺序的一致性。

批注:核心记忆功能 (pprint 模块)

  • pprint.pprint(my_complex_data): 最常用,直接美化打印数据结构。
  • pprint.pformat(my_complex_data): 当你需要获取美化后的字符串而不是直接打印时使用。
  • PrettyPrinter(sort_dicts=False): 如果不希望字典输出时按键排序,可以创建 PrettyPrinter 实例并设置。
pprint 模块代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from print_utils import * # 假设 print_utils.py 可用
import pprint
import datetime # 用于示例数据

## =============================================================
## 0. pprint 模块演示准备
## =============================================================
print_header("pprint 模块功能演示")

def create_complex_data_structure() -> dict:
"""创建一个复杂的嵌套数据结构用于演示。"""
return {
'user_id': 1001,
'username': 'alpha_user',
'is_active': True,
'profile': {
'full_name': 'Alpha Centauri User',
'email': 'alpha@example.com',
'date_joined': datetime.date(2023, 1, 15),
'preferences': {
'theme': 'dark_solarized',
'notifications': ['email', 'push'],
'language': 'en-US'
}
},
'posts': [
{'post_id': 'p001', 'title': 'First Post', 'tags': ['intro', 'python']},
{'post_id': 'p002', 'title': 'Advanced Python Topics', 'tags': ['python', 'advanced', 'performance']},
{'post_id': 'p003', 'title': 'Data Structures in Python',
'comments': [
{'user': 'beta_user', 'text': 'Great article!'},
{'user': 'gamma_user', 'text': 'Very helpful, thanks.'}
]
}
],
'friends_ids': list(range(1020, 1035)) # 一个较长的列表
}

complex_data: dict = create_complex_data_structure()

## =============================================================
## 1. 使用 pprint.pprint() 直接美化打印
## =============================================================
print_subheader("1. 使用 pprint.pprint() 直接美化打印")
print_info("使用普通 print() 打印复杂数据 (对比用):")
## print(complex_data) # 输出会很拥挤

print_info("\n使用 pprint.pprint() 打印复杂数据:")
pprint.pprint(complex_data, indent=2, width=100, sort_dicts=False) # sort_dicts=False 保持字典插入顺序(Python 3.7+)或原始顺序


## =============================================================
## 2. 使用 pprint.pformat() 获取美化后的字符串
## =============================================================
print_subheader("\n2. 使用 pprint.pformat() 获取美化后的字符串")
formatted_string: str = pprint.pformat(complex_data, indent=2, width=80, depth=3, compact=True, sort_dicts=True)
## depth=3: 只打印到第3层嵌套,更深的用 ...
## compact=True: 尝试更紧凑的输出
## sort_dicts=True: 字典按键排序输出,保证一致性
print_info("使用 pformat 获取的格式化字符串 (depth=3, compact=True, sort_dicts=True):")
print(formatted_string)


## =============================================================
## 3. 使用 PrettyPrinter 类进行更细致的控制和复用
## =============================================================
print_subheader("\n3. 使用 PrettyPrinter 类")
## 创建一个 PrettyPrinter 实例,可以预设参数
custom_printer = pprint.PrettyPrinter(
indent=4,
width=120,
depth=None, # None 表示不限制深度
compact=False,
sort_dicts=False # 不排序字典
)
print_info("使用自定义的 PrettyPrinter 实例打印:")
custom_printer.pprint(complex_data['profile'])

print_info("\n使用另一个配置 (字典排序) 的 PrettyPrinter 实例:")
sorting_printer = pprint.PrettyPrinter(indent=2, sort_dicts=True)
sorting_printer.pprint(complex_data['profile']['preferences'])


if __name__ == '__main__':
# # 演示函数已在各自节内调用或其逻辑已在此处
pass
print_header("pprint 模块演示结束。")