目录
函数是Python中最重要、最基础的代码组织和代码复用方式。根据经验,如果你需要多次重复相同或类似的代码,就非常值得写一个可复用的函数。通过给一组Python语句一个函数名,形成的函数可以使你的代码更加可读。
函数声明时使用def关键字,返回时使用return关键字:
def my_function(x, y, z=1.5):
if z > 1:
return z * (x + y)
else:
return z / (x + y)
有多条返回语句是没有问题的。如果Python达到函数的尾部时仍然没有遇到return语句,就会自动返回None。
每个函数都可以有位置参数和关键字参数。关键字参数最常用于指定默认值或可选参数。在前面的函数中,x和y是位置参数,z是关键字参数。这意味着函数可以通过以下任意一种方式进行调用:
my_function(5, 6, z=0.7)
my_function(3.14, 7, 3.5)
my_function(10, 20)
函数参数的主要限制是关键字参数必须跟在位置参数(如果有的话)后。你可以按照任意顺序指定关键字参数,这可以让你不必强行记住函数参数的顺序,而只需用参数名指定。
也可以使用关键字参数向位置参数传参。在前面的例子中,我们也可以这样写:
# 在部分场景中,这样做有助于代码可读性。
my_function(x=5, y=6, z=7)
my_function(y=6, x=5, z=7)
1 命名空间、作用域和本地函数
函数有两种连接变量的方式:全局、本地。在Python中另一种更贴切地描述变量作用域的名称是命名空间。在函数内部,任意变量都是默认分配到本地命名空间的。本地命名空间是在函数被调用时生成的,并立即由函数的参数填充。当函数执行结束后,本地命名空间就会被销毁(除了一些本章范围外的特殊情况)。考虑以下函数:
def func():
a = []
for i in range(5):
a.append(i)
当调用func()时,空的列表a会被创建,五个元素被添加到列表,之后a会在函数退出时被销毁。假设我们像下面这样声明a:
a = []
def func():
for i in range(5):
a.append(i)
在函数外部给变量赋值是可以的,但是那些变量必须使用global关键字声明为全局变量
>>> a = None
>>> def bind_a_variable():
>>> global a
>>> a = []
>>> bind_a_variable()
>>> print(a)
[]
global关键字的用法。通常全局变量用来存储系统中的某些状态。如果你发现你大量使用了全局变量,可能表明你需要面向对象编程(使用类)。
2 返回多个值
def f():
a = 5
b = 6
c = 7
return a, b, c
a, b, c = f()
在数据分析和其他科研应用中,你可能会发现经常需要返回多个值。这里实质上是返回了一个对象,也就是元组,而元组之后又被拆包为多个结果变量。在前面的例子中,我们可以用下面的代码代替:
return_value = f()
在这个例子中,return_value是一个包含3个元素的元组。像之前那样一次返回多个值还有一种潜在的、更有吸引力的实现:
# 具体用哪种技术取决于你需要做什么
def f():
a = 5
b = 6
c = 7
return {'a' : a, 'b' : b, 'c' : c}
3 函数是对象
由于Python的函数是对象,很多在其他语言中比较难的构造在Python中非常容易实现。假设我们正在做数据清洗,需要将一些变形应用到下列字符串列表中:
>>> states = [' Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
>>> 'south carolina##', 'West virginia?']
任何处理过用户提交数据的人都对这样的数据感到凌乱。为了使这些数据整齐、可用于分析,有很是事情需要做:去除空格,移除标点符号,调整适当的大小写。一种方式是使用内建的字符串方法,结合标准库中的正则表达式模块re:
import re
def clean_strings(strings):
result = []
for value in strings:
value = value.strip()
value = re.sub('[!#?]', '', value)
value = value.title()
result.append(value)
return result
# 结果如下
>>> clean_strings(states)
['Alabama',
'Georgia',
'Georgia',
'Georgia',
'Florida',
'South Carolina',
'West Virginia']
另一种会让你觉得有用的实现就是将特定的列表操作应用到某个字符串的集合上:
def remove_punctuation(value):
return re.sub('[!#?]', '', value)
clean_ops = [str.strip, remove_punctuation, str.title]
def clean_strings(strings, ops):
result = []
for value in strings:
for function in ops:
value = function(value)
result.append(value)
return result
>>> clean_strings(states, clean_ops)
['Alabama',
'Georgia',
'Georgia',
'Georgia',
'Florida',
'South Carolina',
'West Virginia']
像这种更为函数化的模式可以使你在更高层次上方便地修改字符串变换方法。clean_strings函数现在也具有更强的复用性和通用性。
你可以将函数作为一个参数传给其他的函数,比如内建的map函数,可以将一个函数应用到一个序列上:
>>> for x in map(remove_punctuation, states):
>>> print(x)
Alabama
Georgia
Georgia
georgia
FlOrIda
south carolina
West virginia
4 匿名(Lambda)函数
Python支持所谓的匿名或lambda函数。匿名函数是一种通过单个语句生成函数的方式,其结果是返回值。匿名函数使用lambda关键字定义,该关键字仅表达“我们声明一个匿名函数”的意思:
def short_function(x):
return x * 2
equiv_anon = lambda x: x * 2
匿名函数在数据分析中非常方便,因为在很多案例中数据变形函数都可以作为函数的参数。匿名函数代码量小(也更为清晰),将它作为参数进行传值,比写一个完整的函数或者将匿名函数赋值给局部变量更好。
def apply_to_list(some_list, f):
return [f(x) for x in some_list]
ints = [4, 0, 1, 5, 6]
apply_to_list(ints, lambda x: x * 2)
# 假设你想要根据字符串中不同字母的数量对一个字符串集合进行排序:
>>> strings = ['foo', 'card', 'bar', 'aaaa', 'abab']
这里我们可以将一个匿名函数传给列表的sort方法
>>> strings.sort(key=lambda x: len(set(list(x))))
>>> strings
['aaaa', 'foo', 'abab', 'bar', 'card']
和def关键字声明的函数不同,匿名函数对象自身并没有一个显式的__name__属性,这是lambda函数被称为匿名函数的一个原因。
5 柯里化:部分参数应用
柯里化是计算机科学术语(以数学家Haskell Curry命名),它表示通过部分参数应用的方式从已有的函数中衍生出新的函数。例如,假设我们有一个不重要的函数,其功能是将两个数加一起:
def add_numbers(x, y):
return x + y
使用这个函数,我们可以衍生出一个只有一个变量的新函数,add_five,可以给参数加上5:
add_five = lambda y: add_numbers(5, y)
第二个参数对于函数add_numers就是柯里化了。这里并没有什么神奇的地方,我们真正做的事只是定义了一个新函数,这个新函数调用了已经存在的函数。内建的functools模块可以使用pratial函数简化这种处理:
from functools import partial
add_five = partial(add_numbers, 5)
6 生成器
通过一致的方式遍历序列,例如列表中的对象或者文件中的一行行内容,这是Python的一个重要特性。这个特性是通过迭代器协议来实现的,迭代器协议是一种令对象可遍历的通用方式。例如,遍历一个字典,获得字典的键:
>>> some_dict = {'a': 1, 'b': 2, 'c': 3}
>>> for key in some_dict:
>>> print(key)
a
b
c
当你写下for key in some_dict的语句时,Python解释器首先尝试根据some_dict生成一个迭代器:
>>> dict_iterator = iter(some_dict)
>>> dict_iterator
<dict_keyiterator at 0x7fbbd5a9f908>
迭代器就是一种用于在上下文中(比如for循环)向Python解释器生成对象的对象。大部分以列表或列表型对象为参数的方法都可以接收任意的迭代器对象。包括内建方法比如min、max和sum,以及类型构造函数比如list和tuple:
>>> list(dict_iterator)
['a', 'b', 'c']
生成器是构造新的可遍历对象的一种非常简洁的方式。普通函数执行并一次返回单个结果,而生成器则“惰性”地返回一个多结果序列,在每一个元素产生之后暂停,直到下一个请求。如需创建一个生成器,只需要在函数中将返回关键字return替换为yield关键字
def squares(n=10):
print('Generating squares from 1 to {0}'.format(n ** 2))
for i in range(1, n + 1):
yield i ** 2
当你实际调用生成器时,代码并不会立即执行:
>>> gen = squares()
>>> gen
<generator object squares at 0x7fbbd5ab4570>
直到你请求生成器中的元素时,它才会执行它的代码:
>>> for x in gen:
>>> print(x, end=' ')
Generating squares from 1 to 100
1 4 9 16 25 36 49 64 81 100
6.1 生成器表达式
用生成器表达式来创建生成器更为简单。生成器表达式与列表、字典、集合的推导式很类似,创建一个生成器表达式,只需要将列表推导式的中括号替换为小括号即可:
>>> gen = (x ** 2 for x in range(100))
>>> gen
<generator object <genexpr> at 0x7fbbd5ab29e8>
上面的代码与下面更为复杂的生成器是等价的:
def _make_gen():
for x in range(100):
yield x ** 2
gen = _make_gen()
在很多情况下,生成器表达式可以作为函数参数用于替代列表推导式:
>>> sum(x ** 2 for x in range(100))
328350
>>> dict((i, i **2) for i in range(5))
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
6.2 itertools模块
标准库中的itertools模块是适用于大多数数据算法的生成器集合。例如,groupby可以根据任意的序列和一个函数,通过函数的返回值对序列中连续的元素进行分组。
>>> import itertools
>>> first_letter = lambda x: x[0]
>>> names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']
>>> for letter, names in itertools.groupby(names, first_letter):
>>> print(letter, list(names)) # names is a generator
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']
一些有用的itertools函数
7 错误和异常处理
优雅地处理Python的错误或异常是构建稳定程序的重要组成部分。在数据分析应用中,很多函数只能处理特定的输入。例如,Python的float函数可以将字符串转换为浮点数字,但是对不正确的输入会产生ValueError:
>>> float('1.2345')
1.2345
>>> float('something')
--------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-198-439904410854> in <module>()
----> 1 float('something')
ValueError: could not convert string to float: 'something'
假设我们想要在float函数运行失败时可以优雅地返回输入参数。我们可以通过将float函数写入一个try/except代码段来实现:
def attempt_float(x):
try:
return float(x)
except:
return x
如果float(x)执行时抛出了异常,则代码段中的except部分代码将会被执行:
>>> attempt_float('1.2345')
1.2345
>>> attempt_float('something')
'something'
float函数还会抛出其他的异常:
>>> float((1, 2))
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-202-842079ebb635> in <module>()
----> 1 float((1, 2))
TypeError: float() argument must be a string or a number, not 'tuple'
你可能只想处理ValueError,因为TypeError(输入的不是字符串或数值)可能表明你的程序中有个合乎语法的错误。为了实现这个目的,在except后面写下异常类型:
def attempt_float(x):
try:
return float(x)
except ValueError:
return x
然后我们可以得到:
>>> attempt_float((1, 2))
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-204-9bdfd730cead> in <module>()
----> 1 attempt_float((1, 2))
<ipython-input-203-3e06b8379b6b> in attempt_float(x)
1 def attempt_float(x):
2 try:
----> 3 return float(x)
4 except ValueError:
5 return x
TypeError: float() argument must be a string or a number, not 'tuple'
你可以通过将多个异常类型写成元组的方式同时捕获多个异常(小括号是必不可少的)
def attempt_float(x):
try:
return float(x)
except (TypeError, ValueError):
return x
某些情况下,你可能想要处理一个异常,但是你希望一部分代码无论try代码块是否报错都要执行。为了实现这个目的,使用finally关键字:
f = open(path, 'w')
try:
write_to_file(f)
finally:
f.close()
这样,我们可以让f在程序结束后总是关闭。类似地,你可以使用else来执行当try代码块成功执行时才会执行的代码:
f = open(path, 'w')
try:
write_to_file(f)
except:
print('Failed')
else:
print('Succeeded')
finally:
f.close()
7.1 IPython中的异常
如果当你正在用%run执行一个脚本或执行任何语句时报错,IPython将会默认打印出完整的调用堆栈跟踪(报错追溯),会将堆栈中每个错误点附近的几行上下文代码打印出:
>>> %run examples/ipython_bug.py
--------------------------------------------------------------------
AssertionError Traceback (most recent call last)
/home/wesm/code/pydata-book/examples/ipython_bug.py in <module>()
13 throws_an_exception()
14
---> 15 calling_things()
/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()
11 def calling_things():
12 works_fine()
---> 13 throws_an_exception()
14
15 calling_things()
/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()
7 a = 5
8 b = 6
----> 9 assert(a + b == 10)
10
11 def calling_things():
AssertionError:
比标准Python解释器提供更多额外的上下文是IPython的一大进步(标准Python解释器不提供任何额外的上下文)。你可以使用%xmode命令来控制上下文的数量,可以从Plain(普通)模式(与标准Python解释器一致)切换到Verbose(复杂)模式(可以显示函数的参数值以及更多有用信息)。你将会在下一章看到如何在错误发生后进入异常堆栈(使用%debug或%pdb命令)进行交互式事后调试。
8 文件与操作系统
打开文件进行读取或写入,需要使用内建函数open和绝对、相对路径:
>>> path = 'examples/segismundo.txt'
>>> f = open(path)
默认情况下,文件是以只读模式'r'打开的。之后我们可以像处理列表一样处理文件f,并遍历f中的行内容,代码如下:
for line in f:
pass
行内容会在行结尾标识(EOL)完整的情况下从文件中全部读出,所以你会经常看到一些代码,功能是将文件中的内容形成不带EOL的列表:
>>> lines = [x.rstrip() for x in open(path)]
>>> lines
['Sue '']
当你使用open来创建文件对象时,在结束操作时显式地关闭文件是非常重要的。关闭文件会将资源释放回操作系统:
>>> f.close()
另一种更简单的关闭文件的方式就是使用with语句:
>>> with open(path) as f:
>>> lines = [x.rstrip() for x in f]
使用with语句,文件会在with代码块结束后自动关闭。
如果我们写下f=open(path,'w'),一个新的文件会在examples/segismundo.txt的位置被创建(请小心!),并在同一路径下覆盖同名文件。还有一种'x'文件模式,它会创建可写的文件,但如果给定路径下已经存在同名文件就会创建失败。
对于可读文件,最常用的方法是read、seek和tell。read返回文件中一定量的字符,构成字符的内容是由文件的编码决定的(例如UTF-8),或者在二进制模式下打开文件读取简单的原生字节:
>>> f = open(path)
>>> f.read(10)
'SueOut[216]: b'Sue\xc3\xb1a el '
read方法通过读取的字节数来推进文件句柄的位置。tell方法可以给出句柄当前的位置:
>>> f.tell()
11
>>> f2.tell()
10
尽管我们从文件中读取了10个字符,但是当前的句柄位置是11,这是因为使用默认编码的情况下需要这么多字节来对10个字符进行解码。你可以用sys模块来检查文件的默认编码:
>>> import sys
>>> sys.getdefaultencoding()
'utf-8'
seek方法可以将句柄位置改变到文件中特定的字节:
>>> f.seek(3)
3
>>> f.read(1)
'/n'
# 之后,请牢记关闭文件
>>> f.close()
>>> f2.close()
Python文件模式
为了将本文写入文件,你可以使用文件对象的write或wirtelines方法。例如,我们可以创建一个没有空行的prof_mod.py,像这样:
>>> with open('tmp.txt', 'w') as handle:
>>> handle.writelines(x for x in open(path) if len(x) > 1)
>>> with open('tmp.txt') as f:
>>> lines = f.readlines()
>>> lines
['Sue 'aunque ninguno lo entiende.\n']
重要的Python文件方法或属性
8.1 字节与Unicode文件
默认的Python文件行为(无论是可读或可写)是文本模式,这意味着你需要处理Python字符串(比如Unicode)。与二进制模式不同,在二进制模式中你可以将b添加到文件模式中,然后进行读写。(包含了UTF-8编码的非ASCⅡ字符):
>>> with open(path) as f:
>>> chars = f.read(10)
>>> chars
'SueUTF-8是一种可变长度的Unicode编码,因此当我从文件中请求一定量的字符时,Python从文件中读取了足够的字节(可以少至10个字节,也可以多至40个字节),并进行了解码。如果我使用'rb'模式代替,则read请求提取了一定量的字节:
>>> with open(path, 'rb') as f:
>>> data = f.read(10)
>>> data
b'Sue\xc3\xb1a el '
根据文本编码,你可能会将字节解码为str对象,但是只有每个已编码的Unicode字符是完整的情况下,才能进行解码:
>>> data.decode('utf8')
'SueUnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 3: unexpecte
d end of data
文本模式下,利用open方法的选项参数encoding,Python提供了一种方便的方法将文件内容从Unicode编码转换为其他类型的编码:
>>> sink_path = 'sink.txt'
>>> with open(path) as source:
>>> with open(sink_path, 'xt', encoding='iso-8859-1') as sink:
>>> sink.write(source.read())
>>> with open(sink_path, encoding='iso-8859-1') as f:
>>> print(f.read(10))
Sue除了二进制模式,在打开文件时使用seek要当心。如果文件的句柄位置恰好在一个Unicode符号的字节中间时,后续的读取会导致错误:
>>> f = open(path)
>>> f.read(5)
'Sue/miniconda/envs/book-env/lib/python3.6/codecs.py in decode(self, input, final)
319 # 对输入解码(考虑buffer)
320 data = self.buffer + input
--> 321 (result, consumed) = self._buffer_decode(data, self.errors, final
)
322 # 保持未解码的输入直到下一次调用
323 self.buffer = data[consumed:]
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb1 in position 0: invalid s
tart byte
>>> f.close()