第四章 Python基础回顾(下):函数定义与调用、文件读写操作、异常处理机制
好,咱们继续往下走。上一章聊了Python的基础数据结构,这一章我带你看看三个更贴近实战的东西——函数、文件操作和异常处理。说实话,这三个东西在自动化测试脚本里几乎是每时每刻都在用。你写一个测试用例,不封装成函数?那代码得乱成什么样。你要读取测试数据?文件操作跑不掉。脚本跑着跑着崩了?异常处理就是你的救命稻草。
4.1 函数定义与调用——把代码装进盒子里
函数是什么?说白了,就是把一段逻辑打包成一个“盒子”。你给它起个名字,传点参数进去,它帮你干活,最后可能还给你返回个结果。
我刚开始写自动化脚本的时候,特别喜欢把所有的代码都堆在main函数里。后来有一次,一个脚本跑了两小时,中间报了个错,我愣是找了半小时才定位到问题。从那以后,我养成了一个习惯:每个独立的功能都封装成函数。你想想看,这样调试起来多方便。
4.1.1 基本定义语法
def 函数名(参数1, 参数2=默认值):
"""文档字符串——说明这个函数是干嘛的"""
# 函数体
return 返回值
嗯,这里要注意:Python的函数定义用def关键字,后面跟函数名和括号。冒号不能少,缩进不能乱。我见过不少新手在这里栽跟头。
4.1.2 参数传递——值传递还是引用传递?
这个问题我面试的时候经常问。其实Python的参数传递既不是纯粹的值传递,也不是纯粹的引用传递。准确的说法是“对象引用传递”。
举个例子你就明白了:
def modify_list(lst):
lst.append(4) # 会修改原列表
lst = [1, 2, 3] # 不会修改原列表
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list) # 输出 [1, 2, 3, 4]
为什么会这样?因为lst.append(4)操作的是同一个对象,而lst = [1, 2, 3]是让局部变量指向了一个新对象。我在项目中遇到过有人因为这个特性踩坑,调试了半天才发现是参数传递的问题。
4.1.3 可变参数与关键字参数
写测试脚本的时候,你经常会遇到参数个数不确定的情况。比如一个日志函数,有时候要记录一条信息,有时候要记录多条。这时候可变参数就派上用场了。
def log_messages(*args, **kwargs):
"""*args接收位置参数,**kwargs接收关键字参数"""
for msg in args:
print(f"[INFO] {msg}")
for key, value in kwargs.items():
print(f"[INFO] {key}: {value}")
log_messages("测试开始", "连接数据库", env="staging", version="2.0")
我个人习惯用*args和**kwargs作为参数名,这是Python社区的约定俗成。当然你也可以用别的名字,但别搞特殊化,团队协作时容易让人困惑。
4.2 文件读写操作——测试数据的命脉
自动化测试离不开文件操作。测试数据存在哪里?配置文件怎么读?测试报告怎么写?全是文件操作。说白了,文件就是脚本和外部世界沟通的桥梁。
4.2.1 打开文件的正确姿势
我建议你永远使用with语句来打开文件。为什么?因为它会自动帮你关闭文件,省心又安全。
# 推荐写法
with open("test_data.txt", "r", encoding="utf-8") as f:
content = f.read()
# 处理文件内容
# 文件自动关闭,不用操心
我曾经见过有人这样写:
# 不推荐写法
f = open("test_data.txt", "r")
content = f.read()
# 忘了写 f.close()
结果呢?文件句柄泄漏,脚本跑久了就报错。你想想看,一个跑通宵的回归测试,中间因为文件没关闭而崩溃,多冤啊。
4.2.2 读取模式——按需选择
| 模式 | 说明 | 适用场景 |
|---|---|---|
'r' |
只读模式 | 读取配置文件、测试数据 |
'w' |
写入模式(覆盖) | 生成测试报告、日志文件 |
'a' |
追加模式 | 追加日志、记录运行历史 |
'rb' |
二进制只读 | 读取图片、二进制测试数据 |
'w'模式时要格外小心。它会清空原文件内容再写入。我曾经有一次不小心用'w'模式打开了一个重要的配置文件,结果...嗯,从那以后我每次写文件前都会先确认模式。
4.2.3 逐行读取——处理大文件的利器
如果你的测试数据文件很大(比如几GB的日志文件),千万别用read()一次性读取。内存会爆的。正确的做法是逐行读取:
with open("large_log.txt", "r", encoding="utf-8") as f:
for line in f:
# 处理每一行
if "ERROR" in line:
print(f"发现错误: {line.strip()}")
这种写法,一次只读一行到内存里。文件再大也不怕。我在做性能测试的时候,经常要分析几十GB的服务器日志,用的就是这种方法。
4.3 异常处理机制——让脚本更健壮
写测试脚本,最怕什么?最怕脚本跑着跑着突然崩了,连个错误信息都没留下。异常处理就是用来解决这个问题的。它让脚本在遇到错误时,不是直接崩溃,而是优雅地处理错误,或者至少留下有用的信息。
4.3.1 try-except 的基本结构
try:
# 可能会出错的代码
result = 10 / 0
except ZeroDivisionError as e:
# 处理特定异常
print(f"除数不能为零: {e}")
except Exception as e:
# 处理其他所有异常
print(f"发生了未知错误: {e}")
else:
# 没有异常时执行
print("计算成功")
finally:
# 不管有没有异常都会执行
print("清理资源")
这个结构我建议你背下来。try、except、else、finally,四个块各有各的用途。我个人习惯把资源清理的代码放在finally里,比如关闭数据库连接、释放文件句柄等。
4.3.2 捕获异常的原则——别太宽也别太窄
我见过有人这样写:
try:
# 一堆代码
except:
pass # 什么都不做
这种写法我称之为“掩耳盗铃式异常处理”。你把所有异常都吞掉了,脚本倒是不会崩了,但问题也找不到了。你想想看,测试脚本跑完了,报告显示全部通过,但实际上中间出了错只是被忽略了——这比直接崩溃更可怕。
- 尽量捕获具体的异常类型,而不是笼统的
Exception - 至少把异常信息打印出来,方便排查问题
- 不要用空的
except块,除非你真的知道自己在做什么
4.3.3 自定义异常——让错误信息更有意义
有时候Python内置的异常类型不够用。比如你在写一个测试框架,需要定义“测试用例执行失败”这种业务相关的异常。这时候可以自定义异常:
class TestCaseFailedError(Exception):
"""测试用例执行失败时抛出的异常"""
def __init__(self, case_name, reason):
self.case_name = case_name
self.reason = reason
super().__init__(f"用例 [{case_name}] 执行失败: {reason}")
# 使用
try:
if actual != expected:
raise TestCaseFailedError("登录测试", "预期结果与实际结果不一致")
except TestCaseFailedError as e:
print(e) # 输出: 用例 [登录测试] 执行失败: 预期结果与实际结果不一致
这样做的好处是,异常信息一目了然。你一看就知道是哪个用例、因为什么原因失败了。我在做自动化测试框架的时候,自定义了十几种异常类型,覆盖了各种测试场景。
4.3.4 异常处理的最佳实践——写在最后
嗯,这里我总结几条经验:
- 不要滥用异常:能用if判断的,就别用try-except。异常处理是有性能开销的。
- 异常要记录:至少用
logging模块把异常信息记下来。别只打印到控制台,脚本跑完控制台一关就没了。 - 考虑异常链:有时候一个异常是由另一个异常引起的。用
raise ... from ...可以保留异常链,方便排查。
try:
with open("config.json") as f:
config = json.load(f)
except FileNotFoundError as e:
raise RuntimeError("配置文件不存在,请检查路径") from e
这样,你既得到了友好的错误提示,又能看到原始异常信息。两全其美。
好了,这一章的内容就到这里。函数、文件操作、异常处理,这三个东西你掌握了,写自动化测试脚本就有了坚实的基础。下一章我们开始真正进入自动化测试的世界,到时候你会看到这些知识是怎么用起来的。