40 KiB
brief, sources
| brief | sources | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 函数是 Python 中封装计算、设计接口并传递行为的基本构件。 |
|
函数
函数是 Python 中组织程序、复用代码、表达计算过程和设计程序接口的基本单位。它把一组语句封装成一个可调用的操作,使脚本不再只是从上到下堆叠语句,而是由职责清晰、可以组合、测试和复用的构件组成。
函数不仅涉及 def、参数和 return,还涉及调用约定、默认参数、关键字参数、作用域、可变对象、异常处理、文件解析、程序结构、库接口设计,以及更高级的函数作为对象、回调函数和匿名函数 lambda。一个好的函数不仅要完成计算,还应当以清楚、灵活、可预测的方式暴露能力。相关主题包括 python functions、python scripts、script to function、code reuse、Python函数设计、函数抽象、高阶函数 和 lambda匿名函数。
学习目标
学习函数后,应能够:
- 使用
def定义自定义函数。 - 理解参数、函数体、返回值和
return的作用。 - 区分位置参数和关键字参数。
- 使用默认参数设计可选行为。
- 为可选参数和布尔标志优先使用关键字调用,提高可读性。
- 理解函数无显式返回值时会返回
None。 - 使用元组实现多个返回值,并理解解包赋值。
- 理解局部变量、全局变量和
global的作用。 - 理解函数参数传递的是对象引用,而不是对象副本。
- 区分修改可变对象与重新绑定局部变量。
- 理解名称必须先定义后使用,以及函数定义顺序和调用顺序的关系。
- 使用文档字符串为函数添加说明。
- 使用可选类型注解表达参数和返回值的预期类型。
- 将简单脚本重构为可复用函数。
- 编写顶层函数来统一执行一个程序的完整流程。
- 在交互模式中调用函数进行测试和调试。
- 理解函数如何通过异常报告错误,并用
try-except处理异常。 - 使用标准库函数和模块减少重复实现。
- 将函数用于真实脚本,例如读取文件、处理 CSV 数据、计算结果和打印报表。
- 设计更灵活的函数接口,例如接收文件类对象或 可迭代对象,而不是只接收文件名。
- 理解 concepts/鸭子类型 在函数接口设计中的价值和风险。
- 理解函数可以作为对象传递给其他函数,例如作为
sort()的key函数。 - 使用
lambda定义简短的一次性匿名函数。 - 理解回调函数、匿名函数和 高阶函数 之间的关系。
前置知识
学习函数前,建议已经理解:
- Python 基本语法:变量、表达式、缩进和语句块。参见 summaries/01_Python。
- 如何运行 Python 程序和使用交互式解释器。参见 summaries/02_Hello_world。
- 文件读取、逐行处理和字符串拆分。参见 summaries/06_Files。
- 基本类型转换,例如
int()、float()。 - 简单控制流,例如
if、while、for。 - 列表、字典、元组和可变对象。参见 summaries/02_Containers。
- 脚本的基本形式:文件中包含一系列按顺序执行的语句。参见 python scripts。
核心解释
什么是函数
函数是一组执行特定任务的语句。它通常接收输入,执行计算或操作,然后返回结果。
def function_name(arguments):
statements
return result
例如:
def sumcount(n):
'''
Returns the sum of the first n integers
'''
total = 0
while n > 0:
total += n
n -= 1
return total
调用函数:
a = sumcount(100)
这里:
def用于定义函数。sumcount是函数名。n是参数。- 函数体是一组缩进语句。
return total指定函数返回值。a = sumcount(100)调用函数,并把返回结果赋给变量a。
函数本质上是带名字的一系列语句。Python 中几乎任何语句都可以写在函数内部,例如 import、print()、help()、循环、条件判断、异常处理等。
def foo():
import math
print(math.sqrt(2))
help(math)
相关主题:python functions、code reuse。
函数调用:位置参数与关键字参数
函数可以通过位置参数调用,也可以通过关键字参数调用。
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)
因此,设计函数时应给参数起短小但有意义的名字。调用者可能会使用关键字参数,IDE、帮助系统和文档工具也会显示这些名字。相关主题:关键字参数、可配置接口。
默认参数与可选行为
如果希望某个参数可选,可以在函数定义中给它默认值:
def read_prices(filename, debug=False):
...
调用时可以省略该参数:
d = read_prices('prices.csv')
e = read_prices('prices.dat', True)
带默认值的参数必须放在参数列表末尾,即所有必需参数应先出现,所有可选参数放后面。
默认参数常用于让一个函数支持更多场景,同时保持简单调用。例如 CSV 解析函数可以设计为:
def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=','):
...
这样,最常见的调用很短:
records = parse_csv('Data/portfolio.csv')
需要额外行为时再使用关键字参数:
records = parse_csv('Data/portfolio.csv', select=['name', 'shares'], types=[str, int])
prices = parse_csv('Data/prices.csv', types=[str, float], has_headers=False)
相关主题:Python函数设计、函数抽象。
参数与返回值
函数的参数是外部传入函数的数据。返回值是函数计算后交还给调用者的数据。
def portfolio_cost(filename):
...
return total_cost
这个函数的输入是 filename,输出是投资组合的总成本。
return 语句用于返回值:
def square(x):
return x * x
如果没有显式 return,或者只写 return 而不带表达式,Python 默认返回 None。
def add(a, b):
total = a + b
x = add(2, 3) # x 是 None
因此,在处理计算任务时,应尽量明确使用 return,让函数结果清楚可见。
多个返回值
Python 函数实际上只能返回一个对象,但这个对象可以是元组。因此,常见的多个返回值本质上是返回一个元组。
def divide(a, b):
q = a // b
r = a % b
return q, r
调用时可以解包:
x, y = divide(37, 5) # x = 7, y = 2
也可以作为一个元组接收:
result = divide(37, 5) # result = (7, 2)
相关主题:元组解包、Python返回值。
函数也是对象
Python 中函数不仅是一段可执行代码,也是一种对象。函数名可以绑定到函数对象,函数对象可以赋给变量、放入数据结构、作为参数传给另一个函数,甚至作为返回值从函数中返回。
这使 Python 函数不仅能表达计算,还能表达行为。例如,在通用 CSV 解析函数中,可以把类型转换函数作为参数传入:
row = [func(val) for func, val in zip(types, row)]
这里 types 可能是:
types = [str, int, float]
str、int 和 float 都是可调用对象,它们被当作数据放在列表中,再由解析函数逐个调用。这体现了 concepts/函数作为对象、可调用对象 和 高阶函数 的思想。
回调函数
当一个函数被传给另一个函数,并由后者在执行过程中调用时,这个被传入的函数通常称为回调函数。回调函数常用于把一段可变行为交给通用函数。
排序是典型例子。普通数值列表可以直接排序:
s = [10, 1, 7, 3]
s.sort()
# [1, 3, 7, 10]
也可以降序排序:
s.sort(reverse=True)
# [10, 7, 3, 1]
但如果列表元素是字典或对象,Python 不一定知道应该按哪个字段排序。此时可以传入 key 函数:
def stock_name(s):
return s['name']
portfolio.sort(key=stock_name)
sort() 会对每个元素调用 stock_name(),用返回值作为排序依据。这里的 stock_name() 就是回调函数。相关主题:concepts/回调函数、排序key函数。
如果元素是对象,也可以按属性排序:
def stock_name(s):
return s.name
portfolio.sort(key=stock_name)
这种模式的意义是:sort() 负责通用排序流程,调用者只提供如何提取排序关键值的函数。函数接口因此变得更灵活。
匿名函数与 lambda
很多回调函数非常短,只在一次调用中使用。例如:
def stock_name(s):
return s.name
portfolio.sort(key=stock_name)
如果这个函数只是为了当前排序临时使用,单独起名可能显得冗长。Python 提供 lambda 创建匿名函数:
portfolio.sort(key=lambda s: s.name)
如果数据是字典,也可以写成:
portfolio.sort(key=lambda s: s['name'])
lambda 创建的是一个未命名函数,用来计算单个表达式。它常用于 sort()、sorted() 等需要短小回调函数的地方。
按股票持有数量排序:
portfolio.sort(key=lambda s: s.shares)
按股票价格排序:
portfolio.sort(key=lambda s: s.price)
lambda 的主要优点是简洁:它把一次性的字段提取逻辑直接写在函数调用中,避免定义额外的命名函数。相关主题:lambda匿名函数、匿名函数、排序key函数。
lambda 的限制
Python 的 lambda 有意保持简单:
- 只能包含一个表达式。
- 不能包含普通语句。
- 不能直接写
while、for、try等语句块。 - 不适合复杂逻辑。
- 最常见用途是传入短小的回调函数,例如排序的
key参数。
因此,选择 def 还是 lambda 的经验规则是:
- 如果逻辑很短,只使用一次,可以考虑
lambda。 - 如果逻辑需要命名、复用、调试、文档说明或多条语句,应使用
def。 - 如果
lambda让代码变难读,应改成普通函数。
变量作用域
局部变量
函数内部赋值产生的变量是局部变量,只在函数调用期间存在。调用结束后,这些局部变量不会保留,也不能在函数外访问。
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 会得到 NameError。
局部变量也不会与函数外部的同名变量冲突。这使函数可以把内部实现细节隐藏起来,调用者只需要关心参数和返回值。相关主题:Python作用域。
全局变量
函数可以读取同一文件中的全局变量:
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 声明必须出现在使用该变量之前,并且对应变量应存在于同一文件中。不过,频繁使用 global 通常是糟糕设计的信号。函数如果需要修改外部状态,更好的方式通常是:
- 把状态作为参数传入并返回新结果。
- 使用可变对象显式承载状态。
- 使用类封装状态和行为。
相关主题:全局变量、状态管理、可预测性。
参数传递与对象绑定
调用函数时,参数变量只是绑定到传入对象的名字。传入的值不会被复制。这个规则对可变对象尤其重要。
def foo(items):
items.append(42)
a = [1, 2, 3]
foo(a)
print(a) # [1, 2, 3, 42]
这里 items 和 a 指向同一个列表,因此 append() 会修改调用者看到的对象。
但是,重新给参数名赋值只是改变局部变量绑定,不会改变调用者的变量:
def bar(items):
items = [4, 5, 6]
b = [1, 2, 3]
bar(b)
print(b) # [1, 2, 3]
关键区别是:
- 修改对象:可能影响函数外部。
- 重新绑定变量名:只影响函数内部的局部名字。
变量赋值不会覆盖内存,它只是让名字绑定到新的对象。相关主题:Python参数传递、变量绑定、可变对象。
名称定义顺序与函数组织
Python 中名称必须在被使用前已经定义。变量名、函数名和导入的模块名都遵循这个规则。
def square(x):
return x * x
a = 42
b = a + 2
z = square(b)
函数可以按任意顺序定义,只要在程序实际执行到函数调用之前,该函数名已经存在即可。
def foo(x):
bar(x)
def bar(x):
print(x)
foo(3)
函数体中引用另一个函数并不要求那个函数在文本上已经提前出现;真正的要求是:运行到调用语句时,被调用的函数已经定义。
常见的组织方式是自底向上:先定义小而简单的函数,再定义依赖这些函数的较高级函数,最后在文件末尾调用顶层函数。
相关主题:自底向上设计、程序结构、模块化编程。
函数作为黑盒与接口
理想情况下,函数应像黑盒:调用者只需要知道输入、输出和功能,不需要知道内部细节。
良好的函数通常具有以下特征:
- 只依赖传入的参数。
- 尽量避免读取或修改不明显的全局变量。
- 避免神秘副作用。
- 相同输入产生可预测的结果。
- 每个函数负责一个清晰任务。
- 参数名和返回值能清楚表达接口。
- 参数类型不要过早绑定到某个具体实现,除非确实需要。
- 如果需要可变行为,可以考虑把函数作为参数传入。
例如,读取价格文件可以写成独立函数:
def read_prices(filename):
prices = {}
with open(filename) as f:
f_csv = csv.reader(f)
for row in f_csv:
prices[row[0]] = float(row[1])
return prices
之后可以重复使用:
oldprices = read_prices('oldprices.csv')
newprices = read_prices('newprices.csv')
这比在脚本中反复复制读取逻辑更清晰,也更容易调试。主要目标包括 模块化编程、可预测性 和 可维护性。
函数接口设计:文件名还是可迭代对象
在文件处理函数中,一个重要设计问题是:函数应该接收文件名,还是接收已经打开的文件类对象或可迭代的行对象?
第一种设计让函数接收文件名,并在函数内部打开文件:
def read_data(filename):
records = []
with open(filename) as f:
for line in f:
...
records.append(r)
return records
第二种设计让函数接收可迭代的文本行:
def read_data(lines):
records = []
for line in lines:
...
records.append(r)
return records
第二种通常更灵活。它不关心输入来自普通文件、压缩文件、标准输入,还是测试时手写的字符串列表;它只关心参数能否被逐行迭代。
lines = open('data.csv')
data = read_data(lines)
lines = gzip.open('data.csv.gz', 'rt')
data = read_data(lines)
lines = sys.stdin
data = read_data(lines)
lines = ['ACME,50,91.1', 'IBM,75,123.45']
data = read_data(lines)
这体现了 concepts/鸭子类型:如果一个对象能像行序列一样被 for line in lines 使用,那么函数就可以把它当作行序列处理,而不必关心它的具体类型。相关主题:接口设计、库设计、可迭代对象、文件处理。
不过,这种设计也有陷阱。字符串本身也是可迭代对象。如果把旧接口中的文件名字符串直接传给新版本函数:
parse_csv('Data/portfolio.csv', types=[str, int, float])
函数可能会把文件名当作一个字符序列逐字符处理,而不是打开该文件。因此,在把函数从接收文件名改为接收行对象时,应考虑安全检查,例如检测参数是否为字符串路径,并抛出更清楚的错误,或在上层函数中负责打开文件。
常见职责划分是:
- 底层解析函数接收行对象,专注于解析。
- 上层业务函数接收文件名,负责打开文件并把文件对象传给解析函数。
- 测试代码可以直接传入字符串列表,避免依赖真实文件。
文档字符串与类型注解
如果函数体的第一条语句是字符串,它会成为函数的文档字符串,也称 docstring。
def greeting(name):
'Issues a greeting'
print('Hello', name)
可以通过 help() 查看:
help(greeting)
好的文档字符串通常包括:
- 一句话概括函数做什么。
- 必要时说明参数含义。
- 必要时说明返回值。
- 必要时提供简短使用示例。
函数定义也可以添加可选类型提示:
def read_prices(filename: str) -> dict:
'''
Read prices from a CSV file of name,price data
'''
prices = {}
...
return prices
类型注解不会改变 Python 的运行行为。它们主要是信息性的,但可以被 IDE、类型检查器、代码检查器和文档工具使用。相关主题:documentation、代码文档化、concepts/类型注解、静态分析。
从脚本到函数
脚本是按顺序执行一系列语句并结束的程序:
statement1
statement2
statement3
Python 很容易写出这种直接的脚本,但随着功能增加,脚本可能变成难以维护的大文件。因此,函数的重要用途之一,是把原本写死在脚本中的逻辑提取出来。
例如,原本脚本可能直接读取某个固定文件并打印结果:
cost = portfolio_cost('Data/portfolio.csv')
print('Total cost:', cost)
更好的结构是把核心计算封装为函数:
def portfolio_cost(filename):
...
return cost
cost = portfolio_cost('Data/portfolio.csv')
print('Total cost:', cost)
这样做的好处包括:
- 可以对不同文件重复调用同一段逻辑。
- 可以在交互模式中单独测试函数。
- 可以在其他程序中导入并复用该函数。
- 可以把计算逻辑和脚本入口逻辑分离。
- 当脚本增长时,更容易继续拆分和维护。
相关主题:script to function、interactive testing。
顶层执行函数
把脚本的完整执行流程封装到一个顶层函数中,可以让程序结构更清楚。
def portfolio_report(portfolio_filename, prices_filename):
portfolio = read_portfolio(portfolio_filename)
prices = read_prices(prices_filename)
report = make_report(portfolio, prices)
print_report(report)
然后文件末尾只保留一个调用:
portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
这种结构让程序更清楚:
- 前面是一组函数定义。
- 最后是一个顶层函数调用。
- 顶层函数描述完整程序流程。
- 低层函数负责具体任务。
如果底层 parse_csv() 已经改为接收文件对象,那么 read_portfolio() 和 read_prices() 这样的上层函数可以继续接收文件名,但在内部打开文件:
def read_portfolio(filename):
with open(filename) as file:
return parse_csv(file, types=[str, int, float])
这样,旧的脚本调用方式保持不变,而底层解析能力变得更灵活。相关主题:script to function、程序结构、模块化编程、python scripts。
函数与标准库
函数不仅可以由用户自定义,也可以来自 Python 标准库。标准库通过 import 使用。
import math
x = math.sqrt(10)
在处理 CSV 文件时,应优先使用标准库中的 csv 模块,而不是手写 line.split(',')。
import csv
f = open('Data/portfolio.csv')
rows = csv.reader(f)
headers = next(rows)
for row in rows:
print(row)
f.close()
csv 模块能处理引号、逗号拆分等底层细节,比手动字符串拆分更可靠。相关主题:python standard library、modules and imports、csv processing、data parsing。
示例:通用 CSV 解析函数
parse_csv() 是函数抽象和接口设计的典型例子。它把调用 csv.reader()、跳过空行、选择列、类型转换、处理表头和分隔符等细节封装在一个可复用函数中。
基础版本把带表头的 CSV 解析为字典列表:
import csv
def parse_csv(lines):
'''
Parse CSV lines into a list of records
'''
rows = csv.reader(lines)
headers = next(rows)
records = []
for row in rows:
if not row:
continue
record = dict(zip(headers, row))
records.append(record)
return records
注意这里的参数名是 lines,不是 filename。这表示该函数需要的是可迭代的文本行,而不是路径字符串。
进一步改进后,可以支持列选择:
shares_held = parse_csv(file, select=['name', 'shares'])
列选择的关键是把列名映射为索引:
indices = [headers.index(colname) for colname in select]
row = [row[index] for index in indices]
还可以支持类型转换:
portfolio = parse_csv(file, types=[str, int, float])
转换逻辑把函数对象当作数据传入:
row = [func(val) for func, val in zip(types, row)]
无表头文件可以用 has_headers=False 返回元组列表:
prices = parse_csv(file, types=[str, float], has_headers=False)
不同分隔符可以通过 delimiter 参数配置:
portfolio = parse_csv(file, types=[str, int, float], delimiter=' ')
这个例子体现了几个重要思想:
- 使用函数隐藏底层细节。
- 使用默认参数提供常见行为。
- 使用关键字参数配置可选功能。
- 使用列表推导式、
zip()、dict()、元组和类型转换组合解决实际问题。 - 将重复的文件解析逻辑抽象成小型库函数。
- 通过接收可迭代行对象,让函数支持普通文件、gzip 文件、标准输入和测试列表。
- 把函数对象作为参数传递,实现可配置的数据转换。
- 在接口更灵活后,需要用安全检查避免把字符串文件名误当作字符序列。
相关主题:CSV解析、列选择、数据投影、类型转换、高阶函数、可调用对象、函数抽象、concepts/鸭子类型、可迭代对象。
示例:排序、key 函数与 lambda
排序展示了函数作为行为参数的另一种常见用法。
如果有一组股票对象:
Stock('AA', 100, 32.2)
Stock('IBM', 50, 91.1)
Stock('CAT', 150, 83.44)
可以按名称排序:
def stock_name(s):
return s.name
portfolio.sort(key=stock_name)
也可以用 lambda 直接写出字段提取逻辑:
portfolio.sort(key=lambda s: s.name)
按股数排序:
portfolio.sort(key=lambda s: s.shares)
按价格排序:
portfolio.sort(key=lambda s: s.price)
这个例子说明:函数接口可以把通用算法和特定行为分离。sort() 实现排序算法;key 函数或 lambda 告诉它按什么值排序。这是 concepts/回调函数、排序key函数、高阶函数 和 lambda匿名函数 的交汇点。
函数与异常
函数通常通过异常报告错误。如果异常没有被处理,函数会中止,整个程序也可能停止。
>>> int('N/A')
Traceback ...
ValueError: invalid literal for int() with base 10: 'N/A'
异常信息通常包含:
- 错误类型,例如
ValueError。 - 错误原因。
- 错误发生的位置。
- traceback,即导致错误的一系列调用路径。
可以使用 try-except 捕获并处理异常。
for line in file:
fields = line.split(',')
try:
shares = int(fields[1])
except ValueError:
print('Could not parse', line)
函数也可以主动抛出异常,用来报告无法继续执行的错误状态。
raise RuntimeError('What a kerfuffle')
在接口设计中,异常也可以用于防止误用。例如,如果函数已经改为接收文件类对象,却收到字符串文件名,可以主动抛出 TypeError,提示调用者先 open() 文件。
相关主题:python exceptions、error handling、exception raising、debugging、robust programming、data cleaning、fault tolerance。
函数与命令行脚本
函数可以与命令行参数结合,使脚本更灵活。
import sys
def portfolio_cost(filename):
...
if len(sys.argv) == 2:
filename = sys.argv[1]
else:
filename = 'Data/portfolio.csv'
cost = portfolio_cost(filename)
print('Total cost:', cost)
运行:
python3 pcost.py Data/portfolio.csv
这样,核心逻辑仍在 portfolio_cost() 函数中,而输入文件名可以从命令行指定。如果底层解析函数接收文件对象,命令行脚本仍可以在上层打开命令行给出的文件名,再把文件对象传入解析函数。相关主题:command line arguments、python scripts。
典型代码示例
简单问候函数
def greeting(name):
'Issues a greeting'
print('Hello', name)
greeting('Guido')
greeting('Paula')
投资组合成本函数
def portfolio_cost(filename):
total_cost = 0.0
f = open(filename)
headers = next(f)
for line in f:
fields = line.split(',')
shares = int(fields[1])
price = float(fields[2])
total_cost += shares * price
f.close()
return total_cost
报表打印函数
def print_report(report):
headers = ('Name', 'Shares', 'Price', 'Change')
print('%10s %10s %10s %10s' % headers)
print(('-' * 10 + ' ') * len(headers))
for row in report:
print('%10s %10d %10.2f %10.2f' % row)
顶层报表函数
def portfolio_report(portfolio_filename, prices_filename):
portfolio = read_portfolio(portfolio_filename)
prices = read_prices(prices_filename)
report = make_report(portfolio, prices)
print_report(report)
接收可迭代行对象的解析函数
def read_data(lines):
records = []
for line in lines:
...
records.append(record)
return records
排序 key 函数
def stock_name(s):
return s.name
portfolio.sort(key=stock_name)
使用 lambda 排序
portfolio.sort(key=lambda s: s.name)
portfolio.sort(key=lambda s: s.shares)
portfolio.sort(key=lambda s: s.price)
常见错误
忘记调用函数
定义函数不会自动运行函数。必须显式调用。
def greeting(name):
print('Hello', name)
greeting('Guido')
调用发生得太早
foo(3)
def foo(x):
print(x)
执行到 foo(3) 时,foo 还没有定义,因此会出错。
忘记 return
def add(a, b):
total = a + b
x = add(2, 3) # None
应写成:
def add(a, b):
return a + b
把打印当作返回值
def add(a, b):
print(a + b)
x = add(2, 3) # x 是 None
如果后续还要使用结果,应使用 return。
可选参数调用不清楚
parse_data(data, False, True)
这种调用难以理解。更好的写法是:
parse_data(data, debug=True, ignore_errors=False)
参数数量不匹配
def greeting(name):
print('Hello', name)
greeting() # 缺少参数
greeting('A', 'B') # 参数过多
缩进错误
Python 使用缩进表示函数体。缩进错误会导致语法错误或逻辑错误。
误解全局变量赋值
name = 'Dave'
def spam():
name = 'Guido'
spam()
print(name) # Dave
函数内部赋值默认创建局部变量,不会修改全局变量。
滥用 global
虽然 global 可以修改全局变量,但通常应避免。若函数需要修改外部状态,优先考虑通过返回值、显式参数或类来表达状态变化。
误解参数传递
def foo(items):
items.append(42)
如果传入列表,这会修改原列表。函数并不会自动复制参数。
混淆修改对象与重新赋值
def bar(items):
items = [4, 5, 6]
这只是让局部变量 items 指向新列表,不会改变调用者的列表。
函数依赖隐藏的全局状态
filename = 'Data/portfolio.csv'
def read_data():
return open(filename).read()
更好的写法是把文件名作为参数传入:
def read_data(filename):
return open(filename).read()
在更通用的库函数中,还可以进一步接收打开的文件或行对象:
def read_data(lines):
for line in lines:
...
把文件名误传给接收行对象的函数
如果函数已经改为接收可迭代行对象,传入文件名字符串会导致函数逐字符处理路径:
parse_csv('Data/portfolio.csv')
这是因为字符串本身也是可迭代对象。应改为:
with open('Data/portfolio.csv') as file:
parse_csv(file)
或者让上层函数继续接收文件名并负责打开文件。
手动解析 CSV 过于简单
fields = line.split(',')
这种方式在简单数据中可用,但遇到带引号、嵌入逗号或复杂格式的 CSV 时容易出错。应优先使用 csv.reader()。
只处理正常数据,不处理异常数据
真实文件可能包含缺失字段、空字符串或格式错误。若直接转换:
shares = int(fields[1])
可能触发 ValueError。应根据需求使用 try-except 处理。
为一次性小函数写过多样板代码
如果函数只用于一次排序,并且只返回某个字段:
def stock_price(s):
return s.price
portfolio.sort(key=stock_price)
可以考虑使用 lambda:
portfolio.sort(key=lambda s: s.price)
但如果逻辑复杂、需要复用或需要文档说明,仍应使用普通 def 函数。
过度使用 lambda
lambda 只能表达单个表达式。如果匿名函数变得很长或难读,应改成命名函数。可读性通常比少写几行代码更重要。
调试提示
- 使用交互式解释器单独调用函数,验证输入和输出。
- 用
python3 -i script.py运行脚本,加载函数后继续实验。 - 先让函数处理一个小输入,再扩大到完整文件。
- 出现异常时仔细阅读 traceback,定位具体文件、行号和调用链。
- 对文件处理函数,先打印读取到的行或字段,确认解析结果是否符合预期。
- 对数据转换语句,例如
int()、float(),重点检查空字符串、缺失字段和非数字内容。 - 使用关键字参数让测试调用更可读。
- 注意函数是否修改了传入的可变对象。
- 使用标准库模块代替脆弱的手写解析逻辑。
- 如果函数越来越长,考虑拆分为多个更小的函数。
- 如果脚本末尾越来越复杂,考虑创建一个顶层函数来表达完整执行流程。
- 尽量让函数输入来自参数,输出来自返回值,减少隐藏依赖和副作用。
- 为关键函数编写简短 docstring,让未来的自己或其他使用者更容易理解。
- 测试文件解析函数时,可以传入字符串列表,而不必总是创建真实文件。
- 当函数改为接收可迭代对象后,专门测试误传字符串文件名的情况。
- 在库函数中尽量面向行为和协议设计接口,同时用清晰错误信息防止常见误用。
- 调试排序时,先确认
key函数或lambda对单个元素返回了预期值。 - 如果
lambda难以调试,可以先改写成命名函数,再单独调用测试。
推荐练习
- 定义一个
greeting(name)函数,调用多次,并使用help(greeting)查看文档字符串。 - 编写
sumcount(n),返回前n个正整数之和。 - 将一个直接运行的脚本改造成函数,例如
portfolio_cost(filename)。 - 使用
python3 -i进入交互模式,手动调用自己写的函数。 - 给
portfolio_cost()加入异常处理,使它能跳过格式错误的数据行。 - 将手写的 CSV 拆分逻辑改为使用
csv.reader()。 - 使用
sys.argv让脚本从命令行接收文件名。 - 编写一个函数,在输入不合法时用
raise主动抛出异常。 - 把报表打印逻辑封装为
print_report(report)。 - 创建顶层函数
portfolio_report(portfolio_filename, prices_filename),让脚本末尾只保留一次顶层函数调用。 - 为一个已有函数添加 docstring 和类型注解。
- 为函数增加默认参数,并用关键字参数调用它。
- 编写
divide(a, b),返回商和余数,并分别用解包和元组两种方式接收结果。 - 实验列表参数:一个函数修改列表,另一个函数重新绑定参数名,观察差异。
- 实现通用
parse_csv(),支持select、types、has_headers和delimiter。 - 将
parse_csv()从接收文件名改为接收文件类对象或可迭代行对象。 - 使用
gzip.open()、sys.stdin和字符串列表测试同一个解析函数。 - 给接收行对象的函数加入安全检查,避免误传文件名字符串。
- 修改
read_portfolio()和read_prices(),让它们负责打开文件并调用新的parse_csv()。 - 定义
stock_name(s),用portfolio.sort(key=stock_name)按股票名称排序。 - 使用
lambda s: s.shares按持股数量排序。 - 使用
lambda s: s.price按股票价格排序。 - 对比命名
key函数和lambda写法,判断哪一种在当前场景更清楚。
关联知识点
- python functions:Python 函数定义、调用、参数和返回值。
- Python函数设计:设计清晰、可读、可复用的函数接口。
- 函数抽象:将重复低层逻辑封装为通用函数。
- 接口设计:为函数和模块设计清晰、稳定、灵活的使用方式。
- 库设计:编写可复用代码时优先面向通用协议和组合能力。
- concepts/鸭子类型:根据对象行为而不是具体类型判断是否可用。
- 可迭代对象:能被
for循环逐项访问的对象。 - 文件处理:打开、读取和管理文件资源。
- 关键字参数:使用参数名调用函数,提高可选参数可读性。
- Python返回值:理解
return、None和元组返回。 - 元组解包:接收函数返回的多个值。
- Python作用域:局部变量、全局变量和名称查找规则。
- 全局变量:函数读取和修改全局状态的规则与风险。
- 状态管理:避免用全局变量隐式管理程序状态。
- Python参数传递:函数参数绑定到对象而不是复制对象。
- 变量绑定:赋值让名字绑定到对象。
- 可变对象:列表、字典等对象可能被函数原地修改。
- code reuse:通过函数减少重复代码。
- documentation:使用文档字符串说明函数用途。
- 代码文档化:为函数和程序添加可读说明。
- concepts/类型注解:为函数参数和返回值提供可选类型信息。
- 静态分析:使用工具检查代码和类型提示。
- script to function:把脚本重构为可复用函数。
- interactive testing:在交互模式中测试函数。
- python standard library:使用标准库中的现成模块和函数。
- modules and imports:通过
import引入模块。 - python exceptions:Python 异常机制。
- error handling:使用
try-except处理错误。 - exception raising:使用
raise主动抛出异常。 - debugging:利用 traceback 和交互测试定位问题。
- csv processing:使用
csv模块处理 CSV 文件。 - CSV解析:把 CSV 文本转换为结构化数据。
- data parsing:将文本数据转换为结构化数据。
- data cleaning:处理缺失、错误和异常数据。
- 列选择:从结构化数据中提取指定字段。
- 数据投影:只保留计算所需的数据列。
- 类型转换:把字符串字段转换为
int、float等类型。 - 高阶函数:把函数对象作为数据传递或接收函数作为参数。
- concepts/函数作为对象:函数可以赋值、传参和组合。
- 可调用对象:可以像函数一样调用的对象。
- concepts/回调函数:传入另一个函数并由后者调用的函数。
- 排序key函数:为排序操作提供比较依据的函数。
- lambda匿名函数:用
lambda定义简短的一次性匿名函数。 - 匿名函数:没有显式名称的函数形式。
- 可配置接口:用参数控制函数的不同行为。
- command line arguments:使用
sys.argv读取命令行参数。 - python scripts:编写可从终端运行的 Python 脚本。
- 程序结构:组织定义、执行流程和依赖关系的方式。
- 模块化编程:把程序拆分成职责明确的小部件。
- 自底向上设计:先构建简单函数,再组合为复杂功能。
- 可预测性:让函数行为容易推理和验证。
- 可维护性:让程序在增长后仍然容易理解和修改。
对应教材来源
来源:Practical Python Programming, https://github.com/dabeaz-course/practical-python
See also: summaries/00_Setup
See also: summaries/00_Overview
See also: summaries/01_Python
See also: summaries/02_Hello_world
See also: summaries/06_Files
See also: summaries/07_Functions
See also: summaries/02_Containers
See also: summaries/03_Formatting
See also: summaries/01_Script
See also: summaries/02_More_functions
See also: summaries/03_Error_checking
See also: summaries/04_Modules
See also: summaries/05_Main_module
See also: summaries/06_Design_discussion
See also: summaries/01_Class
See also: summaries/01_Dicts_revisited
See also: summaries/03_Producers_consumers
See also: summaries/04_More_generators
See also: summaries/01_Variable_arguments
See also: summaries/02_Anonymous_function
See also: summaries/03_Returning_functions
See also: summaries/04_Function_decorators
See also: summaries/01_Introduction__00_Overview