Python 编程教程:用函数和循环处理一组数据

资讯10小时前发布 jdkjadmin
0 00
Python 编程教程:用函数和循环处理一组数据

Python 编程教程:用函数和循环处理一组数据

处理一组数据,是 Python 程序中最常见的任务之一:计算一批成绩的平均值、筛出符合条件的订单、统计日志中某类事件的数量,或者把一组原始记录转换为统一格式。数据可能来自列表、文件、接口或数据库,但很多处理步骤都可以归纳为两件事:用循环逐项检查数据,用函数把步骤组织起来。

本文从一个简单的数字列表开始,逐步介绍如何编写可复用的数据处理函数、处理空数据和异常值,并说明容易忽略的边界与安全问题。示例仅使用 Python 标准语法,可以复制到 Python 3 环境运行。

一、什么时候适合用函数和循环

当一项操作需要对多条记录重复执行时,循环可以避免逐项手写;当操作有明确的输入、处理规则和结果时,函数可以把逻辑集中起来,方便复用和测试。

  • 逐项转换:把温度从摄氏度转换为华氏度,或给每条记录增加计算字段。
  • 筛选记录:只保留金额大于指定值的订单,或只处理状态为“已完成”的任务。
  • 汇总统计:计算总和、数量、平均值、最大值等。
  • 校验数据:检查每条记录是否缺少必需字段,或字段值是否符合预期范围。

如果只是非常简单的一次性表达式,循环不一定比列表推导式更清晰;如果处理过程需要多步判断、记录错误或详细注释,普通 for 循环通常更容易读。选择写法时,应优先考虑可读性和边界行为,而不是追求代码最短。

二、先从一个可运行的统计函数开始

下面的函数接收一组数值,返回数据项数、总和和平均值。它不会在函数内部打印结果,而是通过 return 把结果交给调用者。

def summarize(numbers):
    count = 0
    total = 0

    for number in numbers:
        count += 1
        total += number

    if count == 0:
        average = None
    else:
        average = total / count

    return {
        "count": count,
        "total": total,
        "average": average,
    }


scores = [82, 91, 76, 88]
result = summarize(scores)

print(result)
# {'count': 4, 'total': 337, 'average': 84.25}

函数定义从 def 开始,名称后的圆括号中声明参数。调用 summarize(scores) 时,列表被传入参数 numbers。for number in numbers 会依次取出其中的元素。每处理一个元素,计数器 count 加一,累计值 total 加上当前数值。

return 会结束当前函数并返回结果。字典适合用来返回一组有名称的统计值;调用方可以通过 result["average"] 读取平均值。如果函数只负责打印,其他代码就不容易继续利用计算结果,因此可复用的处理函数通常应返回数据,把展示工作留给调用方。

三、函数和循环各自负责什么

函数适合表达一个有明确目的的操作,循环适合重复执行操作。把两者组合起来,可以得到结构清晰的处理流程。

def double_positive_values(values):
    result = []

    for value in values:
        if value > 0:
            result.append(value * 2)

    return result


numbers = [-3, 0, 4, 7]
print(double_positive_values(numbers))
# [8, 14]

这个函数的职责可以用一句话说明:保留大于零的数,并将它们乘以二。它创建了一个新列表 result,逐项检查输入,并把符合条件的转换结果加入新列表。原列表没有被修改。

函数并不要求处理列表。许多可迭代对象都可以放进 for 循环,例如元组、集合、字典和文件对象。具体能否传入某种对象,取决于函数内部对它做了什么。例如,循环遍历字典默认得到的是键;若要逐项读取键和值,应使用 mapping.items()。

四、用函数筛选记录并计算结果

实际数据往往不是一串数字,而是由字典组成的列表。下面以订单为例,只统计状态为 "paid" 的订单,并计算已付款总额。

def paid_order_total(orders):
    total = 0

    for order in orders:
        if order.get("status") != "paid":
            continue

        amount = order.get("amount")
        if not isinstance(amount, (int, float)):
            raise ValueError("已付款订单的 amount 必须是数字")

        total += amount

    return total


orders = [
    {"id": "A101", "status": "paid", "amount": 120.5},
    {"id": "A102", "status": "pending", "amount": 80},
    {"id": "A103", "status": "paid", "amount": 45},
]

print(paid_order_total(orders))
# 165.5

order.get("status") 在键不存在时返回 None,因此这条记录会被当作非已付款订单跳过。遇到不符合条件的项目时,continue 会直接进入下一轮循环。

示例用 isinstance 检查金额类型,并在不符合约定时抛出 ValueError。这能避免把字符串等意外值悄悄加进结果。不过,bool 是 Python 中 int 的子类,因此 isinstance(True, int) 的结果为真。如果业务规则必须拒绝布尔值,应另加检查:

if isinstance(amount, bool) or not isinstance(amount, (int, float)):
    raise ValueError("amount 必须是数字,且不能是布尔值")

对于金额等财务数据,直接使用二进制浮点数可能出现精度问题。例如,某些十进制小数不能被浮点数精确表示。需要精确十进制计算时,应考虑标准库的 decimal.Decimal,并统一数据转换和舍入规则;不要仅凭示例中的 float 写法推断它适用于所有金额场景。

五、处理空数据、缺失值和异常输入

空数据不是罕见情况。列表可能为空,文件可能没有记录,筛选条件也可能导致一条结果都不剩。若计算平均值,必须决定空集合时应该怎么办。上面的 summarize 返回 None 作为平均值,表达“没有可计算的平均值”,而不是除以零。

另一种做法是把“无数据”视为错误并抛出异常。选择哪一种取决于函数的用途:报告型统计可以返回空结果;要求必须有记录的业务步骤,则可以明确报错。关键是不要让调用者猜测。

def average(values):
    total = 0
    count = 0

    for value in values:
        total += value
        count += 1

    if count == 0:
        raise ValueError("至少需要一个数值")

    return total / count


print(average([10, 20, 30]))
# 20.0

还要区分“字段不存在”“字段值为 None”和“字段值是空字符串”。它们可能代表不同的数据问题。dict.get 返回默认值的行为很方便,但如果需要区分键是否存在,可以用 if "amount" not in order 检查。

异常处理应放在能够做出合理决定的位置。若函数无法继续处理非法数据,可以抛出异常,让调用方决定是停止、记录还是跳过。不要为了让程序“不报错”而无条件捕获所有异常并忽略,否则数据问题可能被掩盖。

六、一次遍历处理多个统计指标

如果需要计算总和、计数和最大值,可以在一次循环中完成。下面的函数还会正确处理空输入:总和为零、数量为零,最大值为 None。

def basic_stats(values):
    count = 0
    total = 0
    maximum = None

    for value in values:
        count += 1
        total += value

        if maximum is None or value > maximum:
            maximum = value

    return {
        "count": count,
        "total": total,
        "maximum": maximum,
    }


print(basic_stats([4, 9, -2]))
# {'count': 3, 'total': 11, 'maximum': 9}

print(basic_stats([]))
# {'count': 0, 'total': 0, 'maximum': None}

这种写法只遍历输入一次,适合输入较大或只能读取一次的数据源。若把计算拆成多个函数,每个函数分别遍历输入,代码可能更容易复用,但重复遍历有成本,而且对生成器等一次性迭代对象并不适用。是否合并处理步骤,应根据数据规模、输入类型和代码清晰度决定。

七、参数设计:接受什么,保证什么

函数参数是调用方和函数实现之间的约定。一个可靠的函数应尽量说明输入要求和异常行为,例如:参数应是可迭代对象,其中每项必须是整数或浮点数;为空时返回什么;遇到非法项是报错还是跳过。

Python 不会仅凭普通函数定义自动验证参数类型。以下定义中的 values 只是参数名称,并不会强制调用者传入列表:

def count_items(values):
    count = 0
    for _ in values:
        count += 1
    return count

它可以处理列表、元组和许多其他可迭代对象,但传入整数时会产生 TypeError。若需要更明确的错误信息,可以在函数开头检查输入;但运行时检查不必无条件堆叠,应围绕真实的调用边界和业务约束设计。

若函数需要多次遍历输入,最好明确要求可重复遍历的数据,或在函数内部把输入保存下来。直接把任意可迭代对象当作列表使用,会遇到生成器已被消费的问题。另一个常见限制是:没有默认值的参数必须提供;有默认值的参数可以省略,但可变对象不适合作为共享默认值。

八、常见错误及其原因

1. 使用可变对象作为默认参数

默认参数在函数定义时创建,而不是每次调用时重新创建。下面的列表会在多次调用之间共享:

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"))
print(add_item("b"))
# 第二次调用会得到 ['a', 'b']

如果希望每次默认使用新列表,应使用 None 作为默认值,并在函数内部创建列表:

def add_item(item, items=None):
    if items is None:
        items = []

    items.append(item)
    return items

2. 把循环变量当作循环外的固定值

循环会更新循环变量。若在循环结束后使用该变量,要确认循环至少运行过一次;空输入时,变量可能根本没有定义。需要在循环外使用的结果,应在循环前初始化,并明确定义空输入时的行为。

3. 一边遍历列表一边删除元素

在循环中直接修改正在遍历的列表,可能导致元素被跳过。较安全的做法是创建新列表,或遍历原列表的副本:

values = [1, 2, 3, 4, 5]
remaining = []

for value in values:
    if value % 2 != 0:
        remaining.append(value)

print(remaining)
# [1, 3, 5]

4. 忘记 return 或把 print 当作返回值

没有执行 return 的函数会返回 None。print(result) 只是把内容显示到终端,并不会把结果交给调用方。需要后续计算时,返回结果;需要输出时,由调用方决定如何打印或写入文件。

5. 混淆真值判断和空值判断

if not value 会把 None、0、空字符串、空列表等都视为假。如果只想判断是否为 None,应写 value is None。否则,合法的零值可能被误当作缺失值。

6. 对浮点数使用精确相等比较

浮点数计算可能产生微小误差。比较计算结果时,通常应结合业务允许的误差使用容差,例如标准库中的 math.isclose。但不能用一个随意的容差掩盖精度或单位问题。

九、处理生成器和一次性数据源

循环可以遍历生成器。生成器通常按需产生数据,不必一次性把所有元素放进内存,但它一般只能从头到尾消费一次。以下函数只遍历一次,因此可以接收生成器:

def sum_values(values):
    total = 0

    for value in values:
        total += value

    return total


numbers = (number * 2 for number in range(5))
print(sum_values(numbers))
# 20

如果先遍历生成器计算数量,再遍历一次计算总和,第二次遍历可能拿不到任何数据。对需要多次使用的数据,可以在合适时机执行 list(values) 保存下来;代价是需要占用与数据量相关的内存。数据规模很大时,应优先设计单次遍历,或使用适合数据源的分块处理方式。

十、数据处理中的安全与可靠性

对输入数据做类型和范围检查,能提高程序可靠性,但不等于获得完整的安全保障。校验规则必须符合业务语义;例如,金额字段即使是数字,也可能超出业务允许范围。对来自用户、文件或网络的数据,还应根据来源和用途考虑大小限制、编码、格式验证、权限和错误记录。

不要为了把字符串“解析成 Python 对象”而对不可信输入调用 eval。eval 会执行 Python 表达式,输入内容可能触发不期望的代码执行。需要读取 JSON 格式时,应使用标准库 json 的解析接口,并在解析后验证数据结构和字段值。解析器解决的是格式读取问题,不会自动证明数据符合业务规则。

同样,不要把日志、用户输入或数据库字段直接拼接进 SQL 命令、Shell 命令或 HTML。不同输出场景需要使用各自适当的参数化接口或转义机制。本文讨论的函数和循环本身并不构成安全边界;授权检查、输入验证和安全输出应由应用程序按具体上下文负责。

处理大批量数据时,循环本身通常不是唯一的性能瓶颈。先确认是否存在不必要的重复遍历、重复转换和过大的中间列表;再用实际数据进行测量。不要仅凭数据量看起来很大,就假设某种写法一定更快。对于表格数据、数值数组等专门场景,也可以评估相应的库,但应先理解数据类型、缺失值和精度规则。

十一、把处理步骤拆成可测试的小函数

函数有助于把业务规则拆开。例如,一个函数负责判断订单是否有效,另一个负责汇总金额。这样可以分别检查边界行为,也避免把解析、验证、计算和输出混成一个很长的循环。

def is_paid_order(order):
    return order.get("status") == "paid"


def total_paid_amount(orders):
    total = 0

    for order in orders:
        if not is_paid_order(order):
            continue

        amount = order.get("amount")
        if isinstance(amount, bool) or not isinstance(amount, (int, float)):
            raise ValueError("已付款订单的 amount 必须是数值")

        total += amount

    return total


sample_orders = [
    {"status": "paid", "amount": 12.5},
    {"status": "pending", "amount": 30},
    {"status": "paid", "amount": 7},
]

assert is_paid_order(sample_orders[0]) is True
assert total_paid_amount(sample_orders) == 19.5
assert total_paid_amount([]) == 0

assert 可以用于开发阶段检查程序假设,也适合展示简单示例。它不应替代面向外部输入的正式校验;Python 在优化运行模式下可以关闭断言,因此不能把安全或关键业务检查只写在 assert 中。

测试时至少覆盖正常输入、空输入、只有一项的数据、边界值和非法类型。若函数规定非法数据应报错,也应测试这一行为。测试用例能帮助确认函数的输入约定没有在修改过程中被意外改变。

十二、实践检查清单

  • 先写清函数输入是什么,以及返回值和异常分别代表什么。
  • 明确空输入、缺失字段、非法类型和边界值的处理规则。
  • 循环前初始化累计变量;需要时记录数量,避免对零做除法。
  • 尽量避免在遍历集合时直接修改同一个集合。
  • 区分打印结果与返回结果;返回值通常更便于复用和测试。
  • 不要使用共享的可变默认参数,也不要对不可信输入使用 eval。
  • 了解输入能否重复遍历;生成器通常只能消费一次。
  • 对浮点精度、金额舍入和异常值制定明确规则。

总结

用函数和循环处理一组数据,核心不是记住某个固定模板,而是把每一步写成清楚的约定:函数接收什么,循环如何逐项处理,筛选规则是什么,结果如何返回,空数据和异常输入怎么办。对于简单任务,少量代码就足够;对于包含多条业务规则的处理流程,应拆分职责并用测试验证边界。

当数据来源变得复杂时,还要考虑迭代对象是否只能使用一次、是否需要控制内存,以及输入校验和安全输出是否符合具体场景。把这些问题提前说明,代码就不仅能在示例数据上运行,也更容易在真实项目中维护。

© 版权声明

相关文章