「来源:|Python爬虫与数据挖掘ID:crawler_python」
回复“书籍”即可获赠Python从入门到进阶共10本电子书
今
日
鸡
汤
诸葛大名垂宇宙,宗臣遗像肃清高。
导读:函数是Python中最重要、最基础的代码组织和代码复用方式。根据经验,如果你需要多次重复相同或类似的代码,就非常值得写一个可复用的函数。通过给一组Python语句一个函数名,形成的函数可以帮助你的代码更加可读。
函数声明时使用def关键字,返回时使用return关键字:
defmy_function(x,y,z=1.5):
ifz1:
returnz*(x+y)
else:
returnz/(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)
在部分场景中,这样做有助于代码可读性
01命名空间、作用域和本地函数
函数有两种连接变量的方式:全局、本地。在Python中另一种更贴切地描述变量作用域的名称是命名空间。在函数内部,任意变量都是默认分配到本地命名空间的。本地命名空间是在函数被调用时生成的,并立即由函数的参数填充。当函数执行结束后,本地命名空间就会被销毁(除了一些特殊情况)。考虑以下函数:
deffunc():
a=[]
foriinrange(5):
a.append(i)
当func()调用时,空的列表会被创建,五个元素被添加到列表,之后a会在函数退出时被销毁。假设我们像下面这样声明a:
a=[]
deffunc():
foriinrange(5):
a.append(i)
在函数外部给变量赋值是可以的,但是那变量必须使用global关键字声明为全局变量:
In[]:a=None
In[]:defbind_a_variable():
.....:globala
.....:a=[]
.....:bind_a_variable()
.....:
In[]:print(a)
[]
我简单的讲下global关键字的用法。通常全局变量用来存储系统中的某些状态。如果你发现你大量使用了全局变量,可能表明你需要面向对象编程(使用类)
02返回多个值
当我在使用Java和C++编程后第一次使用Python编程时,我最喜欢的特性就是使用简单语法就可以从函数中返回多个值。以下是代码:
deff():
a=5
b=6
c=7
returna,b,c
a,b,c=f()
在数据分析和其他科研应用,你可能会发现经常需要返回多个值。这里实质上是返回了一个对象,也就是元组,而元组之后又被拆包为多个结果变量。在前面的例子中,我们可以用下面的代码代替:
return_value=f()
在这个例子中,return_value是一个3个元素的元组。像之前那样一次返回多个值还有一种潜在的、更有吸引力的实现:
deff():
a=5
b=6
c=7
return{a:a,b:b,c:c}
具体用哪种技术取决于你需要做什么的事。
03函数是对象
由于Python的函数是对象,很多在其他语言中比较难的构造在Python中非常容易实现。假设我们正在做数据清洗,需要将一些变形应用到下列字符串列表中:
In[]:states=[Alabama,Georgia!,Georgia,georgia,FlOrIda,
.....:southcarolina##,Westvirginia?]
任何处理过用户提交数据的人都对这样的数据感到凌乱。为了使这些数据整齐、可用于分析,有很多是事情需要去做:去除空格、移除标点符号、调整适当的大小写。一种方式是使用内建的字符串方法,结合标准库中的正则表达式模块re:
importre
defclean_strings(strings):
result=[]
forvalueinstrings:
value=value.strip()
value=re.sub([!#?],,value)
value=value.title()
result.append(value)
returnresult
结果如下:
In[]:clean_strings(states)
Out[]:
[Alabama,
Georgia,
Georgia,
Georgia,
Florida,
SouthCarolina,
WestVirginia]
另一种会让你觉得有用的实现就是将特定的列表操作应用到某个字符串的集合上:
defremove_punctuation(value):
returnre.sub([!#?],,value)
clean_ops=[str.strip,remove_punctuation,str.title]
defclean_strings(strings,ops):
result=[]
forvalueinstrings:
forfunctioninops:
value=function(value)
result.append(value)
returnresult
结果如下:
In[]:clean_strings(states,clean_ops)
Out[]:
[Alabama,
Georgia,
Georgia,
Georgia,
Florida,
SouthCarolina,
WestVirginia]
像这种更为函数化的模式可以使你在更高层次上方便地修改字符串变换方法。clean_strings函数现在也具有更强的复用性、通用性。
你可以将函数作为一个参数传给其他的函数,比如内建的map函数,可以将一个函数应用到一个序列上:
In[]:forxinmap(remove_punctuation,states):
.....:print(x)
Alabama
Georgia
Georgia
georgia
FlOrIda
southcarolina
Westvirginia
04匿名(Lambda)函数
Python支持所谓的匿名或lambda函数。匿名函数是一种通过单个语句生成函数的方式,其结果是返回值。匿名函数使用lambda关键字定义,该关键字仅表达“我们声明一个匿名函数”的意思:
defshort_function(x):
returnx*2
equiv_anon=lambdax:x*2
匿名函数在数据分析中非常方便,因为在很多案例中数据变形函数都可以作为函数的参数。匿名函数代码量小(也更为清晰),将它作为参数进行传值,比写一个完整的函数或者将匿名函数赋值给局部变量更好。举个例子,考虑下面的不佳示例:
defapply_to_list(some_list,f):
return[f(x)forxinsome_list]
ints=[4,0,1,5,6]
apply_to_list(ints,lambdax:x*2)
你也可以写成[x*2forxinints],但是在这里我们能够简单地将一个自定义操作符传递给apply_to_list函数。
另一个例子,假设你想要根据字符串中不同字母的数量对一个字符串集合进行排序:
In[]:strings=[foo,card,bar,aaaa,abab]
这里我们可以将一个匿名函数传给列表的sort方法:
In[]:strings.sort(key=lambdax:len(set(list(x))))
In[]:strings
Out[]:[aaaa,foo,abab,bar,card]
和def关键字声明的函数不同,匿名函数对象自身并没有一个显式的__name__属性,这是lambda函数被称为匿名函数的一个原因。
05柯里化:部分函数应用
柯里化是计算机科学术语(以数学家HaskellCurry命名),它表示通过部分参数应用的方式从已有的函数中衍生出新的函数。例如,假设我们有一个不重要的函数,其功能是将两个数加一起:
defadd_numbers(x,y):
returnx+y
使用这个函数,我们可以衍生出一个只有一个变量的新函数,add_five,可以给参数加上5:
add_five=lambday:add_numbers(5,y)
第二个参数对于函数add_numers就是柯里化了。这里并没有什么神奇的地方,我们真正做的事只是定义了一个新函数,这个新函数调用了已经存在的函数。内建的functools模块可以使用pratial函数简化这种处理:
fromfunctoolsimportpartial
add_five=partial(add_numbers,5)
06生成器
通过一致的方式遍历序列,例如列表中的对象或者文件中的一行行内容,这是Python的一个重要特性。这个特性是通过迭代器协议来实现的,迭代器协议是一种令对象可遍历的通用方式。例如,遍历一个字典,获得字典的键:
In[]:some_dict={a:1,b:2,c:3}
In[]:forkeyinsome_dict:
.....:print(key)
a
b
c
当你写下forkeyinsome_dict的语句时,Python解释器首先尝试根据some_dict生成一个迭代器:
In[]:dict_iterator=iter(some_dict)
In[]:dict_iterator
Out[]:dict_keyiteratorat0x7fbbd5a9f
迭代器就是一种用于在上下文中(比如for循环)向Python解释器生成对象的对象。大部分以列表或列表型对象为参数的方法都可以接收任意的迭代器对象。包括内建方法比如min、max和sum,以及类型构造函数比如list和tuple:
In[]:list(dict_iterator)
Out[]:[a,b,c]
用迭代器构造新的可遍历对象是一种非常简洁的方式。普通函数执行并一次返回单个结果,而生成器则“惰性”地返回一个多结果序列,在每一个元素产生之后暂停,直到下一个请求。如需创建一个生成器,只需要在函数中将返回关键字return替换为yield关键字:
defsquares(n=10):
print(Generatingsquaresfrom1to{0}.format(n**2))
foriinrange(1,n+1):
yieldi**2
当你实际调用生成器时,代码并不会立即执行:
In[]:gen=squares()
In[]:gen
Out[]:generatorobjectsquaresat0x7fbbd5ab
直到你请求生成器中的元素时,它才会执行它的代码:
In[]:forxingen:
.....:print(x,end=)
Generatingsquaresfrom1to
481
1.生成器表达式
生成器表达式来创建生成器更为简单。生成器表达式与列表、字典、集合的推导式很类似,创建一个生成器表达式,只需要将列表推导式的中括号替换为小括号即可:
In[]:gen=(x**2forxinrange())
In[]:gen
Out[]:generatorobjectgenexprat0x7fbbd5ab29e8
上面的代码与下面更为复杂的生成器是等价的:
def_make_gen():
forxinrange():
yieldx**2
gen=_make_gen()
在很多情况下,生成器表达式可以作为函数参数用于替代列表推导式:
In[]:sum(x**2forxinrange())
Out[]:
In[]:dict((i,i**2)foriinrange(5))
Out[]:{0:0,1:1,2:4,3:9,4:16}
2.itertools模块
标准库中的itertools模块是适用于大多数数据算法的生成器集合。例如,groupby可以根据任意的序列和一个函数,通过函数的返回值对序列中连续的元素进行分组,参见下面的例子:
In[]:importitertools
In[]:first_letter=lambdax:x[0]
In[]:names=[Alan,Adam,Wes,Will,Albert,Steven]
In[]:forletter,namesinitertools.groupby(names,first_letter):
.....:print(letter,list(names))#names是一个生成器
A[Alan,Adam]
W[Wes,Will]
A[Albert]
S[Steven]
下表是一些我认为经常用到的itertools函数的列表。你可以通过查询Python官方文档来获得更多关于内建工具库的信息。
07错误和异常处理
优雅地处理Python的错误或异常是构建稳定程序的重要组成部分。在数据分析应用中,很多函数只能处理特定的输入。例如,Python的float函数可以将字符串转换为浮点数字,但是对不正确的输入会产生ValueError:
In[]:float(1.)
Out[]:1.
In[]:float(something)
---------------------------------------------------------------------------
ValueErrorTraceback(mostrecentcalllast)
ipython-input--inmodule()
----1float(something)
ValueError:couldnotconvertstringtofloat:something
假设我们想要在float函数运行失败时可以优雅地返回输入参数。我们可以通过将float函数写入一个try/except代码段来实现:
defattempt_float(x):
try:
returnfloat(x)
except:
returnx
如果float(x)执行时抛出了异常,则代码段中的except部分代码将会被执行:
In[]:attempt_float(1.)
Out[]:1.
In[]:attempt_float(something)
Out[]:something
你可能会注意到,除了ValueError,float函数还会抛出其他的异常:
In[]:float((1,2))
---------------------------------------------------------------------------
TypeErrorTraceback(mostrecentcalllast)
ipython-input--ebbinmodule()
----1float((1,2))
TypeError:float()argumentmustbeastringoranumber,nottuple
你可能只想处理ValueError,因为TypeError(输入的不是字符串或数值)可能表明你的程序中有个合乎语法的错误。为了实现这个目的,在except后面写下异常类型:
defattempt_float(x):
try:
returnfloat(x)
exceptValueError:
returnx
然后我们可以得到:
In[]:attempt_float((1,2))
4exceptValueError:
5returnx
TypeError:float()argumentmustbeastringoranumber,nottuple
你可以通过将多个异常类型写成元组的方式同事捕获多个异常(小括号是必不可少的):
defattempt_float(x):
try:
returnfloat(x)
except(TypeError,ValueError):
returnx
某些情况下,你可能想要处理一个异常,但是你希望一部分代码无论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将会默认打印出完整的调用堆栈跟踪(报错追溯),会将堆栈中每个错误点附近的几行上下文代码打印出:
In[10]:%runexamples/ipython_bug.py
12works_fine()
---13throws_an_exception()
14
15calling_things()
/home/wesm/code/pydata-book/examples/ipython_bug.pyinthrows_an_exception()
7a=5
8b=6
----9assert(a+b==10)
10
11defcalling_things():
AssertionError:
比标准Python解释器提供更多额外的上下文是IPython的一大进步(标准Python解释器不提供任何额外的上下文)。你可以使用%xmode命令来控制上下文的数量,可以从Plain(普通)模式(与标准Python解释器一致)切换到Verbose(复杂)模式(可以显示函数的参数值以及更多有用信息)。
关于作者:韦斯·麦金尼(WesMcKinney)是流行的Python开源数据分析库pandas的创始人。他是一名活跃的演讲者,也是Python数据社区和Apache软件基金会的Python/C++开源开发者。目前他在纽约从事软件架构师工作。
本文摘编自《利用Python进行数据分析》(原书第2版),经出版方授权发布。
延伸阅读《利用Python进行数据分析》