第四章 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

这样,你既得到了友好的错误提示,又能看到原始异常信息。两全其美。

好了,这一章的内容就到这里。函数、文件操作、异常处理,这三个东西你掌握了,写自动化测试脚本就有了坚实的基础。下一章我们开始真正进入自动化测试的世界,到时候你会看到这些知识是怎么用起来的。