目录 | 上一节 (3.1 脚本) | 下一节 (3.3 错误检查)

3.2 深入函数

尽管函数在早先时候介绍了,但有关函数在更深层次上是如何工作的细节却很少提供。本节旨在填补这些空白,并讨论函数调用约定,作用域规则等问题。

调用函数

考虑以下函数:

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 # OR
def foo(x):
statements # No `return` b = foo(4) # b = None

多个返回值

函数只能返回一个值。但是,通过将返回值放到元组中,函数可以返回多个值:

def divide(a,b):
q = a // b # Quotient
r = a % b # Remainder
return q, r # Return a tuple

用例:

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

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

变量作用域

程序给变量赋值:

x = value # Global variable

def foo():
y = value # Local variable

变量赋值发生在函数的内部和外部。定义在函数外部的变量是“全局的”。定义在函数内部的变量是“局部的”。

局部变量

在函数内部赋值的变量是私有的。

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, fieldss 是局部变量。在函数调用之后,这些变量将不会保留或者不可访问。

>>> 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) # Using `name` global variable

但是,函数不能修改全局变量:

name = 'Dave'

def spam():
name = 'Guido' spam()
print(name) # prints 'Dave'

切记:函数中的所有赋值都是局部的

修改全局变量

如果必须修改全局变量,请像下面这样声明它:

name = 'Dave'

def spam():
global name
name = 'Guido' # Changes the global name above

全局声明必须在使用之前出现,并且相应的变量必须与该函数处在同一文件中。看上面这个函数,要知道这是一种糟糕的形式。事实上,如果可以的话,尽量避免使用 global 。如果需要一个函数来修改函数外部的某种状态,最好是使用类来代替(稍后详细介绍)。

参数传递

当调用一个函数的时候,参数变量的传递是引用传递。不拷贝值(参见2.7 节)。如果传递了可变数据类型(如列表,字典),它们可以被原地修改。

def foo(items):
items.append(42) # Modifies the input object a = [1, 2, 3]
foo(a)
print(a) # [1, 2, 3, 42]

关键点:函数不接收输入参数的拷贝。

重新赋值与修改

确保了解修改值与给变量名重新赋值的细微差别。

def foo(items):
items.append(42) # Modifies the input object a = [1, 2, 3]
foo(a)
print(a) # [1, 2, 3, 42] # VS
def bar(items):
items = [4,5,6] # Changes local `items` variable to point to a different object b = [1, 2, 3]
bar(b)
print(b) # [1, 2, 3]

提醒:变量赋值永远不会重写内存。名称只是被绑定到了新的值上面

练习

本组练习实现的内容可能是本课程最强大的和最难的。有很多步骤,并且过去练习中的许多概念被一次性整合在一起。虽然最后的题解只有大约 25 行的代码,但要花点时间,确保你理解每一个部分。

report.py 的中心部分主要用于读取 CSV 文件。例如,read_portfolio() 函数读取包含投资组合数据的文件,read_prices() 函数读取包含价格数据的文件。在这两个函数中,有很多底层的“精细的”事以及相似的特性。例如,它们都打开一个文件并使用 csv 模块来处理,并且将各种字段转换为新的类型。

如果真的需要对大量的文件进行解析,可能需要清理其中的一些内容使其更通用。这是我们的目标。

通过打开 Work/fileparse.py 文件开始本练习,该文件是我们将要写代码的地方。

练习 3.3:读取 CSV 文件

首先,让我们仅关注将 CSV 文件读入字典列表的问题。在 fileparse.py 中,定义一个如下所示的函数:

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

该函数将 CSV 文件读入字典列表中,但是隐藏了打开文件,使用 csv 模块处理,忽略空行等详细信息。

试试看:

提示: python3 -i fileparse.py.

>>> portfolio = parse_csv('Data/portfolio.csv')
>>> portfolio
[{'price': '32.20', 'name': 'AA', 'shares': '100'}, {'price': '91.10', 'name': 'IBM', 'shares': '50'}, {'price': '83.44', 'name': 'CAT', 'shares': '150'}, {'price': '51.23', 'name': 'MSFT', 'shares': '200'}, {'price': '40.37', 'name': 'GE', 'shares': '95'}, {'price': '65.10', 'name': 'MSFT', 'shares': '50'}, {'price': '70.44', 'name': 'IBM', 'shares': '100'}]
>>>

这很好,除了不能使用数据做任何有用的计算之外。因为所有的内容都是用字符串表示。我们将马上解决此问题,先让我们继续在此基础上进行构建。

练习 3.4:构建列选择器

在大部分情况下,你只对 CSV 文件中选定的列感兴趣,而不是所有数据。修改 parse_csv() 函数,以便让用户指定任意的列,如下所示:

>>> # Read all of the data
>>> portfolio = parse_csv('Data/portfolio.csv')
>>> portfolio
[{'price': '32.20', 'name': 'AA', 'shares': '100'}, {'price': '91.10', 'name': 'IBM', 'shares': '50'}, {'price': '83.44', 'name': 'CAT', 'shares': '150'}, {'price': '51.23', 'name': 'MSFT', 'shares': '200'}, {'price': '40.37', 'name': 'GE', 'shares': '95'}, {'price': '65.10', 'name': 'MSFT', 'shares': '50'}, {'price': '70.44', 'name': 'IBM', 'shares': '100'}] >>> # Read only some of the data
>>> shares_held = parse_csv('Data/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.py
import csv def parse_csv(filename, select=None):
'''
Parse a CSV file into a list of records
'''
with open(filename) as f:
rows = csv.reader(f) # Read the file headers
headers = next(rows) # If a column selector was given, find indices of the specified columns.
# Also narrow the set of headers used for resulting dictionaries
if select:
indices = [headers.index(colname) for colname in select]
headers = select
else:
indices = [] records = []
for row in rows:
if not row: # Skip rows with no data
continue
# Filter the row if specific columns were selected
if indices:
row = [ row[index] for index in indices ] # Make a dictionary
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:执行类型转换

修改 parse_csv() 函数,以便可以选择将类型转换应用到返回数据上。例如:

>>> portfolio = parse_csv('Data/portfolio.csv', types=[str, int, float])
>>> portfolio
[{'price': 32.2, 'name': 'AA', 'shares': 100}, {'price': 91.1, 'name': 'IBM', 'shares': 50}, {'price': 83.44, 'name': 'CAT', 'shares': 150}, {'price': 51.23, 'name': 'MSFT', 'shares': 200}, {'price': 40.37, 'name': 'GE', 'shares': 95}, {'price': 65.1, 'name': 'MSFT', 'shares': 50}, {'price': 70.44, 'name': 'IBM', 'shares': 100}] >>> shares_held = parse_csv('Data/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
...

修改 parse_csv() 文件以便通过创建元组列表来处理此类文件。例如:

>>> prices = parse_csv('Data/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 文件非常普遍,但还可能会遇到使用其它列分隔符(如 制表符(tab) 或空格符(space))的文件。例如,如下所示的 Data/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=' ')

修改 parse_csv() 函数以便也允许修改分隔符。

例如:

>>> portfolio = parse_csv('Data/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'price': '32.20', 'name': 'AA', 'shares': '100'}, {'price': '91.10', 'name': 'IBM', 'shares': '50'}, {'price': '83.44', 'name': 'CAT', 'shares': '150'}, {'price': '51.23', 'name': 'MSFT', 'shares': '200'}, {'price': '40.37', 'name': 'GE', 'shares': '95'}, {'price': '65.10', 'name': 'MSFT', 'shares': '50'}, {'price': '70.44', 'name': 'IBM', 'shares': '100'}]
>>>

说明

到目前为止,如果你已经完成,那么你创建了一个非常有用的库函数。你可以使用它去解析任意的 CSV 文件,选择感兴趣的列,执行类型转换,而不用对文件或者 csv 模块的内部工作有太多的担心。

目录 | 上一节 (3.1 脚本) | 下一节 (3.3 错误检查)

注:完整翻译见 https://github.com/codists/practical-python-zh

翻译:《实用的Python编程》03_02_More_functions的更多相关文章

  1. 翻译:《实用的Python编程》InstructorNotes

    实用的 Python 编程--讲师说明 作者:戴维·比兹利(David Beazley) 概述 对于如何使用我的课程"实用的 Python 编程"进行教学的问题,本文档提供一些通用 ...

  2. 翻译:《实用的Python编程》README

    欢迎光临 大约 25 年前,当我第一次学习 Python 时,发现 Python 竟然可以被高效地应用到各种混乱的工作项目上,我立即被震惊了.15 年前,我自己也将这种乐趣教授给别人.教学的结果就是本 ...

  3. 翻译:《实用的Python编程》05_02_Classes_encapsulation

    目录 | 上一节 (5.1 再谈字典) | 下一节 (6 生成器) 5.2 类和封装 创建类时,通常会尝试将类的内部细节进行封装.本节介绍 Python 编程中有关封装的习惯用法(包括私有变量和私有属 ...

  4. 翻译:《实用的Python编程》04_02_Inheritance

    目录 | 上一节 (4.1 类) | 下一节 (4.3 特殊方法) 4.2 继承 继承(inheritance)是编写可扩展程序程序的常用手段.本节对继承的思想(idea)进行探讨. 简介 继承用于特 ...

  5. 翻译:《实用的Python编程》01_02_Hello_world

    目录 | 上一节 (1.1 Python) | 下一节 (1.3 数字) 1.2 第一个程序 本节讨论有关如何创建一个程序.运行解释器和调试的基础知识. 运行 Python Python 程序始终在解 ...

  6. 翻译:《实用的Python编程》03_03_Error_checking

    目录 | 上一节 (3.2 深入函数) | 下一节 (3.4 模块) 3.3 错误检查 虽然前面已经介绍了异常,但本节补充一些有关错误检查和异常处理的其它细节. 程序是如何运行失败的 Python 不 ...

  7. 翻译:《实用的Python编程》03_04_Modules

    目录 | 上一节 (3.3 错误检查) | 下一节 (3.5 主模块) 3.4 模块 本节介绍模块的概念以及如何使用跨多个文件的函数. 模块和导入 任何一个 Python 源文件都是一个模块. # f ...

  8. 翻译:《实用的Python编程》03_05_Main_module

    目录 | 上一节 (3.4 模块) | 下一节 (3.6 设计讨论) 3.5 主模块 本节介绍主程序(主模块)的概念 主函数 在许多编程语言中,存在一个主函数或者主方法的概念. // c / c++ ...

  9. 翻译:《实用的Python编程》04_01_Class

    目录 | 上一节 (3.6 设计讨论) | 下一节 (4.2 继承) 4.1 类 本节介绍 class 语句以及创建新对象的方式. 面向对象编程(OOP) 面向对象编程是一种将代码组织成对象集合的编程 ...

随机推荐

  1. hdu3565 Bi-peak Number (有上界和下界的数位dp)

    Problem Description A peak number is defined as continuous digits {D0, D1 - Dn-1} (D0 > 0 and n & ...

  2. 1561: (More) Multiplication

    Description Educators are always coming up with new ways to teach math to students. In 2011, an educ ...

  3. Educational Codeforces Round 89 (Rated for Div. 2)D. Two Divisors 线性筛质因子

    题目链接:D:Two Divisors 题意: 给你n个数,对于每一个数vi,你需要找出来它的两个因子d1,d2.这两个因子要保证gcd(d1+d2,vi)==1.输出的时候输出两行,第一行输出每一个 ...

  4. Codeforces Round #680 (Div. 2, based on Moscow Team Olympiad) C. Division (数学)

    题意:有两个数\(p\)和\(q\),找到一个最大的数\(x\),使得\(p\ mod\ x=0\)并且\(x\ mod\ q\ne 0\). 题解:首先,如果\(p\ mod\ q\ne0\),那么 ...

  5. Codeforces Round #681 (Div. 2, based on VK Cup 2019-2020 - Final) C. The Delivery Dilemma (贪心,结构体排序)

    题意:你要买\(n\)份午饭,你可以选择自己去买,或者叫外卖,每份午饭\(i\)自己去买需要消耗时间\(b_i\),叫外卖需要\(a_i\),外卖可以同时送,自己只能买完一份后回家再去买下一份,问最少 ...

  6. python2.* 版本 与 3.* 版本中的区别

    目录 Unicode编码 print函数 raw_input() 和 input( ) 不等运算符 数据类型 除法 map 和 filter Unicode编码 python2.x 解释器默认编码格式 ...

  7. codeforces - 15C Industrial Nim(位运算+尼姆博弈)

    C. Industrial Nim time limit per test 2 seconds memory limit per test 64 megabytes input standard in ...

  8. spring-cloud-netflix-hystrix-dashboard

    Hystrix-dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix Dashboard我们可以在直观地看到各Hystrix Command的请求响应时间, 请求成功率等数 ...

  9. hdu5693D++游戏 区间DP-暴力递归

    主要的收获是..如何优化你递推式里面不必要的决策 之前的代码 这个代码在HDU超时了,这就对了..这个复杂度爆炸.. 但是这个思路非常地耿直..那就是只需要暴力枚举删两个和删三个的情况,于是就非常耿直 ...

  10. SpringBoot进阶教程(七十)SkyWalking

    流行的APM(Application Performance Management工具有很多,比如Cat.Zipkin.Pinpoint.SkyWalking.优秀的监控工具还有很多,其它比如还有za ...