跳到主要内容
预计阅读 34 分钟

04|函数封装:消除重复代码的工程实践

场景引入:报表生成中的重复逻辑

你正在开发一个月度销售报表生成工具。计算总销售额、平均订单金额、同比增长率——这些逻辑在日报、周报、月报中都会用到。如果每次都复制粘贴同一段代码,一旦计算公式需要调整,你就得在多个地方同时修改,遗漏一处就会导致数据不一致。

解决方案是函数:将一段逻辑封装起来,命名后可在任意位置调用。函数是代码复用的基本单元,也是构建大型程序的基石。

核心原则:定义一次,调用多次。封装一个 calc_growth_rate(prev, curr) 函数后,所有需要计算增长率的地方只需调用它。如果公式有误,修改一处即可全局生效。


内置函数速览

Python 提供了 70 多个内置函数,以下是开发中使用频率最高的:

# abs() —— 绝对值
print(abs(-42))          # 42

# max() / min() —— 极值
print(max(7, 3, 9, 1))  # 9
print(min(7, 3, 9, 1))  # 1

# len() —— 长度
print(len("automation")) # 10
print(len([10, 20, 30])) # 3

# round() —— 四舍五入
print(round(2.71828, 2)) # 2.72

# sum() —— 求和
print(sum([100, 200, 300])) # 600

# sorted() —— 排序(返回新列表)
print(sorted([4, 2, 7, 1]))            # [1, 2, 4, 7]
print(sorted([4, 2, 7], reverse=True))  # [7, 4, 2]

# type() —— 类型检查
print(type(3.14))        # <class 'float'>

类型转换函数:

print(int("256"))        # 256
print(int(9.7))          # 9(截断)
print(float(7))          # 7.0
print(str(256))          # "256"
print(bool(0))           # False
print(bool("content"))   # True

定义自定义函数

基本语法

def function_name(param1, param2, ...):
    """文档字符串(可选):描述函数的用途"""
    # 函数体
    return result

第一个自定义函数

def welcome(username):
    """向用户发送欢迎消息"""
    print(f"欢迎回来,{username}!系统已就绪。")

welcome("operator_a")    # 欢迎回来,operator_a!系统已就绪。
welcome("operator_b")    # 欢迎回来,operator_b!系统已就绪。
welcome("admin")         # 欢迎回来,admin!系统已就绪。

逻辑只写了一次,却能对不同用户重复使用。

带返回值的函数

def compute_tax(revenue, rate):
    """计算税额"""
    return revenue * rate

tax_amount = compute_tax(10000, 0.13)
print(tax_amount)           # 1300.0
print(compute_tax(5000, 0.06))  # 300.0

# 返回值可以参与后续运算
total_tax = compute_tax(10000, 0.13) + compute_tax(5000, 0.06)
print(total_tax)            # 1600.0

return 语句的两个作用:

  1. 将值传回给调用方
  2. 立即终止函数执行

没有 return 语句(或 return 后无值)的函数返回 None

def log_event(msg):
    print(f"[LOG] {msg}")

result = log_event("启动完成")   # 输出:[LOG] 启动完成
print(result)                    # None

返回多个值

Python 函数可以返回多个值(本质是返回元组):

def analyze_dataset(data):
    """返回数据集的最大值和最小值"""
    ceiling = max(data)
    floor = min(data)
    return ceiling, floor

hi, lo = analyze_dataset([23, 7, 45, 12, 38])
print(f"最大值:{hi},最小值:{lo}")
# 最大值:45,最小值:7

占位函数:pass

在规划代码结构时,可以用 pass 占位:

def export_report():
    pass    # 暂未实现

# 不会报错,后续再填充具体逻辑

函数参数详解

参数机制让函数具备高度灵活性。Python 支持多种参数类型。

位置参数

最基本的参数形式,按声明顺序传入:

def calc_power(base, exp):
    """计算 base 的 exp 次方"""
    result = 1
    while exp > 0:
        result *= base
        exp -= 1
    return result

print(calc_power(3, 4))   # 81
print(calc_power(10, 3))  # 1000

参数顺序很重要——calc_power(3, 4)calc_power(4, 3) 结果不同。

默认参数

给参数预设默认值,调用时可省略:

def calc_power(base, exp=2):
    """默认计算平方"""
    result = 1
    while exp > 0:
        result *= base
        exp -= 1
    return result

print(calc_power(7))       # 49(默认 exp=2)
print(calc_power(7, 3))   # 343(显式传入 exp=3)

**规则:**默认参数必须放在非默认参数之后。

def format_log(message, level="INFO"):
    print(f"[{level}] {message}")

format_log("服务启动")              # [INFO] 服务启动
format_log("连接超时", "WARNING")   # [WARNING] 连接超时

关键陷阱:默认值不要使用可变对象

# 错误做法
def append_record(record, history=[]):
    history.append(record)
    return history

print(append_record("log_1"))    # ['log_1']
print(append_record("log_2"))    # ['log_1', 'log_2']  ← 共享了同一个列表

# 正确做法
def append_record(record, history=None):
    if history is None:
        history = []
    history.append(record)
    return history

print(append_record("log_1"))    # ['log_1']
print(append_record("log_2"))    # ['log_2']

这是 Python 中一个经典的陷阱,务必牢记。

可变位置参数(*args)

不确定调用方会传入多少个参数时,使用 *args

def total_revenue(*amounts):
    """计算任意数量金额的总和"""
    running_sum = 0
    for val in amounts:
        running_sum += val
    return running_sum

print(total_revenue(100, 200))              # 300
print(total_revenue(100, 200, 300, 400))    # 1000
print(total_revenue())                       # 0

# 已有列表可用 * 解包传入
monthly = [1200, 1500, 1100, 1800]
print(total_revenue(*monthly))              # 5600

函数内部,amounts 是一个元组。

可变关键字参数(**kwargs)

使用 **kwargs 接收任意数量的关键字参数:

def create_profile(uid, display_name, **metadata):
    """创建用户档案"""
    print(f"用户ID:{uid}")
    print(f"显示名:{display_name}")
    for field, value in metadata.items():
        print(f"{field}{value}")
    print()

create_profile("u001", "运营组长")
create_profile("u002", "技术主管", dept="研发部", location="上海")

# 已有字典可用 ** 解包传入
extra = {"dept": "市场部", "rank": "P7"}
create_profile("u003", "市场经理", **extra)

函数内部,metadata 是一个字典。

参数组合顺序

多种参数可以组合使用,但顺序有固定要求:**位置参数 -> 默认参数 -> *args -> 关键字专用参数(keyword-only)-> kwargs

def dispatch(action, target, *extra_targets, retries=3, **options):
    print(f"action={action}, target={target}, retries={retries}")
    print(f"extra_targets={extra_targets}")
    print(f"options={options}")

dispatch("deploy", "server_1")
dispatch("deploy", "server_1", "server_2", "server_3", retries=5, region="east")

注意这里 retries=3 放在了 *extra_targets 之后。如果把默认参数放在 *args 之前,第三个位置参数会被赋给 retries 而非进入 extra_targets,容易造成混淆。将默认参数放在 *args 之后,调用方必须以关键字形式传入,语义更加明确。

实际开发中,最常用的组合是位置参数 + 默认参数*args**kwargs 主要用于设计需要高灵活性的接口。


变量作用域

变量的可访问范围取决于它在哪里被定义。

局部变量与全局变量

# 全局变量
app_version = "3.2.1"

def show_version():
    # 局部变量
    build_tag = "release"
    print(app_version)     # 可以读取全局变量
    print(build_tag)       # 可以读取局部变量

show_version()
print(app_version)         # 全局变量在函数外可访问
# print(build_tag)         # 报错:局部变量在函数外不可见

作用域规则:

  1. 函数外部定义的变量是全局变量
  2. 函数内部定义的变量是局部变量
  3. 函数内部可以读取全局变量
  4. 函数内部要修改全局变量,需用 global 声明
connection_count = 0

def new_connection():
    global connection_count
    connection_count += 1

new_connection()
new_connection()
print(connection_count)   # 2

**最佳实践:**尽量避免使用 global。函数应通过参数接收输入、通过返回值产出结果,不依赖外部状态。这样的函数更容易测试和复用。

# 不推荐
running_total = 0
def accumulate(val):
    global running_total
    running_total += val

# 推荐
def accumulate(current, val):
    return current + val

running_total = accumulate(running_total, 50)

同名变量隔离

config = "global_config"

def load():
    config = "local_config"    # 新的局部变量,不影响全局
    print(f"函数内:{config}")

load()                          # 函数内:local_config
print(f"函数外:{config}")      # 函数外:global_config

递归函数

函数在内部调用自身,称为递归。递归适合”大问题可分解为结构相同的小问题”的场景。

阶乘示例

n 的阶乘定义:n! = n * (n-1) * ... * 1

递归视角:n! = n * (n-1)!

def factorial(n):
    """计算 n 的阶乘"""
    if n <= 1:
        return 1                    # 基础条件(递归出口,0! = 1, 1! = 1)
    return n * factorial(n - 1)     # 递归步骤

print(factorial(6))    # 720
print(factorial(10))   # 3628800

递归的两个要素:

  1. 基础条件:终止递归的边界
  2. 递归步骤:将问题缩小为同结构的子问题

执行追踪 factorial(5)

factorial(5)
= 5 * factorial(4)
= 5 * 4 * factorial(3)
= 5 * 4 * 3 * factorial(2)
= 5 * 4 * 3 * 2 * factorial(1)
= 5 * 4 * 3 * 2 * 1
= 120

斐波那契数列

1, 1, 2, 3, 5, 8, 13, 21…(每项等于前两项之和)

def fib(n):
    """返回斐波那契数列第 n 项"""
    if n <= 2:
        return 1
    return fib(n - 1) + fib(n - 2)

for pos in range(1, 12):
    print(fib(pos), end=" ")
# 1 1 2 3 5 8 13 21 34 55 89

递归的局限与替代

Python 默认递归深度上限为 1000 层。超出会触发 RecursionError。对于需要深度递归的场景,建议改用循环:

def factorial_iter(n):
    product = 1
    for multiplier in range(1, n + 1):
        product *= multiplier
    return product

print(factorial_iter(1000))   # 正常运行

lambda 匿名函数

当需要一个简单的、一次性使用的小函数时,lambda 表达式比 def 更简洁:

基本语法

# lambda 参数: 表达式

# 等价于 def compute_margin(cost, revenue): return revenue - cost
compute_margin = lambda cost, revenue: revenue - cost
print(compute_margin(80, 120))   # 40

lambda 只能包含一个表达式,不支持复杂逻辑。

典型应用场景

排序:

products = [
    {"label": "键盘", "cost": 299},
    {"label": "鼠标", "cost": 79},
    {"label": "显示器", "cost": 1899},
]

# 按价格升序
products.sort(key=lambda p: p["cost"])
for p in products:
    print(f"{p['label']}: {p['cost']} 元")
# 鼠标: 79 元
# 键盘: 299 元
# 显示器: 1899 元

# 按价格降序
products.sort(key=lambda p: p["cost"], reverse=True)
# 按字符串长度排序
tags = ["deploy", "ci", "monitoring", "log"]
tags.sort(key=lambda t: len(t))
print(tags)   # ['ci', 'log', 'deploy', 'monitoring']
# 函数工厂
def make_converter(factor):
    """返回一个乘以 factor 的转换函数"""
    return lambda val: val * factor

km_to_miles = make_converter(0.621371)
kg_to_lbs = make_converter(2.20462)

print(km_to_miles(100))   # 62.1371
print(kg_to_lbs(75))      # 165.3465

常用高阶函数

map()——批量转换

readings = [1, 4, 9, 16, 25]
roots = list(map(lambda v: v ** 0.5, readings))
print(roots)   # [1.0, 2.0, 3.0, 4.0, 5.0]

# 批量类型转换
raw_values = ["10", "20", "30"]
numeric = list(map(int, raw_values))
print(numeric)   # [10, 20, 30]

filter()——条件筛选

signals = [12, -3, 7, -8, 15, 0, -1, 9]
positive_only = list(filter(lambda s: s > 0, signals))
print(positive_only)   # [12, 7, 15, 9]

# 过滤空字符串
tokens = ["GET", "", "POST", "", "PUT"]
valid_tokens = list(filter(None, tokens))
print(valid_tokens)   # ['GET', 'POST', 'PUT']

zip()——并行配对

departments = ["研发", "市场", "运维"]
headcounts = [45, 22, 18]

for dept, count in zip(departments, headcounts):
    print(f"{dept}部:{count} 人")

# 快速构建字典
dept_map = dict(zip(departments, headcounts))
print(dept_map)   # {'研发': 45, '市场': 22, '运维': 18}

enumerate()——带序号遍历

milestones = ["需求评审", "开发完成", "测试通过", "正式发布"]
for seq, milestone in enumerate(milestones, start=1):
    print(f"阶段 {seq}{milestone}")

综合实战:单位换算工具箱

def convert_temperature(value, direction):
    if direction == "c2f":
        return value * 9 / 5 + 32
    elif direction == "f2c":
        return (value - 32) * 5 / 9
    else:
        return "不支持的转换方向"

def convert_distance(value, direction):
    if direction == "km2mi":
        return value * 0.621371
    elif direction == "mi2km":
        return value / 0.621371
    else:
        return "不支持的转换方向"

def convert_weight(value, direction):
    if direction == "kg2lb":
        return value * 2.20462
    elif direction == "lb2kg":
        return value / 2.20462
    else:
        return "不支持的转换方向"

def unit_converter():
    """单位换算主程序"""
    categories = {
        "1": ("温度", convert_temperature, [("c2f", "摄氏→华氏"), ("f2c", "华氏→摄氏")]),
        "2": ("距离", convert_distance, [("km2mi", "千米→英里"), ("mi2km", "英里→千米")]),
        "3": ("重量", convert_weight, [("kg2lb", "千克→磅"), ("lb2kg", "磅→千克")]),
    }

    print("====== 单位换算工具 ======")

    while True:
        print("\n选择类别:")
        for key, (name, _, _) in categories.items():
            print(f"  {key}. {name}")
        print("  q. 退出")

        choice = input("\n你的选择:")

        if choice == "q":
            print("再见!")
            break

        if choice not in categories:
            print("无效选择")
            continue

        cat_name, converter, directions = categories[choice]
        print(f"\n{cat_name}转换方向:")
        for idx, (code, desc) in enumerate(directions, 1):
            print(f"  {idx}. {desc}")

        dir_choice = int(input("选择方向:")) - 1
        if dir_choice < 0 or dir_choice >= len(directions):
            print("无效选择")
            continue

        direction_code = directions[dir_choice][0]

        try:
            input_val = float(input("输入数值:"))
        except ValueError:
            print("请输入有效数字")
            continue

        output_val = converter(input_val, direction_code)
        print(f"\n结果:{input_val}{output_val:.4f}")

unit_converter()

本章回顾

要点内容
内置函数abs() max() len() sum() sorted()
定义函数def name(params): body
return返回值并终止函数,支持多返回值
位置参数按声明顺序传入
默认参数带默认值,调用时可省略
*args接收任意数量位置参数(元组)
**kwargs接收任意数量关键字参数(字典)
作用域局部变量 vs 全局变量,global 声明
递归函数调用自身,须有基础条件
lambda匿名函数:lambda params: expr
高阶函数map() filter() zip() enumerate()

补充:文档字符串与函数帮助

Python 的文档字符串(docstring)不只是注释——它会被内置的 help() 函数读取并展示:

def calc_growth_rate(previous, current):
    """计算同比增长率。

    Args:
        previous: 上一期数值
        current: 当前期数值

    Returns:
        增长率(浮点数),例如 0.15 表示增长 15%
    """
    if previous == 0:
        return float('inf')
    return (current - previous) / previous

# 查看函数文档
help(calc_growth_rate)

养成编写 docstring 的习惯,不仅方便团队协作,也是专业代码的标志。

补充:函数作为一等公民

在 Python 中,函数是”一等公民”——它们可以赋值给变量、存入列表或字典、作为参数传递给其他函数:

def greet_cn(name):
    return f"你好,{name}"

def greet_en(name):
    return f"Hello, {name}"

# 函数赋值给变量
current_greeter = greet_cn
print(current_greeter("运维组"))    # 你好,运维组

# 函数存入字典
greetings = {"cn": greet_cn, "en": greet_en}
lang = "en"
print(greetings[lang]("Team"))      # Hello, Team

这种特性是 Python 强大灵活性的来源之一,也是理解后续装饰器、回调函数等高级概念的基础。

实践任务

  1. 编写 is_palindrome(text) 函数,判断字符串是否为回文
  2. 编写 convert_temp(celsius) 函数,将摄氏度转为华氏度
  3. 用递归实现斐波那契数列第 n 项的计算
  4. 编写 word_count(paragraph) 函数,统计文本中的单词数
  5. 使用 lambda 和 sorted() 对商品列表按价格降序排列
  6. 编写 safe_div(numerator, denominator, fallback=0) 函数,除以零时返回 fallback 值

下一章,我们将从一个数据管理项目出发,深入掌握列表、字典、元组和集合——Python 中组织和处理批量数据的核心工具。

购买课程解锁全部内容

零基础到独立开发:Python 自动化与 Web 实战

¥29.90