学习
学习
从初学者的角度入手,帮助您更好的学习开发SRA所涉及到的Python部分。
先决条件
环境:
- Python 3.11.5
- PyCharm Community Edition 或 Visual Studio Code
- Windows 10 x64及以上版本
- 如果你有更喜欢的配置,可以尝试使用自己的配置。
提示
如果你是初学者,建议使用Visual Studio Code。
如果你考虑长期学习Python,那么PyCharm将是更好的选择
基础
Python 是一种高级编程语言,它的语法简单易懂,易于学习。
变量、数据类型
Python中的变量是用来存储数据的命名内存位置。与许多其他编程语言不同,Python是动态类型语言,这意味着你不需要在使用变量之前明确声明它的数据类型。Python解释器会根据你赋给变量的值自动推断其类型。Python支持多种内置数据类型,包括数字(整数、浮点数、复数)、字符串、布尔值、列表、元组、字典和集合等。
# 数字类型 (Numbers)
# 整数 (int): 表示没有小数部分的数字。
age = 25 # 这是一个整数变量,存储了数字25。
# 浮点数 (float): 表示带有小数部分的数字。
height = 1.75 # 这是一个浮点数变量,存储了数字1.75。
# 复数 (complex): 表示复数,由实部和虚部组成,虚部用'j'或'J'表示。
# complex_num = 1 + 2j # 这是一个复数变量。
# 字符串类型 (String)
# 字符串 (str): 表示文本数据,由单引号、双引号或三引号包围。
name = "小明" # 这是一个字符串变量,存储了"小明"。字符串是不可变的序列。
message = '你好,世界!' # 也可以使用单引号定义字符串。
long_text = """这是一个
多行字符串
示例。""" # 三引号用于定义多行字符串。
# 布尔类型 (Boolean)
# 布尔值 (bool): 表示真或假,只有两个值:True和False。常用于条件判断。
is_student = True # 这是一个布尔变量,表示“是学生”。
has_license = False # 另一个布尔变量,表示“没有驾照”。
# 类型转换 (Type Conversion/Casting)
# Python允许在不同数据类型之间进行转换,这在处理输入或特定操作时非常有用。
str_num = "42" # 一个字符串,内容是数字"42"。
num = int(str_num) # 使用int()函数将字符串"42"转换为整数42。现在num是一个整数。
float_num = float("3.14") # 使用float()函数将字符串"3.14"转换为浮点数3.14。
num_to_str = str(123) # 使用str()函数将整数123转换为字符串"123"。
核心概念:
- 动态类型: 变量的类型在运行时根据赋值自动确定。
- 强类型: Python不允许隐式地进行不兼容类型之间的操作(例如,数字和字符串的直接相加),这有助于减少运行时错误。
- 不可变与可变: 某些数据类型(如数字、字符串、元组)是不可变的,一旦创建就不能修改。而其他类型(如列表、字典、集合)是可变的,可以在创建后修改其内容。
条件语句
条件语句是编程中用于根据特定条件执行不同代码块的结构。它们允许程序根据数据的不同状态做出决策,从而实现更复杂的逻辑。Python使用 if
、elif
(else if 的缩写)和 else
关键字来构建条件语句,并通过缩进来定义代码块的范围。
age = 18 # 定义一个整数变量age,并赋值为18。
# if 语句:检查age是否小于18。
# 如果条件age < 18为True,则执行if代码块内的语句。
if age < 18:
print("未成年") # 输出“未成年”。
# elif 语句:当前面的if条件为False时,检查age是否等于18。
# 如果条件age == 18为True,则执行elif代码块内的语句。
elif age == 18:
print("刚好成年") # 输出“刚好成年”。
# else 语句:当前面所有的if和elif条件都为False时,执行else代码块内的语句。
# else语句是可选的,它捕获所有不满足前面条件的情况。
else:
print("已成年") # 输出“已成年”。
# 另一个条件语句的例子:判断一个数字是正数、负数还是零。
number = -5
if number > 0:
print("这是一个正数。")
elif number < 0:
print("这是一个负数。")
else:
print("这是零。")
# 嵌套条件语句:在一个条件块内部包含另一个条件语句。
score = 85
if score >= 60:
if score >= 90:
print("优秀")
elif score >= 80:
print("良好")
else:
print("及格")
else:
print("不及格")
核心概念:
- 条件表达式:
if
、elif
后面的表达式,其结果必须是布尔值(True
或False
)。 - 缩进: Python使用缩进来表示代码块的层次结构。属于同一条件块的语句必须有相同的缩进。
- 逻辑顺序:
if
、elif
、else
语句按顺序执行,一旦某个条件满足并执行了其对应的代码块,整个条件语句就结束了,后续的elif
和else
块将不会被检查或执行。 - 多重条件: 可以使用
and
、or
、not
等逻辑运算符组合多个条件,创建更复杂的判断逻辑。
循环语句
循环语句是编程中用于重复执行特定代码块的结构。当需要多次执行相同的操作,或者遍历数据集合时,循环就显得非常有用。Python主要提供两种类型的循环:for
循环和 while
循环。
for
循环
for
循环通常用于遍历序列(如列表、元组、字符串)或其他可迭代对象中的元素,或者执行固定次数的循环。
# for循环示例1:遍历数字序列
# range(5) 生成一个从0开始(包含)到5结束(不包含)的整数序列:0, 1, 2, 3, 4。
# 循环将依次把这些数字赋值给变量i,并执行循环体。
print("--- for 循环示例1 ---")
for i in range(5): # 循环5次,i从0到4
print(f"当前数字: {i}") # 每次循环打印当前i的值。
# for循环示例2:遍历列表
print("\n--- for 循环示例2 ---")
fruits = ["苹果", "香蕉", "橙子"]
for fruit in fruits:
print(f"我喜欢吃: {fruit}")
# for循环示例3:遍历字符串
print("\n--- for 循环示例3 ---")
word = "Python"
for char in word:
print(f"字符: {char}")
# for循环示例4:结合enumerate获取索引和值
print("\n--- for 循环示例4 ---")
for index, fruit in enumerate(fruits):
print(f"索引 {index}: {fruit}")
while
循环
while
循环用于在给定条件为 True
时重复执行代码块。只要条件保持为 True
,循环就会一直进行。因此,在使用 while
循环时,务必确保循环条件最终会变为 False
,否则会导致无限循环。
# while循环示例1:计数器
print("\n--- while 循环示例1 ---")
count = 0 # 初始化计数器变量。
while count < 5: # 只要count小于5,循环就继续。
print(f"当前计数: {count}") # 打印当前计数。
count += 1 # 每次循环结束后,将count加1,确保循环最终会停止。
# while循环示例2:用户输入直到满足条件
print("\n--- while 循环示例2 ---")
password = ""
while password != "secret":
password = input("请输入密码: ")
if password == "secret":
print("密码正确,欢迎!")
else:
print("密码错误,请重试。")
循环控制语句:
break
: 立即终止当前循环,跳出循环体,执行循环后面的代码。continue
: 终止当前循环的本次迭代,跳过循环体中continue
后面剩余的代码,直接进入下一次迭代。
# break 和 continue 示例
print("\n--- break 和 continue 示例 ---")
for i in range(10):
if i == 3:
print("遇到3,跳过本次循环的剩余部分 (continue)")
continue # 当i为3时,跳过下面的print,直接进入下一次循环。
if i == 7:
print("遇到7,终止整个循环 (break)")
break # 当i为7时,立即终止整个for循环。
print(f"当前数字: {i}")
核心概念:
- 迭代:
for
循环通过迭代可迭代对象中的每个元素来工作。 - 条件:
while
循环依赖于一个布尔条件,只要条件为真就持续执行。 - 无限循环: 如果
while
循环的条件永远为True
,就会导致无限循环,程序将无法终止。务必确保循环条件在某个时刻会变为False
。 - 循环嵌套: 循环可以嵌套,即一个循环内部包含另一个循环,这在处理多维数据结构时非常常见。
函数
函数是组织好的、可重复使用的、用于执行特定任务的代码块。它们允许你将程序分解成更小、更易于管理的部分,从而提高代码的模块化、可读性和复用性。在Python中,使用 def
关键字来定义函数。
定义函数
# 定义函数 (Function Definition)
# def 关键字用于定义函数,后面跟着函数名和括号(),括号内可以包含参数。
# 冒号(:)表示函数头的结束,函数体通过缩进来定义。
def calculate_area(length, width): # 定义一个名为calculate_area的函数,它接受两个参数:length和width。
"""计算矩形面积的函数""" # 这是一个文档字符串(docstring),用于描述函数的功能。良好的文档字符串是代码可读性的关键。
# return 语句用于从函数中返回值。如果函数没有明确的return语句,它将默认返回None。
return length * width # 函数体:计算length和width的乘积并返回结果。
# 另一个函数定义示例:不带参数,不返回值(隐式返回None)
def say_hello():
print("Hello, Python!")
调用函数
定义函数后,可以通过函数名后跟括号()来调用它。如果函数定义了参数,调用时需要提供相应的值。
# 调用函数 (Function Call)
# 调用calculate_area函数,并传入参数5和3。
area = calculate_area(5, 3) # 函数执行后,返回的结果(15)会被赋值给变量area。
print(f"面积是:{area}") # 输出计算出的面积。
# 调用不带参数的函数
say_hello() # 执行say_hello函数,它会打印"Hello, Python!"。
函数参数
Python函数支持多种类型的参数,使得函数的使用更加灵活。
- 位置参数 (Positional Arguments): 调用函数时,参数的顺序必须与函数定义时的顺序一致。
- 关键字参数 (Keyword Arguments): 调用函数时,可以通过
参数名=值
的形式指定参数,这样参数的顺序就不重要了。 - 默认参数 (Default Arguments): 在函数定义时为参数指定一个默认值。如果调用函数时没有为该参数提供值,则使用默认值。
# 带默认参数的函数 (Functions with Default Arguments)
# 定义一个greet函数,name参数有一个默认值"访客"。
def greet(name="访客", greeting="你好"): # name和greeting都是带默认值的参数。
return f"{greeting},{name}!" # 返回一个格式化的问候语。
# 调用greet函数:
print(greet()) # 不传入任何参数,使用所有默认值。输出: "你好,访客!"
print(greet("小明")) # 传入name参数,使用默认的greeting。输出: "你好,小明!"
print(greet(name="小红", greeting="早上好")) # 使用关键字参数,顺序可以颠倒。输出: "早上好,小红!"
print(greet(greeting="晚上好", name="李华")) # 关键字参数的顺序不影响。
- 可变参数 (
*args
和**kwargs
): 允许函数接受不定数量的位置参数和关键字参数。*args
(arguments): 将所有额外的位置参数收集到一个元组中。**kwargs
(keyword arguments): 将所有额外的关键字参数收集到一个字典中。
def collect_info(name, *args, **kwargs):
print(f"姓名: {name}")
print(f"其他位置信息: {args}") # args是一个元组
print(f"其他关键字信息: {kwargs}") # kwargs是一个字典
collect_info("张三", 25, "男", city="北京", job="工程师")
# 输出:
# 姓名: 张三
# 其他位置信息: (25, '男')
# 其他关键字信息: {'city': '北京', 'job': '工程师'}
核心概念:
- DRY (Don't Repeat Yourself): 函数通过封装可重用逻辑来遵循这一原则。
- 参数与返回值: 函数通过参数接收输入,通过
return
语句提供输出。 - 作用域: 函数内部定义的变量(局部变量)只在该函数内部可见,而函数外部的变量(全局变量)可以在函数内部访问(但不建议直接修改,除非明确声明)。
- 文档字符串: 使用三引号字符串在函数定义下方提供函数说明,可以通过
help(function_name)
或function_name.__doc__
查看。
列表、字典、集合、元组
Python提供了多种内置的数据结构,它们是组织和存储数据的方式,每种数据结构都有其独特的特性和适用场景。理解这些数据结构对于高效地编写Python代码至关重要。
列表 (List)
列表是Python中最常用的数据结构之一,它是一个有序的、可变的(mutable)集合,可以包含任意类型的对象。列表用方括号 []
表示。
# 列表(List):有序、可变、可包含重复元素
fruits = ["苹果", "香蕉", "橙子"] # 创建一个包含三个字符串的列表。
print(f"原始列表: {fruits}")
fruits.append("葡萄") # 使用append()方法在列表末尾添加一个元素。
print(f"添加元素后: {fruits}")
fruits.insert(1, "草莓") # 使用insert()方法在指定索引位置插入元素。
print(f"插入元素后: {fruits}")
fruits.remove("香蕉") # 使用remove()方法移除指定值的第一个匹配项。
print(f"移除元素后: {fruits}")
popped_fruit = fruits.pop() # 使用pop()方法移除并返回列表末尾的元素(默认)。
print(f"弹出元素: {popped_fruit}, 列表变为: {fruits}")
# 列表索引和切片
print(f"第一个元素: {fruits[0]}") # 访问列表的第一个元素(索引从0开始)。
print(f"从索引1到2的元素: {fruits[1:3]}") # 切片操作,获取子列表。
# 列表可以包含不同类型的数据
mixed_list = [1, "hello", 3.14, True]
print(f"混合类型列表: {mixed_list}")
字典 (Dictionary)
字典是无序的、可变的键值对(key-value pairs)集合。每个键(key)必须是唯一的,且不可变(如字符串、数字、元组),值(value)可以是任意类型。字典用花括号 {}
表示。
# 字典(Dictionary):无序、可变、键值对存储
person = { # 创建一个字典,包含姓名、年龄和城市信息。
"name": "小明",
"age": 18,
"city": "北京"
}
print(f"原始字典: {person}")
print(f"姓名: {person["name"]}") # 通过键访问字典中的值。
person["age"] = 19 # 修改字典中键对应的值。
print(f"修改年龄后: {person}")
person["gender"] = "男" # 添加新的键值对。
print(f"添加性别后: {person}")
del person["city"] # 使用del关键字删除键值对。
print(f"删除城市后: {person}")
# 遍历字典
print("\n遍历字典的键:")
for key in person.keys(): # 获取所有键。
print(key)
print("\n遍历字典的值:")
for value in person.values(): # 获取所有值。
print(value)
print("\n遍历字典的键值对:")
for key, value in person.items(): # 获取所有键值对。
print(f"{key}: {value}")
集合 (Set)
集合是无序的、可变的、不包含重复元素的集合。主要用于成员测试和消除重复元素。集合用花括号 {}
表示,但创建空集合必须使用 set()
。
# 集合(Set):无序、可变、元素唯一
numbers = {1, 2, 3, 3, 4} # 创建一个集合,重复的3会被自动去除。
print(f"原始集合: {numbers}") # 结果是{1, 2, 3, 4}
numbers.add(5) # 添加元素。
print(f"添加元素后: {numbers}")
numbers.remove(1) # 移除元素。
print(f"移除元素后: {numbers}")
# 集合操作:并集、交集、差集
set_a = {1, 2, 3}
set_b = {3, 4, 5}
print(f"并集: {set_a.union(set_b)}") # 或 set_a | set_b
print(f"交集: {set_a.intersection(set_b)}") # 或 set_a & set_b
print(f"差集 (A-B): {set_a.difference(set_b)}") # 或 set_a - set_b
元组 (Tuple)
元组是Python中另一种有序的集合,但与列表不同的是,元组是不可变的(immutable)。一旦创建,元组中的元素就不能被修改、添加或删除。元组用圆括号 ()
表示。
# 元组(Tuple):有序、不可变、可包含重复元素
coordinates = (10, 20) # 创建一个包含两个数字的元组。
print(f"原始元组: {coordinates}")
# 尝试修改元组会报错
# coordinates[0] = 15 # 这会引发TypeError
# 元组的解包
x, y = coordinates # 将元组的元素分别赋值给x和y。
print(f"解包后的x: {x}, y: {y}")
# 包含单个元素的元组需要逗号
single_element_tuple = (1,) # 必须有逗号,否则会被认为是普通表达式。
print(f"单元素元组: {single_element_tuple}")
# 元组常用于函数返回多个值,或作为字典的键(因为其不可变性)。
核心概念总结:
- 可变性 (Mutability): 列表、字典、集合是可变的,意味着它们的内容在创建后可以被修改。元组是不可变的,一旦创建就不能改变其内容。
- 有序性 (Order): 列表和元组是序的,元素有固定的排列顺序,可以通过索引访问。字典在Python 3.7+版本中保持插入顺序,但传统上被认为是无序的。集合是无序的。
- 元素唯一性: 集合中的元素必须是唯一的,重复元素会被自动忽略。列表、字典和元组可以包含重复元素。
- 适用场景:
- 列表: 适用于需要频繁添加、删除或修改元素的有序集合。
- 字典: 适用于通过键快速查找、存储和组织数据,例如表示记录或配置信息。
- 集合: 适用于需要快速成员测试、去除重复元素或执行数学集合操作(如并集、交集)的场景。
- 元组: 适用于存储固定不变的数据集合,例如坐标、函数返回的多个值,或作为字典的键(因为其不可变性)。
进阶
文件操作
文件操作是程序与外部数据交互的重要方式,允许程序读取现有文件中的数据或将数据写入新文件或现有文件。Python提供了简洁直观的内置函数和 with
语句来处理文件操作,确保文件在使用后能够被正确关闭,即使发生错误也不例外。
文件操作概述
文件操作主要包括创建、读取、写入、追加和删除文件等操作。Python通过内置的 open()
函数和文件对象提供这些功能。文件操作通常涉及以下关键概念:
- 文件路径:可以是相对路径(相对于当前工作目录)或绝对路径(从根目录开始的完整路径)。
- 文件模式:决定文件是用于读取、写入还是追加,以及是文本模式还是二进制模式。
- 编码:文本文件操作时需要指定正确的字符编码(如UTF-8),特别是处理非ASCII字符时。
- 缓冲区:文件操作通常使用缓冲区来提高性能,数据不会立即写入磁盘。
- 文件指针:表示当前在文件中的位置,读取和写入操作会移动指针位置。
打开文件
使用 open()
函数来打开文件。它至少需要一个参数:文件路径。常用的模式有:
'r'
(read): 读取模式(默认)。文件必须存在。'w'
(write): 写入模式。如果文件存在,则清空文件内容;如果文件不存在,则创建新文件。'a'
(append): 追加模式。如果文件存在,则在文件末尾添加内容;如果文件不存在,则创建新文件。'x'
(exclusive creation): 排他创建模式。如果文件已存在,则操作失败并引发FileExistsError
。'b'
(binary): 二进制模式。用于处理非文本文件(如图片、音频)。't'
(text): 文本模式(默认)。用于处理文本文件。
代码示例:
# 安全打开文件的最佳实践
try:
# 以读取模式打开文本文件,显式指定UTF-8编码
with open('data.txt', 'r', encoding='utf-8') as file:
content = file.read()
print(content)
except FileNotFoundError:
print("文件未找到,请检查路径是否正确")
except UnicodeDecodeError:
print("文件编码错误,请尝试其他编码方式")
except Exception as e:
print(f"打开文件时发生错误: {e}")
核心概念:
- 上下文管理器 (
with
语句): 确保文件在使用后自动关闭,即使发生异常也不例外。 - 文件模式组合: 可以组合使用模式,如
'rb'
表示以二进制模式读取,'w+'
表示读写模式。 - 编码问题: 处理文本文件时,指定正确的编码至关重要,否则可能导致乱码或解码错误。
- 异常处理: 文件操作可能引发多种异常(如
FileNotFoundError
,PermissionError
等),应妥善处理。
写入文件
# 写入文件 (Writing to a File)
# 使用 'w' 模式打开文件,如果文件不存在则创建,如果存在则清空内容。
# encoding='utf-8' 指定了文件的编码格式,这对于处理包含非ASCII字符(如中文)的文本非常重要。
# 'with' 语句确保文件在操作完成后自动关闭,即使发生异常。
file_path_write = "example_write.txt"
with open(file_path_write, "w", encoding="utf-8") as f:
# write() 方法将字符串写入文件,不会自动添加换行符
f.write("这是一个示例文件。\n") # 写入一行文本到文件。
f.write("这是第二行内容。\n") # 继续写入第二行。
# writelines() 方法可以写入字符串列表,同样不会自动添加换行符
lines = ["第三行内容\n", "第四行内容\n"]
f.writelines(lines)
print(f"内容已写入到 {file_path_write}")
# 文件写入的缓冲区:默认情况下,写入操作会先写入内存缓冲区,而不是立即写入磁盘
# 可以使用 flush() 方法强制将缓冲区内容写入磁盘
# 或者设置 buffering 参数来控制缓冲区大小
**核心概念:**
* **写入方法**:
- `write()`: 写入单个字符串
- `writelines()`: 写入字符串序列
* **换行处理**: Python不会自动添加换行符,需要显式添加 `\n`
* **缓冲区**: 写入操作通常使用缓冲区,可通过 `flush()` 强制写入或设置 `buffering` 参数
* **原子写入**: 对于关键数据,可能需要临时文件+重命名的方式确保原子性
* **性能考虑**: 大量小写入操作比少量大写入操作性能差
# 追加内容到文件 (Appending to a File)
# 使用 'a' 模式打开文件,如果文件存在则在末尾追加内容。
file_path_append = "example_append.txt"
with open(file_path_append, "a", encoding="utf-8") as f:
f.write("这是追加的第一行。\n")
f.write("这是追加的第二行。\n")
print(f"内容已追加到 {file_path_append}")
读取文件
#### 读取文件
```python
# 读取文件 (Reading from a File)
# 使用 'r' 模式打开文件。
file_path_read = "example_write.txt" # 读取之前写入的文件。
try:
with open(file_path_read, "r", encoding="utf-8") as f:
# read() 方法读取文件的所有内容作为一个字符串。可以指定读取的字符数。
content_full = f.read()
print(f"\n从 {file_path_read} 读取到的全部内容:\n{content_full}")
# seek(0) 将文件指针重置到文件开头,以便重新读取。
f.seek(0)
# readline() 方法读取文件的一行内容,包括换行符。
first_line = f.readline()
print(f"\n从 {file_path_read} 读取到的第一行:\n{first_line.strip()}")
f.seek(0)
# readlines() 方法读取所有行并返回一个字符串列表,每行包含换行符。
all_lines = f.readlines()
print(f"\n从 {file_path_read} 读取到的所有行(列表形式):\n{all_lines}")
f.seek(0)
# 也可以逐行读取文件,这是处理大文件的推荐方式。
print(f"\n逐行从 {file_path_read} 读取到的内容:")
for line in f: # 迭代文件对象可以逐行读取,内存效率高。
print(f"行: {line.strip()}") # strip() 移除行末的换行符。
except FileNotFoundError:
print(f"错误: 文件 {file_path_read} 未找到。")
except Exception as e:
print(f"读取文件时发生错误: {e}")
# 读取大文件时,逐行读取或分块读取更高效,避免一次性加载所有内容到内存。
核心概念:
- 读取方法:
read()
: 读取整个文件或指定数量的字符。readline()
: 读取文件的一行。readlines()
: 读取所有行并返回一个列表。- 迭代文件对象: 最推荐的方式,逐行读取,内存效率最高。
- 文件指针:
seek(offset, whence)
方法用于移动文件指针。offset
: 偏移量。whence
: 0(文件开头,默认)、1(当前位置)、2(文件末尾)。
- 性能优化: 对于大文件,应避免一次性读取所有内容到内存,推荐逐行读取或分块读取。
**核心概念:**
* **文件对象**: `open()` 函数返回一个文件对象,通过它进行读写操作。
* **`with` 语句**: 推荐使用 `with open(...) as f:` 语法,它被称为上下文管理器。它能确保文件在使用完毕后(无论是正常结束还是发生异常)自动关闭,从而避免资源泄露。
* **文件模式**: 根据读写需求选择合适的模式(`'r'`, `'w'`, `'a'`, `'x'` 等)。
* **编码**: 处理文本文件时,指定正确的 `encoding` 参数(如 `'utf-8'`)非常重要,以避免乱码问题。
* **文件指针**: 读取和写入操作会移动文件内部的指针。`seek()` 方法可以用来改变文件指针的位置。
* **缓冲区**: 文件操作通常涉及缓冲区,理解其工作原理有助于优化性能。
* **路径操作**: Python的 `os.path` 模块提供了处理文件路径的工具,如判断路径是否存在、获取文件名、目录名等。
* **文件系统操作**: `os` 模块还提供了创建、删除目录,重命名文件等功能。
### 异常处理
异常(Exception)是在程序执行期间发生的事件,它会中断程序的正常流程。当Python脚本遇到无法处理的错误时,它会抛出一个异常。异常处理机制允许程序优雅地处理这些错误,而不是突然崩溃。Python使用 `try`、`except`、`else` 和 `finally` 关键字来构建异常处理结构。
#### `try-except` 块
* `try` 块:包含可能会引发异常的代码。
* `except` 块:当 `try` 块中的代码引发特定类型的异常时,`except` 块中的代码会被执行。
**代码示例:**
```python
# 异常处理示例
print("--- 异常处理示例 ---")
try:
# 尝试执行可能出错的代码
number_str = input("请输入一个数字:") # 从用户获取输入,这是一个字符串。
number = int(number_str) # 尝试将字符串转换为整数。如果输入不是有效数字,会引发ValueError。
result = 10 / number # 尝试执行除法。如果number为0,会引发ZeroDivisionError。
print(f"结果是:{result}") # 如果以上操作都成功,打印结果。
except ValueError: # 捕获ValueError异常。
print("错误:请输入有效的整数!") # 当用户输入非数字字符串时执行。
except ZeroDivisionError: # 捕获ZeroDivisionError异常。
print("错误:不能除以零!") # 当用户输入0时执行。
except Exception as e: # 捕获所有其他未被特定except捕获的异常。
print(f"发生了未知错误: {e}") # 打印错误信息。
print("程序继续执行...") # 无论是否发生异常,程序都会继续执行到这里。
else
块 (可选)
else
块:如果try
块中的代码没有引发任何异常,else
块中的代码就会被执行。它通常用于放置那些只有在try
块成功执行后才需要运行的代码。
代码示例:
# try-except-else 示例
print("\n--- try-except-else 示例 ---")
try:
num1 = int(input("请输入第一个数字: "))
num2 = int(input("请输入第二个数字: "))
division_result = num1 / num2
except ValueError:
print("输入无效,请确保输入的是数字。")
except ZeroDivisionError:
print("除数不能为零。")
else:
# 如果try块没有发生异常,则执行此处的代码
print(f"两个数字相除的结果是: {division_result}")
finally
块 (可选)
finally
块:无论try
块中是否发生异常,finally
块中的代码总是会被执行。它通常用于执行清理操作,例如关闭文件或释放资源。
代码示例:
# try-except-finally 示例
print("\n--- try-except-finally 示例 ---")
file = None
try:
file = open("non_existent_file.txt", "r") # 尝试打开一个不存在的文件,会引发FileNotFoundError。
content = file.read()
print(content)
except FileNotFoundError:
print("文件未找到。")
finally:
# 无论是否发生异常,都会执行这里的代码
if file:
file.close() # 确保文件被关闭,释放资源。
print("文件已关闭。")
print("程序执行完毕。")
核心概念:
- 异常类型: Python有许多内置的异常类型(如
ValueError
,TypeError
,FileNotFoundError
,IndexError
等),可以根据需要捕获特定的异常。了解常见的内置异常有助于编写更健壮的代码。 - 异常捕获顺序:
except
块会按顺序匹配异常。更具体的异常(子类)应该放在前面,更通用的异常(父类,如Exception
)应该放在后面,否则通用异常会捕获所有异常,导致具体异常无法被捕获。 as
关键字: 可以使用as
关键字将捕获到的异常对象赋值给一个变量,以便获取更多关于异常的信息,例如错误消息或堆栈跟踪。raise
语句: 可以使用raise
语句手动引发异常。这在自定义错误处理逻辑、验证输入或在特定条件下强制程序中断时非常有用。- 资源管理:
finally
块对于确保资源(如文件、网络连接、数据库连接)被正确关闭或释放至关重要,即使在try
块中发生异常也能保证清理工作。with
语句(上下文管理器)是更推荐的资源管理方式。 - 自定义异常: 可以通过继承
Exception
类来创建自定义异常,这有助于提高代码的可读性和可维护性,使错误类型更具业务含义。 - 异常链: Python 3 引入了异常链,允许在捕获一个异常后引发另一个异常,同时保留原始异常的信息,有助于调试。
类和对象
面向对象编程(Object-Oriented Programming, OOP)是Python中一种强大的编程范式,它通过“类”和“对象”的概念来组织代码,模拟现实世界中的实体和它们之间的关系。OOP的核心思想是将数据(属性)和操作数据的方法(行为)封装在一起,形成一个独立的单元。
类 (Class)
类是创建对象的蓝图或模板。它定义了对象的属性(数据)和方法(函数)。在Python中,使用 class
关键字来定义类。
代码示例:
# 定义一个名为Student的类
class Student:
# 类的属性(通常在__init__方法中定义实例属性)
school_name = "某某大学" # 这是一个类属性,所有Student的实例共享。
# 构造方法 (Constructor): __init__
# 这是一个特殊的方法,当创建类的新实例(对象)时会自动调用。
# self 参数是必须的,它代表了类的实例本身。
# name 和 age 是实例属性,它们属于每个Student对象独有。
def __init__(self, name, age):
self.name = name # 将传入的name参数赋值给实例的name属性。
self.age = age # 将传入的age参数赋值给实例的age属性。
print(f"学生 {self.name} 已创建。")
# 实例方法 (Instance Method)
# 方法是定义在类中的函数,用于描述对象的行为。它们也必须接受self作为第一个参数。
def introduce(self):
# 访问实例属性和类属性
return f"我叫{self.name},今年{self.age}岁,就读于 {Student.school_name}。"
def celebrate_birthday(self):
self.age += 1 # 修改实例属性
print(f"{self.name} 庆祝了生日,现在 {self.age} 岁了!")
对象 (Object / Instance)
对象是类的具体实例。通过类定义,我们可以创建多个具有相同属性和行为但数据值不同的对象。
代码示例:
# 创建对象 (Creating Objects / Instances)
# 通过调用类名并传入__init__方法所需的参数来创建Student类的实例。
student1 = Student("小明", 18) # 创建第一个Student对象。
student2 = Student("小红", 19) # 创建第二个Student对象。
# 访问对象的属性
print(f"学生1的姓名: {student1.name}")
print(f"学生2的年龄: {student2.age}")
print(f"学生1的学校: {student1.school_name}") # 可以通过实例访问类属性
print(f"学生2的学校: {Student.school_name}") # 也可以通过类名访问类属性
# 调用对象的方法
print(student1.introduce()) # 调用student1对象的introduce方法。
print(student2.introduce()) # 调用student2对象的introduce方法。
student1.celebrate_birthday() # 调用student1的生日方法
print(student1.introduce())
核心概念:
- 封装 (Encapsulation): 将数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个独立的单元(类)。这有助于隐藏内部实现细节,只暴露必要的接口,提高代码的安全性和可维护性。
self
: 在类的方法中,self
总是指向当前实例(对象)本身。它是访问实例属性和调用实例方法的约定,必须是方法的第一个参数。__init__
方法: 构造方法,用于初始化新创建的对象的属性。它在对象被创建时自动调用,是类的特殊方法之一。- 实例属性 vs. 类属性:
- 实例属性: 属于类的每个独立对象,每个对象都有自己的一份副本(如
name
,age
)。通过self.attribute_name
访问。 - 类属性: 属于类本身,所有类的实例共享同一个副本(如
school_name
)。通过ClassName.attribute_name
或self.attribute_name
访问。
- 实例属性: 属于类的每个独立对象,每个对象都有自己的一份副本(如
- 方法: 定义在类中的函数,用于描述对象的行为。分为实例方法、类方法和静态方法。
- 实例方法: 接收
self
作为第一个参数,操作实例属性。 - 类方法: 使用
@classmethod
装饰器,接收cls
(类本身)作为第一个参数,操作类属性或创建类实例。 - 静态方法: 使用
@staticmethod
装饰器,不接收self
或cls
,行为独立于实例和类,类似于普通函数,但逻辑上与类相关。
- 实例方法: 接收
继承
继承是面向对象编程的另一个核心概念,它允许一个类(子类或派生类)从另一个类(父类、基类或超类)中继承属性和方法。这促进了代码的重用性,并建立了“is-a”关系(例如,“老师是一种人”)。子类可以扩展或修改父类的行为。
基本继承
# 定义父类 (Parent Class / Base Class)
class Person:
def __init__(self, name, age):
self.name = name # 实例属性:姓名
self.age = age # 实例属性:年龄
print(f"Person {self.name} 已创建。")
def greet(self):
"""父类方法:打招呼"""
return f"你好,我是{self.name},我今年{self.age}岁。"
# 定义子类 (Child Class / Derived Class)
# Teacher类继承自Person类,在类名后括号中指定父类。
class Teacher(Person):
# 子类的构造方法
# 当子类有自己的__init__方法时,它会覆盖父类的__init__方法。
# 如果需要调用父类的__init__方法来初始化父类的属性,必须显式调用super().__init__()。
def __init__(self, name, age, subject):
# 调用父类Person的构造方法,初始化name和age属性。
# super() 函数返回父类的代理对象,允许你调用父类的方法。
super().__init__(name, age)
self.subject = subject # 子类特有的属性:教授科目。
print(f"Teacher {self.name} (教授{self.subject}) 已创建。")
# 子类特有的方法
def teach(self):
"""子类特有方法:教学"""
return f"我叫{self.name},我教{self.subject}。"
# 创建子类对象
teacher = Teacher("张老师", 35, "数学")
# 调用继承自父类的方法
print(teacher.greet()) # 输出: 你好,我是张老师,我今年35岁。
# 调用子类特有的方法
print(teacher.teach()) # 输出: 我叫张老师,我教数学。
# 检查对象类型
print(f"teacher是Person的实例吗? {isinstance(teacher, Person)}") # True
print(f"teacher是Teacher的实例吗? {isinstance(teacher, Teacher)}") # True
# 检查类之间的继承关系
print(f"Teacher是Person的子类吗? {issubclass(Teacher, Person)}") # True
print(f"Person是Teacher的子类吗? {issubclass(Person, Teacher)}") # False
方法重写 (Method Overriding)
子类可以定义与父类中同名的方法。当子类对象调用这个方法时,会执行子类中定义的方法,而不是父类的方法。这被称为方法重写。
class Animal:
def speak(self):
"""父类方法:动物发出声音"""
return "动物发出声音"
class Dog(Animal):
def speak(self):
"""子类重写方法:狗发出声音"""
# 可以在重写的方法中调用父类的方法,以扩展而非完全替换父类行为
# return f"{super().speak()},然后汪汪叫"
return "汪汪叫"
class Cat(Animal):
def speak(self):
"""子类重写方法:猫发出声音"""
return "喵喵叫"
animal = Animal()
dog = Dog()
cat = Cat()
print(animal.speak()) # 输出: 动物发出声音
print(dog.speak()) # 输出: 汪汪叫 (子类重写了方法)
print(cat.speak()) # 输出: 喵喵叫 (子类重写了方法)
多重继承 (Multiple Inheritance)
Python支持一个类继承自多个父类。当存在多个父类时,Python会按照特定的顺序(MRO - Method Resolution Order)来查找方法和属性。MRO遵循C3线性化算法。
class Flyer:
def fly(self):
return "我会飞!"
class Swimmer:
def swim(self):
return "我会游泳!"
class Duck(Flyer, Swimmer):
def quack(self):
return "嘎嘎!"
duck = Duck()
print(duck.fly()) # 输出: 我会飞!
print(duck.swim()) # 输出: 我会游泳!
print(duck.quack()) # 输出: 嘎嘎!
# 查看方法解析顺序 (MRO)
print(Duck.__mro__)
# 输出示例: (<class '__main__.Duck'>, <class '__main__.Flyer'>, <class '__main__.Swimmer'>, <class 'object'>)
核心概念:
- 代码重用: 继承允许子类重用父类的代码和行为,减少代码冗余,提高开发效率。
super()
: 一个内置函数,用于调用父类(超类)的方法。在子类的__init__
方法中调用父类的构造方法是其常见用法,也可以在重写方法中调用父类的同名方法来扩展其功能。- 多态 (Polymorphism): 指不同类的对象对同一消息(方法调用)可以有不同的响应方式。它允许我们编写更通用、更灵活的代码,例如,一个函数可以接受不同类型的对象,只要它们都实现了某个共同的方法。
isinstance()
: 用于检查一个对象是否是某个类或其子类的实例。返回True
或False
。issubclass()
: 用于检查一个类是否是另一个类的子类。返回True
或False
。- 多重继承: Python支持一个类继承自多个父类。这使得子类可以结合多个父类的功能。然而,多重继承可能导致方法解析顺序(MRO)的复杂性,需要谨慎使用。
- 方法解析顺序 (MRO): 当使用多重继承时,Python通过MRO来确定方法和属性的查找顺序。Python 3 采用C3线性化算法来计算MRO,可以通过
ClassName.__mro__
或help(ClassName)
查看。
模块和包
随着Python项目的增长,代码量会越来越大,为了更好地组织和管理代码,Python引入了“模块”和“包”的概念。它们有助于将代码分解成逻辑单元,提高代码的可维护性、可重用性和可读性。
模块 (Module)
模块是一个包含Python定义和语句的文件。文件名就是模块名加上 .py
后缀。模块可以定义函数、类和变量,也可以包含可执行的代码。使用模块可以把相关的代码组织在一起,方便重用。
# 导入整个模块
# import math 语句导入Python的内置math模块,该模块提供了许多数学函数和常量。
import math
print(f"圆周率 (math.pi): {math.pi}") # 通过模块名.属性名的方式访问模块中的常量。
print(f"16的平方根 (math.sqrt(16)): {math.sqrt(16)}") # 访问模块中的函数。
# 从模块中导入特定函数或变量
# from random import randint 语句只导入random模块中的randint函数,可以直接使用randint而不需要加random前缀。
from random import randint
random_number = randint(1, 10) # 生成一个1到10之间的随机整数(包含1和10)。
print(f"随机整数 (randint(1, 10)): {random_number}")
# 导入模块并使用别名
# import numpy as np 导入numpy模块并给它一个别名np,方便后续使用。
# import collections as coll
# from datetime import datetime as dt
# 导入模块中的所有内容(不推荐,可能导致命名冲突)
# from os import *
# print(getcwd()) # 可以直接使用getcwd函数
包 (Package)
包是一种组织Python模块的方式,它是一个包含多个模块和其他子包的目录。包的目录中必须包含一个特殊的 __init__.py
文件(在Python 3.3+版本中,这个文件可以是空的,甚至可以省略,但为了兼容性和明确性,通常还是会保留)。__init__.py
文件告诉Python解释器,该目录应该被视为一个Python包。
包的结构示例:
my_project/
├── main.py
└── my_package/
├── __init__.py
├── module_a.py
└── sub_package/
├── __init__.py
└── module_b.py
# 假设有上述包结构,并且在my_package/module_a.py中有如下内容:
# def func_a():
# return "Hello from module_a!"
# 假设在my_package/sub_package/module_b.py中有如下内容:
# def func_b():
# return "Hello from module_b!"
# 导入包中的模块
import my_package.module_a # 导入my_package包中的module_a模块
print(my_package.module_a.func_a()) # 访问方式:包名.模块名.函数名
# 从包中导入特定模块
from my_package import module_a
print(module_a.func_a()) # 访问方式:模块名.函数名
# 从包中的模块导入特定函数
from my_package.module_a import func_a
print(func_a()) # 访问方式:函数名
# 导入子包中的模块
from my_package.sub_package import module_b
print(module_b.func_b()) # 访问方式:模块名.函数名
# 使用 __all__ 控制导入行为
# 在 __init__.py 中定义 __all__ = ['module1', 'module2'] 可以控制 from mypackage import * 的行为
# 相对导入(在包内部使用)
# from . import module1 # 导入同级模块
# from .. import parent_module # 导入上级模块
# from .subpackage import module3 # 导入子包模块
核心概念:
- 命名空间: 模块和包有助于避免命名冲突。每个模块都有自己的命名空间,通过
.
运算符可以访问其中的内容。 - 可重用性: 将代码组织成模块和包,可以方便地在不同的项目中重用。
- 可维护性: 将大型项目分解为更小的、独立的模块,使得代码更容易理解、测试和维护。
__init__.py
: 标记一个目录为Python包。它也可以包含包的初始化代码,例如定义__all__
变量来控制from package import *
时的导入行为。__all__
: 在__init__.py
中定义,是一个字符串列表,用于控制from package import *
时导入哪些模块。- 相对导入: 在包内部可以使用相对导入(如
from . import module
),使用点号表示当前包和父包。 - 模块搜索路径: Python在导入模块时会按照
sys.path
中的路径顺序搜索模块。 if __name__ == '__main__'
: 用于判断模块是直接运行还是被导入,常用于模块测试代码。
装饰器
装饰器是Python中一个非常强大和常用的功能,它允许你修改或增强函数或类的功能,而无需修改其源代码。本质上,装饰器是一个函数,它接收一个函数作为输入,并返回一个新的函数作为输出。这种设计模式在日志记录、性能测试、事务处理、缓存、权限校验等方面非常有用。
装饰器的工作原理
装饰器通过 @decorator_name
语法糖应用在一个函数定义之前。当一个函数被装饰时,实际上发生的是:decorated_function = decorator_name(original_function)
然后,当你调用 decorated_function()
时,实际上是调用了 decorator_name
返回的新函数。
# 定义一个简单的装饰器
# my_decorator 是一个装饰器函数,它接收一个函数 func 作为参数。
def my_decorator(func):
"""
基础装饰器示例
:param func: 被装饰的函数
:return: 包装后的函数
"""
# wrapper 是一个内部函数,它将替代原始函数 func 被调用。
# *args 和 **kwargs 允许 wrapper 函数接受任意数量的位置参数和关键字参数,并将它们传递给原始函数 func。
def wrapper(*args, **kwargs):
print("Something is happening before the function is called.") # 在原始函数执行前打印一条消息。
result = func(*args, **kwargs) # 调用原始函数 func,并将其返回值存储在 result 中。
print("Something is happening after the function is called.") # 在原始函数执行后打印一条消息。
return result # 返回原始函数的执行结果。
return wrapper # 装饰器返回 wrapper 函数。
# 使用 @ 语法糖应用装饰器
# @my_decorator 相当于 say_hello = my_decorator(say_hello)。
# 当 say_hello() 被调用时,实际上是调用了 my_decorator 返回的 wrapper 函数。
@my_decorator
def say_hello():
"""被装饰的函数示例"""
print("Hello!")
# 调用被装饰的函数
say_hello()
# 输出:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.
print("\n-- 带有参数的函数装饰器 --")
@my_decorator
def greet(name):
"""带参数的被装饰函数"""
print(f"Hello, {name}!")
greet("Alice")
# 输出:
# Something is happening before the function is called.
# Hello, Alice!
# Something is happening after the function is called.
print("\n-- 带有返回值的函数装饰器 --")
@my_decorator
def add(a, b):
"""带返回值的被装饰函数"""
print(f"Adding {a} and {b}")
return a + b
sum_result = add(5, 3)
print(f"Sum: {sum_result}")
# 输出:
# Something is happening before the function is called.
# Adding 5 and 3
# Something is happening after the function is called.
# Sum: 8
print("\n-- 带参数的装饰器 --")
def repeat(num_times):
"""
带参数的装饰器工厂函数
:param num_times: 重复执行的次数
:return: 装饰器函数
"""
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet_repeated(name):
print(f"Hello, {name}!")
greet_repeated("Bob")
# 输出:
# Hello, Bob!
# Hello, Bob!
# Hello, Bob!
print("\n-- 类装饰器 --")
class CountCalls:
"""
类装饰器示例,记录函数调用次数
"""
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello_counted():
print("Hello!")
say_hello_counted()
# 输出:
# Call 1 of say_hello_counted
# Hello!
say_hello_counted()
# 输出:
# Call 2 of say_hello_counted
# Hello!
核心概念:
- 函数即对象: 在Python中,函数是第一类对象,可以作为参数传递给其他函数,也可以作为其他函数的返回值。
- 闭包: 装饰器内部的
wrapper
函数是一个闭包,它可以访问其外部(my_decorator
函数)作用域的变量,即使外部函数已经执行完毕。 - 语法糖
@
: 简化了装饰器的应用,使得代码更具可读性。 - 链式装饰器: 可以将多个装饰器堆叠在一个函数上,它们会从下到上依次执行。
- 带参数的装饰器: 装饰器本身也可以接受参数,这需要额外的嵌套层级来实现(装饰器工厂函数)。
- 类装饰器: 类也可以作为装饰器,通过实现
__call__
方法来使类的实例可调用。 - functools.wraps: 使用
@functools.wraps(func)
装饰器可以保留原始函数的元数据(如__name__
,__doc__
等)。 - 装饰器应用顺序: 当多个装饰器应用于同一个函数时,最内层的装饰器最先执行,最外层的装饰器最后执行。
- 装饰器用途: 常见用途包括日志记录、性能测试、事务处理、缓存、权限校验、输入验证等。
生成器
生成器(Generator)是Python中一种特殊的迭代器,它允许你以惰性(lazy)的方式生成序列中的元素,而不是一次性生成所有元素并存储在内存中。这使得生成器在处理大量数据时非常高效,因为它只在需要时才生成下一个值,从而节省了大量的内存空间。生成器是实现迭代器协议的一种简洁方式,它们在迭代过程中按需生成数据,而不是预先计算并存储所有数据。
生成器的创建
生成器可以通过两种主要方式创建:
- 生成器函数(Generator Function):包含
yield
关键字的函数。当函数中包含yield
语句时,它就变成了一个生成器函数。每次调用yield
时,函数会暂停执行并返回一个值,并在下次调用next()
时从上次暂停的地方继续执行。 - 生成器表达式(Generator Expression):类似于列表推导式,但使用圆括号
()
而不是方括号[]
。它返回一个生成器对象,而不是一个列表。
# 1. 生成器函数示例
# my_generator 是一个生成器函数,因为它包含了 yield 关键字。
def my_generator():
print("开始生成...")
yield 1 # 第一次调用 next() 时,函数执行到这里,返回1,并暂停。
print("继续生成...")
yield 2 # 第二次调用 next() 时,从上次暂停的地方继续,执行到这里,返回2,并暂停。
print("再次生成...")
yield 3 # 第三次调用 next() 时,从上次暂停的地方继续,执行到这里,返回3,并暂停。
print("生成结束。")
# 创建一个生成器对象
gen = my_generator()
# 使用 next() 函数获取生成器中的下一个值
print(f"第一次 next(): {next(gen)}")
print(f"第二次 next(): {next(gen)}")
print(f"第三次 next(): {next(gen)}")
# 当没有更多值可生成时,再次调用 next() 会引发 StopIteration 异常。
try:
print(f"第四次 next(): {next(gen)}")
except StopIteration:
print("生成器已耗尽,捕获到 StopIteration 异常。")
print("\n-- 使用 for 循环遍历生成器 --")
# 生成器是可迭代的,所以可以使用 for 循环来遍历它,for 循环会自动处理 StopIteration 异常。
for value in my_generator():
print(f"通过 for 循环获取的值: {value}")
print("\n-- 2. 生成器表达式示例 --")
# 创建一个生成器表达式,类似于列表推导式,但使用圆括号。
# 它不会立即计算所有值,而是创建一个生成器对象。
gen_exp = (x * x for x in range(5))
print(f"生成器表达式对象: {gen_exp}") # 打印的是生成器对象本身,而不是值。
# 遍历生成器表达式获取值
print("遍历生成器表达式:")
for num in gen_exp:
print(num)
print("\n-- 内存效率示例 --")
# 比较列表推导式和生成器表达式的内存使用
import sys
# 列表推导式会一次性生成所有元素并存储在内存中
my_list = [i for i in range(1000000)]
print(f"列表占用内存: {sys.getsizeof(my_list)} bytes")
# 生成器表达式只在需要时生成元素,内存占用很小
my_generator_exp = (i for i in range(1000000))
print(f"生成器占用内存: {sys.getsizeof(my_generator_exp)} bytes")
核心概念:
- 惰性计算(Lazy Evaluation): 生成器只在请求时才生成下一个值,而不是一次性生成所有值。这种按需生成数据的特性使得生成器在处理无限序列或大数据集时非常有用。
yield
关键字: 是生成器函数的核心。当函数执行到yield
语句时,它会暂停执行,将yield
后面的表达式的值作为结果返回,并保存当前的所有状态(包括局部变量和指令指针)。下次调用next()
时,函数会从上次暂停的地方继续执行。- 迭代器协议: 生成器自动实现了Python的迭代器协议,这意味着它们拥有
__iter__()
和__next__()
方法。因此,生成器对象可以直接用于for
循环、列表推导式以及其他需要迭代器的场景。 - 内存效率: 生成器不会一次性将所有数据加载到内存中,而是逐个生成数据。这使得它们在处理大型数据集时具有极高的内存效率,避免了内存溢出的问题。
StopIteration
: 当生成器函数执行完毕,或者遇到return
语句(没有返回值),或者没有更多的yield
语句时,next()
函数会引发StopIteration
异常。for
循环会自动捕获并处理这个异常,从而优雅地结束迭代。- 生成器表达式: 提供了一种简洁的方式来创建生成器,语法类似于列表推导式,但使用圆括号
()
。它比列表推导式更节省内存,因为它不会立即构建整个列表。
上下文管理器
上下文管理器(Context Manager)是Python中用于管理资源(如文件、网络连接、锁等)的一种机制,它确保资源在被使用后能够正确地获取和释放,即使在操作过程中发生错误也能保证资源的清理。这主要通过 with
语句来实现,它使得代码更加简洁、安全和易读。
工作原理
上下文管理器通过实现两个特殊方法来工作:
__enter__(self)
: 在with
语句块执行之前被调用。它负责设置资源,并返回一个值(通常是资源本身),这个值会被赋给as
关键字后面的变量。__exit__(self, exc_type, exc_val, exc_tb)
: 在with
语句块执行完毕后(无论是正常结束还是发生异常)被调用。它负责清理资源。参数exc_type
,exc_val
,exc_tb
分别表示异常类型、异常值和回溯信息。如果with
块中没有发生异常,这三个参数都将是None
。
# 1. 自定义上下文管理器类
# FileManager 类实现了上下文管理器协议,用于安全地处理文件。
class FileManager:
"""
一个自定义的文件上下文管理器,确保文件在使用后被正确关闭。
"""
# 构造函数,初始化文件名为 filename。
def __init__(self, filename, mode='r'):
self.filename = filename
self.mode = mode
self.file = None
# 进入上下文时调用此方法。
def __enter__(self):
print(f"进入上下文:尝试打开文件 {self.filename} (模式: {self.mode})")
try:
self.file = open(self.filename, self.mode) # 打开文件。
except Exception as e:
print(f"打开文件失败: {e}")
# 重新抛出异常,或者根据需要处理
raise
return self.file # 返回文件对象,它将被赋给 with 语句中的变量(如 f)。
# 退出上下文时调用此方法。
# exc_type, exc_val, exc_tb 分别是异常类型、异常值和回溯信息。
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"退出上下文:尝试关闭文件 {self.filename}")
if self.file: # 确保文件对象存在。
self.file.close() # 关闭文件,释放资源。
print(f"文件 {self.filename} 已成功关闭。")
# 如果 __exit__ 方法返回 True,则表示已经处理了异常,异常不会继续传播。
# 如果返回 False 或 None,则异常会继续传播。
if exc_type: # 如果发生了异常
print(f"在上下文管理器中捕获到异常:{exc_type.__name__}: {exc_val}")
# 可以选择在这里处理异常,例如记录日志,然后返回 True 阻止异常传播。
# return True # 如果返回 True,异常将被抑制。
return False # 默认返回 False,让异常正常传播。
# 使用自定义上下文管理器
file_path = 'my_test_file.txt'
print("\n--- 写入文件示例 ---")
with FileManager(file_path, 'w') as f: # 进入上下文,文件被打开。
f.write('Hello, Python Context Manager!\n')
f.write('This is a test line.\n')
print(f"文件 '{file_path}' 已写入并关闭。") # 退出上下文,文件被关闭。
print("\n--- 读取文件示例 ---")
with FileManager(file_path, 'r') as f: # 进入上下文,文件被打开。
content = f.read()
print(f"文件 '{file_path}' 内容:\n{content}")
print(f"文件 '{file_path}' 已读取并关闭。") # 退出上下文,文件被关闭。
print("\n--- 异常处理示例 ---")
try:
with FileManager(file_path, 'r') as f: # 进入上下文
print("尝试读取文件...")
# 故意制造一个错误,例如尝试对文件对象进行不存在的操作
f.non_existent_method()
except AttributeError as e:
print(f"在外部捕获到异常: {e}")
print("程序继续执行。")
# 2. 使用 contextlib 模块的 @contextmanager 装饰器
# 对于简单的上下文管理器,可以使用 contextlib 模块的 @contextmanager 装饰器来简化实现。
# 它将一个生成器函数转换为上下文管理器。
from contextlib import contextmanager
@contextmanager
def open_file_context(path, mode):
"""
使用 @contextmanager 装饰器实现的上下文管理器,用于文件操作。
"""
file = None
try:
file = open(path, mode)
yield file # yield 之前是 __enter__ 的逻辑,yield 之后是 __exit__ 的逻辑。
finally:
if file:
file.close()
print(f"文件 {path} 已通过 contextmanager 装饰器关闭。")
print("\n--- 使用 @contextmanager 装饰器示例 ---")
with open_file_context('another_test_file.txt', 'w') as f:
f.write('Hello from contextmanager decorator!\n')
print("文件 'another_test_file.txt' 已写入并关闭 (通过装饰器)。")
print("\n--- 锁的上下文管理器示例 ---")
import threading
lock = threading.Lock()
# 使用 with 语句管理锁的获取和释放
with lock:
print("锁已被获取,执行临界区代码...")
# 临界区代码
print("临界区代码执行完毕。")
# 离开 with 块时,锁会自动释放
print("锁已自动释放。")
print("\n--- 数据库连接的上下文管理器示例 (伪代码) ---")
# 假设有一个数据库连接类
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"连接到数据库: {self.db_name}")
# 模拟数据库连接操作
self.connection = f"Connection to {self.db_name}"
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"关闭数据库连接: {self.db_name}")
# 模拟关闭数据库连接操作
self.connection = None
if exc_type:
print(f"数据库操作中发生异常: {exc_val}")
with DatabaseConnection("my_database") as db:
print(f"执行数据库查询: {db}")
# 模拟执行查询
# raise ValueError("模拟数据库错误") # 可以测试异常处理
print("数据库连接已关闭。")
**核心概念:**
* **资源管理**: 上下文管理器的核心目的是确保资源(如文件句柄、网络连接、数据库连接、线程锁等)在使用后能够被正确地获取和释放,即使在代码执行过程中发生异常也能保证资源的清理,从而避免资源泄露。
* **`with` 语句**: Python 提供的一种语法糖,用于简化资源管理。它确保在进入 `with` 块时资源被正确设置,并在退出 `with` 块时(无论是正常退出还是因异常退出)资源被正确清理。
* **`__enter__` 方法**: 当 `with` 语句执行时,首先调用上下文管理器的 `__enter__` 方法。此方法负责资源的获取和初始化,并返回一个值(通常是资源本身),这个值会被赋给 `as` 关键字后面的变量。
* **`__exit__` 方法**: 当 `with` 语句块执行完毕或发生异常时,`__exit__` 方法会被调用。它负责资源的清理和释放。该方法接收三个参数:`exc_type`(异常类型)、`exc_val`(异常值)和 `exc_tb`(回溯信息)。如果 `with` 块中没有发生异常,这三个参数都将是 `None`。
* **异常安全**: `__exit__` 方法的强大之处在于其异常处理能力。即使 `with` 块内部发生未捕获的异常,`__exit__` 方法也总会被调用,从而保证资源得到清理。如果 `__exit__` 方法返回 `True`,则表示它已经处理了异常,该异常将不会向外传播;如果返回 `False` 或 `None`,则异常会继续传播。
* **`contextlib` 模块**: Python 标准库中的 `contextlib` 模块提供了用于创建和使用上下文管理器的实用工具。其中最常用的是 `@contextmanager` 装饰器,它允许你使用简单的生成器函数来创建上下文管理器,而无需手动实现 `__enter__` 和 `__exit__` 方法,极大地简化了上下文管理器的编写。
* **常见应用场景**: 文件操作(自动关闭文件)、数据库连接(自动关闭连接)、线程锁(自动释放锁)、网络连接、临时目录创建与清理等。
### 多线程和多进程
在Python中,多线程(Multithreading)和多进程(Multiprocessing)是实现并发(Concurrency)和并行(Parallelism)的两种主要方式。它们允许程序同时执行多个任务,从而提高程序的效率和响应速度。理解这两种机制的差异和适用场景对于编写高性能的Python程序至关重要。
#### 1. 多线程 (Multithreading)
线程是进程中的一个执行单元。一个进程可以包含多个线程,这些线程共享进程的内存空间。Python的 `threading` 模块提供了多线程支持。
**特点:**
* **共享内存**: 线程共享进程的内存空间,因此数据共享方便,但需要注意线程安全问题,避免数据竞争。
* **GIL (Global Interpreter Lock)**: Python的全局解释器锁(GIL)是一个互斥锁,它限制了在任何给定时刻只有一个线程可以执行Python字节码。这意味着Python多线程在CPU密集型任务上无法实现真正的并行计算(因为GIL会阻止多个线程同时执行Python代码),但在I/O密集型任务(如网络请求、文件读写)上仍然可以提高效率,因为在等待I/O时GIL会被释放,允许其他线程运行。
* **开销小**: 线程的创建和销毁开销相对较小,上下文切换速度快。
```python
import threading # 导入Python的线程模块。
import time # 导入时间模块,用于模拟耗时操作。
import os # 导入os模块,用于获取进程ID。
# 定义一个共享资源
shared_data = 0
# 创建一个锁对象,用于保护共享资源
data_lock = threading.Lock()
# 定义一个线程函数,这将是每个线程执行的任务。
def thread_worker(name, duration):
global shared_data
pid = os.getpid() # 获取当前进程的ID。所有线程都属于同一个进程,所以PID会相同。
thread_id = threading.get_ident() # 获取当前线程的唯一标识符。
print(f"线程 {name} (PID: {pid}, Thread ID: {thread_id}) 开始工作,预计持续 {duration} 秒...")
time.sleep(duration) # 模拟一个耗时的操作,例如网络请求或文件读写。
# 使用锁来保护共享数据
with data_lock:
shared_data += 1
print(f"线程 {name} 更新共享数据,当前值为: {shared_data}")
print(f"线程 {name} (PID: {pid}, Thread ID: {thread_id}) 完成工作。")
print("\n--- 多线程示例 ---")
threads = [] # 用于存储所有线程对象的列表。
# 创建并启动多个线程。
for i in range(3):
# threading.Thread 创建一个线程对象。
# target 参数指定线程启动后要执行的函数(即 thread_worker)。
# args 参数以元组形式传递给 target 函数的参数。这里传递了线程名称和模拟持续时间。
t = threading.Thread(target=thread_worker, args=(f"Thread-{i}", 1))
threads.append(t) # 将创建的线程对象添加到列表中。
t.start() # 启动线程。调用 start() 方法后,线程会开始执行其 target 函数。
# 等待所有线程完成。
# t.join() 方法会阻塞当前线程(在这里是主线程),直到 t 线程执行完毕。
# 确保所有子线程都执行完成后,主线程才继续执行后续代码。
for t in threads:
t.join()
print(f"所有线程任务完成。主线程继续执行。最终共享数据: {shared_data}")
2. 多进程 (Multiprocessing)
进程是操作系统分配资源(如内存、CPU时间)的基本单位。每个进程都有自己独立的内存空间。Python的 multiprocessing
模块提供了多进程支持,它通过创建新的进程来绕过GIL的限制,从而实现真正的并行计算。
特点:
- 独立内存: 每个进程有独立的内存空间,它们之间的数据默认是隔离的。因此,数据共享需要特定的机制(如队列
Queue
、管道Pipe
、共享内存Value
/Array
)进行进程间通信(IPC)。 - 无GIL限制: 每个进程都有自己的Python解释器和GIL,因此在多核CPU上可以实现真正的并行计算,适用于CPU密集型任务(如复杂的数学计算、图像处理)。
- 开销大: 进程的创建和销毁开销相对较大,上下文切换也比线程慢。
import multiprocessing # 导入Python的多进程模块。
import time # 导入时间模块,用于模拟耗时操作。
import os # 导入os模块,用于获取进程ID。
# 定义一个进程函数,这将是每个进程执行的任务。
def process_worker(name, duration, queue):
pid = os.getpid() # 获取当前进程的ID。每个进程都会有不同的PID。
print(f"进程 {name} (PID: {pid}) 开始工作,预计持续 {duration} 秒...")
time.sleep(duration) # 模拟一个耗时的CPU密集型操作。
result = f"{name} 完成工作,PID: {pid}"
queue.put(result) # 将结果放入队列,用于进程间通信
print(f"进程 {name} (PID: {pid}) 完成工作。")
print("\n--- 多进程示例 ---")
processes = [] # 用于存储所有进程对象的列表。
# 创建一个队列,用于进程间通信
results_queue = multiprocessing.Queue()
# 创建并启动多个进程。
for i in range(3):
# multiprocessing.Process 创建一个进程对象。
# target 参数指定进程启动后要执行的函数(即 process_worker)。
# args 参数以元组形式传递给 target 函数的参数。
p = multiprocessing.Process(target=process_worker, args=(f"Process-{i}", 1, results_queue))
processes.append(p) # 将创建的进程对象添加到列表中。
p.start() # 启动进程。调用 start() 方法后,进程会开始执行其 target 函数。
# 等待所有进程完成。
# p.join() 方法会阻塞当前进程(在这里是主进程),直到 p 进程执行完毕。
# 确保所有子进程都执行完成后,主进程才继续执行后续代码。
for p in processes:
p.join()
print("所有进程任务完成。主进程继续执行。")
# 从队列中获取所有进程的结果
print("\n--- 进程通信结果 --- ")
while not results_queue.empty():
print(results_queue.get())
核心概念:
- 并发 vs. 并行:
- 并发(Concurrency): 指的是在同一时间段内处理多个任务的能力,通过任务的快速切换(例如,多线程在单核CPU上通过时间片轮转实现)。任务看起来是同时进行的,但实际上在任何一个瞬间只有一个任务在执行。
- 并行(Parallelism): 指的是在同一时刻真正同时执行多个任务的能力,通常需要多核CPU或多台机器。多进程在多核CPU上可以实现真正的并行。
- GIL (Global Interpreter Lock): 全局解释器锁(Global Interpreter Lock)。Python解释器的一个特性,它确保在任何时候只有一个线程在执行Python字节码。这是Python多线程无法实现CPU密集型任务并行的主要原因。对于I/O密集型任务,GIL会在等待I/O时释放,因此多线程仍然有效。
- 线程同步与进程同步:
- 线程同步: 由于多线程共享进程的内存空间,多个线程同时访问和修改共享数据可能导致数据不一致或错误(即"竞态条件")。因此,需要使用同步机制来保护共享资源,确保数据操作的原子性。常见的线程同步机制包括:
- 锁(
threading.Lock
): 最基本的同步原语,用于保护临界区,确保一次只有一个线程访问共享资源。 - 递归锁(
threading.RLock
): 允许同一个线程多次获取同一把锁,适用于一个线程中需要多次获取锁的场景。 - 条件变量(
threading.Condition
): 允许线程等待某个条件满足,并在条件满足时被其他线程唤醒。 - 信号量(
threading.Semaphore
): 用于控制对共享资源的访问数量,例如限制同时访问数据库连接的线程数。 - 事件(
threading.Event
): 线程之间通信的简单标志,一个线程发出事件,其他线程等待事件。
- 锁(
- 进程同步: 类似于线程同步,但由于进程间内存独立,同步机制通常涉及操作系统提供的原语或
multiprocessing
模块提供的工具,如multiprocessing.Lock
、multiprocessing.Semaphore
等。
- 线程同步: 由于多线程共享进程的内存空间,多个线程同时访问和修改共享数据可能导致数据不一致或错误(即"竞态条件")。因此,需要使用同步机制来保护共享资源,确保数据操作的原子性。常见的线程同步机制包括:
- 进程间通信 (IPC): 由于多进程拥有独立的内存空间,它们之间需要通过特定的机制进行数据交换和协调。常见的IPC机制包括:
- 队列(
multiprocessing.Queue
): 最常用的IPC方式,适用于多生产者-多消费者模型,进程可以将数据放入队列,其他进程可以从队列中取出数据。 - 管道(
multiprocessing.Pipe
): 用于两个进程之间的双向通信。 - 共享内存(
multiprocessing.Value
和multiprocessing.Array
): 允许不同进程直接读写同一块内存区域,但需要手动处理同步问题。 - 管理器(
multiprocessing.Manager
): 提供了一种创建可以在不同进程之间共享的Python对象的方式,例如列表、字典等,并自动处理同步问题。
- 队列(
- 适用场景:
- 多线程: 适用于I/O密集型任务,例如网络请求、文件读写、数据库操作等,因为在等待I/O时可以释放GIL,允许其他线程执行。由于GIL的存在,不适合CPU密集型任务。
- 多进程: 适用于CPU密集型任务,例如科学计算、图像处理、数据分析等,因为每个进程都有独立的解释器和GIL,可以充分利用多核CPU的并行处理能力。由于进程创建和销毁开销较大,不适合频繁创建和销毁的场景。
异步编程
异步编程(Asynchronous Programming)是一种编程范式,它允许程序在等待某些操作(如I/O操作、网络请求、数据库查询等)完成时,不阻塞主线程,而是去执行其他任务。当等待的操作完成后,程序会回到之前暂停的地方继续执行。这与传统的同步编程(一步一步执行,每一步都阻塞直到完成)形成对比,能够显著提高程序的并发性和响应速度,尤其是在I/O密集型应用中。
Python通过 asyncio
库提供了对异步编程的原生支持,并引入了 async
和 await
关键字。
核心概念
- 协程 (Coroutines): 使用
async def
定义的函数称为协程。协程是可暂停和可恢复的函数,它们在遇到await
表达式时暂停执行,等待异步操作完成,然后从暂停点继续执行。 - 事件循环 (Event Loop): 异步编程的核心。它负责管理和调度协程的执行,当一个协程暂停等待I/O时,事件循环会切换到另一个准备就绪的协程执行。
async
关键字: 用于定义一个协程函数。await
关键字: 只能在协程函数内部使用,用于暂停当前协程的执行,等待一个可等待对象(如另一个协程、asyncio.sleep()
、asyncio.gather()
等)完成。- 可等待对象 (Awaitables): 可以被
await
的对象,包括协程、Task 和 Future。 - 任务 (Tasks):
asyncio.create_task()
用于将协程包装成一个Task
对象,并将其安排到事件循环中运行。Task 是 Future 的子类,用于并发调度协程。
import asyncio
import time
# 定义一个异步函数(协程)
# async def 关键字表示这是一个协程函数。
async def fetch_data(delay):
print(f"[{time.strftime('%H:%M:%S')}] 开始获取数据,预计延迟 {delay} 秒...")
# await asyncio.sleep(delay) 暂停当前协程的执行,等待指定秒数。
# 在等待期间,事件循环可以切换到其他协程执行,实现非阻塞。
await asyncio.sleep(delay)
print(f"[{time.strftime('%H:%M:%S')}] 数据获取完成,延迟 {delay} 秒。")
return f"Data after {delay}s"
# 定义主异步函数
async def main():
print(f"[{time.strftime('%H:%M:%S')}] 主程序开始运行。")
# 创建任务:将协程包装成 Task 对象,并将其安排到事件循环中运行。
# 任务会立即开始执行,但不会阻塞当前协程。
task1 = asyncio.create_task(fetch_data(2)) # 创建一个延迟2秒的任务。
task2 = asyncio.create_task(fetch_data(1)) # 创建一个延迟1秒的任务。
# 等待任务完成:asyncio.gather() 并发地运行多个可等待对象,并等待它们全部完成。
# 它会收集所有任务的结果,并以列表形式返回。
results = await asyncio.gather(task1, task2)
print(f"[{time.strftime('%H:%M:%S')}] 所有任务完成: {results}")
print("\n--- 另一个异步函数示例 ---")
await another_async_function()
async def another_async_function():
print(f"[{time.strftime('%H:%M:%S')}] 另一个异步函数开始。")
await asyncio.sleep(0.5)
print(f"[{time.strftime('%H:%M:%S')}] 另一个异步函数结束。")
# 运行主异步函数
# asyncio.run(main()) 是运行异步程序的入口点。它负责启动事件循环,运行 main 协程,并在 main 协程完成后关闭事件循环。
if __name__ == "__main__":
asyncio.run(main())
# 预期输出(时间戳可能不同,但顺序和并发性是关键):
# [HH:MM:SS] 主程序开始运行。
# [HH:MM:SS] 开始获取数据,预计延迟 2 秒...
# [HH:MM:SS] 开始获取数据,预计延迟 1 秒。
# [HH:MM:SS] 数据获取完成,延迟 1 秒。
# [HH:MM:SS] 数据获取完成,延迟 2 秒。
# [HH:MM:SS] 所有任务完成: ['Data after 2s', 'Data after 1s']
# [HH:MM:SS] 另一个异步函数开始。
# [HH:MM:SS] 另一个异步函数结束。
核心概念:
- 非阻塞I/O: 异步编程的核心优势,允许程序在等待I/O时执行其他任务。
async
和await
: Python中定义和使用协程的关键关键字。- 事件循环: 异步任务的调度器。
- 并发而非并行: 异步编程实现的是并发(通过任务切换),而不是真正的并行(同时执行)。真正的并行通常需要多进程。
- 适用场景: 适用于I/O密集型任务,如网络爬虫、Web服务器、数据库操作等。
单元测试
单元测试(Unit Testing)是软件开发中的一种测试方法,用于验证程序中最小的可测试单元(通常是函数、方法或类)是否按预期工作。通过编写单元测试,可以确保代码的质量、减少bug、提高代码的可维护性,并为代码重构提供安全保障。
Python标准库提供了 unittest
模块,这是一个功能丰富的单元测试框架,类似于Java的JUnit或NUnit。
unittest
模块的基本组成:
- 测试用例 (Test Case): 继承自
unittest.TestCase
的类。每个测试用例类通常包含一个或多个测试方法。 - 测试方法 (Test Method): 测试用例类中以
test_
开头的方法。每个测试方法都是一个独立的测试。 - 断言 (Assertions):
unittest.TestCase
类提供了各种断言方法(如assertEqual
,assertTrue
,assertRaises
等),用于检查测试结果是否符合预期。 - 测试固件 (Test Fixtures):
setUp()
和tearDown()
方法。setUp()
在每个测试方法执行前运行,用于准备测试环境;tearDown()
在每个测试方法执行后运行,用于清理测试环境。 - 测试套件 (Test Suite): 用于组织和运行多个测试用例。
- 测试运行器 (Test Runner): 用于执行测试套件中的测试,并报告结果。
import unittest # 导入 Python 内置的单元测试框架。
# 待测试的类或函数
class Calculator:
"""一个简单的计算器类,用于演示单元测试。"""
def add(self, a, b):
"""返回两个数的和。"""
return a + b
def subtract(self, a, b):
"""返回两个数的差。"""
return a - b
def multiply(self, a, b):
"""返回两个数的乘积。"""
return a * b
def divide(self, a, b):
"""返回两个数的商。"""
if b == 0:
raise ValueError("除数不能为零!")
return a / b
# 编写测试用例
# TestCalculator 类继承自 unittest.TestCase,表示这是一个测试用例。
class TestCalculator(unittest.TestCase):
# setUp 方法在每个测试方法执行前运行,用于初始化测试环境。
def setUp(self):
print("\nSetting up Calculator instance...")
self.calc = Calculator() # 为每个测试方法创建一个新的 Calculator 实例。
# tearDown 方法在每个测试方法执行后运行,用于清理测试环境。
def tearDown(self):
print("Tearing down Calculator instance...")
del self.calc # 删除 Calculator 实例。
# 测试方法必须以 test_ 开头。
def test_add(self):
print("Running test_add...")
# 使用 assertEqual 断言方法检查 add 方法的返回值是否符合预期。
self.assertEqual(self.calc.add(3, 5), 8) # 3 + 5 应该等于 8。
self.assertEqual(self.calc.add(-1, 1), 0) # -1 + 1 应该等于 0。
self.assertEqual(self.calc.add(-1, -1), -2) # -1 + -1 应该等于 -2。
def test_subtract(self):
print("Running test_subtract...")
self.assertEqual(self.calc.subtract(10, 5), 5)
self.assertEqual(self.calc.subtract(-1, -1), 0)
def test_multiply(self):
print("Running test_multiply...")
self.assertEqual(self.calc.multiply(2, 3), 6)
self.assertEqual(self.calc.multiply(-2, 3), -6)
def test_divide(self):
print("Running test_divide...")
self.assertEqual(self.calc.divide(10, 2), 5)
self.assertAlmostEqual(self.calc.divide(10, 3), 3.333333, places=6) # 浮点数比较使用 assertAlmostEqual。
# 测试异常:使用 assertRaises 断言方法检查是否抛出了预期的异常。
with self.assertRaises(ValueError): # 期望当除数为零时抛出 ValueError。
self.calc.divide(10, 0)
# 运行测试
# if __name__ == '__main__': 确保只有在直接运行此脚本时才执行测试。
# unittest.main() 发现并运行当前文件中所有的测试用例和测试方法。
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False) # exit=False 避免在Jupyter等环境中退出。
核心概念:
- 测试驱动开发 (TDD): 一种开发方法,先写测试,再写代码,然后重构。
- 测试覆盖率: 衡量测试代码执行了多少生产代码的指标。
- 回归测试: 在代码修改后,重新运行旧的测试,以确保修改没有引入新的bug或破坏现有功能。
- Mocking/Patching: 在测试中模拟(mock)或替换(patch)依赖项,以便隔离被测试单元。
- 持续集成 (CI): 将自动化测试集成到开发流程中,每次代码提交后自动运行测试。
文档字符串和类型提示
在Python中,文档字符串(Docstrings)和类型提示(Type Hints)是提高代码可读性、可维护性以及协作效率的重要工具。它们有助于开发者更好地理解代码的功能、预期输入和输出,并在开发过程中提供静态分析和错误检查。
1. 文档字符串 (Docstrings)
文档字符串是Python中用于解释模块、类、函数或方法用途的字符串字面量。它们通常位于定义的第一行,并用三引号("""
或 '''
)括起来。文档字符串可以通过 __doc__
属性访问,并且可以被工具(如Sphinx、PyCharm)用于自动生成文档或提供上下文帮助。
推荐的文档字符串格式:
Python社区通常推荐使用 reStructuredText 或 Google 风格的文档字符串,其中包含:
- 简要摘要: 一句话描述功能。
- 详细描述: 更详细的解释,包括算法、设计选择等。
- 参数 (Args): 列出所有参数及其类型和描述。
- 返回值 (Returns): 描述返回值及其类型。
- 异常 (Raises): 列出可能抛出的异常及其条件。
- 示例 (Examples): 提供使用示例。
from typing import List, Dict, Union # 导入类型提示所需的模块。
# 示例:带有文档字符串和类型提示的函数
def calculate_average(numbers: List[Union[int, float]]) -> float:
"""计算数字列表的平均值。
此函数接受一个包含整数或浮点数的列表,并返回它们的算术平均值。
如果输入列表为空,则会引发 ValueError。
Args:
numbers (List[Union[int, float]]): 包含数字(整数或浮点数)的列表。
Returns:
float: 列表中所有数字的算术平均值。
Raises:
ValueError: 如果 `numbers` 列表为空。
Examples:
>>> calculate_average([1, 2, 3, 4, 5])
3.0
>>> calculate_average([10, 20, 30])
20.0
>>> calculate_average([])
Traceback (most recent call last):
...
ValueError: 列表不能为空
"""
if not numbers:
raise ValueError("列表不能为空")
return sum(numbers) / len(numbers)
# 访问文档字符串
print("\n--- 文档字符串示例 ---")
print(calculate_average.__doc__)
# 示例:带有文档字符串的类
class MyClass:
"""这是一个示例类,演示文档字符串的使用。"""
def __init__(self, name: str):
"""初始化 MyClass 实例。
Args:
name (str): 实例的名称。
"""
self.name = name
def get_name(self) -> str:
"""获取实例的名称。
Returns:
str: 实例的名称。
"""
return self.name
print("\n--- 类和方法的文档字符串示例 ---")
print(MyClass.__doc__)
print(MyClass.get_name.__doc__)
2. 类型提示 (Type Hints)
类型提示(在PEP 484中引入)允许开发者在代码中声明变量、函数参数和返回值的预期类型。Python本身是动态类型语言,不会在运行时强制执行类型检查,但类型提示可以被静态类型检查工具(如MyPy、Pyright)用于在代码运行前发现潜在的类型错误,提高代码的可靠性。同时,它们也极大地增强了代码的可读性,并为IDE提供了更好的自动补全和重构支持。
类型提示的优点:
- 提高代码可读性: 明确函数预期接收的参数类型和返回的类型,使代码意图更清晰。
- 增强代码可维护性: 减少因类型不匹配导致的运行时错误,便于团队协作和代码重构。
- 静态分析: 配合静态类型检查工具(如MyPy、Pyright),可以在代码运行前发现潜在的类型错误。
- IDE支持: 现代IDE(如PyCharm、VS Code)可以利用类型提示提供更智能的代码补全、导航和重构功能。
- 文档生成: 类型提示可以作为代码文档的一部分,自动生成API文档。
常用类型和 typing
模块:
Python的内置类型(如 int
, float
, str
, list
, dict
)可以直接用于类型提示。对于更复杂的类型,typing
模块提供了丰富的类型提示工具:
- 基本类型:
int
,float
,str
,bool
,bytes
,None
。 - 集合类型:
List[T]
: 列表,例如List[int]
表示只包含整数的列表。Dict[K, V]
: 字典,例如Dict[str, int]
表示键为字符串、值为整数的字典。Tuple[T1, T2, ...]
: 元组,例如Tuple[str, int, bool]
表示包含一个字符串、一个整数和一个布尔值的元组。对于任意长度的同类型元组,可以使用Tuple[int, ...]
。Set[T]
: 集合,例如Set[str]
表示只包含字符串的集合。
- 可选类型:
Optional[T]
等同于Union[T, None]
,表示该值可以是类型T
或None
。 - 联合类型:
Union[T1, T2, ...]
表示该值可以是列出类型中的任意一种。 - 函数类型:
Callable[[Arg1Type, Arg2Type], ReturnType]
用于提示函数作为参数的类型。 - 任意类型:
Any
表示可以是任何类型,通常用于类型信息不明确或需要兼容旧代码的情况。 - 泛型:
TypeVar
用于创建泛型函数或类,使其能够处理多种类型而保持类型安全。 - 协议:
Protocol
用于定义结构化类型,即只要对象具有某些方法或属性,就符合该类型,而无需显式继承。
类型提示的实践:
- 逐步引入: 可以在现有项目中逐步引入类型提示,不必一次性完成所有代码的修改。
- 保持更新: 当代码逻辑或数据结构发生变化时,及时更新类型提示。
- 使用工具: 结合MyPy、Pyright等静态类型检查工具,在开发过程中发现并修复类型错误。
- 文档一致性: 确保文档字符串中的类型信息与类型提示保持一致。
# 导入常用的类型提示
from typing import List, Dict, Tuple, Set, Optional, Union, Callable, Any, TypeVar, Protocol
# 变量类型提示
def process_data(data: Dict[str, List[int]]) -> None:
"""处理一个字典,其中键是字符串,值是整数列表。
Args:
data: 要处理的字典数据,格式为 {字符串: 整数列表}
"""
for key, values in data.items():
print(f"Key: {key}, Values: {values}")
# 函数参数和返回值类型提示
def greet(name: str) -> str:
"""根据提供的名称返回一个问候语。
Args:
name: 要问候的名字
Returns:
格式化后的问候字符串
"""
return f"Hello, {name}!"
# 可选参数
def get_user_info(user_id: int, active: Optional[bool] = None) -> Dict[str, Any]:
"""获取用户信息,active参数是可选的。
Args:
user_id: 用户ID
active: 可选的活动状态
Returns:
包含用户信息的字典
"""
info = {"id": user_id, "name": f"User{user_id}"}
if active is not None:
info["active"] = active
return info
# 联合类型
def process_item(item: Union[str, int]) -> str:
"""处理一个可以是字符串或整数的项。
Args:
item: 要处理的项,可以是字符串或整数
Returns:
处理后的字符串结果
"""
if isinstance(item, str):
return f"Processed string: {item.upper()}"
else:
return f"Processed number: {item * 2}"
# 函数作为参数
def apply_function(func: Callable[[int, int], int], a: int, b: int) -> int:
"""将一个接受两个整数并返回一个整数的函数应用于a和b。
Args:
func: 要应用的函数
a: 第一个参数
b: 第二个参数
Returns:
函数应用后的结果
"""
return func(a, b)
# 泛型
T = TypeVar('T')
def first_element(data: List[T]) -> Optional[T]:
"""返回列表中第一个元素,如果列表为空则返回None。
Args:
data: 要处理的列表
Returns:
第一个元素或None
"""
if data:
return data[0]
return None
# 协议
class SupportsLen(Protocol):
def __len__(self) -> int:
...
def get_length(obj: SupportsLen) -> int:
"""返回支持len()操作的对象的长度。
Args:
obj: 支持长度协议的对象
Returns:
对象的长度
"""
return len(obj)
# 示例调用
print("\n--- 类型提示示例 ---")
# 变量类型示例
process_data({"a": [1, 2], "b": [3, 4, 5]})
# 函数参数和返回值示例
print(greet("Alice")) # 输出: Hello, Alice!
# 可选参数示例
print(get_user_info(101)) # 输出: {'id': 101, 'name': 'User101'}
print(get_user_info(102, active=True)) # 输出: {'id': 102, 'name': 'User102', 'active': True}
# 联合类型示例
print(process_item("hello")) # 输出: Processed string: HELLO
print(process_item(123)) # 输出: Processed number: 246
# 函数作为参数示例
print(apply_function(lambda x, y: x + y, 5, 3)) # 输出: 8
# 泛型示例
print(first_element([1, 2, 3])) # 输出: 1
print(first_element(["a", "b"])) # 输出: a (泛型T自动推断为str)
# 协议示例
print(get_length("world")) # 输出: 5
print(get_length([1, 2, 3, 4])) # 输出: 4
核心概念:
- 自文档化: 文档字符串使代码本身成为其最佳文档。
- 静态分析: 类型提示允许工具在代码运行前发现潜在错误。
- IDE支持: 增强IDE的自动补全、重构和错误检查功能。
- 代码可读性与可维护性: 明确的文档和类型信息使代码更容易理解和维护。
- PEP 257: Python文档字符串的约定。
- PEP 484: Python类型提示的规范。