简介
尽管函数在前面已经介绍过,但关于它们在更深层次上实际如何工作的细节却很少。本节旨在填补一些空白,并讨论诸如调用约定、作用域规则等问题。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
尽管函数在前面已经介绍过,但关于它们在更深层次上实际如何工作的细节却很少。本节旨在填补一些空白,并讨论诸如调用约定、作用域规则等问题。
考虑以下函数:
def read_prices(filename, debug):
...
你可以使用位置参数调用该函数:
prices = read_prices('prices.csv', True)
或者你可以使用关键字参数调用该函数:
prices = read_prices(filename='prices.csv', debug=True)
有时你希望某个参数是可选的。如果是这样,可以在函数定义中为其指定一个默认值。
def read_prices(filename, debug=False):
...
如果指定了默认值,那么在函数调用时该参数就是可选的。
d = read_prices('prices.csv')
e = read_prices('prices.dat', 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)
在大多数情况下,关键字参数能提高代码的清晰度,尤其是对于用作标志或与可选功能相关的参数。
始终为函数参数赋予简短但有意义的名称。
使用函数的人可能希望采用关键字调用方式。
d = read_prices('prices.csv', debug=True)
Python 开发工具会在帮助功能和文档中显示这些名称。
return
语句返回一个值
def square(x):
return x * x
如果没有给出返回值或者缺少 return
语句,则返回 None
。
def bar(x):
statements
return
a = bar(4) ## a = None
## 或者
def foo(x):
statements ## 没有 `return`
b = foo(4) ## b = None
函数只能返回一个值。不过,函数可以通过返回一个元组来返回多个值。
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)
程序会给变量赋值。
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
都是局部变量。在函数调用之后,这些变量不会被保留或访问。
>>> stocks = read_portfolio('portfolio.csv')
>>> fields
Traceback (most recent call last):
File "<stdin>", line 1, in?
NameError: name 'fields' is not defined
>>>
局部变量也不会与其他地方的变量冲突。
函数可以自由访问在同一文件中定义的全局变量的值。
name = 'Dave'
def greeting():
print('Hello', name) ## 使用 `name` 全局变量
然而,函数不能修改全局变量:
name = 'Dave'
def spam():
name = 'Guido'
spam()
print(name) ## 输出 'Dave'
记住:函数中的所有赋值都是局部的。
如果你必须修改全局变量,那么你必须将其声明为全局变量。
name = 'Dave'
def spam():
global name
name = 'Guido' ## 修改上面的全局变量 name
global
声明必须在使用该变量之前出现,并且相应的变量必须与函数位于同一文件中。了解到这一点后,要知道这被认为是一种不好的形式。实际上,如果可以的话,尽量完全避免使用 global
。如果你需要一个函数来修改函数外部的某种状态,最好使用类来代替(稍后会详细介绍)。
当你调用一个函数时,参数变量是指向所传递值的名称。这些值不是副本。如果传递的是可变数据类型(例如列表、字典),则可以在原地对其进行修改。
def foo(items):
items.append(42) ## 修改输入对象
a = [1, 2, 3]
foo(a)
print(a) ## [1, 2, 3, 42]
关键点:函数接收的不是输入参数的副本。
确保你理解修改值和重新赋值变量名之间的细微差别。
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] ## 将局部变量 `items` 改为指向不同的对象
b = [1, 2, 3]
bar(b)
print(b) ## [1, 2, 3]
提醒:变量赋值永远不会覆盖内存。变量名只是绑定到一个新的值。
这组练习要求你实现课程中可能是最强大且最困难的部分。有很多步骤,并且之前练习中的许多概念会一次性综合起来。最终的解决方案大约只有 25 行代码,但请慢慢来,确保你理解每一部分。
你的 report.py
程序的一个核心部分集中在 CSV 文件的读取上。例如,函数 read_portfolio()
读取一个包含投资组合数据行的文件,函数 read_prices()
读取一个包含价格数据行的文件。在这两个函数中,有很多底层的“琐碎”部分和类似的特性。例如,它们都打开一个文件并用 csv
模块包装它,并且它们都将各种字段转换为新的类型。
如果你真的要进行大量的文件解析,你可能会想要清理其中一些内容并使其更通用。这就是我们的目标。
通过打开名为 fileparse.py
的文件开始这个练习。这就是我们要进行工作的地方。
首先,让我们专注于将 CSV 文件读取为字典列表的问题。在 fileparse_3.3.py
文件中,定义一个如下所示的函数:
## fileparse_3.3.py
import csv
def parse_csv(filename):
'''
将 CSV 文件解析为记录列表
'''
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
此函数将 CSV 文件读取为字典列表,同时隐藏了打开文件、用 csv
模块包装它、忽略空行等细节。
试试看:
提示:python3 -i fileparse_3.3.py
。
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]
>>>
这样很好,只是你无法对这些数据进行任何有用的计算,因为所有内容都表示为字符串。我们很快会解决这个问题,但让我们继续在此基础上构建。
在许多情况下,你只对 CSV 文件中的某些特定列感兴趣,而不是所有数据。修改 parse_csv()
函数,使其可以根据用户指定,有选择地提取列数据,如下所示:
>>> ## 读取所有数据
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]
>>> ## 仅读取部分数据
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'])
>>> shares_held
[{'name': 'AA','shares': '100'}, {'name': 'IBM','shares': '50'}, {'name': 'CAT','shares': '150'}, {'name': 'MSFT','shares': '200'}, {'name': 'GE','shares': '95'}, {'name': 'MSFT','shares': '50'}, {'name': 'IBM','shares': '100'}]
>>>
在练习 2.23 中给出了一个列选择器的示例。不过,这里有一种实现方法:
## fileparse_3.4.py
import csv
def parse_csv(filename, select=None):
'''
将 CSV 文件解析为记录列表
'''
with open(filename) as f:
rows = csv.reader(f)
## 读取文件头
headers = next(rows)
## 如果提供了列选择器,找到指定列的索引。
## 同时缩小用于生成结果字典的标题集
if select:
indices = [headers.index(colname) for colname in select]
headers = select
else:
indices = []
records = []
for row in rows:
if not row: ## 跳过无数据的行
continue
## 如果选择了特定列,则过滤该行
if indices:
row = [ row[index] for index in indices ]
## 创建字典
record = dict(zip(headers, row))
records.append(record)
return records
这部分有一些棘手的地方。可能最重要的一点是将列选择映射到行索引。例如,假设输入文件有以下标题:
>>> headers = ['name', 'date', 'time','shares', 'price']
>>>
现在,假设选择的列如下:
>>> select = ['name','shares']
>>>
为了进行正确的选择,你必须将所选列名映射到文件中的列索引。这就是这一步所做的事情:
>>> indices = [headers.index(colname) for colname in select ]
>>> indices
[0, 3]
>>>
换句话说,“name”是第 0 列,“shares”是第 3 列。当你从文件中读取一行数据时,这些索引用于过滤它:
>>> row = ['AA', '6/11/2007', '9:50am', '100', '32.20' ]
>>> row = [ row[index] for index in indices ]
>>> row
['AA', '100']
>>>
修改 /home/labex/project/fileparse_3.5.py
目录下的 parse_csv()
函数,使其可以选择对返回的数据进行类型转换。例如:
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv', types=[str, int, float])
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'], types=[str, int])
>>> shares_held
[{'name': 'AA','shares': 100}, {'name': 'IBM','shares': 50}, {'name': 'CAT','shares': 150}, {'name': 'MSFT','shares': 200}, {'name': 'GE','shares': 95}, {'name': 'MSFT','shares': 50}, {'name': 'IBM','shares': 100}]
>>>
你已经在练习 2.24 中探讨过这个问题。你需要在你的解决方案中插入以下代码片段:
...
if types:
row = [func(val) for func, val in zip(types, row) ]
...
有些 CSV 文件不包含任何标题信息。例如,prices.csv
文件看起来像这样:
"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
...
修改 /home/labex/project/fileparse_3.6.py
中的 parse_csv()
函数,使其能够处理此类文件,方法是创建一个元组列表。例如:
>>> prices = parse_csv('/home/labex/project/prices.csv', types=[str,float], has_headers=False)
>>> prices
[('AA', 9.22), ('AXP', 24.85), ('BA', 44.85), ('BAC', 11.27), ('C', 3.72), ('CAT', 35.46), ('CVX', 66.67), ('DD', 28.47), ('DIS', 24.22), ('GE', 13.48), ('GM', 0.75), ('HD', 23.16), ('HPQ', 34.35), ('IBM', 106.28), ('INTC', 15.72), ('JNJ', 55.16), ('JPM', 36.9), ('KFT', 26.11), ('KO', 49.16), ('MCD', 58.99), ('MMM', 57.1), ('MRK', 27.58), ('MSFT', 20.89), ('PFE', 15.19), ('PG', 51.94), ('T', 24.79), ('UTX', 52.61), ('VZ', 29.26), ('WMT', 49.74), ('XOM', 69.35)]
>>>
要进行此更改,你需要修改代码,以便第一行数据不被解释为标题行。此外,你需要确保不创建字典,因为不再有任何列名可用于作为键。
虽然 CSV 文件相当常见,但你也可能会遇到使用不同列分隔符的文件,比如制表符或空格。例如,portfolio.dat
文件看起来是这样的:
name shares price
"AA" 100 32.20
"IBM" 50 91.10
"CAT" 150 83.44
"MSFT" 200 51.23
"GE" 95 40.37
"MSFT" 50 65.10
"IBM" 100 70.44
csv.reader()
函数允许指定不同的列分隔符,如下所示:
rows = csv.reader(f, delimiter=' ')
修改你在 /home/labex/project/fileparse_3.7.py
中的 parse_csv()
函数,使其也能允许更改分隔符。
例如:
>>> portfolio = parse_csv('/home/labex/project/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]
>>>
如果你已经完成了到目前为止的内容,那么你已经创建了一个非常实用的库函数。你可以使用它来解析任意的 CSV 文件,选择感兴趣的列,进行类型转换,而无需过多担心文件的内部工作原理或 csv
模块。
恭喜你!你已经完成了“更多关于函数”的实验。你可以在 LabEx 中练习更多实验来提高你的技能。