当前位置: 代码网 > it编程>前端脚本>Python > 入门初学者短期内快速掌握Python的经典全面教程(专家学习笔记)

入门初学者短期内快速掌握Python的经典全面教程(专家学习笔记)

2026年01月02日 Python 我要评论
本博客将自己初步学习python过程中的阶段性整理与沉淀。难免存在理解不够严谨或表述不够完善之处,欢迎各位读者在评论区留言指正、交流探讨,这对我和后续读者都会非常有价值。同时说明一下:从当前基础学习和

本博客将自己初步学习python过程中的阶段性整理与沉淀。难免存在理解不够严谨或表述不够完善之处,欢迎各位读者在评论区留言指正、交流探讨,这对我和后续读者都会非常有价值。

同时说明一下:从当前基础学习和实践验证来看,当前准备的这些分享内容,确实是可以在一天半的时间内就可以快速学习完成并可以理解的,希望对我们彼此都有所收获!!!

一、快速了解 python 和 环境准备

(一)python 快速介绍

python 是一种 简洁、强大、易读 的编程语言,广泛应用于 web 开发、数据分析、人工智能、自动化运维领域。它由 guido van rossum 在 1991 年设计,因其清晰的语法和强大的生态系统,迅速成为全球最受欢迎的编程语言之一。

2017 年底stack overflow 发布的数据显示,python 已经超越 javascript 成为了该平台上提问流量最多的编程语言。

(二)jupyter notebook:从梦想到行业标配

jupyter notebook 的诞生源于创始人 fernando pérez一个大胆的想法:打造一个能整合 julia(ju)、python(py)和 r(ter) 这三种科学计算语言的通用计算平台。但目前jupyter 早已超越最初的设想,成为一个支持几乎所有编程语言的多功能计算环境。

2017 年起,北美顶尖高校纷纷全面拥抱 jupyter notebook:

  • 学术界:斯坦福的 cs231n《计算机视觉与神经网络》 课程2017 年便完全迁移到 jupyter notebook、uc berkeley 的 《数据科学基础》 课程也在同年改用 jupyter 作为唯一的作业平台。
  • 工业界: facebook尽管大型后端开发仍依赖传统 ide,但几乎所有 内部分析、机器学习训练和数据探索 都基于 jupyter notebook 运行。同时 google 的 ai research 部门 google brain,也是清一色地全部使用 jupyter notebook(改进定制版,叫 google colab)。

(三)mac 上安装 python 和 jupyter notebook 的步骤

步骤命令说明
1. 安装 homebrew/bin/bash -c "$(curl -fssl https://raw.githubusercontent.com/homebrew/install/head/install.sh)"mac 的包管理工具(如果未安装)
2. 安装 python 3brew install python安装最新的 python 3
3. 验证 python 版本python3 --version确保安装成功,显示 python 3.x.x
4. 安装 jupyter notebookpip3 install jupyter通过 pip3 安装 jupyter
5. 检查 jupyter 是否可用jupyter --version确保 jupyter 已正确安装
6. 启动 jupyter notebookjupyter notebook打开 jupyter notebook,浏览器自动打开

这样,我们就可以在 mac 上使用 jupyter notebook 进行 python 开发了!

备注:

  • 如果你不习惯也可以直接idea,毕竟这么多年习惯了java开发,我就是两个都安装了,无所谓!
  • 如果不想安装也可以在线使用:jupyter 官方在线工具

二、python 基础:变量、数据类型与输入输出

python 以其 简洁、易读 的语法受到欢迎,而理解变量、数据类型和输入输出是学习 python 的第一步。这部分内容构成了 python 编程的基石。这部分代码我写的时候都是直接在idea中,网络版本有时很慢。jupyter 官方在线工具

(一)变量:数据的存储与引用

变量(variable是用于存储数据的容器,在 python 中变量不需声明类型,直接赋值即可创建。

1. 变量的定义与赋值

x = 10  # 整数变量
name = "alice"  # 字符串变量
pi = 3.14159  # 浮点数变量
is_python_fun = true  # 布尔变量

python 是动态类型语言,变量的类型是根据赋值内容自动推导的。

2. 变量命名规则

  • 变量名必须 以字母或下划线(_)开头,不能以数字开头。

  • 变量名只能包含 字母、数字和下划线,不能包含空格或特殊符号。

  • 变量名区分大小写(ageage 是不同的变量)。

  • 推荐使用 小写字母+下划线snake_case)风格,如 user_name,符合 python 代码规范(pep 8)。

示例:正确与错误的变量命名

valid_name = "ok"  # 正确
_valid_123 = 42  # 正确
2nd_value = "wrong"  # ❌ 错误:不能以数字开头
user-name = "error"  # ❌ 错误:不能包含 `-`

(二)数据类型(data types)

python 具有丰富的数据类型,主要包括:

数据类型示例说明
整数(int)x = 42存储整数,如 10, -5, 1000
浮点数(float)pi = 3.14存储小数,如 3.1415, -0.5
字符串(str)name = "zyf"存储文本,如 "hello"
布尔值(bool)is_valid = true只有 truefalse
列表(list)nums = [1, 2, 3]有序可变的集合
元组(tuple)colors = ('red', 'blue')有序不可变的集合
字典(dict)person = {"name": "alice", "age": 25}键值对存储
集合(set)unique_nums = {1, 2, 3}无序不重复元素集合

python 提供 type() 函数来查看变量的数据类型:

x = 42
print(type(x))  # 输出:<class 'int'>

python 允许不同类型之间的转换:

age = "25"  
age = int(age)  # 将字符串转换为整数
pi = 3.14
pi_str = str(pi)  # 将浮点数转换为字符串

(三)输入与输出(input & output)

1. 标准输入(input)

input() 用于从用户获取输入,所有输入默认是 字符串类型

name = input("请输入你的名字: ")
print("你好,", name)

如果需要数值类型,需要进行 类型转换

age = int(input("请输入你的年龄: "))  # 输入默认是字符串,需要转换成整数
print("你明年就", age + 1, "岁了!")

基本验证:

2. 标准输出(print)

print() 用于向控制台输出内容:

name = "alice"
age = 25
print("姓名:", name, "年龄:", age)  # 多个参数用逗号分隔

格式化输出(推荐使用 f-string,python 3.6+ 支持):

print(f"姓名: {name}, 年龄: {age}") # 推荐的写法

✅ 小练习题

尝试自己编写代码练习以下问题:

  1. 定义一个变量 temperature,存储 36.6,并使用 print() 输出 "体温: 36.6 摄氏度"

  2. 编写一个程序,提示用户输入姓名和年龄,并打印 "你好,xx!你今年 yy 岁"(其中 xxyy 由用户输入)。

python 的变量、数据类型和输入输出构成了编程的基础,熟练掌握这些概念后,就可以进行更复杂的逻辑编写了!

三、控制流:让代码具备决策能力

在编程中,控制流 决定了代码的执行顺序,使程序能够做出决策(条件判断),或重复执行任务(循环)。通过 布尔值、逻辑运算、if 语句、for 和 while 循环,我们可以让 python 代码变得更加智能和高效。这部分我会总结下这些关键概念,其是构建更具逻辑性程序的基础。

(一)布尔值与逻辑判断

在 python 中,布尔值(boolean) 是控制程序逻辑的基础。它用于 条件判断、循环控制和逻辑运算,让代码具备决策能力。

1. 布尔值(boolean)

布尔值只有两个取值:

true  # 代表 "真"
false  # 代表 "假"

布尔值本质上是整数的特殊形式,其中:

true == 1  # 结果为 true
false == 0  # 结果为 true
布尔值的基本使用
is_python_fun = true
is_raining = false

print(is_python_fun)  # 输出:true
print(type(is_python_fun))  # 输出:<class 'bool'>

2. 比较运算符(comparison operators)

比较运算符用于比较两个值,返回 truefalse

运算符含义示例结果
==等于5 == 5true
!=不等于5 != 3true
>大于10 > 3true
<小于2 < 8true
>=大于等于5 >= 5true
<=小于等于3 <= 2false
代码示例
a = 10
b = 5

print(a > b)   # true
print(a == b)  # false
print(a != b)  # true

3. 布尔运算符(boolean operators)

python 提供了 andornot 三种逻辑运算符,用于组合多个布尔表达式。

运算符含义示例结果
and逻辑与(都为 true 时才为 truetrue and falsefalse
or逻辑或(只要一个为 true 就是 truetrue or falsetrue
not逻辑非(取反)not truefalse
3.1and逻辑与
x = 5
y = 10
print(x > 0 and y > 5)  # true,因为两个条件都成立
print(x > 0 and y < 5)  # false,因第二个条件不成立
3.2or逻辑或
x = 5
y = 10
print(x > 0 or y < 5)  # true,只要有一个条件为 true 即可
print(x < 0 or y < 5)  # false,两个条件都为 false
3.3not逻辑非
is_python_fun = true
print(not is_python_fun)  # false,因为取反了

4. 混合布尔运算(优先级)

运算符的 优先级 从高到低依次为:not(最高)、and、or(最低)

知道黄晓明的“闹太套”英文梗,这里直接记成“闹安套”就好了

print(true or false and false)  
# 等价于 true or (false and false) → true or false → true

print(not true or false)  
# 等价于 (not true) or false → false or false → false

5. 布尔值与其他数据类型的转换

在 python 中,所有数据类型 都可以转换为布尔值:

  • 以下情况视为 false

    • 0(整数 0)

    • 0.0(浮点数 0)

    • ""(空字符串)

    • [](空列表)

    • {}(空字典)

    • none(特殊值,表示“空”)

  • 其他情况均为 true

5.1 使用bool()进行类型转换
print(bool(0))       # false
print(bool(""))      # false
print(bool([]))      # false
print(bool(42))      # true
print(bool("hello")) # true
5.2 在if语句中使用
name = "alice"

if name:  # 相当于 if bool(name) == true
    print("名字有效")  
else:
    print("名字为空")

输出:

名字有效

✅ 小练习题

练习 1:布尔运算

判断以下表达式的结果:

  • true and false or true

  • not (false or true)

  • (10 > 5) and (3 != 3)

练习 2:用户输入比较

让用户输入两个数字,比较它们的大小,并输出 "第一个数字更大""第二个数字更大"

# 提示:使用 input() 和 if 语句

布尔值和逻辑判断是 条件判断和循环 的基础,熟练掌握它们后,代码将更加智能化! 🚀

(二)🌟 条件判断(if语句)

在编程中,我们经常需要根据不同条件来执行不同的代码。这就要用到 条件判断语句(if statement)。python 中使用 ifelifelse 关键字来实现条件分支。

1. 基本 if 语句

语法结构:

if 条件:
    语句块

当“条件”为 true 时,语句块才会被执行。

示例:

age = 20

if age >= 18:
    print("你已经成年了")

输出:

你已经成年了

2. if-else 结构(两分支选择)

用于处理“要么这样,要么那样”的情况。

语法结构:

if 条件:
    语句块1
else:
    语句块2

示例:

temperature = 15

if temperature > 20:
    print("北京天气今天很暖和")
else:
    print("北京天气今天有点冷")

输出:

北京天气今天有点冷

3. if-elif-else 结构(多分支选择)

用于处理多个条件判断的情况(相当于“多项选择”)。

语法结构:

if 条件1:
    语句块1
elif 条件2:
    语句块2
elif 条件3:
    语句块3
else:
    默认语句块

示例:

score = 85

if score >= 90:
    print("优秀")
elif score >= 80:
    print("良好")
elif score >= 60:
    print("及格")
else:
    print("不及格")

输出:

良好

💡 elif 是 "else if" 的缩写。python 中没有 switch 语句,if-elif-else 是推荐的替代方案。

4. 嵌套 if 语句

嵌套 指的是在一个 if 块内部再写 if 判断。可以用来表示更复杂的逻辑结构。

示例:

age = 25
is_student = true

if age < 30:
    if is_student:
        print("你是年轻的学生")
    else:
        print("你是年轻的上班族")

输出:

你是年轻的学生

⚠️ 注意缩进层级,python 是靠缩进来识别代码块的!

5. 条件表达式(三元运算符)

python 支持一种简洁的写法:在一行中完成 if-else 判断

语法:

变量 = 值1 if 条件 else 值2

示例:

age = 16
status = "成年" if age >= 18 else "未成年"
print(status)

输出:

未成年

适合用于 根据条件选择一个值赋给变量 的情况。

小结一下吧

类型场景示例
if只有一个条件
if-else两种可能,二选一
if-elif-else多种情况,依次判断
嵌套 if条件套条件,多层判断
三元表达式简洁地赋值,适合一行判断

✅ 小练习题

练习 1:分数等级判断器

让用户输入一个 0~100 的整数,判断其属于哪个等级:

  • 90 以上:优秀

  • 80~89:良好

  • 70~79:中等

  • 60~69:及格

  • 低于 60:不及格

  • 其他情况提示“输入有误”

练习 2:三角形合法性判断

输入三条边的长度,判断是否能组成一个三角形(任意两边之和大于第三边),并进一步判断:

  • 是等边三角形

  • 是等腰三角形

  • 是普通三角形

  • 否则输出“不合法的三角形”

练习 3:模拟 atm 登录与权限检查(嵌套)

假设用户名为 admin,密码为 8888,登录成功后再检查是否为管理员(管理员输入 "yes" 才能继续操作)。

练习 4:三元运算小测试

输入一个数字,输出 "正数""负数""零"用三元运算符 实现。

(三)循环(loops)

在编程中,我们常常需要重复执行某些操作,比如遍历列表、处理每一行数据、执行某个动作直到满足条件等等。python 提供了两种主要的循环结构:while 循环和 for 循环配合控制语句(如 breakcontinue可以构建出丰富的循环逻辑。

1. while循环(基于条件重复执行)

✅ 基本语法
while 条件表达式:
    循环体
  • 每次循环前都会检查“条件表达式”的值;

  • 条件为 true → 执行循环体;

  • 条件为 false → 结束循环。

🧪 示例:打印 1 到 5 的数字
i = 1
while i <= 5:
    print(i)
    i += 1
🔁break语句(提前终止循环)

break 用于立即跳出整个循环结构,不管循环条件是否还为 true

i = 1
while true:
    print(i)
    if i == 3:
        break
    i += 1

输出:1, 2, 3,然后退出循环

🔁continue语句(跳过当前迭代)

continue 用于跳过本次循环中剩下的语句,直接进入下一次判断。

i = 0
while i < 5:
    i += 1
    if i == 3:
        continue
    print(i)

输出:1, 2, 4, 5(跳过了 3)

2.for循环(用于遍历序列)

✅ 基本语法
for 变量 in 可迭代对象:
    循环体

可迭代对象包括:字符串、列表、字典、集合、元组等。

🔁 遍历字符串
for ch in "hello":
    print(ch)
🔁 遍历列表
fruits = ['apple', 'banana', 'orange']
for fruit in fruits:
    print(fruit)
🔁 遍历字典
info = {"name": "tom", "age": 20}
for key in info:
    print(key, "=>", info[key])

或使用 .items() 遍历键值对:

for key, value in info.items():
    print(f"{key}: {value}")
🔁 遍历集合
s = {"apple", "banana", "cherry"}
for item in s:
    print(item)
🔁range()函数与数值循环
for i in range(1, 6):
    print(i)
  • range(n):0 到 n-1

  • range(start, end):start 到 end-1

  • range(start, end, step):按步长生成

🔁enumerate()结合索引遍历

想同时获取元素和下标时使用 enumerate()

colors = ['red', 'green', 'blue']
for index, color in enumerate(colors):
    print(f"{index}: {color}")

3. 循环控制语句

✅break:提前终止整个循环

通常与 if 配合使用,用于在满足某个条件时立即退出循环。

for i in range(1, 10):
    if i == 5:
        break
    print(i)
✅continue:跳过当前迭代

用于跳过某些不符合条件的值。

for i in range(1, 6):
    if i % 2 == 0:
        continue
    print(i)  # 输出 1 3 5(跳过偶数)
✅else语句在循环中的作用

else 可以与 forwhile 循环一起使用:

  • 当循环没有被 break 中断时,else 中的语句会执行。

    for i in range(1, 5):
        if i == 10:
            break
    else:
        print("未被 break,循环正常结束")
    

    这在处理搜索类问题时特别有用:

    nums = [1, 3, 5, 7]
    target = 4
    for num in nums:
        if num == target:
            print("找到目标")
            break
    else:
        print("未找到目标")
    

小结一下吧

循环类型用途特点
while条件控制循环不确定次数时使用
for遍历序列更简洁,适合固定结构
break跳出循环终止整个循环体
continue跳过本次迭代继续下一轮
else循环结构补充仅在循环未被 break时执行

✅ 小练习题

🧪 练习题 1:打印 1~100 中所有能被 7 整除的数
🧪 练习题 2:使用while计算从 1 累加到 n 的和(n 由用户输入)
🧪 练习题 3:找出列表中第一个大于 50 的元素,并输出其值和索引
🧪 练习题 4:输出 1~30 中除了能被 3 整除的数(使用 continue)
🧪 练习题 5:统计一段字符串中元音字母的个数(不区分大小写)
🧪 练习题 6:使用for和else实现一个猜数字小游戏

四、函数与相关基础知识:让代码会“思考”的魔法工具

在编程的世界里,函数就像是程序的“积木”:它们能把一段可复用的操作封装起来,想用就调用,修改也方便,堪称优雅代码的起点。

无论是打印一行文字,计算一个数列的和,还是训练一个 ai 模型,本质上你做的都是“定义功能 + 调用功能” —— 这正是函数的使命。

(一)函数基础

1. 什么是函数?为什么要使用函数?

在编程中,函数是一个非常重要的概念,它可以被理解为“功能块”,用来封装一段代码,供其他地方调用。通过函数,开发者可以:

  • 提高代码复用性:避免重复写相同的代码,简化开发和维护。

  • 增强可读性:将复杂的任务拆解成小的、独立的单元,便于理解。

  • 分隔逻辑:函数让代码的逻辑结构更加清晰和模块化,有助于团队协作开发。

例如,我们可以定义一个函数来计算两个数字的和,而不需要每次都重复写加法的操作。只要定义一次,后续就可以随时调用。

2. 使用def定义函数

在 python 中,函数是通过 def 关键字来定义的。它的基本语法如下:

def function_name(parameters):
    # 函数体
    return result
  • def 是 python 中定义函数的关键字

  • function_name 是你给函数命名的名称,符合命名规则

  • parameters 是函数的输入(可以没有)

  • return 是返回结果的关键字(如果不返回任何值,则默认为 none

示例:一个简单的加法函数
def add(a, b):
    return a + b

# 调用函数
result = add(3, 5)
print(result)  # 输出: 8

这里我们定义了一个 add 函数,输入参数是 ab,它们会被加在一起并返回结果。

3. 函数的调用和执行流程

定义函数后,调用函数就能执行该函数的代码。python 会根据函数调用的顺序进入函数体,并执行其中的代码。

  • 当函数被调用时,程序会暂停当前位置,跳转到函数体执行代码,直到遇到 return 语句(或者函数执行完毕)才返回。

  • 如果没有 return 语句,则返回 none

示例:简单的函数调用与返回值
def greet(name):
    return f"hello, {name}!"

message = greet("alice")
print(message)  # 输出: hello, alice!

这里的 greet 函数通过 return 语句返回了一个字符串,调用时传入了参数 "alice",最终返回 "hello, alice!"

4.print()与return的区别与使用场景

在 python 中,print()return 都用于输出数据,但它们有显著的不同:

  • print() 用于将信息输出到控制台,主要用于调试、输出中间结果或与用户交互。

  • return 用于函数的输出,它将值返回给调用者,可以被其他代码再次使用

示例:print()和return对比
# 使用 print()
def greet_print(name):
    print(f"hello, {name}!")

greet_print("alice")  # 输出: hello, alice!

# 使用 return
def greet_return(name):
    return f"hello, {name}!"

message = greet_return("alice")
print(message)  # 输出: hello, alice!
  • greet_print 使用 print() 输出值,但无法把它传递给其他部分的代码。

  • greet_return 使用 return 返回结果,这个返回值可以在代码的其他地方使用或存储。

小结一下吧

  • 函数是代码复用、模块化的核心工具。

  • 使用 def 定义函数,return 返回值。

  • print() 用于输出调试信息,return 用于返回计算结果。

(二)函数参数

函数参数是函数接收外部输入的方式。理解和掌握函数参数的使用,不仅能帮助你编写更灵活的代码,还能让你的程序更具扩展性和可维护性。

1. 位置参数

位置参数是最常见的函数参数类型,它是指参数传递时位置的顺序决定了每个参数的含义。

例如,在函数定义时,参数 ab 的位置决定了它们的意义。当调用函数时,传递的参数值会根据位置匹配到相应的参数。

示例:位置参数
def add(a, b):
    return a + b

result = add(3, 5)  # 位置参数:3 被赋值给 a,5 被赋值给 b
print(result)  # 输出: 8

这里的 add 函数有两个位置参数:ab。我们调用 add(3, 5) 时,3 会赋值给 a5 会赋值给 b,最终返回它们的和。

2. 关键字参数

关键字参数允许你在调用函数时显式地指定每个参数的名称。这样可以不关心参数的顺序,只需要知道参数名。

示例:关键字参数
def greet(name, age):
    return f"hello, {name}! you are {age} years old."

message = greet(age=25, name="alice")  # 参数顺序不重要
print(message)  # 输出: hello, alice! you are 25 years old.

在调用 greet 函数时,agename 的顺序不再重要,关键字参数通过指定 age=25name="alice" 来传递值。

3. 默认参数

默认参数是指在定义函数时,为某些参数提供默认值。如果调用时没有提供这些参数的值,python 会使用默认值。

示例:默认参数
def greet(name, age=18):  # age 有默认值
    return f"hello, {name}! you are {age} years old."

message1 = greet("alice")  # 调用时没有提供 age,使用默认值
message2 = greet("bob", 30)  # 调用时提供了 age,覆盖默认值

print(message1)  # 输出: hello, alice! you are 18 years old.
print(message2)  # 输出: hello, bob! you are 30 years old.

greet 函数中,age 有默认值 18。如果调用时没有传入 age,就会使用默认值;如果传入了 age,则会覆盖默认值。

4. 可变参数:*args与**kwargs

有时候我们不知道函数需要接收多少个参数,这时可以使用 可变参数

  • *args 用于接收位置参数,它将接收多余的位置参数并将其打包成一个元组。

  • **kwargs 用于接收关键字参数,它将接收多余的关键字参数并将其打包成一个字典。

示例:*args和**kwargs
def example(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)

# 调用函数时传入不同数量的参数
example(1, 2, 3, name="alice", age=25)

# 输出:
# args: (1, 2, 3)
# kwargs: {'name': 'alice', 'age': 25}
  • *args 将所有位置参数(1, 2, 3)打包成一个元组 (1, 2, 3)

  • **kwargs 将所有关键字参数(name="alice", age=25)打包成一个字典 {'name': 'alice', 'age': 25}

5. 参数顺序规范

当一个函数同时使用位置参数、默认参数和可变参数时,有一定的顺序规范,必须遵循以下顺序:

  • 位置参数

  • 默认参数

  • *args

  • **kwargs

示例:参数顺序
def function(a, b=2, *args, c, **kwargs):
    print(a, b, args, c, kwargs)

# 调用时,按照顺序传递参数
function(1, c=3, d=4, e=5)
# 输出: 1 2 () 3 {'d': 4, 'e': 5}
  • a 是位置参数,必须最先提供。

  • b 是默认参数,如果不提供则使用默认值。

  • *args 用于接收多余的所有位置参数。

  • c 是一个关键字参数,调用时必须显式指定。

  • **kwargs 用于接收多余的所有关键字参数。

小结一下吧

  • 位置参数:根据参数顺序传递值。

  • 关键字参数:显式指定参数名和值。

  • 默认参数:函数定义时为参数提供默认值。

  • 可变参数:*args 用于接收多位置参数,**kwargs 用于接收多关键字参数。

  • 参数顺序:位置参数 > 默认参数 > *args > **kwargs

(三)函数返回值

在 python 中,函数不仅可以执行某些操作,还可以将结果“返回”给调用者。这就是 返回值(return value) 的概念。

通过 return 语句,函数可以把处理的结果交给外部使用。如果函数没有 return,那它默认返回的是 none

1. 使用return返回结果

基本语法:

def function_name(...):
    ...
    return result

示例:

def square(x):
    return x * x

result = square(5)
print(result)  # 输出: 25

函数 square 返回的是 x 的平方,调用后我们可以拿到这个结果并继续使用。

2. 返回多个值(元组)

python 支持从一个函数中返回多个值,这些值实际上被打包成一个元组返回。

示例:

def get_name_and_age():
    name = "alice"
    age = 30
    return name, age

n, a = get_name_and_age()
print(n)  # alice
print(a)  # 30

多值返回的本质是:return name, age 等价于 return (name, age),然后通过拆包语法接收。

3. 函数无返回值时的表现

如果函数中没有 return 语句,或者只是 return 而没有值,那默认返回的是 none

示例:

def say_hello():
    print("hello!")

result = say_hello()
print(result)  # 输出: none

这个函数 say_hello() 虽然做了事情(打印),但没有 return,因此返回值是 none

4. 提前终止函数执行

return 不仅能返回值,还能用来提前终止函数的执行

示例:

def divide(a, b):
    if b == 0:
        return "除数不能为 0"
    return a / b

print(divide(10, 0))  # 输出: 除数不能为 0
print(divide(10, 2))  # 输出: 5.0

b 是 0 时,函数遇到第一个 return 后立即终止执行,不会继续往下运行。

5. 将返回值作为表达式使用

函数返回值可以被用在其他表达式或函数中,像这样:

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

print(add(2, 3) * 10)  # 输出: 50

add(2, 3) 会返回 5,然后再与 10 相乘。

小结一下吧

内容示例返回值说明
返回单个值return x返回一个对象
返回多个值return x, y返回一个元组
无 return 或 return nonereturn / 无 return返回 none
提前结束函数if ...: return ...直接退出函数
返回值可以参与表达式add(1, 2) * 3函数结果用于计算

(四)函数返回值类型注解(type hint / type annotation)

1. python 3 引入的类型注解(type hints)

先用一句话理解:

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

👉 -> int 的意思是:

这个函数“设计上”应该返回一个 int

不是强制,只是“声明”。这是 给“人”和“工具”看的,不是给 python 解释器强制执行的

注意python 不会强制检查!下面的代码 是合法的

def add(a: int, b: int) -> int:
    return "not a number"

python 不会报错。❗类型注解的作用在于:

  • ide(pycharm)

  • 静态检查工具(mypy、pyright)

  • 阅读代码的人(包括未来的你)

2. 为什么现在的 python 项目都强烈推荐用?

2.1 pycharm 会变得非常聪明

def foo() -> dict[str, int]:
    return {"a": 1}

你输入:

result = foo()
result["a"].

pycharm 会自动提示:👉 这是 int,有哪些方法

2.2 提前发现 bug(不用等运行)

def get_age() -> int:
    return "18"

pycharm / mypy 会直接警告你:

expected int, got str

3. 常见的返回值写法(你一定会遇到)

3.1 返回多个类型(union)

from typing import union

def parse(s: str) -> union[int, none]:
    if s.isdigit():
        return int(s)
    return none

python 3.10+ 更推荐:

def parse(s: str) -> int | none:
    ...

3.2 返回列表 / 字典

def get_names() -> list[str]:
    return ["alice", "bob"]

def get_scores() -> dict[str, int]:
    return {"math": 90}

3.3 返回自定义对象

class user:
    ...

def get_user() -> user:
    return user()

五、字符串操作模块

在 python 中,字符串(str)是最常用、最重要的数据类型之一。无论你是在处理用户输入、读取文件、构建网页内容、分析文本数据,几乎都绕不开对字符串的处理。

现在我们总结掌握 python 字符串的各种操作方法,从最基本的创建与访问,到复杂的格式化,我们需要学会如何清洗、切割、替换、判断和重组字符串,并为后续的数据处理与文本分析打下坚实基础。

(一)字符串基础

在 python 中,字符串(str)是用来表示文本的数据类型。本部分介绍字符串的创建方式、基本特性、访问技巧以及常见的基本操作。

1. 字符串的定义与创建

字符串可以用 单引号 '双引号 "三引号 ''' / """ 括起来。

# 单引号
s1 = 'hello'

# 双引号
s2 = "world"

# 三引号(支持多行)
s3 = '''this is
a multi-line
string.'''

✔️ python 中单引号和双引号作用相同,主要用于避免转义冲突。

2. 字符串是不可变对象

字符串是 不可变的(immutable):创建后无法修改,只能重新赋值。

s = "hello"
# s[0] = 'h'  # ❌ 错误!字符串不能直接修改
s = "hello"   # ✅ 只能重新赋值

3. 字符串的索引与切片

字符串可以看作字符的序列,可以通过索引或切片访问其部分内容。

s = "python"

# 索引(从 0 开始)
print(s[0])   # p
print(s[-1])  # n(最后一个字符)

# 切片 [start:end:step]
print(s[0:2])     # py(不包括 end)
print(s[::2])     # pto(每隔1个)
print(s[::-1])    # nohytp(反转字符串)

✅ 切片非常强大,是字符串处理中不可或缺的工具。

4. 字符串的连接与重复

# 拼接字符串
a = "hello"
b = "world"
c = a + " " + b
print(c)  # hello world

# 重复字符串
print("ha" * 3)   # hahaha

5. 字符串的成员运算

s = "python"
print("p" in s)      # true
print("z" not in s)  # true

6. 字符串与类型转换

num = 123
text = str(num)  # 数字转字符串
print("数字是:" + text)

# 也可以使用 f-string(推荐)
print(f"数字是:{num}")

小结一下吧

操作类型常用方法 / 语法
创建字符串'abc', "abc", '''abc'''
索引s[0], s[-1]
切片s[1:4], s[::-1]
拼接与重复+, *
判断是否存在in, not in
类型转换str(x), f"{x}"

(二)字符串常见操作方法

python 的字符串类型(str)内建了大量实用的方法,帮助我们完成查找、替换、对齐、大小写转换、格式化等各类文本处理任务。本节将系统讲解所有主流、实用的方法,并配合用例说明。

1. 大小写转换

方法作用
.lower()转为小写
.upper()转为大写
.capitalize()首字母大写,其余小写
.title()每个单词首字母大写
.swapcase()大小写互换
s = "hello world"
print(s.lower())       # hello world
print(s.upper())       # hello world
print(s.capitalize())  # hello world
print(s.title())       # hello world
print(s.swapcase())    # hello world

2. 查找与判断

方法功能
.find(sub)找到子串首次出现的位置(找不到返回 -1)
.rfind(sub)从右侧开始查找
.index(sub)与 find 类似,但找不到抛出异常
.startswith(prefix)是否以某前缀开头
.endswith(suffix)是否以某后缀结尾
.in判断子串是否存在
s = "hello python"
print(s.find("py"))       # 6
print(s.startswith("he")) # true
print(s.endswith("on"))   # true
print("py" in s)          # true

3. 删除空白符与字符清洗

方法功能
.strip()删除两端空白符
.lstrip()删除左侧空白符
.rstrip()删除右侧空白符
.replace(old, new)替换子串
s = "  hello world  "
print(s.strip())     # "hello world"

s2 = "python,java,c"
print(s2.replace(",", " | "))  # python | java | c

4. 字符串对齐与填充

方法说明
.center(width, char)居中填充
.ljust(width, char)左对齐填充
.rjust(width, char)右对齐填充
.zfill(width)用 0 填充左侧数字部分
print("hi".center(10, "-"))  # ----hi----
print("hi".ljust(10, "."))   # hi........
print("hi".rjust(10, "*"))   # ********hi
print("42".zfill(5))         # 00042

5. 拆分与连接

方法功能
.split(sep)拆分为列表
.rsplit(sep)从右拆分
.splitlines()拆分多行字符串
'sep'.join(list)用分隔符连接列表为字符串
s = "apple,banana,grape"
print(s.split(","))         # ['apple', 'banana', 'grape']

lines = "line1\nline2\nline3"
print(lines.splitlines())   # ['line1', 'line2', 'line3']

words = ['one', 'two', 'three']
print("-".join(words))      # one-two-three

6. 内容判断类方法(返回布尔值)

方法判断类型
.isalpha()是否全字母
.isdigit()是否全数字
.isalnum()是否全是字母或数字
.isspace()是否全是空白符
.isupper()是否全为大写
.islower()是否全为小写
.istitle()是否符合标题规范(首字母大写)
print("abc".isalpha())     # true
print("123".isdigit())     # true
print("abc123".isalnum())  # true
print("   ".isspace())     # true

7. 高级字符串格式化(f-string)

name = "alice"
age = 30

# 推荐方式:f-string(python 3.6+)
print(f"{name} is {age} years old.")  # alice is 30 years old

# 老式:format()
print("{} is {} years old.".format(name, age))

# 占位对齐控制
print(f"|{name:^10}|{age:>3}|")  # 居中+右对齐:|  alice   | 30|

8. 转义字符与原始字符串

字符含义
\n换行
\t制表符
\\反斜杠
r""原始字符串,不转义
print("hello\nworld")     # 换行
print(r"c:\new\folder")   # c:\new\folder

9. 其他实用技巧

  • 长字符串拼接:

    s = (
        "this is a very long string "
        "split across multiple lines "
        "but still valid."
    )
    
  • 字符串比较支持 <, >, == 等字典序对比:

    print("apple" < "banana")  # true

小结:常用字符串方法分类对照表

类型常用方法
查找判断find(), startswith(), in
修改清洗strip(), replace()
大小写处理lower(), capitalize()
对齐填充center(), zfill()
格式化f"", format()
判断内容isdigit(), isalpha()
分割拼接split(), join()
其他len(), 转义、比较等

(三)字符串切片与索引

1. 字符串索引(正向索引 / 负向索引)

python 字符串是序列类型,每个字符都有对应的索引位置。我们可以通过索引访问字符串中的单个字符。

📍 正向索引(从 0 开始)
s = "python"
print(s[0])  # p
print(s[1])  # y
print(s[5])  # n
字符python
索引012345
📍 负向索引(从 -1 开始,表示从右往左)
s = "python"
print(s[-1])  # n
print(s[-2])  # o
print(s[-6])  # p
字符python
索引-6-5-4-3-2-1

注意

  • 超出索引范围(如 s[10])会抛出 indexerror

  • 字符串是 不可变对象,不能通过索引修改某个字符。

2. 字符串切片([start:stop:step])

切片(slice)是从字符串中提取子串的一种方式,语法格式为:

s[start : stop : step]
  • start:起始索引(包含)

  • stop:终止索引(不包含)

  • step:步长(默认为 1)

基础切片
s = "hello, python!"

print(s[0:5])   # hello
print(s[7:13])  # python
print(s[:5])    # hello(从头开始)
print(s[7:])    # python!(直到结尾)
使用负数索引切片
s = "abcdefg"
print(s[-4:-1])  # def
print(s[-7:-4])  # abc
添加步长(step)
s = "0123456789"
print(s[::2])     # 02468(每两个字符取一个)
print(s[1::3])    # 147(从索引 1 开始,每隔 3 个取)
反向切片(步长为负数)
s = "abcdef"
print(s[::-1])   # fedcba(字符串反转)
print(s[-1:-7:-1]) # fedcba(等同上行)
切片边界记忆口诀
  • 包头不包尾:起始位置包含,结束位置不包含。

  • 步长方向一致:正数步长从左到右,负数步长从右到左。

  • 索引可以为负:负索引从末尾向前数。

3. 字符串的遍历

遍历字符串可以逐个访问其字符,常用于统计、查找、生成新字符串等任务。

基础遍历
s = "hello"
for c in s:
    print(c)

输出:

h
e
l
l
o
带索引遍历(推荐:使用 enumerate)
s = "hello"
for index, char in enumerate(s):
    print(f"{index} -> {char}")

输出:

0 -> h
1 -> e
2 -> l
3 -> l
4 -> o

经典应用场景小结

场景示例
提取子串s[3:7]
字符串反转s[::-1]
判断回文s == s[::-1]
截取前缀/后缀s[:3]s[-3:]
每隔一个字符取s[::2]

(四)字符串格式化

python 提供了三种主要的字符串格式化方式:

  • % 占位符格式化(经典写法)

  • str.format() 方法格式化(兼容写法)

  • f-string 格式化(推荐方式,python 3.6+)

1.%占位符格式化(c 风格)

这是最早期的格式化方式,语法类似于 c 语言:

name = "alice"
age = 25
print("my name is %s and i am %d years old." % (name, age))
占位符含义
%s字符串
%d整数
%f浮点数(默认小数点后 6 位)
%.2f浮点数保留 2 位小数

缺点:可读性差、容易出错,不推荐用于新代码。

2.str.format()方法格式化

引入于 python 2.7 / 3.0,支持位置参数、关键字参数,更灵活:

name = "alice"
age = 25
print("my name is {} and i am {} years old.".format(name, age))
🎯 支持位置参数 / 关键字参数
print("hello, {0}. you are {1}.".format("bob", 30))
print("hello, {name}. you are {age}.".format(name="bob", age=30))
🎯 支持格式控制
pi = 3.1415926
print("pi is {:.2f}".format(pi))  # pi is 3.14

缺点:写法较冗长,推荐 f-string 替代。

3.f-string 格式化(python 3.6+,推荐✅)

最现代、最简洁的格式化方式,代码更清晰,推荐作为首选写法。

name = "alice"
age = 25
print(f"my name is {name} and i am {age} years old.")
🎯 支持表达式
a = 5
b = 3
print(f"{a} + {b} = {a + b}")
🎯 支持格式化控制
pi = 3.1415926
print(f"pi rounded to 2 digits: {pi:.2f}")
🎯 与函数、数据结构结合
user = {"name": "bob", "score": 88}
print(f"{user['name']} scored {user['score']}")

三种方式对比

特性% 格式化str.format()f-string
可读性✅✅✅
功能强大一般✅✅✅✅✅
写法简洁一般✅✅✅
推荐程度❌ 不推荐✅ 过渡用法✅✅✅ 首选

小结一下吧

  • 新代码推荐 f-string,语法简洁、表达能力强。

  • str.format() 适用于兼容旧版本 python。

  • % 占位符方式适合了解,不建议用于正式项目中

六、列表、字典与集合 —— python 中的数据组织能力

(一)为什么需要“数据结构”

在前面的章节中,我们已经学习了 变量、基本数据类型、控制流和函数以及字符串。这些知识可以帮助我们描述一个值、处理一次逻辑、完成一个功能

但在真实的程序中,我们面对的往往不是“一个值”,而是:

  • 一组数据

  • 多条记录

  • 多个对象

  • 一批状态或配置项

这正是 “数据结构”存在的意义

1. 单个变量的局限性

先看一个最简单的例子。如果我们只需要保存一个人的名字,用一个变量就足够:

name = "张彦峰"

但如果现在要保存 10 个、100 个、甚至更多名字 呢?你显然不可能这样写:

name1 = "卢政伯" name2 = "吕鸿昌" name3 = "秦学磊"

这种写法存在明显问题:

  • 变量数量不可控

  • 无法方便地进行循环处理

  • 代码几乎不可维护

这时,我们需要一种方式,把“一组相关的数据”组织在一起。

2. 从“一个值”到“一组值”

继续刚才的例子,如果我们希望把多个名字放在一起,python 提供了 列表(list) 这样的数据结构:

names = ["张彦峰","卢政伯","吕鸿昌","秦学磊"]

这样做的好处是:

  • 所有名字属于同一个整体

  • 可以通过循环统一处理

  • 可以按顺序访问、修改、删除

这一步,本质上是从:“一个变量保存一个值”升级为:“一个变量保存一组有结构的数据”

3. 一条记录 vs 多条记录

再看一个更贴近实际开发的例子。假设我们要表示一个用户的信息:

user_name = "张彦峰" user_age = 18 user_email = "zyf@alibaba.com"

这种写法的问题在于:

  • 信息是分散的

  • 很难整体传递或管理

  • 字段之间缺乏明确的“归属关系”

这时,更合理的做法是使用 字典(dict)

user = {
    "name": "张彦峰",
    "age": 18,
    "email": "zyf@alibaba.com"
}

字典的特点是:

  • 通过 key(键) 来访问数据

  • 每个字段都有清晰的含义

  • 非常适合描述“一条记录”

如果现在有 多条用户记录,我们就可以把多个字典放进一个列表中:

users = [
    {"name": "张彦峰", "age": 18},
    {"name": "张琴", "age": 18}
]

你可以看到:数据结构是可以组合使用的这正是后面处理嵌套数据(如 json)的基础。

4. 最常用的几种数据结构

在本章中,我们将系统学习 python 中最常用的几种数据结构:

  • 列表(list):用于存储有序、可变的一组数据

  • 字典(dict):用于存储键值映射关系

  • 集合(set):用于去重和集合运算

  • 元组(tuple):用于表示不可变的数据组合

通过这一章,你将逐步建立起这样的能力:

根据问题特点,选择合适的数据结构,并高效地操作它们

这也是后续学习 嵌套结构、json 数据解析、配置处理 的必要基础。

(二)列表(list):有序数据的容器

列表是 python 中最常用且强大的数据结构之一,广泛用于存储有序、可变的数据。作为一个容器类型,它不仅能存储多种数据类型,还能高效支持数据的添加、删除、修改和访问。掌握列表的使用是理解 python 数据处理能力的重要基础。

1. 列表的基本概念

列表(list)是一个有序的容器,可以存储任意类型的数据,包括整数、字符串、布尔值,甚至是其他列表。列表的核心特点包括:

  • 有序性:列表中的元素是按顺序排列的,每个元素都有一个对应的索引,索引从 0 开始。

  • 可变性:列表是可变的,可以在原地修改列表的内容,例如添加、删除或更新元素。

  • 支持多种数据类型:列表可以包含不同类型的元素,甚至可以存储其他容器类型(如列表、字典等)。

# 示例:包含整数的列表
numbers = [1, 2, 3, 4, 5]

# 示例:包含不同数据类型的列表
info = ["alice", 18, true, 3.14, ["nested", "list"]]

关键点与字符串(str)类似,列表也支持通过索引访问和切片操作,但字符串是不可变的,而列表可以直接修改。

2. 列表的创建与访问

创建列表

列表的创建非常简便,可以通过方括号 [] 来定义。可以创建空列表、包含多个元素的列表,或是包含混合类型元素的列表。

# 空列表
empty_list = []

# 含有多个整数的列表
numbers = [1, 2, 3, 4, 5]

# 含有不同类型的元素的列表
mixed = [1, "alice", true, 3.14]
访问列表元素

通过索引访问列表中的元素,索引从 0 开始。列表还支持负索引,允许从列表的末尾开始访问元素。

numbers = [10, 20, 30, 40]

# 正向索引
print(numbers[0])  # 10
print(numbers[3])  # 40

# 负向索引
print(numbers[-1])  # 40
print(numbers[-2])  # 30

此外,列表支持切片操作,可以通过指定起始和结束索引来获取子列表。

# 切片示例
print(numbers[1:3])  # [20, 30]
print(numbers[:2])   # [10, 20]
print(numbers[2:])   # [30, 40]

注意:切片操作返回的是新列表,原列表内容不受影响。

3. 列表的常用修改操作

列表是可变的,意味着可以在原地修改其内容。常见的修改操作包括添加元素删除元素修改元素

添加元素
  • append():将元素添加到列表的末尾。

  • insert():将元素插入到指定的索引位置。

  • extend():批量追加

# 示例:添加元素
nums = [1, 2, 3]
nums.append(4)  # 在末尾添加元素
nums.insert(1, 1.5)  # 在索引 1 处插入元素
nums.extend([3, 4])   # [1, 2, 3, 4]

print(nums)  # 输出: [1, 1.5, 2, 3, 4, 3, 4]
删除元素
  • remove():删除第一个匹配到的元素。

  • pop():删除并返回指定索引位置的元素(默认删除最后一个元素)。

  • del:根据索引删除元素。

  • clear():清空列表

# 示例:删除元素
nums.remove(1.5)  # 删除元素 1.5
popped_element = nums.pop()  # 删除并返回最后一个元素
del nums[0]  # 删除索引 0 处的元素

print(nums)  # 输出: [2, 3, 4, 3]
修改元素

通过索引可以直接修改列表中的元素:

nums[0] = 10  # 将索引 0 处的元素修改为 10
print(nums)    # 输出: [10, 3]

4. 列表的遍历与循环配合

遍历列表元素

遍历列表元素时,最常用的是 for 循环。可以直接遍历列表的每个元素,或者使用 range() 函数配合索引来遍历列表。

# 直接遍历元素
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)  # 输出每个水果

# 使用索引遍历
for i in range(len(fruits)):
    print(i, fruits[i])  # 输出索引和值

# 使用 enumerate 获取索引和值
for idx, fruit in enumerate(fruits):
    print(idx, fruit)  # 输出索引和值

应用场景

  • 只关心值:直接遍历元素。

  • 需要索引:使用 range()enumerate()

5. 列表的常用内置操作与函数

python 提供了丰富的内置函数和方法来操作列表。以下是一些常用操作:

  • len():返回列表的长度。

  • in:检查元素是否存在于列表中。

  • count():统计元素在列表中出现的次数。

  • index():查找元素首次出现的位置。

  • sort():原地排序。

  • sorted():返回一个新排序的列表。

  • reverse():原地反转列表。

nums = [4, 2, 7, 1]

print(len(nums))     # 输出: 4
print(2 in nums)     # 输出: true
print(nums.count(2)) # 输出: 1
print(nums.index(7)) # 输出: 2

nums.sort()          # 原地排序
sorted_nums = sorted(nums)  # 返回新列表
nums.reverse()       # 原地反转

注意sort()reverse() 会改变原列表,而 sorted() 返回新列表。

6. 列表推导式(list comprehension)

列表推导式是 python 中非常强大且简洁的语法,它允许在一行代码内创建一个新的列表。相比于传统的 for 循环,列表推导式更加简洁且执行效率高。

# 基本语法:生成平方数列表
squares = [x**2 for x in range(5)]
print(squares)  # 输出: [0, 1, 4, 9, 16]

# 带条件的推导式:生成偶数的平方数列表
even_squares = [x**2 for x in range(5) if x % 2 == 0]
print(even_squares)  # 输出: [0, 4, 16]

列表推导式的优势:

  • 简洁性:可以用一行代码替代多行。

  • 可读性:语法清晰、易懂。

  • 性能:避免了手动使用 append()

7. 浅拷贝(shallow copy)与深拷贝(deep copy)

在 python 中,理解浅拷贝与深拷贝的前提,是理解一个核心事实:

python 中的变量,本质上保存的是对象的引用(reference),而不是对象本身。

因此,“拷贝”并不等价于“生成一个完全独立的新对象”,是否真正独立,取决于拷贝的层级。

7.1 对象、引用与容器的关系模型

先从一个最小示例理解引用关系:

a = [1, 2, 3]
b = a

此时:

  • ab 指向 同一个 list 对象

  • a 的任何修改,都会体现在 b

a[0] = 100
print(b)  # [100, 2, 3]

这不是拷贝,而是 引用绑定(aliasing)

7.2 浅拷贝(shallow copy)

浅拷贝的定义:创建一个新的容器对象,但容器中保存的元素仍然是对原对象中元素的引用。

换句话说:

  • 外层容器是新的

  • 内层对象是共享的

常见的浅拷贝方式

以下方式都会产生浅拷贝:

b = a.copy()
b = a[:]
b = list(a)

浅拷贝的行为示意

a = [[1, 2], [3, 4]]
b = a.copy()

内存关系可以抽象为:

a ──▶ list ──▶ [ l1 , l2 ]
b ──▶ list ──▶ [ l1 , l2 ]
  • ab 是两个不同的列表对象

  • 但它们内部的 l1l2 指向同一个子列表对象

浅拷贝的典型“坑”

a = [[1, 2], [3, 4]]
b = a.copy()

b[0][0] = 100
print(a)  # [[100, 2], [3, 4]]

原因分析:

  • b[0]a[0] 指向同一子列表

  • 修改子列表,会影响所有引用它的容器

浅拷贝何时是安全的

列表中包含 不可变对象,后续操作只涉及 整体替换,不修改内部对象

a = [1, 2, 3, "abc"]
b = a.copy()

b[0] = 100  # 不影响 a

print(a) # [1, 2, 3, 'abc']
print(b) # [100, 2, 3, 'abc']
7.3 深拷贝(deep copy)

深拷贝的定义:递归地复制对象本身以及其内部引用的所有子对象,生成完全独立的对象树。

特点:

  • 外层容器独立

  • 内层对象也完全独立

  • 修改任何层级都不会相互影响

深拷贝的标准方式

import copy

b = copy.deepcopy(a)

深拷贝的行为示意

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)

内存模型:

a ──▶ list ──▶ [ l1 , l2 ]
b ──▶ list ──▶ [ l1' , l2' ]
  • l1 ≠ l1'

  • l2 ≠ l2'

深拷贝示例验证

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)

b[0][0] = 100
print(a)  # [[1, 2], [3, 4]]
7.4 浅拷贝 vs 深拷贝的核心对比
维度浅拷贝深拷贝
外层容器新对象新对象
内层对象共享引用完全独立
性能相对较低
内存占用
修改风险高(嵌套结构)极低

8. list * n与浅拷贝的本质关系

a = [[0]] * 3

等价于:

sub = [0]
a = [sub, sub, sub]

因此:

a[0][0] = 1
print(a)  # [[1], [1], [1]]

这是浅拷贝的一种极端形式:

  • 外层列表新建

  • 内层对象全部指向同一个引用

正确构造独立子列表的方式

a = [[0] for _ in range(3)]

9. 列表的常见坑

浅拷贝问题

当使用 * 运算符复制包含可变元素的列表时,可能会发生意外的引用问题。例如:

a = [[1, 2]] * 3
a[0][0] = 100
print(a)  # 输出: [[100, 2], [100, 2], [100, 2]]

原因:所有子列表都引用了同一个内存地址,修改一个元素会影响所有子列表。可以使用 copy.deepcopy() 来避免这种问题。

遍历时修改列表

直接在遍历列表时修改列表,可能会导致不预期的结果。例如:

nums = [1, 2, 3, 4]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)  # 删除偶数元素
print(nums)  # 输出: [1, 3]

解决办法:遍历列表的副本,如 nums[:],或者使用列表推导式来过滤元素。

# 解决方法:遍历副本
for n in nums[:]:
    if n % 2 == 0:
        nums.remove(n)

小结一下吧

  • 列表是 有序、可变 的数据容器,是 python 数据结构的核心之一

  • 支持索引访问、切片操作、原地修改和高效遍历

  • 提供大量内置方法,配合列表推导式可以写出高效、优雅的代码

  • 常见风险点主要集中在:

    • 浅拷贝与引用共享

    • 遍历过程中修改列表

    • 误用 list * n 构造嵌套结构

熟练掌握列表,将为后续学习字典、集合、嵌套数据结构以及 json / 数据分析场景打下坚实基础。

(三)字典(dict):键值映射的数据结构

字典(dict)是 python 中最核心、使用频率极高的一种内置数据结构,用于存储 键值对(key-value)映射关系。它通过 key 直接定位 value,而不是像列表那样依赖下标位置,因此在表达“记录型数据”“配置数据”“映射关系”时具有不可替代的优势。

从底层实现角度看,python 字典基于 哈希表(hash table) 实现,在绝大多数场景下,其查找、插入、删除操作的时间复杂度都接近 o(1)

1. 字典的设计思想

为什么需要字典?

列表适合解决以下问题:

  • 数据 有明确顺序

  • 主要通过 位置(索引) 访问

但在很多真实业务中,我们更关心的是:

  • “这个人的 年龄 是多少?”

  • “这个配置项 timeout 对应的值是多少?”

此时,如果仍然依赖下标访问:

  • 可读性差

  • 容易出错

  • 不利于扩展和维护

字典的核心思想是:

用有意义的 key,直接映射到对应的 value

对比示例

# 使用列表表示用户信息(语义不清晰)
user_list = ["alice", 18, "alice@example.com"]

# 使用字典表示用户信息(结构清晰)
user_dict = {
    "name": "alice",
    "age": 18,
    "email": "alice@example.com"
}

字典的优势

  • 字段含义清晰,代码自解释

  • 不依赖顺序,结构稳定

  • 便于新增、删除字段

  • 非常贴近 json / 配置文件 / 数据库记录的结构

2. 字典的基本使用

创建字典
# 使用花括号(最常见)
user = {"name": "alice", "age": 18}

# 使用 dict 构造函数
user2 = dict(name="bob", age=20)

# 从键值对列表创建
pairs = [("name", "charlie"), ("age", 22)]
user3 = dict(pairs)
访问 value
print(user["name"])        # alice
print(user.get("age"))     # 18
重要约束说明
  • key 必须唯一:后定义的 key 会覆盖之前的值

  • key 必须是不可变类型

    • 合法:strintfloattuple

    • 非法:listdictset

# 合法
d = {(1, 2): "ok"}

# 非法(会抛出 typeerror)
d = {[1, 2]: "error"}

3. 字典的增删改查(crud)

新增 / 修改(统一语法)
user["email"] = "alice@example.com"  # 新增
user["age"] = 19                       # 修改

字典不会区分“新增”和“修改”,是否存在 key 决定行为。

删除元素
# 删除指定 key
del user["email"]

# 删除并返回 value
age = user.pop("age")
# 指定默认值,避免 keyerror
age = user.pop("age", none)
查询
user["name"]                 # 不存在会抛 keyerror
user.get("email")             # 不存在返回 none
user.get("email", "n/a")     # 返回默认值
是否存在某个 key
"name" in user      # true / false
"email" not in user # true / false

4. 字典的遍历方式

遍历 key(默认行为)
for key in user:
    print(key, user[key])
遍历 value
for value in user.values():
    print(value)
遍历 key-value 对(最常用)
for key, value in user.items():
    print(key, value)
遍历时的注意事项
  • 遍历过程中 不建议修改字典结构(增删 key)

  • 若需要修改,可先遍历 list(user.keys())

5. 字典的常用方法与典型模式

常用方法速览
user.keys()        # 所有 key(视图对象)
user.values()      # 所有 value
user.items()       # (key, value) 对
user.clear()       # 清空字典
user.copy()        # 浅拷贝
setdefault:安全设置默认值
user.setdefault("age", 18)

等价于:

if "age" not in user:
    user["age"] = 18
update:批量更新
user.update({"email": "alice@example.com", "age": 20})
典型模式一:计数器
words = ["apple", "banana", "apple", "cherry"]
counter = {}

for word in words:
    counter[word] = counter.get(word, 0) + 1

print(counter)
典型模式二:分组
students = [
    ("alice", "a"),
    ("bob", "b"),
    ("charlie", "a")
]

groups = {}
for name, grade in students:
    groups.setdefault(grade, []).append(name)

print(groups) #{'a': ['alice', 'charlie'], 'b': ['bob']}

6. 字典的拷贝与可变性

浅拷贝(shallow copy)
d1 = {"a": [1, 2]}
d2 = d1.copy()

d2["a"].append(3)
print(d1)  # {'a': [1, 2, 3]}

浅拷贝只复制“引用”,不会复制内部的可变对象。

深拷贝(deep copy)
import copy

d1 = {"a": [1, 2]}
d2 = copy.deepcopy(d1)

d2["a"].append(3)
print(d1)  # {'a': [1, 2]}

7. 字典与列表的组合(复杂结构建模)

列表中放字典
users = [
    {"name": "alice", "age": 18},
    {"name": "bob", "age": 20}
]

for user in users:
    print(user["name"], user["age"])
字典中放列表
grades = {
    "alice": [90, 95, 88],
    "bob": [75, 80, 82]
}

for name, scores in grades.items():
    avg = sum(scores) / len(scores)
    print(name, avg)
多层嵌套字典
users = {
    "alice": {"age": 18, "email": "alice@example.com"},
    "bob": {"age": 20, "email": "bob@example.com"}
}

print(users["alice"]["email"])

小结一下吧

  • 字典是 键值映射容器,核心优势是按 key 快速访问

  • 基于哈希表实现,查找效率高

  • 支持完整的增删改查操作

  • 提供丰富的方法与常见编程模式

  • 可与列表自由组合,构建任意复杂数据结构

熟练掌握字典,是后续学习 json 处理、配置管理、web 开发、数据分析 的关键基础。

(四)集合(set):去重与集合运算

集合(set)是 python 中用于存储唯一元素并支持数学集合运算的一种内置数据结构。其核心价值在于:

  • 天然去重

  • 高效的成员判断(o(1) 平均时间复杂度)

  • 直观表达集合关系(交集、并集、差集等)

在实际开发中,集合常用于数据清洗、权限控制、标签计算、关系判断等场景。

1. 什么是集合(set)

集合的概念来源于数学中的集合:一组不重复的元素构成的整体

# 使用大括号创建集合
fruits = {"apple", "banana", "cherry"}
print(fruits)
# 输出顺序不固定,例如:{'banana', 'apple', 'cherry'}

# 使用 set() 构造函数
nums = set([1, 2, 3, 2, 1])
print(nums)  # {1, 2, 3},自动去重

核心特性

  • 无序(unordered)不支持索引和切片操作;元素顺序不稳定,每次打印结果可能不同

  • 元素唯一(unique)相同元素只会保留一份,适合用于去重、排重逻辑

  • 成员判断高效x in set 的时间复杂度为 o(1),明显快于列表的 o(n)

  • 仅支持可哈希(hashable)类型合法元素:intfloatstrtuple;非法元素:listdictset

s = {(1, 2), "abc", 10}   # 合法
# s = {[1, 2], 3}         # typeerror: unhashable type: 'list'

2. 集合的创建方式

# 空集合(注意不能用 {})
empty_set = set()

# 从列表 / 元组 / 字符串创建
set([1, 2, 2, 3])
set(("a", "b", "a"))
set("hello")   # {'h', 'e', 'l', 'o'}

⚠️ {} 表示空字典,而不是空集合。

3. 集合的基本操作

添加元素
nums = {1, 2, 3}
nums.add(4)
print(nums)  # {1, 2, 3, 4}
  • add():一次只能添加一个元素

删除元素
nums.remove(2)    # 元素不存在会抛 keyerror
nums.discard(5)   # 元素不存在不会报错
popped = nums.pop()  # 随机删除并返回一个元素
nums.clear()      # 清空集合

对比说明

方法元素不存在时使用建议
remove抛异常确保元素一定存在
discard不报错更安全,推荐
pop随机删除不依赖具体元素
成员判断
nums = {1, 2, 3}
print(2 in nums)       # true
print(5 not in nums)  # true

集合是做“是否存在”判断的首选结构。

4. 集合运算(重点)

集合支持与数学集合一致的运算语义。

a = {1, 2, 3}
b = {3, 4, 5}
并集(union)
print(a | b)
print(a.union(b))
# {1, 2, 3, 4, 5}
交集(intersection)
print(a & b)
print(a.intersection(b))
# {3}
差集(difference)
print(a - b)
print(a.difference(b))
# {1, 2}
对称差集(symmetric difference)
print(a ^ b)
print(a.symmetric_difference(b))
# {1, 2, 4, 5}

5. 集合关系判断

a = {1, 2}
b = {1, 2, 3}
print(a.issubset(b))      # true  子集
print(b.issuperset(a))   # true  超集
print(a.isdisjoint({4})) # true  是否无交集

常用于:权限校验、条件覆盖判断。

6. 可变集合与不可变集合

set(可变)
  • 支持 add / remove

  • 不能作为字典的 key

frozenset(不可变)
fs = frozenset([1, 2, 3])
  • 一旦创建不可修改

  • 可作为字典 key 或集合元素

  • 常用于配置、常量集合

7. 典型应用场景

去重
nums = [1, 2, 2, 3, 4, 4]
unique_nums = list(set(nums))
权限 / 标签判断
user_roles = {"admin", "editor"}
required_roles = {"editor", "viewer"}

if user_roles & required_roles:
    print("有权限")
查找共同元素
team_a = {"alice", "bob", "charlie"}
team_b = {"bob", "david"}

common = team_a & team_b
数据差异分析
old_users = {1, 2, 3}
new_users = {2, 3, 4}

lost = old_users - new_users   # 流失用户
added = new_users - old_users  # 新增用户

8. 与列表、字典的对比

结构是否有序是否可重复核心用途
list顺序数据
dict是(3.7+)key 不可重复映射关系
set去重 / 关系计算

小结一下吧

  • 集合是 无序、不重复 的数据结构

  • 成员判断和集合运算效率极高

  • 核心能力:

    • 去重

    • 并 / 交 / 差 / 对称差

    • 子集 / 超集判断

  • 是处理“关系型数据”和“唯一性约束”的利器

掌握集合后,你可以用更简洁、更高效的方式解决大量实际问题,为后续复杂数据结构和算法学习打下坚实基础。

(五)元组(tuple):不可变序列

元组(tuple)是 python 中的一种有序、不可变的序列类型。它在语法和使用方式上与列表(list)非常相似,但核心差异在于:一旦创建,元组的结构不能被修改

在实际开发中,元组常用于:

  • 表示固定含义的数据组合

  • 作为函数的多返回值

  • 作为字典的 key

  • 在某些场景下,替代列表以提升安全性和性能

1. 元组的基本概念

元组的核心特性

  • 有序(ordered)元素按顺序存储,可以通过索引访问

  • 不可变(immutable)元组创建后,不能修改、添加、删除元素

  • 可包含任意类型支持混合类型,支持嵌套结构

point = (10, 20)
info = ("alice", 18, true, ["python", "go"])

元素访问

print(point[0])   # 10
print(info[1])    # 18
print(info[-1])   # ['python', 'go']

元组 vs 列表(核心对比)

特性列表 list元组 tuple
是否可变可变不可变
是否有序
是否可作为 dict key
使用场景动态数据固定数据
语法[ ]( )

2. 创建元组的多种方式

使用圆括号(最常见)
t = (1, 2, 3)
省略圆括号(语法层面支持)
t = 1, 2, 3

✅ 常用于函数返回多个值或简单赋值场景
❌ 不建议初学阶段频繁使用,容易降低可读性

单元素元组(非常重要)
single = (5,)     # 正确
single2 = (5)     # 错误,这是整数

📌 结论是否为元组,关键不在于括号,而在于 逗号

使用tuple()构造函数
nums = [1, 2, 3]
chars = "abc"

print(tuple(nums))   # (1, 2, 3)
print(tuple(chars))  # ('a', 'b', 'c')

3. 元组的索引、切片与遍历

索引访问
t = (10, 20, 30, 40)

print(t[0])    # 10
print(t[-1])   # 40
切片操作
print(t[1:3])  # (20, 30)
print(t[:2])   # (10, 20)

注意

  • 切片返回的是 新元组

  • 原元组不会发生任何变化

遍历元组
for item in t:
    print(item)

4. 元组的不可变性(核心理解)

不允许的操作
t = (1, 2, 3)

# t[0] = 10        # typeerror
# t.append(4)     # attributeerror
# del t[1]         # typeerror
元组中包含可变对象的情况(重点)
t = (1, [2, 3])
t[1].append(4)

print(t)  # (1, [2, 3, 4])

如何理解?

  • 元组不可变:指的是
    👉 t[1] 这个“引用”不能被替换

  • 列表可变
    👉 列表内部的内容仍然可以修改

📌 一句话总结:

元组保证的是“结构不变”,而不是“内容绝对不变”

5. 元组解包(非常重要的能力)

基本解包
point = (10, 20)
x, y = point

print(x, y)  # 10 20
解包与函数返回值
def get_min_max(nums):
    return min(nums), max(nums)

min_val, max_val = get_min_max([3, 7, 1, 9])
print(min_val, max_val) # 1 9
使用*进行扩展解包
a, *b = (1, 2, 3, 4)
print(a)  # 1
print(b)  # [2, 3, 4]

6. 元组的常见应用场景

作为字典的 key
location = {}
location[(10, 20)] = "home"

print(location)

原因:

  • 字典 key 必须是 不可变类型

  • 列表不能作为 key,元组可以

函数返回多个值(pythonic 写法)
def user_info():
    return "alice", 18, true

name, age, active = user_info()
表示“业务含义固定”的数据
host = "localhost"
port = 8080
config = (host, port)

📌 语义层面表达:

“这组数据不应该被修改”

7. 元组的常用方法与内置函数

元组方法非常少,这是其设计的一部分:

t = (1, 2, 2, 3)

print(t.count(2))   # 2
print(t.index(3))   # 3

常配合使用的内置函数:

len(t)
max(t)
min(t)
sum(t)

8. 元组与列表的深入对比

维度列表元组
内存占用较高较低
创建速度较慢较快
安全性易被修改更安全
可读性偏数据容器偏结构表达

小结一下吧

  • 元组是 不可变的有序序列

  • 与列表相似,但语义更偏向 “固定结构”

  • 常见用途:

    • 函数多返回值

    • 字典 key

    • 不可修改的配置数据

  • 支持:

    • 索引、切片、遍历

    • 解包(非常重要)

(六)总结:如何选择合适的数据结构

✅ 至此,你已经系统掌握 python 的四大基础数据结构::

  • 列表(list):有序、可变的序列

  • 字典(dict):键值映射

  • 集合(set):无序、不重复

  • 元组(tuple):不可变的序列

每种数据结构都有自己的特点和适用场景,选择合适的数据结构能够让程序更高效、更易维护、更易理解

1. 数据结构选择对比表

数据结构特性使用场景注意事项
列表 list有序,可变,可包含任意类型保存顺序数据,循环处理,多次增删改遍历修改时要小心;切片返回新列表
字典 dict键值映射,key 唯一,可变记录型数据(用户信息、配置、状态映射),快速查找key 必须不可变;get 可提供默认值
集合 set无序,不重复,高效成员判断去重、集合运算、权限判断、标签判断无索引;遍历顺序不可控
元组 tuple有序,不可变,可包含任意类型固定数据、函数多值返回、字典 key内部可变对象仍可修改;不可修改元素

2. 如何选择数据结构

选择数据结构可以按以下几个维度:

1️⃣ 是否需要顺序
  • 是 → 列表或元组

  • 否 → 集合或字典

2️⃣ 是否需要唯一性
  • 是 → 集合

  • 否 → 列表或元组

3️⃣ 是否需要通过名字访问
  • 是 → 字典

  • 否 → 列表或集合

4️⃣ 是否需要可变性
  • 可变 → 列表、字典、集合

  • 不可变 → 元组、不可变字典(扩展应用)

3. 常见组合场景

在实际开发中,这些数据结构经常组合使用:

场景数据结构组合示例
多条记录,每条记录有多个字段列表 + 字典users = [{"name": "alice", "age":18}, {"name": "bob", "age":20}]
字典中某字段有多个值字典 + 列表grades = {"alice":[90,95], "bob":[80,82]}
去重的列表列表 + 集合unique_nums = list(set(nums))
函数返回多个值元组min_val, max_val = get_min_max([3,7,1,9])

4. 选用原则总结

  • 顺序性优先 → 列表/元组

  • 键值访问优先 → 字典

  • 唯一性 / 集合运算 → 集合

  • 数据固定不变 → 元组

  • 复杂嵌套 → 根据组合场景选择最合适的容器

🔑 核心思想:
数据结构是工具,目的是高效组织和操作数据
不要只记语法,要根据问题特性选择最合适的数据结构。

七、结构化数据:python 中的数据组织与处理

(一)为什么需要结构化数据建模

在前面的章节中,我们已经学习了 python 提供的基础数据结构:列表(list)、字典(dict)、集合(set)和元组(tuple)
这些数据结构在表达简单数据时非常高效,但当我们开始处理真实世界中的业务数据时,很快就会发现:仅靠基础数据结构,往往难以支撑复杂数据的长期演进与工程化使用

结构化数据建模,正是为了解决这一问题而出现的。

1. 现实世界数据的天然复杂性

现实中的数据几乎从来不是“单值”的,而是具有以下典型特征:

  • 多字段(multi-attribute)一个用户不仅有名字,还包含年龄、邮箱、状态、注册时间等多个属性;一个订单不仅有金额,还包含商品列表、支付方式、物流信息等

  • 层级关系(hierarchical)用户包含地址信息;订单包含多个商品;商品又包含规格、价格、库存等子结构

  • 实体之间存在关联一对多:一个用户对应多个订单;多对一:多个订单属于同一个商家;多对多:用户与标签、商品与分类

  • 状态与行为并存数据不仅是“静态描述”,还会涉及状态变化(如订单状态流转),以及业务行为(如校验、计算、转换)

这些特征决定了:现实世界的数据,本质上是结构化、层级化、并且语义明确的

2. 基础数据结构的能力边界

python 的 list 和 dict 是非常强大的工具,但它们的设计初衷是通用容器,而不是“领域模型”

以字典为例:

  • 字典可以存任意键值对

  • 但它并不关心:

    • 这个字段是否必须存在

    • 这个字段应该是什么类型

    • 不同字段之间是否存在约束关系

随着业务复杂度的上升,单纯使用 dict 会逐渐暴露出问题:

  • 结构不透明字段依赖“约定”而非“定义”,阅读代码时无法一眼看出数据结构

  • 缺乏约束键名拼写错误只能在运行时报错,类型错误难以及早发现

  • 可维护性下降字段增加或调整会波及大量代码,缺乏统一的数据入口

  • 业务语义缺失data["status"] == 1 本身无法说明业务含义,需要额外注释或上下文才能理解

这意味着:基础数据结构更适合“存数据”,但并不适合“表达数据含义”

3. 从“数据容器”到“数据模型”的转变

当系统规模逐渐扩大时,我们需要的不再只是一个“能装东西的容器”,而是一个明确表达业务含义的数据模型(data model)

数据模型关注的不仅是“数据长什么样”,还包括:

  • 字段的业务语义

  • 字段之间的结构关系

  • 数据的合法性规则

  • 数据可支持的操作与行为

这种转变,本质上是:从“如何存数据”,升级为“如何描述现实世界中的对象”。

4. python 在结构化数据建模上的天然优势

python 之所以适合结构化数据建模,主要得益于以下能力:

  • 灵活的基础数据结构(list / dict)

  • 完整的面向对象体系(class)

  • 强大的类型注解支持

  • 内建 json 处理能力

  • dataclass 等高级建模工具

这些能力,使得 python 可以自然地完成从:原始数据 → 半结构化数据 → 领域模型 → 工程化数据体系的逐层演进。

小结一下吧

  • 现实世界的数据天然是结构化的

  • 基础数据结构适合存储,但不适合长期建模

  • 结构化数据建模是代码工程化的必经之路

  • python 提供了从简单到复杂的完整建模能力

在接下来的学习中,我们将从嵌套数据结构开始,逐步引入 json 处理,最终过渡到 基于 class 的结构化数据建模,构建可维护、可扩展的 python 数据体系。

(二)python 中的嵌套结构化数据

在明确了为什么需要结构化数据建模之后,我们首先要面对的是:如何在 python 中表达“多字段 + 多层级”的数据结构

在不引入面向对象之前,python 最直接、最常见的做法,就是使用嵌套的数据结构——即在 list 或 dict 中再包含 list 或 dict。

嵌套结构,是 python 迈向结构化数据处理的第一步

1. 嵌套结构化数据的基本思想

所谓嵌套结构,本质上是:用 dict 表达“对象”,用 list 表达“集合”,并通过层级组合还原现实关系。

这种思想非常直观,也几乎不需要额外语法成本,因此被广泛用于:

  • 接口返回数据

  • 配置文件

  • 临时数据组织

  • json 原始载体

2. 字典嵌套字典:表达对象的层级属性

适用场景:一个对象内部存在子结构

user = {
    "id": 1001,
    "name": "alice",
    "profile": {
        "age": 28,
        "email": "alice@example.com",
        "address": {
            "city": "beijing",
            "zipcode": "100000"
        }
    }
}

结构理解

  • user:整体对象

  • profile:用户的扩展属性

  • address:更细粒度的子结构

这种结构清晰地反映了现实语义:地址不属于用户本身,而属于用户的资料信息。

访问方式

city = user["profile"]["address"]["city"]

此时你已经可以感受到第一个问题:

  • 访问路径依赖结构稳定

  • 任意一层缺失,都会导致运行期错误

3. 字典嵌套列表:表达一对多关系

适用场景:一个主体拥有多个子对象

order = {
    "order_id": "ord-001",
    "user_id": 1001,
    "items": [
        {"sku": "sku-1", "price": 100, "quantity": 2},
        {"sku": "sku-2", "price": 50, "quantity": 1}
    ]
}

结构理解

  • order 是订单对象

  • items 是订单下的商品集合

  • 每个商品仍然是一个“对象”

典型处理逻辑

total_amount = 0
for item in order["items"]:
    total_amount += item["price"] * item["quantity"]

此时一个非常关键的变化已经出现:业务计算逻辑,开始直接写在“嵌套结构遍历”之中

这是后续引入 class 建模的重要动机。

4. 列表嵌套字典:同构对象的集合

适用场景:多个结构相同的对象

users = [
    {"id": 1, "name": "alice", "active": true},
    {"id": 2, "name": "bob", "active": false},
    {"id": 3, "name": "charlie", "active": true}
]

遍历与筛选

active_users = []
for user in users:
    if user["active"]:
        active_users.append(user)

这种结构在接口返回数据中极为常见,但它也暴露了嵌套结构的本质问题之一:

  • 字段完全依赖字符串约定

  • 没有任何显式结构定义

  • 错误只能在运行期发现

5. 多层混合嵌套:真实系统中的常态

在真实系统中,嵌套结构往往是多种形式的组合:

response = {
    "code": 0,
    "data": {
        "user": {
            "id": 1001,
            "orders": [
                {
                    "order_id": "ord-001",
                    "items": [
                        {"sku": "sku-1", "price": 100}
                    ]
                }
            ]
        }
    }
}

访问路径示例

sku = response["data"]["user"]["orders"][0]["items"][0]["sku"]

此类代码的特征非常明显:

  • 访问链路极长

  • 强依赖数据结构细节

  • 可读性和可维护性迅速下降

6. 防御式访问:必要但有限的改进

为了避免异常,开发中常使用防御式写法:

user = response.get("data", {}).get("user", {})
orders = user.get("orders", [])

if orders:
    first_order = orders[0]

这种方式可以:

  • 降低运行期错误概率

  • 提高代码健壮性

但需要清醒认识到:防御式访问解决的是“不崩溃”,而不是“结构合理”。

7. 嵌套结构的工程性问题总结

通过以上示例,可以清晰看到嵌套结构在工程中的局限:

  • 结构定义分散:数据结构存在于代码路径中,而非集中定义

  • 业务语义缺失:字段只是字符串,无法承载规则和含义

  • 强耦合:结构变更会影响大量访问代码

  • 逻辑与数据混杂:业务计算被迫写在遍历和索引中

当代码中大量出现如下形式时:

data["a"]["b"][0]["c"]["d"]

通常意味着:嵌套结构已经达到了可维护性的上限

8. 嵌套结构在整体体系中的正确定位

嵌套 dict / list 并不是“错误做法”,而是阶段性工具

使用场景评价
原始 json 承载合适
临时数据处理合适
脚本 / demo合适
核心业务模型不合适
长期维护系统不合适

它更适合作为:数据的“中间形态”,而非最终模型。

小结一下吧

  • 嵌套结构是 python 表达复杂数据的基础方式

  • 它通过 dict + list 组合还原现实关系

  • 随着层级加深,维护成本迅速上升

  • 这正是引入 json 规范与 class 建模的直接动因

接着我们将进入 半结构化数据:json 在 python 中的处理,你会发现:json 的便利性,并没有从根本上解决嵌套结构的问题,而只是让它“标准化”了

(三)半结构化数据:json 在 python 中的处理

在上一节中,我们已经看到:通过 list 与 dict 的嵌套,python 可以完整表达复杂数据结构,但可维护性迅速下降

在真实工程中,这类嵌套结构往往并不是“手写”的,而是来自:

  • http 接口返回

  • 配置文件

  • 消息队列

  • 日志或存储系统

这些数据最常见的载体形式,就是 json(javascript object notation)

1. 什么是半结构化数据

从数据组织角度看,数据大致可以分为三类:

  • 非结构化数据:文本、图片、音频

  • 结构化数据:表结构、强约束模型

  • 半结构化数据:结构存在,但约束较弱

json 正是典型的半结构化数据格式,其特点是:

  • 结构清晰(层级、键值)

  • 可嵌套

  • 无强类型约束

  • 字段可选、可变

这使得 json 非常适合数据交换,但不适合直接作为业务模型

2. json 的数据模型与 python 的映射关系

json 本身支持的数据类型非常有限:

json 类型python 对应类型
objectdict
arraylist
stringstr
numberint / float
booleanbool
nullnone

也正因如此,json 在 python 中的本质就是:嵌套的 dict 与 list

换句话说:json 并没有解决嵌套结构的问题,只是把它“标准化”了。

3. python 中的 json 解析(反序列化)

python 内置 json 模块,用于 json 与 python 对象之间的转换。

import json

json_str = '''
{
    "id": 1001,
    "name": "alice",
    "active": true,
    "roles": ["admin", "editor"]
}
'''

data = json.loads(json_str)

解析后:

  • data 是一个 dict

  • 所有字段完全不受约束

  • 访问方式与普通嵌套结构一致

name = data["name"]
roles = data["roles"]

4. 从文件加载 json 数据

json 文件是配置与数据交换的常见形式(这里暂时先理解,后续这部分会在新博客单独讲解):

import json

with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

此时你获得的是:

  • 一个完全动态的嵌套结构

  • 所有字段都只能“运行期验证”

5. json 的典型嵌套结构示例

考虑一个真实接口返回示例:

response = {
    "code": 0,
    "message": "success",
    "data": {
        "user": {
            "id": 1001,
            "name": "alice",
            "orders": [
                {
                    "order_id": "ord-001",
                    "items": [
                        {"sku": "sku-1", "price": 100, "quantity": 2}
                    ]
                }
            ]
        }
    }
}

业务代码往往会这样写:

orders = response["data"]["user"]["orders"]
first_item_price = orders[0]["items"][0]["price"]

到这里,你应该已经非常熟悉这种访问模式——它与嵌套结构没有任何本质区别

6. json 带来的“结构假象”

json 的一个常见误区是:“有格式 ≠ 有模型”

虽然 json 看起来“结构清晰”,但它存在以下天然问题:

  • 字段完全不受约束缺字段不会报错,多字段也不会报错

  • 类型不可靠price 可能是数字,也可能是字符串,错误只能在运行期暴露

  • 业务语义缺失status = 1 的含义无法自解释,需要依赖外部文档或注释

  • 结构变更极具破坏性任意字段调整都会影响访问链

json 解决的是“数据如何传输”,而不是“数据如何使用

7. 防御式 json 解析的常见写法

为了避免运行期异常,代码中常见如下写法:

data = response.get("data", {})
user = data.get("user", {})
orders = user.get("orders", [])

if orders:
    order_id = orders[0].get("order_id")

这种写法与上面讲的嵌套方式依旧没啥区别,更重要的是:仍然没有一个“明确的数据模型”

8. json 在工程体系中的正确角色

在成熟系统中,json 的定位应当非常明确:

角色是否合适
网络传输格式非常合适
配置文件合适
接口输入 / 输出合适
业务内部模型不合适
核心领域对象不合适

json 更适合作为:系统边界上的数据格式,而非系统内部的结构表达。

系统引入结构化建模的阶段python 给出的答案是:

  • class

  • dataclass

  • 明确的数据模型定义

  • json ↔ model 的转换边界

小结一下吧

  • json 是典型的半结构化数据格式

  • 在 python 中,json 本质仍是嵌套 dict / list

  • json 适合传输,不适合直接承载业务模型

  • 防御式解析无法从根本上解决结构问题

  • 这直接引出了:为什么必须使用 class 进行结构化建模

(四)面向对象的结构化建模:class 的引入

在前面的讲解中无论哪一步,数据始终以同一种形态存在:dict,只是内容变“干净”了而已。

接下来将通过一组连续示例说明:为什么必须引入 class,以及 class 在工程中应该如何使用。

1. 问题起点:业务逻辑正在“侵蚀”数据结构

先看一个非常真实的场景,接口返回订单数据:

order_data = {
    "order_id": "ord-001",
    "price": "100",
    "quantity": 2
}

业务中需要计算订单金额,代码往往这样写:

total_amount = int(order_data["price"]) * order_data["quantity"]

这段代码没有语法问题,但存在严重的工程隐患

  • 类型转换是“临时的”

  • 规则是“隐含的”

  • 任意地方都可以重复这段逻辑

此时系统出现了第一个危险信号:业务规则开始散落在 dict 操作中

2. 第一次建模:用 class 固定结构与类型

我们引入最基础的 class,仅做一件事:把“字段定义”和“类型规则”集中到一个地方

class order:
    def __init__(self, order_id, price, quantity):
        self.order_id = order_id
        self.price = int(price)
        self.quantity = int(quantity)

此时发生了三件非常重要的变化:

  • 结构集中order 明确有哪些字段

  • 类型前置类型转换只发生一次

  • 使用成本下降后续代码不再关心字段细节

业务代码变为:

order = order("ord-001", "100", 2)

3. 第二次建模:让业务行为回归模型

现在再看金额计算逻辑。如果继续写在外部:

total = order.price * order.quantity

虽然比 dict 好,但语义仍然分散。正确的工程做法是:只要逻辑“属于对象”,就应进入对象本身

class order:
    def __init__(self, order_id, price, quantity):
        self.order_id = order_id
        self.price = int(price)
        self.quantity = int(quantity)

    def total_amount(self):
        return self.price * self.quantity

此时:

  • 金额计算的业务含义被明确命名

  • 外部代码无需关心实现细节

total = order.total_amount()

4. 工程规范一:class 不是“数据壳”,而是“业务单元”

到这里,可以明确一个重要规范

一个 class 不应该只是 dict 的替代品,而应该是一个“可用对象”。

如果一个 class:

  • 只有属性

  • 没有任何方法

那它的价值是非常有限的。

5. 第三次建模:让 class 成为解析与清洗的边界

回到真实输入数据的问题。接口返回的往往是 不可信 json

raw_data = {
    "order_id": "ord-001",
    "price": none,
    "quantity": "2"
}

工程上正确的做法是:让 class 成为“不可信数据 → 可信对象”的边界

class order:
    def __init__(self, data: dict):
        self.order_id = data.get("order_id")
        self.price = self._to_int(data.get("price"), default=0)
        self.quantity = self._to_int(data.get("quantity"), default=1)

    def _to_int(self, value, default=0):
        try:
            return int(value)
        except (typeerror, valueerror):
            return default

此时:

  • 所有防御逻辑被“封装”

  • 外部世界不再处理脏数据

  • 构造完成的对象即“可用对象”

6. 工程规范二:业务代码只接触“可信对象”

正确的调用方式应当是:

order = order(raw_data)
total = order.total_amount()

而不是:

# ❌ 不推荐
total = int(raw_data["price"]) * int(raw_data["quantity"])

规范总结:

解析、清洗、校验,必须发生在模型内部
业务代码不应再做任何字段判断

7. 第四次建模:用对象组合替代嵌套结构

考虑一个更真实的订单结构:

raw_order = {
    "order_id": "ord-001",
    "items": [
        {"sku": "sku-1", "price": "100"},
        {"sku": "sku-2", "price": "50"}
    ]
}

如果继续使用嵌套 dict:

total = 0
for item in raw_order["items"]:
    total += int(item["price"])

正确的建模方式是:用对象组合替代嵌套结构

class orderitem:
    def __init__(self, data):
        self.sku = data.get("sku")
        self.price = int(data.get("price", 0))

class order:
    def __init__(self, data):
        self.order_id = data.get("order_id")
        self.items = [orderitem(item) for item in data.get("items", [])]

    def total_amount(self):
        return sum(item.price for item in self.items)

此时,结构发生了质变:

  • 嵌套 dict → 对象关系

  • 遍历逻辑 → 模型内部能力

8. 工程规范三:嵌套数据 ≠ 嵌套 dict,而是“对象层级”

这是结构化建模中非常重要的一条规范

嵌套关系应通过“对象组合”表达,而不是嵌套 dict 访问路径。

一旦遵循这条规范:

  • 访问路径显著变短

  • 代码可读性成倍提升

  • 结构演进成本大幅下降

小结一下吧

通过示例可以总结出 class 建模的核心工程规范:

  • class 是模型,不是数据壳

  • 结构、语义、行为必须统一

  • 解析与清洗是模型的职责

  • 业务代码只操作可信对象

  • 嵌套关系用对象组合表达

到这里,你已经完成了:

从“操作数据” → “使用模型” 的关键转变

(五)复杂对象组合与领域建模

在真实系统中,所谓“复杂数据”,并不仅仅是 层级多,而是同时具备:

  • 多层嵌套结构

  • 不同子结构承担不同职责

  • 局部规则 + 全局规则并存

  • 数据来自不可信边界(接口 / mq / db)

这一节,我们用一个 接近真实电商/交易系统的数据模型,完整展示 python 中的复杂领域建模方式

1. 原始输入:近真实系统级 json 结构

这是一个 近真实工程中常见复杂度 的订单数据(已适度抽象):

raw_order = {
    "order_id": "ord-9001",
    "user": {
        "id": "u-1001",
        "profile": {
            "name": "alice",
            "level": "vip"
        }
    },
    "items": [
        {
            "sku": "sku-1",
            "pricing": {
                "unit_price": "100",
                "currency": "cny"
            },
            "quantity": 2,
            "discounts": [
                {"type": "promotion", "amount": "10"},
                {"type": "vip", "amount": "5"}
            ]
        }
    ],
    "payment": {
        "method": "credit_card",
        "transactions": [
            {"tx_id": "tx-1", "amount": "185"},
            {"tx_id": "tx-2", "amount": "10"}
        ]
    }
}

特点:

  • 最深层级:5 层

  • 同时包含:列表嵌套 + 对象嵌套

  • 金额、规则、行为全部混在 json 中

如果直接用 dict 操作,这类代码几乎不可维护。

2. 第一步:识别领域与聚合边界(不是拆字段)

复杂建模的第一步不是写类,而是划分领域对象

从业务语义出发:

json 片段领域对象
orderorder(聚合根)
useruser
profileuserprofile
itemsorderitem
pricingpricing
discountsdiscount
paymentpayment
transactionspaymenttransaction

原则:

只要一个子结构有“独立业务含义”,就值得一个 class。

3. 底层值对象(value object)建模

discount(最小业务单元)

class discount:
    def __init__(self, data):
        self.type = data.get("type")
        self.amount = int(data.get("amount", 0))

pricing(价格不是一个字段,而是一组规则)

class pricing:
    def __init__(self, data):
        self.unit_price = int(data.get("unit_price", 0))
        self.currency = data.get("currency")

    def total(self, quantity):
        return self.unit_price * quantity

4. orderitem:多层对象的“局部聚合”

class orderitem:
    def __init__(self, data):
        self.sku = data.get("sku")
        self.quantity = int(data.get("quantity", 1))

        self.pricing = pricing(data.get("pricing", {}))
        self.discounts = [
            discount(d) for d in data.get("discounts", [])
        ]

    def discount_amount(self):
        return sum(d.amount for d in self.discounts)

    def subtotal(self):
        return self.pricing.total(self.quantity) - self.discount_amount()

注意这里已经出现了领域行为协同

  • 折扣规则不在 order

  • 价格规则不在 order

  • orderitem 自洽完成金额计算

5. 用户领域:嵌套对象中的语义建模

class userprofile:
    def __init__(self, data):
        self.name = data.get("name")
        self.level = data.get("level")

    def is_vip(self):
        return self.level == "vip"


class user:
    def __init__(self, data):
        self.user_id = data.get("id")
        self.profile = userprofile(data.get("profile", {}))

6. 支付领域:独立聚合,不混入订单逻辑

class paymenttransaction:
    def __init__(self, data):
        self.tx_id = data.get("tx_id")
        self.amount = int(data.get("amount", 0))

class payment:
    def __init__(self, data):
        self.method = data.get("method")
        self.transactions = [
            paymenttransaction(tx)
            for tx in data.get("transactions", [])
        ]

    def paid_amount(self):
        return sum(tx.amount for tx in self.transactions)

7. order:真正的“领域聚合根”

class order:
    def __init__(self, data):
        self.order_id = data.get("order_id")

        self.user = user(data.get("user", {}))
        self.items = [orderitem(i) for i in data.get("items", [])]
        self.payment = payment(data.get("payment", {}))

    def order_amount(self):
        return sum(item.subtotal() for item in self.items)

    def is_fully_paid(self):
        return self.payment.paid_amount() >= self.order_amount()

注意:

  • order 不参与任何底层计算

  • order 只做“聚合协调”

  • 各领域职责清晰、不交叉

8. 工程级规范总结(复杂场景)

这一示例隐含了一套非常重要的工程规范

规范一:不要让聚合根“变胖”
  • 金额计算下沉到 orderitem

  • 折扣规则不写在 order

规范二:多层嵌套 ≠ 多层 dict
  • 每一层都是对象

  • 每一层都有行为

规范三:领域隔离
  • payment 不知道 orderitem 的存在

  • order 不知道 discount 的细节

规范四:结构变化可控
  • json 结构变化只影响局部 class

这一节完成的不是“示例升级”,而是复杂度跃迁

  • 从 2 层嵌套 → 5 层嵌套

  • 从数据组合 → 领域协作

  • 从“能跑” → “可演进、可维护”

你现在已经具备:用 python class 构建真实业务领域模型的能力

(六)json ↔ class 的双向转换:边界层与 dto 设计

在 (五) 中,我们已经构建了一套复杂、内聚、可演进的领域模型。但此时如果直接将这些 class 用在接口层或存储层,会立即遇到新的问题:

  • 接口层需要 json

  • mq / rpc 需要可序列化结构

  • 数据库存储结构与领域模型不一致

  • 外部系统字段经常变动

这引出了一个非常重要的工程问题领域模型不应直接暴露在系统边界。

1. 为什么不能让领域模型“裸奔”到边界

一个常见但危险的做法是:

json.dumps(order.__dict__)

这种方式存在严重问题:

  • 结构不可控:私有字段、内部状态一并暴露

  • 语义污染:内部对象结构被外部依赖

  • 变更风险极高:内部模型调整会破坏接口

  • 无法做版本治理:接口字段无法演进

工程结论:领域模型 ≠ 接口数据模型

2. 边界层的核心职责

在工程上,通常需要一个边界层(boundary layer),它负责:

  • json → dto

  • dto → domain model

  • domain model → dto

  • dto → json

其中,dto(data transfer object) 是核心中介。

3. dto 的设计原则(非常重要)

dto 与领域模型的关系,应遵循以下原则:

  • 只包含数据,不包含业务行为

  • 结构扁平,面向接口需求

  • 允许字段冗余或命名不一致

  • 变化频率远高于领域模型

  • 边界转换集中化

    一个成熟系统中,应当遵循:

    所有 json ↔ domain 的转换,都集中在边界层完成

    而不是:controller 中拼 dict; service 中解析字段; model 中关心接口格式

3.1 示例:为 order 定义输入 dto

接口输入 json:

{
  "order_id": "ord-9001",
  "user_id": "u-1001",
  "items": [
    {
      "sku": "sku-1",
      "unit_price": "100",
      "quantity": 2
    }
  ]
}

对应 dto:

class ordercreatedto:
    def __init__(self, data):
        self.order_id = data.get("order_id")
        self.user_id = data.get("user_id")
        self.items = data.get("items", [])

dto 的特点:

  • 不做复杂解析

  • 不包含嵌套对象建模

  • 只服务接口需求

3.2 dto → domain model(反序列化边界)

将 dto 转换为领域模型,是边界层的核心工作之一

class orderfactory:
    @staticmethod
    def from_create_dto(dto: ordercreatedto):
        raw = {
            "order_id": dto.order_id,
            "user": {"id": dto.user_id},
            "items": [
                {
                    "sku": item["sku"],
                    "pricing": {"unit_price": item["unit_price"]},
                    "quantity": item["quantity"],
                    "discounts": []
                }
                for item in dto.items
            ]
        }
        return order(raw)

注意几点:

  • dto 不知道领域模型

  • 领域模型不依赖 dto

  • 转换逻辑集中在 factory / assembler 中

3.3 domain model → dto(序列化边界)

对外输出时,同样不能直接暴露领域模型。

class orderresponsedto:
    def __init__(self, order: order):
        self.order_id = order.order_id
        self.total_amount = order.order_amount()
        self.paid = order.is_fully_paid()

再由 dto 转为 json:

response = json.dumps(order_response_dto.__dict__)

3.4. 复杂结构下的 dto 拆分策略

当结构进一步复杂时,应避免:

class bigdto:
    ...

而采用:

  • orderdto

  • itemdto

  • paymentdto

再由 assembler 负责组合。

3.5 防腐层(anti-corruption layer)视角

在对接外部系统时,dto 还承担着一个更重要的角色:

防止外部模型“污染”内部领域模型

字段改名、结构变更、类型异常,都被限制在 dto 层。

小结一下吧

通过本节,你应该建立起以下清晰认知:

  • 领域模型只服务领域逻辑

  • dto 是系统边界的语言

  • json 永远不应直接进入领域模型

  • factory / assembler 是关键角色

  • 边界清晰,系统才能长期演进

(七)使用 dataclass 与类型系统增强结构化建模

在前面的分享中,我们已经用“原生 class”完成了复杂领域建模。但你可能已经感受到几个现实问题:

  • __init__ 代码大量重复

  • 字段类型只能靠约定

  • 对象结构难以一眼看清

  • 重构时缺乏工具辅助

这正是 dataclass 与类型系统 登场的时机。

1. 为什么在复杂建模阶段引入 dataclass

dataclass 并不是“语法糖”,而是结构化建模的工程工具

它解决的核心问题是:

问题原生 classdataclass
字段声明分散在 __init__集中声明
类型表达隐式显式
可读性中等
维护成本

工程结论:当模型成为系统核心资产时,必须使用 dataclass 或等价机制。

2. 从原生 class 到 dataclass:逐步演进

先看之前写的的一个典型类:

class discount:
    def __init__(self, data):
        self.type = data.get("type")
        self.amount = int(data.get("amount", 0))

使用 dataclass 重构后:

from dataclasses import dataclass

@dataclass
class discount:
    type: str
    amount: int

但这只是第一步,真实工程不会直接这样写

3. dataclass 中的构造边界:__post_init__

在领域模型中:

  • 字段是“结构”

  • 校验、转换是“规则”

dataclass 提供 __post_init__ 来承载规则。

from dataclasses import dataclass

@dataclass
class discount:
    type: str
    amount: int

    def __post_init__(self):
        self.amount = int(self.amount)

规范:类型转换、校验逻辑应放在 __post_init__,而不是外部代码。

4. 复杂对象中的 dataclass 组合

重构 pricing:

from dataclasses import dataclass

@dataclass
class pricing:
    unit_price: int
    currency: str = "cny"

    def total(self, quantity: int) -> int:
        return self.unit_price * quantity

orderitem:

from dataclasses import dataclass, field
from typing import list

@dataclass
class orderitem:
    sku: str
    pricing: pricing
    quantity: int = 1
    discounts: list[discount] = field(default_factory=list)

    def subtotal(self) -> int:
        discount = sum(d.amount for d in self.discounts)
        return self.pricing.total(self.quantity) - discount

此时,模型结构已经一眼可读

5. 类型系统带来的“隐性收益”

引入类型标注后,产生的收益包括:

  • ide 自动补全

  • 静态检查(mypy / pyright)

  • 重构安全性

  • 模型即文档

例如:

def calculate_total(items: list[orderitem]) -> int:
    ...

类型本身就表达了设计意图

6. dataclass 与不可变建模(高级用法)

在部分领域中(如支付、账务),对象应当是不可变的

@dataclass(frozen=true)
class paymenttransaction:
    tx_id: str
    amount: int

不可变对象的工程意义:

  • 防止状态被随意修改

  • 更容易推理

  • 更适合并发场景

7. dataclass ≠ dto:职责仍然要分清

一个常见误区是:“既然 dataclass 很方便,那 dto 也用它吧?”

可以,但要遵守原则:

对象是否可用 dataclass是否包含行为
dto可以不应该
domain model强烈推荐必须

关键点不是工具,而是职责。

8. 工程规范:领域模型的现代 python 写法

综合前面内容,可以形成一套明确规范:

  • 领域模型使用 dataclass

  • 字段必须有类型

  • 规则写在方法或 __post_init__

  • 嵌套关系通过类型表达

  • 不可变对象优先用于关键领域

小结一下吧

通过 dataclass 与类型系统:

  • 模型从“能用”变为“可靠”

  • 结构从“隐式”变为“显式”

  • 重构从“危险”变为“可控”

(八)领域模型与数据库模型的边界:orm 与持久化设计

前面整体学习了解了:

  • 复杂领域建模

  • 明确的边界与 dto

  • dataclass + 类型系统增强

系统几乎不可避免地会进入一个危险阶段:为了“省事”,开始让领域模型向数据库结构妥协

这一节的目标,是明确划清领域模型与持久化模型的边界

1. 一个常见但致命的误区

很多系统会出现如下代码风格:

class order(base):
    __tablename__ = "orders"

    id = column(string, primary_key=true)
    total_amount = column(integer)

看似优雅,实际上隐藏着几个问题:

  • 领域模型被 orm 污染

  • 数据库结构决定了领域结构

  • 业务规则被挤出模型

工程结论:orm model ≠ domain model

2. 三种模型的职责划分(必须牢记)

在成熟系统中,通常存在三类模型:

模型类型关注点变化频率
dto接口 / 边界
domain model业务规则
persistence model(orm)表结构

它们解决的是完全不同的问题

3. 领域模型为什么不应该“继承 orm”

原因并不抽象:

  • 数据库字段不是业务语言

  • orm 强制扁平化结构

  • 懒加载/会话污染领域行为

  • 测试复杂度急剧上升

领域模型应当:

  • 与数据库无关

  • 可以脱离 orm 独立运行与测试

4. 正确的工程结构示意

一个合理的工程分层应当是:

领域模型永远不直接操作数据库

5. orm model:只做“表的映射”

以 sqlalchemy 为例(仅作为概念说明):

class orderorm(base):
    __tablename__ = "orders"

    order_id = column(string, primary_key=true)
    total_amount = column(integer)

特点:

  • 结构与表一一对应

  • 没有业务行为

  • 允许妥协命名

6. repository:连接领域与持久化的桥梁

repository 是最关键、但最容易被忽略的一层

class orderrepository:
    def save(self, order: order):
        raise notimplementederror

    def get(self, order_id: str) -> order:
        raise notimplementederror

领域层只依赖 接口,而不是实现。

7. orm → domain 的装配(反序列化)

class orderrepositoryimpl(orderrepository):
    def get(self, order_id: str) -> order:
        orm = self.session.query(orderorm).get(order_id)

        raw = {
            "order_id": orm.order_id,
            "items": self._load_items(orm),
            "payment": self._load_payment(orm)
        }

        return order(raw)

注意:

  • orm 结构被“翻译”

  • domain model 完全不知道 orm 的存在

8. domain → orm 的持久化(序列化)

def save(self, order: order):
    orm = orderorm(
        order_id=order.order_id,
        total_amount=order.order_amount()
    )
    self.session.add(orm)

即使结构重复,也要接受。重复是隔离的代价,也是稳定性的来源。

9. 工程规范:持久化永远是“外部关心的事”

领域模型中 绝对不应出现

  • session

  • commit

  • save()

  • orm 注解

如果你在领域模型中看到这些,说明:领域已经被侵蚀

小结一下吧

通过本节,你应当形成以下坚定认知:

  • 领域模型不为数据库服务

  • orm 只是技术实现

  • repository 隔离变化

  • 结构重复是合理且必要的

  • 领域模型必须可以脱离数据库独立存在

(九)校验、异常与领域约束建模

现在我们已经解决了:

  • 复杂结构如何建模

  • 边界如何隔离

  • 如何工程化增强模型

  • 如何与数据库解耦

但到目前为止,仍有一个问题被刻意回避了:如果数据是“非法的”,怎么办?

现实系统中,非法数据是常态,而不是例外

1. 一个常见但危险的现状

很多系统对“非法数据”的处理方式是:

if price <= 0:
    return none

或者:

try:
    ...
except exception:
    pass

这些写法的问题不在于“能不能跑”,而在于:

  • 错误被吞掉

  • 规则被隐式化

  • 系统进入“不可预期状态”

工程结论:校验不是工具层问题,而是领域模型的职责

2. 什么是“领域约束”(domain constraint)

领域约束指的是:

  • 业务上不允许被破坏的规则

  • 与技术、存储、接口无关

  • 一旦违反,系统就不应继续执行

例如:

  • 订单金额不能为负

  • 支付金额不能小于 0

  • 已完成订单不能再次支付

这些规则 不属于 controller,也不属于 service,而是:领域模型自身的边界

3. 领域异常:用“异常”表达规则破坏

在领域建模中,异常不是“程序错误”,而是:对业务规则被破坏的明确声明

首先,定义领域异常基类:

class domainerror(exception):
    pass

然后为关键规则定义具体异常:

class invalidpriceerror(domainerror):
    pass

class paymentamounterror(domainerror):
    pass

class orderstateerror(domainerror):
    pass

规范:不要在领域中直接抛 valueerrortypeerror

4. 在模型内部执行校验(而不是外部 if)

orderitem 为例:

from dataclasses import dataclass

@dataclass
class orderitem:
    sku: str
    quantity: int

    def __post_init__(self):
        if self.quantity <= 0:
            raise invalidpriceerror("quantity must be positive")

此时:

  • 非法对象 根本无法被构造

  • 错误在最早阶段暴露

5. 行为级校验:规则与动作绑定

不是所有校验都发生在构造阶段。

例如:支付行为。

class order:
    ...

    def pay(self, amount: int):
        if amount <= 0:
            raise paymentamounterror("payment must be positive")

        if self.is_fully_paid():
            raise orderstateerror("order already paid")

        self.payment.add_transaction(amount)

关键原则:规则应当与“动作”绑定,而不是与“字段”绑定。

6. 领域状态与约束的结合

复杂系统中,状态本身就是约束来源

class orderstatus:
    created = "created"
    paid = "paid"
    canceled = "canceled"


class order:
    ...

    def cancel(self):
        if self.status == orderstatus.paid:
            raise orderstateerror("paid order cannot be canceled")

        self.status = orderstatus.canceled

此时:

  • 状态转换路径被严格限制

  • 非法流程无法发生

7. 校验逻辑的分层原则(非常重要)

一个成熟系统中,校验应分为三层:

层级校验内容示例
接口层格式、必填json 字段是否存在
边界层基本合法性字符串能否转 int
领域层业务规则金额、状态、约束

绝对禁止:在 controller 中写业务规则判断。

8. 异常如何“向外传播”

领域异常的传播路径应当是:

controller 的职责不是“判断规则”,而是:

try:
    order.pay(amount)
except domainerror as e:
    return error_response(str(e))

9. 工程规范:失败是系统能力的一部分

一个系统是否健壮,不取决于:能否处理正常数据

而取决于:能否正确拒绝不合法行为

领域异常的价值在于:

  • 规则显式

  • 行为可追踪

  • 错误不可忽略

八、总结

至此,本篇内容对 python 基础知识进行了一个相对系统的梳理与总结。

从运行环境与工具选择,到变量、控制流、函数、字符串处理,再到列表、字典、集合等核心数据结构,本博客力求用工程视角 + 可运行示例的方式,期望在较短时间内建立起对 python 编程的整体认知和基本实战能力。

需要说明的是,这份内容同样也是我在学习 python 过程中的阶段性整理与沉淀。难免存在理解不够严谨或表述不够完善之处,欢迎各位读者在评论区留言指正、交流探讨,这对我和后续读者都会非常有价值。

如果你能通过本文:

  • 理解 python 的核心语法思想

  • 看懂并独立编写基础程序

  • 对后续深入学习(如数据处理、web、自动化或 ai 方向)不再畏惧

那么这篇总结就达到了它应有的意义。

python 是一门上手友好、上限极高的语言,但真正的成长来自持续实践,而不是一次性阅读。希望你在未来的学习过程中,多动手、多思考、多调试,把“会写”逐步变成“写得好、写得稳”。

以上就是入门初学者短期内快速掌握python的经典全面教程(专家学习笔记)的详细内容,更多关于python基础学习总结的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com