【翻译】《利用Python进行数据分析·第2版》第3章(下)Python的数据结构、函数和文件
作者:SeanCheney Python爱好者社区专栏作者
简书专栏:https://www.jianshu.com/u/130f76596b02
前文传送门:
【翻译】《利用Python进行数据分析·第2版》第1章 准备工作
【翻译】《利用Python进行数据分析·第2版》第2章(上)Python语法基础,IPython和Jupyter
【翻译】《利用Python进行数据分析·第2版》第2章(中)Python语法基础,IPython和Jupyter
【翻译】《利用Python进行数据分析·第2版》第2章(下)Python语法基础,IPython和Jupyter
【翻译】《利用Python进行数据分析·第2版》第3章(上)Python的数据结构、函数和文件
【翻译】《利用Python进行数据分析·第2版》第3章(中)Python的数据结构、函数和文件
3.2 函数
函数是Python中最主要也是最重要的代码组织和复用手段。作为最重要的原则,如果你要重复使用相同或非常类似的代码,就需要写一个函数。通过给函数起一个名字,还可以提高代码的可读性。
函数使用def
关键字声明,用return
关键字返回值:
def my_function(x, y, z=1.5): if z > 1: return z * (x + y) else: return z / (x + y)
同时拥有多条return语句也是可以的。如果到达函数末尾时没有遇到任何一条return语句,则返回None。
函数可以有一些位置参数(positional)和一些关键字参数(keyword)。关键字参数通常用于指定默认值或可选参数。在上面的函数中,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)
这种写法可以提高可读性。
命名空间、作用域,和局部函数
函数可以访问两种不同作用域中的变量:全局(global)和局部(local)。Python有一种更科学的用于描述变量作用域的名称,即命名空间(namespace)。任何在函数中赋值的变量默认都是被分配到局部命名空间(local namespace)中的。局部命名空间是在函数被调用时创建的,函数参数会立即填入该命名空间。在函数执行完毕之后,局部命名空间就会被销毁(会有一些例外的情况,具体请参见后面介绍闭包的那一节)。看看下面这个函数:
def func(): a = [] for i in range(5): a.append(i)
调用func()之后,首先会创建出空列表a,然后添加5个元素,最后a会在该函数退出的时候被销毁。假如我们像下面这样定义a:
a = [] def func(): for i in range(5): a.append(i)
虽然可以在函数中对全局变量进行赋值操作,但是那些变量必须用global关键字声明成全局的才行:
In [168]: a = None In [169]: def bind_a_variable(): .....: global a .....: a = [] .....: bind_a_variable() .....: In [170]: print(a) []
注意:我常常建议人们不要频繁使用global关键字。因为全局变量一般是用于存放系统的某些状态的。如果你发现自己用了很多,那可能就说明得要来点儿面向对象编程了(即使用类)。
返回多个值
在我第一次用Python编程时(之前已经习惯了Java和C++),最喜欢的一个功能是:函数可以返回多个值。下面是一个简单的例子:
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}
取决于工作内容,第二种方法可能很有用。
函数也是对象
由于Python函数都是对象,因此,在其他语言中较难表达的一些设计思想在Python中就要简单很多了。假设我们有下面这样一个字符串数组,希望对其进行一些数据清理工作并执行一堆转换:
In [171]: 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
结果如下所示:
In [173]: clean_strings(states) Out[173]: ['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
然后我们就有了:
In [175]: clean_strings(states, clean_ops) Out[175]: ['Alabama', 'Georgia', 'Georgia', 'Georgia', 'Florida', 'South Carolina', 'West Virginia']
这种多函数模式使你能在很高的层次上轻松修改字符串的转换方式。此时的clean_strings也更具可复用性!
还可以将函数用作其他函数的参数,比如内置的map函数,它用于在一组数据上应用一个函数:
In [176]: for x in map(remove_punctuation, states): .....: print(x) Alabama Georgia Georgia georgia FlOrIda south carolina West virginia
匿名(lambda)函数
Python支持一种被称为匿名的、或lambda函数。它仅由单条语句组成,该语句的结果就是返回值。它是通过lambda关键字定义的,这个关键字没有别的含义,仅仅是说“我们正在声明的是一个匿名函数”。
def short_function(x): return x * 2 equiv_anon = lambda x: x * 2
本书其余部分一般将其称为lambda函数。它们在数据分析工作中非常方便,因为你会发现很多数据转换函数都以函数作为参数的。直接传入lambda函数比编写完整函数声明要少输入很多字(也更清晰),甚至比将lambda函数赋值给一个变量还要少输入很多字。看看下面这个简单得有些傻的例子:
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)
虽然你可以直接编写[x *2for x in ints],但是这里我们可以非常轻松地传入一个自定义运算给apply_to_list函数。
再来看另外一个例子。假设有一组字符串,你想要根据各字符串不同字母的数量对其进行排序:
In [177]: strings = ['foo', 'card', 'bar', 'aaaa', 'abab']
这里,我们可以传入一个lambda函数到列表的sort方法:
In [178]: strings.sort(key=lambda x: len(set(list(x)))) In [179]: strings Out[179]: ['aaaa', 'foo', 'abab', 'bar', 'card']
笔记:lambda函数之所以会被称为匿名函数,与def声明的函数不同,原因之一就是这种函数对象本身是没有提供名称name属性。
柯里化:部分参数应用
柯里化(currying)是一个有趣的计算机科学术语,它指的是通过“部分参数应用”(partial argument application)从现有函数派生出新函数的技术。例如,假设我们有一个执行两数相加的简单函数:
def add_numbers(x, y): return x + y
通过这个函数,我们可以派生出一个新的只有一个参数的函数——add_five,它用于对其参数加5:
add_five = lambda y: add_numbers(5, y)
add_numbers的第二个参数称为“柯里化的”(curried)。这里没什么特别花哨的东西,因为我们其实就只是定义了一个可以调用现有函数的新函数而已。内置的functools模块可以用partial函数将此过程简化:
from functools import partial add_five = partial(add_numbers, 5)
生成器
能以一种一致的方式对序列进行迭代(比如列表中的对象或文件中的行)是Python的一个重要特点。这是通过一种叫做迭代器协议(iterator protocol,它是一种使对象可迭代的通用方式)的方式实现的,一个原生的使对象可迭代的方法。比如说,对字典进行迭代可以得到其所有的键:
In [180]: some_dict = {'a': 1, 'b': 2, 'c': 3} In [181]: for key in some_dict: .....: print(key) a b c
当你编写for key in some_dict时,Python解释器首先会尝试从some_dict创建一个迭代器:
In [182]: dict_iterator = iter(some_dict) In [183]: dict_iterator Out[183]: <dict_keyiterator at 0x7fbbd5a9f908>
迭代器是一种特殊对象,它可以在诸如for循环之类的上下文中向Python解释器输送对象。大部分能接受列表之类的对象的方法也都可以接受任何可迭代对象。比如min、max、sum等内置方法以及list、tuple等类型构造器:
In [184]: list(dict_iterator) Out[184]: ['a', 'b', 'c']
生成器(generator)是构造新的可迭代对象的一种简单方式。一般的函数执行之后只会返回单个值,而生成器则是以延迟的方式返回一个值序列,即每返回一个值之后暂停,直到下一个值被请求时再继续。要创建一个生成器,只需将函数中的return替换为yeild即可:
def squares(n=10): print('Generating squares from 1 to {0}'.format(n ** 2)) for i in range(1, n + 1): yield i ** 2
调用该生成器时,没有任何代码会被立即执行:
In [186]: gen = squares() In [187]: gen Out[187]: <generator object squares at 0x7fbbd5ab4570>
直到你从该生成器中请求元素时,它才会开始执行其代码:
In [188]: for x in gen: .....: print(x, end=' ') Generating squares from 1 to 100 1 4 9 16 25 36 49 64 81 100
生成器表达式
另一种更简洁的构造生成器的方法是使用生成器表达式(generator expression)。这是一种类似于列表、字典、集合推导式的生成器。其创建方式为,把列表推导式两端的方括号改成圆括号:
In [189]: gen = (x ** 2 for x in range(100)) In [190]: gen Out[190]: <generator object <genexpr> at 0x7fbbd5ab29e8>
它跟下面这个冗长得多的生成器是完全等价的:
def _make_gen(): for x in range(100): yield x ** 2 gen = _make_gen()
生成器表达式也可以取代列表推导式,作为函数参数:
In [191]: sum(x ** 2 for x in range(100)) Out[191]: 328350 In [192]: dict((i, i **2) for i in range(5)) Out[192]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
itertools模块
标准库itertools模块中有一组用于许多常见数据算法的生成器。例如,groupby可以接受任何序列和一个函数。它根据函数的返回值对序列中的连续元素进行分组。下面是一个例子:
In [193]: import itertools In [194]: first_letter = lambda x: x[0] In [195]: names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven'] In [196]: 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']
表3-2中列出了一些我经常用到的itertools函数。建议参阅Python官方文档,进一步学习。
表3-2 一些有用的itertools函数
错误和异常处理
优雅地处理Python的错误和异常是构建健壮程序的重要部分。在数据分析中,许多函数函数只用于部分输入。例如,Python的float函数可以将字符串转换成浮点数,但输入有误时,有ValueError
错误:
In [197]: float('1.2345') Out[197]: 1.2345 In [198]: 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的错误,让它返回输入值。我们可以写一个函数,在try/except中调用float:
def attempt_float(x): try: return float(x) except: return x
当float(x)抛出异常时,才会执行except的部分:
In [200]: attempt_float('1.2345') Out[200]: 1.2345 In [201]: attempt_float('something') Out[201]: 'something'
你可能注意到float抛出的异常不仅是ValueError:
In [202]: 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错误(输入不是字符串或数值)可能是合理的bug。可以写一个异常类型:
def attempt_float(x): try: return float(x) except ValueError: return x
然后有:
In [204]: 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()
IPython的异常
如果是在%run一个脚本或一条语句时抛出异常,IPython默认会打印完整的调用栈(traceback),在栈的每个点都会有几行上下文:
In [10]: %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标准解释器的极大优点。你可以用魔术命令%xmode
,从Plain(与Python标准解释器相同)到Verbose(带有函数的参数值)控制文本显示的数量。后面可以看到,发生错误之后,(用%debug或%pdb magics)可以进入stack进行事后调试。
3.3 文件和操作系统
本书的代码示例大多使用诸如pandas.read_csv之类的高级工具将磁盘上的数据文件读入Python数据结构。但我们还是需要了解一些有关Python文件处理方面的基础知识。好在它本来就很简单,这也是Python在文本和文件处理方面的如此流行的原因之一。
为了打开一个文件以便读写,可以使用内置的open函数以及一个相对或绝对的文件路径:
In [207]: path = 'examples/segismundo.txt' In [208]: f = open(path)
默认情况下,文件是以只读模式('r')打开的。然后,我们就可以像处理列表那样来处理这个文件句柄f了,比如对行进行迭代:
for line in f: pass
从文件中取出的行都带有完整的行结束符(EOL),因此你常常会看到下面这样的代码(得到一组没有EOL的行):
In [209]: lines = [x.rstrip() for x in open(path)] In [210]: lines Out[210]: ['Sueña el rico en su riqueza,', 'que más cuidados le ofrece;', '', 'sueña el pobre que padece', 'su miseria y su pobreza;', '', 'sueña el que a medrar empieza,', 'sueña el que afana y pretende,', 'sueña el que agravia y ofende,', '', 'y en el mundo, en conclusión,', 'todos sueñan lo que son,', 'aunque ninguno lo entiende.', '']
如果使用open创建文件对象,一定要用close关闭它。关闭文件可以返回操作系统资源:
In [211]: f.close()
用with语句可以可以更容易地清理打开的文件:
In [212]: with open(path) as f:
.....: lines = [x.rstrip() for x in f]
这样可以在退出代码块时,自动关闭文件。
如果输入f =open(path,'w'),就会有一个新文件被创建在examples/segismundo.txt,并覆盖掉该位置原来的任何数据。另外有一个x文件模式,它可以创建可写的文件,但是如果文件路径存在,就无法创建。表3-3列出了所有的读/写模式。
表3-3 Python的文件模式
对于可读文件,一些常用的方法是read、seek和tell。read会从文件返回字符。字符的内容是由文件的编码决定的(如UTF-8),如果是二进制模式打开的就是原始字节:
In [213]: f = open(path) In [214]: f.read(10) Out[214]: 'Sueña el r' In [215]: f2 = open(path, 'rb') # Binary mode In [216]: f2.read(10) Out[216]: b'Sue\xc3\xb1a el '
read模式会将文件句柄的位置提前,提前的数量是读取的字节数。tell可以给出当前的位置:
In [217]: f.tell() Out[217]: 11 In [218]: f2.tell() Out[218]: 10
尽管我们从文件读取了10个字符,位置却是11,这是因为用默认的编码用了这么多字节才解码了这10个字符。你可以用sys模块检查默认的编码:
In [219]: import sys In [220]: sys.getdefaultencoding() Out[220]: 'utf-8'
seek将文件位置更改为文件中的指定字节:
In [221]: f.seek(3) Out[221]: 3 In [222]: f.read(1) Out[222]: 'ñ'
最后,关闭文件:
In [223]: f.close() In [224]: f2.close()
向文件写入,可以使用文件的write或writelines方法。例如,我们可以创建一个无空行版的prof_mod.py:
In [225]: with open('tmp.txt', 'w') as handle: .....: handle.writelines(x for x in open(path) if len(x) > 1) In [226]: with open('tmp.txt') as f: .....: lines = f.readlines() In [227]: lines Out[227]: ['Sueña el rico en su riqueza,\n', 'que más cuidados le ofrece;\n', 'sueña el pobre que padece\n', 'su miseria y su pobreza;\n', 'sueña el que a medrar empieza,\n', 'sueña el que afana y pretende,\n', 'sueña el que agravia y ofende,\n', 'y en el mundo, en conclusión,\n', 'todos sueñan lo que son,\n', 'aunque ninguno lo entiende.\n']
表3-4列出了一些最常用的文件方法。
表3-4 Python重要的文件方法或属性
文件的字节和Unicode
Python文件的默认操作是“文本模式”,也就是说,你需要处理Python的字符串(即Unicode)。它与“二进制模式”相对,文件模式加一个b。我们来看上一节的文件(UTF-8编码、包含非ASCII字符):
In [230]: with open(path) as f: .....: chars = f.read(10) In [231]: chars Out[231]: 'Sueña el r'
UTF-8是长度可变的Unicode编码,所以当我从文件请求一定数量的字符时,Python会从文件读取足够多(可能少至10或多至40字节)的字节进行解码。如果以“rb”模式打开文件,则读取确切的请求字节数:
In [232]: with open(path, 'rb') as f: .....: data = f.read(10) In [233]: data Out[233]: b'Sue\xc3\xb1a el '
取决于文本的编码,你可以将字节解码为str对象,但只有当每个编码的Unicode字符都完全成形时才能这么做:
In [234]: data.decode('utf8') Out[234]: 'Sueña el ' In [235]: data[:4].decode('utf8') --------------------------------------------------------------------------- UnicodeDecodeError Traceback (most recent call last) <ipython-input-235-300e0af10bb7> in <module>() ----> 1 data[:4].decode('utf8') UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 3: unexpecte d end of data
文本模式结合了open的编码选项,提供了一种更方便的方法将Unicode转换为另一种编码:
In [236]: sink_path = 'sink.txt' In [237]: with open(path) as source: .....: with open(sink_path, 'xt', encoding='iso-8859-1') as sink: .....: sink.write(source.read()) In [238]: with open(sink_path, encoding='iso-8859-1') as f: .....: print(f.read(10)) Sueña el r
注意,不要在二进制模式中使用seek。如果文件位置位于定义Unicode字符的字节的中间位置,读取后面会产生错误:
In [240]: f = open(path) In [241]: f.read(5) Out[241]: 'Sueña' In [242]: f.seek(4) Out[242]: 4 In [243]: f.read(1) --------------------------------------------------------------------------- UnicodeDecodeError Traceback (most recent call last) <ipython-input-243-7841103e33f5> in <module>() ----> 1 f.read(1) /miniconda/envs/book-env/lib/python3.6/codecs.py in decode(self, input, final) 319 # decode input (taking the buffer into account) 320 data = self.buffer + input --> 321 (result, consumed) = self._buffer_decode(data, self.errors, final ) 322 # keep undecoded input until the next call 323 self.buffer = data[consumed:] UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb1 in position 0: invalid s tart byte In [244]: f.close()
如果你经常要对非ASCII字符文本进行数据分析,通晓Python的Unicode功能是非常重要的。更多内容,参阅Python官方文档。
3.4 结论
我们已经学过了Python的基础、环境和语法,接下来学习NumPy和Python的面向数组计算。
赞赏作者
Python爱好者社区历史文章大合集:
Python爱好者社区历史文章列表(每周append更新一次)
关注后在公众号内回复“课程”即可获取:
小编的Python入门视频课程!!!
崔老师爬虫实战案例免费学习视频。
丘老师数据科学入门指导免费学习视频。
陈老师数据分析报告制作免费学习视频。
玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。
丘老师Python网络爬虫实战免费学习视频。