Files

11 KiB
Raw Permalink Blame History

doc_type, full_text
doc_type full_text
short sources/02_More_functions.md

02_More_functions 总结

本文深入讲解 Python 函数的调用方式、默认参数、返回值、作用域、参数传递语义,并通过一系列练习逐步构建一个通用的 CSV 文件解析函数 parse_csv()。它承接前文脚本化程序的基础,为后续错误检查和更健壮的数据处理做准备。

函数调用方式

Python 函数可以用两种主要方式调用:

  • 位置参数:按定义顺序传入参数。
  • 关键字参数:显式指定参数名,提高可读性。

示例:

def read_prices(filename, debug):
    ...

prices = read_prices('prices.csv', True)
prices = read_prices(filename='prices.csv', debug=True)

对于布尔开关、调试选项、可选行为等参数,推荐使用关键字参数,因为:

parse_data(data, False, True)

难以理解,而下面的形式更清晰:

parse_data(data, ignore_errors=True)
parse_data(data, debug=True)
parse_data(data, debug=True, ignore_errors=True)

相关概念:Python函数设计、关键字参数

默认参数

函数参数可以指定默认值,使其成为可选参数:

def read_prices(filename, debug=False):
    ...

调用时可以省略默认参数:

d = read_prices('prices.csv')
e = read_prices('prices.dat', True)

注意:带默认值的参数必须放在参数列表末尾,也就是所有必需参数应先出现。

参数命名与 API 设计

函数参数名应短小但有意义。原因包括:

  • 调用者可能使用关键字参数调用函数。
  • 开发工具和帮助文档会显示参数名。
  • 好的参数名能让函数接口更自解释。

例如:

d = read_prices('prices.csv', debug=True)

这里 debug 明确表达了参数用途。

返回值

return 语句用于从函数返回值:

def square(x):
    return x * x

如果函数没有显式返回值,或仅写 return 而不带表达式,则返回 None:

def bar(x):
    statements
    return

 def foo(x):
    statements

这两个函数调用结果都会是 None。

多个返回值

Python 函数实际只能返回一个对象,但可以返回一个元组,从而实现“多个返回值”的效果:

def divide(a, b):
    q = a // b
    r = a % b
    return q, r

调用时可以解包:

x, y = divide(37, 5)   # x = 7, y = 2

也可以作为一个元组接收:

x = divide(37, 5)      # x = (7, 2)

相关概念:元组解包、Python返回值

变量作用域

Python 中变量根据定义位置分为:

  • 全局变量:定义在函数外部。
  • 局部变量:定义在函数内部。
x = value  # 全局变量

def foo():
    y = value  # 局部变量

局部变量

函数内部赋值产生的变量是局部变量,只在函数调用期间存在,调用结束后不可访问。

示例:

def read_portfolio(filename):
    portfolio = []
    for line in open(filename):
        fields = line.split(',')
        s = (fields[0], int(fields[1]), float(fields[2]))
        portfolio.append(s)
    return portfolio

其中 filename、portfolio、line、fields、s 都是局部变量。

函数调用结束后,外部不能访问 fields:

>>> fields
NameError: name 'fields' is not defined

局部变量也不会与函数外部的同名变量冲突。

全局变量

函数可以读取同一文件中的全局变量:

name = 'Dave'

def greeting():
    print('Hello', name)

但函数内部的赋值默认会创建局部变量,而不是修改全局变量:

name = 'Dave'

def spam():
    name = 'Guido'

spam()
print(name)  # Dave

核心规则:函数中的所有赋值默认都是局部赋值。

修改全局变量

如果必须修改全局变量,需要使用 global 声明:

name = 'Dave'

def spam():
    global name
    name = 'Guido'

但文中强调应尽量避免 global。如果函数需要修改外部状态,更好的设计通常是使用类来封装状态。

相关概念:Python作用域、全局变量、状态管理

参数传递:引用而非复制

调用函数时,参数名会绑定到传入对象上。传入的值不会被复制。

如果传入的是可变对象,例如列表或字典,函数可以原地修改它:

def foo(items):
    items.append(42)

a = [1, 2, 3]
foo(a)
print(a)  # [1, 2, 3, 42]

关键点:函数不会自动获得输入参数的副本。

修改对象 vs 重新绑定变量

文中强调了一个重要区别:

  • 修改对象:影响调用者持有的同一个对象。
  • 重新赋值变量名:只改变局部变量绑定,不影响外部变量。

修改对象:

def foo(items):
    items.append(42)

a = [1, 2, 3]
foo(a)
print(a)  # [1, 2, 3, 42]

重新绑定局部变量:

def bar(items):
    items = [4, 5, 6]

b = [1, 2, 3]
bar(b)
print(b)  # [1, 2, 3]

变量赋值不会覆盖内存,而是让名字绑定到新的对象。

相关概念:concepts/Python-可变对象、concepts/变量绑定、concepts/Python-参数传递

练习目标:构建通用 CSV 解析函数

练习部分围绕 Work/fileparse.py 展开,目标是把前面 read_portfolio() 和 read_prices() 中重复的底层 CSV 处理逻辑抽象成一个通用函数 parse_csv()。

该函数最终支持:

  • 打开 CSV 文件。
  • 使用 csv.reader() 读取内容。
  • 跳过空行。
  • 将带表头的 CSV 转换为字典列表。
  • 选择指定列。
  • 对字段执行类型转换。
  • 支持无表头文件。
  • 支持自定义分隔符。

相关概念:CSV解析、数据清洗、函数抽象

Exercise 3.3:读取 CSV 为字典列表

初始版本的 parse_csv() 将 CSV 文件解析成字典列表:

import csv

def parse_csv(filename):
    '''
    Parse a CSV file into a list of records
    '''
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)
        records = []
        for row in rows:
            if not row:
                continue
            record = dict(zip(headers, row))
            records.append(record)
    return records

核心机制:

  • 第一行作为表头。
  • 每一行数据与表头通过 zip(headers, row) 配对。
  • 使用 dict() 转换成字典。
  • 所有记录组成列表返回。

示例结果:

[{'price': '32.20', 'name': 'AA', 'shares': '100'}, ...]

此时所有字段仍是字符串,尚不能直接用于数值计算。

Exercise 3.4:选择指定列

接着扩展 parse_csv(),增加 select 可选参数,用于只读取部分列:

shares_held = parse_csv('Data/portfolio.csv', select=['name', 'shares'])

关键步骤是把列名映射为列索引:

indices = [headers.index(colname) for colname in select]

例如:

headers = ['name', 'date', 'time', 'shares', 'price']
select = ['name', 'shares']
indices = [0, 3]

读取每行后,用索引过滤字段:

row = [row[index] for index in indices]

该练习体现了列表推导式、索引映射和数据投影的组合应用。

相关概念:concepts/列表推导式、列选择、数据投影

Exercise 3.5:执行类型转换

进一步扩展 parse_csv(),增加 types 参数,用于对字段执行类型转换:

portfolio = parse_csv('Data/portfolio.csv', types=[str, int, float])

转换逻辑:

if types:
    row = [func(val) for func, val in zip(types, row)]

这里 types 是一组可调用对象,例如 str、int、float。它们与行中的字段一一配对,并把字符串转换成所需类型。

示例:

{'price': 32.2, 'name': 'AA', 'shares': 100}

这一节展示了函数对象也可以作为数据传递。

相关概念:类型转换、高阶函数、可调用对象

Exercise 3.6:处理无表头 CSV 文件

有些 CSV 文件没有表头,例如价格文件:

"AA",9.22
"AXP",24.85
"BA",44.85

此时无法构造字典,因为没有列名作为键。因此 parse_csv() 需要支持 has_headers=False,并返回元组列表:

prices = parse_csv('Data/prices.csv', types=[str, float], has_headers=False)

结果形式:

[('AA', 9.22), ('AXP', 24.85), ('BA', 44.85), ...]

这一扩展要求函数根据是否存在表头改变解析策略:

  • 有表头:返回字典列表。
  • 无表头:返回元组列表。

Exercise 3.7:支持不同分隔符

虽然 CSV 常用逗号分隔,但实际数据也可能使用空格、制表符等分隔符。

例如 portfolio.dat 使用空格分隔:

name shares price
"AA" 100 32.20
"IBM" 50 91.10

csv.reader() 可以通过 delimiter 参数指定分隔符:

rows = csv.reader(f, delimiter=' ')

于是 parse_csv() 增加 delimiter 参数:

portfolio = parse_csv(
    'Data/portfolio.dat',
    types=[str, int, float],
    delimiter=' '
)

这使函数可以处理更广泛的结构化文本数据。

相关概念:文件解析、CSV模块、可配置接口

最终函数的设计意义

经过这些练习,parse_csv() 从一个简单函数逐步演化为一个可复用的数据解析工具。它体现了多个重要编程思想:

  1. 隐藏低层细节:调用者不需要关心文件打开、CSV 包装、跳过空行等细节。
  2. 通过可选参数增强通用性:select、types、has_headers、delimiter 让函数适应多种场景。
  3. 用关键字参数提升可读性:例如 has_headers=False、delimiter=' '。
  4. 组合已有概念解决实际问题:字典、元组、列表推导式、函数对象、文件读取等概念被整合到一个实用函数中。
  5. 形成小型库函数:该函数可以被其他程序导入和复用。

关键知识点汇总

  • 函数可以通过位置参数或关键字参数调用。
  • 可选参数应使用默认值,并通常放在参数列表末尾。
  • 布尔标志和可选功能推荐使用关键字参数调用。
  • 函数无显式返回值时返回 None。
  • 多返回值本质上是返回元组。
  • 函数内部赋值默认创建局部变量。
  • 函数可以读取全局变量,但修改全局变量需要 global。
  • 应尽量避免 global,改用更好的状态管理方式。
  • 函数参数传递的是对象引用,不是对象副本。
  • 修改可变对象会影响调用者;重新绑定局部变量不会。
  • 通用函数可以通过可选参数逐步增强能力。
  • parse_csv() 是函数抽象、数据解析和接口设计的综合练习。

可延伸的概念页面

  • Python函数设计
  • 关键字参数
  • Python作用域
  • Python参数传递
  • concepts/变量绑定
  • 可变对象
  • CSV解析
  • 类型转换
  • 函数抽象
  • 可配置接口