更多关于函数

PythonPythonBeginner
立即练习

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

在这个例子中,filenameportfoliolinefieldss 都是局部变量。在函数调用之后,这些变量不会被保留或访问。

>>> 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 的文件开始这个练习。这就是我们要进行工作的地方。

练习 3.3:读取 CSV 文件

首先,让我们专注于将 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'}]
>>>

这样很好,只是你无法对这些数据进行任何有用的计算,因为所有内容都表示为字符串。我们很快会解决这个问题,但让我们继续在此基础上构建。

✨ 查看解决方案并练习

练习 3.4:构建列选择器

在许多情况下,你只对 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']
>>>
✨ 查看解决方案并练习

练习 3.5:进行类型转换

修改 /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) ]
...
✨ 查看解决方案并练习

练习 3.6:处理无标题的文件

有些 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)]
>>>

要进行此更改,你需要修改代码,以便第一行数据不被解释为标题行。此外,你需要确保不创建字典,因为不再有任何列名可用于作为键。

✨ 查看解决方案并练习

练习 3.7:选择不同的列分隔符

虽然 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 中练习更多实验来提高你的技能。