3.2 函数
函数是Python中最重要、最基础的代码组织和代码复用方式。根据经验,如果你需要多次重复相同或类似的代码,就非常值得写一个可复用的函数。
有多条返回语句(return)没问题。如果Python达到函数的尾部时仍然没有遇到return语句,就会自动返回None。
这里要补充一个点,因为最近的内容都没函数,我已经快忘记函数要怎么掉用了。这里需要复习一下:
def wang(a,b):
if a > b:
z = a
return z
def liu():
if wang(2,1) > 1:
print(wang(2,1))
liu()
之前我一直没动return的意思,现在明白了,简单来说就是wang(a,b)的值。在liu()这个函数中,我设置wang的形参为2和1。在wang函数内部,对应的形参是a,b。随之做出判断,当a(2)>b(1)的时候,变量z等于变量a的值。
这里再用return返回,即是z = a(2)。然后进入liu中判断,如果wang(2,1)【其实也就是z=a=2】大于1,则输出wang这个函数。
这里的结果自然就是‘2’。
3.2.1 命名空间、作用域和本地函数
函数有两种连接变量的方式:全局、本地。在Python中另一种更贴切地描述变量作用域的名称是命名空间。在函数内部,任意变量都是默认分配到本地命名空间的。本地命名空间是在函数被调用时产生的,并立即由函数的参数填充。当函数执行结束后,本地命名空间就会被销毁。
def func():
a = []
for i in range(5):
a.append(i)
print(a)
func()
当调用func()时,空的列表a会被创建,五个元素被添加到列表,之后a会在函数退出时被销毁。假设我们像下面这样声明a:
a = []
def func():
for i in range(5):
a.append(i)
print(a)
func()
在函数外部给变量赋值是可以的,但是那些变量必须使用global关键字声明为全局变量:
a = None
def func():
for i in range(5):
global a
a = []
print(a)
func()
这里确实不好懂,这也是我发现python各种教程中的问题。我认为,在讲一些逻辑的时候,表现形式应该是越简洁越好。能直接定义,就别搞一个for循环。有啥意义呢?网上一些教程也是,说一个东西,非要贴一大堆代码。这不是难为我大雄吗?
这里说一下我的理解:
1、函数、类内的变量叫局部变量,非函数、类内的叫全局变量。一般情况下,两者没啥交集,但是假设在一个py文件里,全局有个变量叫a,值是2(a = 2 )。而在函数func中,也有个变量a,值是1。那么变量a到底是1还是2呢?
很显然,在func函数内,a是局部函数,值是1。而在函数外,a = 2,事实上无论局部函数怎么妖魔乱舞,作为全局函数的a,值都是1。
那么问题来了,我能不能在函数内,直接修改全局函数呢?可以,这就涉及到上面代码里写的global a了。
In [3]: a = 2
In [4]: def func():
...: a = 1
...:
In [5]: func()
In [6]: a
Out[6]: 2
In [7]: def lalala():
...: global a
...: a = 250
...:
In [8]: a
Out[8]: 2
In [9]: lalala()
In [10]: a
Out[10]: 250
不知道这样看着是不是简单一些。
当不使用global的时候,局部变量怎么跳舞,全局变量都不会变。而到lalala这个函数的时候,使用global就可以声明“我这个看似是局部变量,实际上是全局变量”。
但是需要注意一点,函数写完之后,如果不执行,则全局变量还是2(看上面代码的In[9])。
那么会有人想了(谁?到底是谁想了?),这种奇怪的场景什么时候能遇到呢?
其实这个场景是非常常见的。
# 五颗苹果被两个小朋友吃了N个,还剩几个?
apple = 5
def xiaoming():
eat = 1
global apple
apple = apple - 1
print(f"小明吃了{eat}颗苹果,还剩{apple}颗苹果!")
xiaoming()
def xiaohong():
eat = 2
global apple
apple = apple - 2
print(f"小红吃了{eat}颗苹果,还剩{apple}颗苹果!")
xiaohong()
>>>>>>>>>>>>>>>>>
小明吃了1颗苹果,还剩4颗苹果!
小红吃了2颗苹果,还剩2颗苹果!
定义全局变量这个功能是非常重要的,很多场景都用的上。一定要记住。
注意:如果函数内部没有定义变量,则函数内部对变量进行各种二次操作,这个时候全局变量是会变的。个人理解,简单点说,只要内部没有给全局变量定义(也就是加上=号),内部的操作是可以影响全局变量的。
In [11]: a = []
In [12]: def wang():
...: a.append(1)
...:
In [13]: wang()
In [14]: a
Out[14]: [1]
3.2.2 返回多个值
函数可以返回多个值,一个函数可以返回多个值。那要怎么拆开用呢?
def piaoxiaomin():
xiongwei = 34
yaowei = 24
tunwei = 36
return xiongwei,yaowei,tunwei
piaoxiaomin()
mygirlxiongwei,_,_ = piaoxiaomin()
print(mygirlxiongwei)
_,mygirlyaowei,_ = piaoxiaomin()
print(mygirlyaowei)
_,_,mygirltunwei = piaoxiaomin()
print(mygirltunwei)
除此之外,还可以直接在函数内,对数据进行字典化、元组化或列表化:
def piaoxiaomin():
xiongwei = 34
yaowei = 24
tunwei = 36
return {'xiongwei':xiongwei,'yaowei':yaowei,'tunwei':tunwei}
xiongwei,_,_ = piaoxiaomin().values()
print(xiongwei)
def piaoxiaomin():
xiongwei = 34
yaowei = 24
tunwei = 36
return (xiongwei,yaowei,tunwei)
yaowei = piaoxiaomin()[1]
print(yaowei)
def piaoxiaomin():
xiongwei = 34
yaowei = 24
tunwei = 36
return [xiongwei,yaowei,tunwei]
tunwei = piaoxiaomin()[2]
print(tunwei)
3.2.3 函数是对象
由于Python的函数是对象,很多在其他语言中比较难的构造在Python中非常容易实现。假设我们正在做数据清洗,需要将一些变形应用到下列字符串列表中:
import re
name = ['piaoxiaomin','quan!xiaosheng','piaozhi?yan']
def qingxi(name):
result = []
for name in name:
name = name.strip()
name = re.sub('[!%#?]','',name)
name = name.title()
result.append(name)
return result
print(qingxi(name))
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
['Piaoxiaomin', 'Quanxiaosheng', 'Piaozhiyan']
这节粗讲了一下正则表达式……事实上,我觉得这部分是应该讲到透彻的啊。
不过好在之前学了点爬虫基础知识,要是忘了就回头去看吧。
3.2.4匿名(lambda)函数
Python支持所谓的匿名或lambda函数。匿名函数是一种通过单个语句生成函数的方式,其结果是返回值。匿名函数使用lambda关键字定义,该关键字仅表达“我们声明一个匿名函数”的意思:
def wang(x):
return x*2
liu = lambda x: x*2
简单来说,就是以前写函数都是def XXX啥的,很麻烦,不舒服。现在有种方式,一句话就可以创建一个函数了。这个函数自身没有名字(所以叫匿名函数),但是可以赋给任何变量名。
我们拿个简单的例子:
In [17]: liu = lambda x,y: x*y
# liu是赋给的变量名
# lambda x,y ,这两个是形参
# x*y 是表达式
In [18]: liu(2,3)
Out[18]: 6
这是简单的方式,还有一个比较复杂的方式:
def a(b,c):
return [c(i) for i in b]
d = [1,2,3,4]
print(a(d,lambda i :i*2))
这里我最不能理解的就是c(i)这个部分。最后大致理解了,iambda里面的表达式,也是需要实参才能运行的,而c(i)就相当于lambda的实参。
def wang(a):
b = 3
c = a(b)
return c
print(wang(lambda i :i*2))
这回就很好理解了。首先函数wang有一个形参a,但形参a其实自己也是个函数。a的表达是i:i*2,也就是说它的形参是i,表达式是i*2。
那么首先要找到a的实参,这里就把b(也就是3)赋给了a。a开始运行表达式,3 = b = i,i *2 = 3*2,那就是6。
然后这个6赋给了变量c,再返回这个c,成为函数wang的值。
3.2.5 柯里化:部分参数应用
柯里化表示通过部分参数应用的方式从已有的函数中衍生新的函数。例如,我们有一个不重要的函数,其功能是将两个数加在一起。
使用这个函数,我们可以衍生出一个只有一个变量的新函数,
In [22]: a = [1,2,3,4,5]
In [23]: def a(b,c):
...: return b+c
...:
In [24]: d = lambda c:a(5,c)
In [25]: d(8)
Out[25]: 13
看着可复杂了,实际上就是定义了一个新函数d。d里面是一个匿名化函数,里面包含了形参c,表达式是函数a(b,c)。
a函数里面的表达式是b+c。
在匿名函数中,已经给b定义了一个5,但是c还没有定义。整个匿名函数给了d之后,d再给形参c一个实参。
最后b+c,就是5+8。
学习到如今,感觉代码就是圆环套圆环,为什么代码非常考验逻辑呢。因为逻辑差点你都圆不上来。
通过内建的functools模块可以使用pratial函数来简化处理:
In [26]: from functools import partial
In [27]: def a(b,c):
...: return b+c
...:
In [28]: d = partial(a,5)
In [29]: d(8)
Out[29]: 13
3.2.6 生成器
通过抑制的方式遍历序列,例如列表中的对象或者文件中的一行行内容,这是Python的一个重要特性。这个特性是通过迭代器协议来实现的,迭代器协议是一种令对象可比案例的通用方式。
In [37]: for key in zidian:
...: print(key)
...:
a
b
In [43]: zidian={'a':1,'b':2,'c':3}
In [44]: for key,value in zidian.items():
...: print(key,value)
...:
...:
a 1
b 2
c 3
生成器是构造新的可比案例对象的一种非常简洁的方式。普通函数执行并以此返回单个结果,而生成器则“惰性”地返回一个多结果序列,在每一个元素产生之后暂停,直到下一个请求。
如需创建一个生成器,只需要在函数中将返回关键字的return替换为yield关键字:
In [45]: def bianli():
...: a = [1,2,3,4,5]
...: for i in a:
...: yield i *2
...:
In [46]: for z in bianli():
...: print(z)
...:
2
4
6
8
10
需要注意的是,当你实际调用生成器时,代码并不会立即执行,直到你请求生成器中的元素时,它才会执行它的代码(看上述代码In[46])。
3.2.6.1 生成器表达式
这个就比较熟悉了。跟列表推导式基本一样,就是把[]换成()。
a = [1,2,3,4,5]
c = (i * 2 for i in a)
for z in c:
print(z)
在很多情况下,生成器表达式可以作为函数参数用于替代列表推导式:
In [47]: sum(i+1 for i in range(1,100))
Out[47]: 5049
3.2.6.2 itertools 模块
itertools 模块是适用于大多数数据算法的生成器集合。例如,groupby可以根据任意的序列和一个函数,通过函数的返回值对序列中连续的元素进行分组,参见下面的例子:
In [48]: import itertools
In [49]: fl =lambda x:x[0]
In [50]: name = ['quanxiaosheng','piaoxiaomin','xinyuanjieyi','piaozhiyan','jinxuanya']
In [51]: for leteer,name in itertools.groupby(name,fl):
...: print(leteer,list(name))
...:
q ['quanxiaosheng']
p ['piaoxiaomin']
x ['xinyuanjieyi']
p ['piaozhiyan']
j ['jinxuanya']
感觉这块也不是特别好理解,最核心的其实就是itertools.groupby这个函数的用法,书上基本就没写。我百度了一下,大概了解了。groupby的两个形参,第一个是数据,第二个是key。根据第二个参数对数据进行处理。
至于为什么fl作为第二个实参,会被赋给第一个leteer,我也没弄懂。只能说人家就是这么设计的吧。
3.2.7 错误和异常处理
这部分跟之前书中写的一样,略过。
3.2.7.1 ipython中的异常
如果当你正在用%run执行一个脚本或执行任何语句报错时,ipython将会默认打印出完整的调用堆栈跟踪,会将堆栈中每个错误点附近的几行上下文代码打印出来:
In [55]: %run suanfa.py
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/Volumes/job/python工程文件/shujufenxi1/suanfa.py in <module>
2 name = ['quanxiaosheng','piaoxiaomin','piaozhiyan','xinyuanjieyi','jinxuanya']
3 f = lambda x:x[0]
----> 4 for name,le in itertools.groupby(f,name):
5 print(le,list(name))
TypeError: 'function' object is not iterable
剩下的部分都是之前笔记里写过的,这里就不重复了。
总算把第三章完成了,撒花~
胭惜雨
2021年03月23日