Python 函数详解
从基础语法到高级应用的完整指南
作为测试工程师,你是否遇到过这些问题:自动化脚本中重复代码越来越多?用例执行日志到处都是难以维护?同一个功能在多处实现导致修改困难?函数就是解决这些问题的最佳方案!掌握函数,让你的测试代码更优雅、更高效、更易维护。
函数就是一段可重用的代码块,用来实现特定功能。就像一个"黑盒子",你给它输入(参数),它给你输出(返回值)。在测试自动化中,函数可以帮我们封装登录、断言、数据准备等常用操作。
# 函数的基本结构# def 函数名(参数):# """文档字符串"""# 函数体# return 返回值# 最简单的函数 - 打印测试日志def print_test_log(): """打印测试日志""" print("=" * 50) print(f"测试开始时间: 2024-01-15 10:00:00") print(f"测试用例编号: TC001") print("=" * 50)# 调用函数print_test_log()print_test_log() # 可以多次调用
💡 小贴士
函数名应该见名知意,用动词开头(如 login、assert_equal、generate_test_data)。文档字符串(docstring)是好习惯,团队协作时别人一看就知道函数怎么用。
参数是函数的"输入",让函数更加灵活。在测试中,我们经常需要给函数传入用户名、密码、期望结果等参数。
# ===== 1. 单个参数的函数def login(username): """模拟用户登录""" print(f"用户 [{username}] 正在登录系统...") # 这里可以写真正的登录逻辑 return Truelogin("admin") # 用户 [admin] 正在登录系统...login("tester001") # 用户 [tester001] 正在登录系统...# ===== 2. 多个参数的函数def assert_equal(actual, expected, case_name): """断言相等,测试中最常用的函数""" print(f"\n执行用例: {case_name}") print(f"实际结果: {actual}") print(f"期望结果: {expected}") if actual == expected: print("✅ 测试通过!") return True else: print("❌ 测试失败!") return False# 调用断言函数result1 = assert_equal(200, 200, "接口响应码校验")result2 = assert_equal("success", "success", "登录状态校验")result3 = assert_equal(3, 5, "用户数量校验")# ===== 3. 带返回值的函数def calculate_coverage(passed, total): """计算测试覆盖率""" if total == 0: return 0.0 coverage = (passed / total) * 100 return round(coverage, 2)rate = calculate_coverage(85, 100)print(f"\n测试覆盖率: {rate}%") # 测试覆盖率: 85.0%
⚠️ 踩坑经验
没有 return 语句的函数默认返回 None!不要把 print() 当成返回值。很多新手会写:def add(a,b): print(a+b),然后 result = add(1,2),结果 result 是 None!
Python 支持多种参数类型,让函数调用更加灵活。位置参数是最基础的,默认参数可以减少重复传参。
# ===== 1. 位置参数(必须按顺序传递)def api_request(url, method, headers): """发送API请求""" print(f"发送 {method} 请求到: {url}") print(f"请求头: {headers}")# 必须按顺序传参api_request("https://api.example.com/users", "GET", {"Content-Type": "application/json"})# ===== 2. 默认参数(有默认值的参数,可以不传)def api_request_v2(url, method="GET", headers=None): """带默认值的API请求函数""" if headers is None: headers = {"Content-Type": "application/json"} print(f"发送 {method} 请求到: {url}") print(f"请求头: {headers}")# 只传必填参数api_request_v2("https://api.example.com/users") # 默认 GET# 覆盖默认值api_request_v2("https://api.example.com/users", "POST")# ===== 3. 测试场景:生成测试报告def generate_report(test_results, format="HTML", output_dir="./reports"): """生成测试报告 Args: test_results: 测试结果列表 format: 报告格式 (HTML/JSON/XML) output_dir: 输出目录 """ passed = sum(1 for r in test_results if r == "PASS") total = len(test_results) print(f"\n生成 {format} 格式报告...") print(f"测试结果: {passed}/{total} 通过") print(f"输出目录: {output_dir}")# 使用默认参数generate_report(["PASS", "PASS", "FAIL", "PASS"])# 自定义参数generate_report(["PASS", "PASS"], format="JSON", output_dir="./test_reports")
⚠️ 踩坑经验
永远不要用可变对象(列表、字典)作为默认参数!因为默认参数只在函数定义时创建一次,多次调用会共享同一个对象。正确写法是 def func(x=None): if x is None: x = []。
关键字参数让调用更清晰,不定长参数让函数接受任意数量的输入,这在处理测试数据时特别有用。
# ===== 1. 关键字参数(调用时指定参数名)def create_user(username, password, email, role="tester"): """创建测试用户""" print(f"\n创建用户: {username}") print(f"邮箱: {email}") print(f"角色: {role}")# 使用关键字参数,顺序可以任意create_user( username="test_user", email="test@example.com", password="123456", role="admin")# ===== 2. *args - 接收任意数量的位置参数(元组)def run_multiple_tests(*test_cases): """批量运行多个测试用例""" print(f"\n准备运行 {len(test_cases)} 个测试用例:") for i, case in enumerate(test_cases, 1): print(f" {i}. 运行: {case}")# 可以传任意数量的参数run_multiple_tests("登录测试", "注册测试", "查询测试")run_multiple_tests("接口测试1", "接口测试2")# 也可以传入一个列表(需要拆包)cases = ["UI测试", "性能测试", "安全测试"]run_multiple_tests(*cases) # 用 * 拆包# ===== 3. **kwargs - 接收任意数量的关键字参数(字典)def log_test_step(**kwargs): """记录测试步骤,可传入任意字段""" print("\n" + "-" * 40) for key, value in kwargs.items(): print(f"{key}: {value}") print("-" * 40)# 任意传参log_test_step( step="用户登录", username="admin", timestamp="2024-01-15 10:30:00", status="PASS", duration="0.5s")log_test_step( step="数据库查询", sql="SELECT * FROM users", rows_returned=100)# 也可以传入一个字典(需要拆包)step_data = { "step": "API调用", "url": "/api/users", "method": "GET", "response_code": 200}log_test_step(**step_data) # 用 ** 拆包
💡 小贴士
记忆口诀:一个星号是"多值"(元组),两个星号是"多键值"(字典)。参数顺序必须是:位置参数 → *args → 默认参数 → **kwargs,否则会报错!
作用域决定了变量在哪里可以被访问。理解作用域能避免"变量找不到"或"值被意外修改"等bug。
# ===== 1. 局部变量:函数内部定义,外部无法访问def test_login(): # 这是局部变量,只在函数内有效 test_username = "test_user" test_password = "123456" print(f"登录测试: {test_username}")test_login()# print(test_username) # ❌ 报错!NameError: name 'test_username' is not defined# ===== 2. 全局变量:函数外部定义,整个文件都可以访问TEST_ENV = "production" # 全局变量,通常大写def print_env(): print(f"当前测试环境: {TEST_ENV}") # 可以读取全局变量print_env()# ===== 3. 修改全局变量需要 global 关键字test_count = 0 # 全局计数器def run_test(): global test_count # 声明要使用全局变量 test_count += 1 # 修改全局变量 print(f"已执行第 {test_count} 个测试")run_test() # 已执行第 1 个测试run_test() # 已执行第 2 个测试run_test() # 已执行第 3 个测试print(f"总共执行了 {test_count} 个测试")# ===== 4. 测试场景:全局配置示例CONFIG = { "base_url": "https://api.example.com", "timeout": 30, "retry_times": 3}def get_config(key): """获取配置项""" return CONFIG.get(key)def update_config(key, value): """更新配置(字典是可变对象,不需要global)""" CONFIG[key] = valueprint(f"超时时间: {get_config('timeout')}") # 30update_config("timeout", 60)print(f"修改后: {get_config('timeout')}") # 60
⚠️ 踩坑经验
如果函数内部定义了和全局同名的变量,Python会认为你要创建一个新的局部变量,而不是修改全局的!比如全局有 x = 10,函数里写 x = 20,这不会改变全局的 x,而是创建了一个新的局部变量 x!
递归就是"函数自己调用自己"。在测试中,递归特别适合处理嵌套的数据结构,比如多层嵌套的JSON、树形菜单等。
# ===== 递归的两个关键要素:# 1. 基线条件(Base Case):什么时候停止递归# 2. 递归条件(Recursive Case):什么时候继续递归# ===== 经典例子:阶乘def factorial(n): """计算 n! = n * (n-1) * ... * 1""" # 基线条件:n = 1 时返回 1 if n == 1: return 1 # 递归条件:n * (n-1)! else: return n * factorial(n - 1)print(f"5! = {factorial(5)}") # 5! = 120# ===== 测试场景1:遍历嵌套JSON查找指定keyapi_response = { "code": 200, "data": { "user": { "id": 1001, "name": "test_user", "permissions": ["read", "write"] }, "meta": { "pagination": { "page": 1, "total": 100 } } }}def find_nested_value(data, target_key): """递归查找嵌套字典中的值""" # 如果是字典 if isinstance(data, dict): # 先检查当前层有没有目标key if target_key in data: return data[target_key] # 递归遍历每个值 for value in data.values(): result = find_nested_value(value, target_key) if result is not None: return result # 如果是列表,递归遍历每个元素 elif isinstance(data, list): for item in data: result = find_nested_value(item, target_key) if result is not None: return result # 都没找到 return None# 使用递归函数查找深层嵌套的值print(f"用户ID: {find_nested_value(api_response, 'id')}") # 1001print(f"总记录数: {find_nested_value(api_response, 'total')}") # 100print(f"用户名: {find_nested_value(api_response, 'name')}") # test_user# ===== 测试场景2:遍历目录结构def count_files(path, depth=0): """递归统计目录下的文件数量(模拟)""" import os if not os.path.exists(path): return 0 count = 0 for item in os.listdir(path): item_path = os.path.join(path, item) indent = " " * depth if os.path.isdir(item_path): print(f"{indent}📁 {item}/") count += count_files(item_path, depth + 1) else: print(f"{indent}📄 {item}") count += 1 return count# 统计当前目录的文件# total = count_files(".")# print(f"\n总文件数: {total}")
💡 小贴士
Python 默认递归深度限制是 1000,超过会报 RecursionError。写递归一定要确保基线条件正确,否则会无限递归直到栈溢出!
装饰器是 Python 的特色语法,可以"在不修改原函数代码的情况下,给函数增加额外功能"。在测试中,装饰器特别适合做日志记录、性能计时、重试机制等。
# 装饰器原理:函数可以作为参数传递,可以作为返回值# 装饰器就是一个"接收函数,并返回新函数"的函数import timeimport functools# ===== 1. 最简单的装饰器:日志记录def log_decorator(func): """记录函数调用日志的装饰器""" @functools.wraps(func) # 保留原函数的元信息 def wrapper(*args, **kwargs): # 调用前的操作 print(f"\n📝 开始执行: {func.__name__}") print(f" 参数: args={args}, kwargs={kwargs}") start_time = time.time() # 调用原函数 result = func(*args, **kwargs) # 调用后的操作 end_time = time.time() print(f" 返回值: {result}") print(f" 耗时: {(end_time - start_time):.4f}秒") return result return wrapper# 使用装饰器@log_decoratordef login(username, password): """用户登录函数""" print(f" 正在验证用户: {username}") time.sleep(0.5) # 模拟网络延迟 return {"status": "success", "user": username}@log_decoratordef search_user(keyword): """搜索用户""" print(f" 正在搜索: {keyword}") time.sleep(0.3) return ["user1", "user2"]# 调用被装饰的函数(自动加上了日志功能)login("admin", "123456")search_user("test")# ===== 2. 测试场景:重试装饰器def retry(max_times=3, delay=1): """失败重试装饰器 Args: max_times: 最大重试次数 delay: 重试间隔(秒) """ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_times + 1): try: return func(*args, **kwargs) except Exception as e: print(f"⚠️ 第 {attempt} 次尝试失败: {str(e)}") if attempt < max_times: print(f" {delay} 秒后重试...") time.sleep(delay) raise Exception(f"❌ 经过 {max_times} 次尝试后仍然失败") return wrapper return decorator# 使用带参数的装饰器@retry(max_times=3, delay=2)def unstable_api_call(): """模拟不稳定的接口""" import random if random.random() < 0.7: # 70% 概率失败 raise Exception("网络超时") return {"code": 200, "data": "success"}# 测试重试机制try: result = unstable_api_call() print(f"✅ 最终成功: {result}")except Exception as e: print(e)# ===== 3. 测试场景:性能计时装饰器def timer(func): """性能计时装饰器""" @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() print(f"⏱️ {func.__name__} 执行耗时: {(end - start):.4f}秒") return result return wrapper@timerdef generate_test_data(count): """生成大量测试数据""" data = [f"user_{i}" for i in range(count)] return data# 测试性能data = generate_test_data(10000)print(f"生成了 {len(data)} 条测试数据")
💡 小贴士
装饰器是 Python 的"语法糖",核心原理是闭包和函数作为一等公民。记住要加 @functools.wraps(func),否则原函数的名称、文档字符串等元信息会丢失!
分享几个测试工作中高频使用的函数设计技巧,帮你写出更专业的测试代码。
# ===== 技巧1:函数返回字典,便于断言def run_test_case(case_id): """执行测试用例,返回结构化结果""" # 模拟测试执行 import random passed = random.choice([True, False]) return { "case_id": case_id, "status": "PASS" if passed else "FAIL", "start_time": "2024-01-15 10:00:00", "duration": 2.5, "error_msg": "断言失败" if not passed else "" }# 使用起来非常清晰result = run_test_case("TC001")print(f"用例 {result['case_id']}: {result['status']}")# ===== 技巧2:函数注解(类型提示)def add(a: int, b: int) -> int: """两个数相加 Args: a: 第一个数 b: 第二个数 Returns: 两个数的和 """ return a + b# 测试框架常用的类型提示def assert_response( response: dict, expected_code: int = 200, expected_data: dict = None) -> bool: """断言API响应 Args: response: 实际响应 expected_code: 期望的状态码 expected_data: 期望的数据(可选) Returns: 断言是否通过 """ actual_code = response.get("code", 0) if actual_code != expected_code: print(f"❌ 状态码不匹配: 期望 {expected_code}, 实际 {actual_code}") return False print("✅ 状态码校验通过") return True# ===== 技巧3:函数作为参数传递(回调函数)def execute_tests(test_list, callback=None): """批量执行测试,支持回调函数""" results = [] for test in test_list: print(f"\n执行: {test}") result = {"name": test, "status": "PASS"} results.append(result) # 如果有回调函数,执行它 if callback: callback(result) return results# 定义回调函数:发送测试结果到钉钉def send_to_dingtalk(result): """测试结果回调""" print(f"📤 发送结果到钉钉: {result['name']} - {result['status']}")# 执行测试并使用回调tests = ["登录测试", "注册测试", "搜索测试"]execute_tests(tests, send_to_dingtalk)# ===== 技巧4:lambda 匿名函数(简单的一行函数)# 语法: lambda 参数: 表达式square = lambda x: x * xprint(f"5的平方: {square(5)}") # 25# 常用于排序test_cases = [ {"id": "TC003", "priority": "high"}, {"id": "TC001", "priority": "low"}, {"id": "TC002", "priority": "medium"},]# 按优先级排序priority_order = {"low": 1, "medium": 2, "high": 3}test_cases.sort(key=lambda x: priority_order[x["priority"]], reverse=True)print("\n按优先级排序后的用例:")for case in test_cases: print(f" {case['id']} - {case['priority']}")
参数类型对比
变量作用域对比
💡 给测试工程师的建议
1. DRY原则:Don't Repeat Yourself,重复代码超过3次就要封装成函数2. 单一职责:一个函数只做一件事,不要写"万能函数"3. 好命名:函数名用动词开头,见名知意,不用写注释也能懂4. 返回结构化数据:优先返回字典或对象,而不是零散的多个值5. 善用装饰器:日志、重试、计时这类通用逻辑用装饰器统一处理
函数是编程中最重要的概念之一,也是测试自动化的基础。掌握了函数的各种用法,你的代码将从"能跑"升级到"优雅、高效、可维护"。记住:好的函数像乐高积木,可以自由组合,构建出强大的测试框架!