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日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据