Python:函数和lambda表达式

 函数实质性特定任务的一段代码,程序通过将一段代码定义成函数,并为该函数指定一个函数名,这样即可在需要的时候多次调用这段代码。因此,函数是代码复用的重要手段。

与函数紧密相关的一个知识点就是lambda表达式。lambda表达式可作为表达式、函数参数或函数返回值,因此使用lambda表达式使得程序更加简洁。

5.1 函数入门

函数就是Python程序的重要组成单位,一个Python程序可以由多个函数组成。

5.1.1 理解函数

我们之前也用过大量的函数,如len()、max()等,使用函数是真正开始编程的第一步。

比如在程序中定义了一段代码,这段代码用于实现一个特定的功能。为了使一段代码可以多次使用,我们将实现特定功能的代码定义成一个函数,每次当程序需要实现该功能时,只要执行(调用)该函数。

通俗来讲,所谓函数,就是指为一段实现特定功能的代码“取“一个名字,以后即可通过改名字执行(调用)该函数。通常,函数可以接收零个或多个参数,也可以返回零个或多个值。从函数使用者的角度来看。函数就像一个“黑匣子”,程序将零个或多个参数传入这个“黑匣子”,该“黑匣子”经过一番计算即可返回零个或多个值。

对于“黑匣子”的内部细节(就是函数内部实现细节),函数的使用者并不需要关心。就像前面在调用len()、max()、min()等函数时,我们只负责传入参数、接收返回值,置于函数内部的实现细节,我们并不关心。

图片

从函数定义者(实现函数的人)的角度来看,其至少需要想清楚以下三点:

  1. 函数需要几个关键的需要动态变化的数据,这些数据应该被定义成函数的参数。
  2. 函数需要传出几个重要数据(就是调用函数的人希望得到的数据),这些数据应该被定义成返回值。
  3. 函数的内部实现过程。

从介绍不难看出,定义函数比调用函数难得多,而学习如何实现函数的定义就是需要学习的内容。

5.1.2 定义函数和调用函数

在使用之前必须先定义函数,定义函数的语法格式如下:

def 函数名(形参列表):
    ##由零条到多条可执行语句组成的函数
    [return [返回值]]   

Python声明函数必须使用def关键字,对函数语法格式的详细说明如下:

  1. 函数名:从语法角度来看,函数名只要是一个合法的标识符即可;从程序的可读性角度来看,函数名应该由一个或多个有意义的单词连缀而成,每个单词的字母全部小写,单词与单词之间使用下划线分隔。
  2. 形参列表:用于定义该函数可以接收的参数。形参列表由多个形参名组成,多个形参名之间以英文逗号(,)隔开。一旦在定义函数时指定了形参列表,调用该函数时就必须传入对应的参数值——谁调用函数,谁负责为形参赋值。

在函数体中多条可执行语句之间有严格的执行顺序,排在函数体前面的语句总是先执行,排在函数体后面的语句总是后执行。

#定义一个函数,声明两个形参
def my_max(x,y):
    #定义一个变量z,该变量等于x、y中较大值
    z=x if x>y else y
    #返回变量z的值
    return z
#定义一个函数,声明 一个形参
def say_hi(name):
    print("===正在执行say_hi()函数===")
    return name+",您好!"
a=6
b=9
#调用my_max()函数,将函数返回值赋值给result变量
result=my_max(a,b)
print("result:",result)
#调用say_hi()函数,直接输出函数的返回值
print(say_hi("孙悟空"))
"""
result: 9
===正在执行say_hi()函数===
孙悟空,您好!
"""

在函数体中使用return语句可以显式地返回一个值,return语句返回的值既可以是有值的变量,也可以是一个表达式。

5.1.3 为函数提供文档

前面介绍过可以使用Python内置的help()函数来查看其他函数的帮助文档,我们也经常提供help()函数查看指定函数的帮助信息,这对于Python开发者来说非常重要。

我们还可以为函数编写说明文档——只要把一段字符串放在函数声明之后、函数体之前,这段字符串将被作为函数部分,这个文档就是函数的说明文档。

程序即可提供help()函数查看的说明文档,也可以通过函数的__doc__属性访问韩硕的说明文档。

def my_max(x,y):
    '''
    获取两个数值之间的较大数的值

    my_max(x,y)
        返回x,y两个参数之间较大的那个数
    '''
    #定义一个变量z,该变量等于x、y之间的较大值
    z=x if x>y else y
    #返回变量z的值
    return z
#使用help()函数查看my_max的帮助文档
help(my_max)
print(my_max.__doc__)
"""
Help on function my_max in module __main__:
my_max(x, y)
    获取两个数值之间的较大数的值
    my_max(x,y)
        返回x,y两个参数之间较大的那个数
    获取两个数值之间的较大数的值
    my_max(x,y)
        返回x,y两个参数之间较大的那个数
"""

上面程序使用多行字符串的语法为my_max()函数编写了说明文档,接下来程序即可通过help()函数查看该函数的说明文档,也可以通过__doc__属性访问该函数的说明文档。

5.1.4 多个返回值

如果程序需要有多个返回值,既可以将多个值包装成列表之后返回,也可以直接返回多个值。如果Python函数直接返回多个值,Python会自动将多个返回值封装成元组。

def sum_and_avg(list):
    sum=0
    count=0
    for e in list:
        #如果元素e是值
        if isinstance(e,int) or isinstance(e,float):
            count+=1
            sum+=e
    return sum,sum/count
my_list=[20,15,2.8,'a',35,5.9,-1.8]
#获取sum_and_avg函数返回的多个值,多个返回值被封装成元组
tp=sum_and_avg(my_list)
print(tp)#(76.9, 12.816666666666668)

此外,也可以使用Python提供的序列解包功能,直接使用多个变量接收函数返回的多个值。

s,avg=sum_and_avg(my_list)
print(s)#76.9
print(avg)#12.816666666666668

5.1.5 递归函数

在一个函数体内调用它自身,被称为函数递归。函数递归包含了一种隐式的循环,他会重复执行某段代码,但这种重复执行无需循环控制。

例如:已知有一个数列:f(0)=1,f(1)=4,f(n+2)=2*f(n+1)+f(n),其中n是大于0的整数,求f(10)的值。这道题可以使用递归来求得。

def fn(n):
    if n==0:
        return 1
    elif n==1:
        return 4
    else:
        #在函数体内调用它自身,就是函数递归
        return 2*fn(n-1)+fn(n-2)
#输出fn(10)的结果
print(fn(10))#10497

在上面的fn()函数体中再次调用了fn()函数,这就是函数递归。注意fn()函数体中调用fn()的形式:

return 2*fn(n-1)+fn(n-2)

仔细看上面递归的过程,当一个函数不断调用它自身时,必须在某个时刻函数的返回值是确定的,即不再调用它自身;否则,这种递归就变成了无穷递归,类似于死循环。因此,在定义递归函数时有一条最重要的规定:规定一定要像已知的方向进行。

递归是非常有用的,例如程序希望遍历某个路径下的所有文件,但这个路径下的文件路径是未知的,那么就可以使用递归来实现这个需求。系统可以定义一个函数,该函数接收一个文件路径作为参数,该函数可遍历出当前路径下的所有文件和文件路径——在该函数体中再次调用函数自身来处理该路径下的文件路径。

总之,只要在一个函数体中调用函数自身,就是函数递归。递归一定要向已知方向进行。

5.2 函数的参数

在定义Python函数时可定义形参(形式参数),这些形参的值要等调用时才能确定下来,由函数的调用者负责为形参传入参数值。简单来说,就是谁调用函数,谁负责传入参数值。

5.2.1 关键词(keyword)参数

Python函数的参数名不是没有意义的,Python允许在调用函数时通过名字来传入参数值。因此,Python函数的参数名应该具有更好的语义——程序可以立刻明确传入函数的每个参数的含义。

按照形参的位置传入的参数被称为位置参数。如果使用位置参数的方式来传入参数值,则必须严格按照定义函数时指定的顺序来传入参数值;如果根据参数名来传入参数值,则无需遵守定义形参的顺序,这种方式被称为关键字参数。

#定义一个函数
def girth(width,height):
    print("width:",width)
    print("heigth:",height)
    return 2*(width+height)
#传统调用函数的方式,根据位置传入参数值
print(girth(3.5,4.8))
#根据关键字参数来传入参数值
print(girth(width=3.5,height=4.8))
#在使用关键字参数时可交换位置
print(girth(height=4.8,width=3.5))
#部分使用关键字参数,部分使用位置参数
print(girth(3.5,height=4.8))

需要说明的是,如果希望在调用函数时混合使用关键字的参数和位置参数,则关键字参数必须位于位置参数之后。换句话来说,在关键字参数之后的只能是关键字参数。

#必须将位置参数放在关键字参数之前,下面代码错误
print(girth(width=3.5,4.8))
"""
  File "", line 15
    print(girth(width=3.5,4.8))
                             ^
SyntaxError: positional argument follows keyword argument
"""

5.2.2 参数默认值

在某些情况下,程序需要在定义函数时,为一个或多个元素指定默认值——这样在调用函数时就可省略为该形参传入参数,而是直接使用该形参的默认值。

为形参指定默认值的语法格式如下:

形参名=默认值

从上面的语法来看,形参的默认值紧跟在形参之后,中间以英文“=”隔开。

#为两个参数指定默认值
def say_hi(name="孙悟空",message="欢迎来到极乐净土"):
    print("name:",name)
    print("消息是:",message)
#全部使用参数默认值
say_hi()
#只有一个参数使用默认值
say_hi("白骨精")
#两个参数都不使用默认值
say_hi("白骨精","欢迎来到我的世界")
#只有name参数使用默认参数
say_hi(message="欢迎学习Python")
"""
name: 白骨精
消息是: 欢迎来到极乐净土
name: 白骨精
消息是: 欢迎来到我的世界
name: 孙悟空
消息是: 欢迎学习Python
"""

从上面程序可以看出,如果只传入一个位置参数,由于参数位于第一位,系统将该参数值传给name参数。因此,不能按如下方式调用函数:

say_hi("欢迎学习Python")

上面调用时传入的是给name,而不是message。

say_hi(name="白骨精","欢迎学习Python")

因为Python规定:关键字参数必须位于位置参数的后买你。因此提示错误:Positional argument after keyword argument。

那么,能不能单纯地将上面两个参数交换位置呢?

say_hi("欢迎学习Python",name="白骨精")

上面调用依然是错误的,因为第一个字符串没有指定关键字参数,因此将使用位置参数为name参数传入参数值,第二个参数使用关键字参数的从形式再次为name参数传入参数值,这意味着两个参数数值其实都会传入name参数,程序name参数传入了多个参数值。因此提示错误:say_hi() got multiple values for argument 'name'。

将函数调用改为如下两种形式是正确的:

say_hi("白骨精",message="欢迎学习Python")
say_hi(name="白骨精",message="欢迎学习Python")

由于Python要求在调用函数时关键字参数必须位于位置参数的后面,因此在定义函数时指定默认值的参数(关键字参数)必须在没有默认值的参数之后。

#定义一个打印三角形的函数,默认值的参数必须放在后面
def printTriangle(char,height=5):
    for i in range(1,height+1):
        #先打印一排空格
        for j in range(height-i):
            print(' ',end='')
        #在打印一排特殊字符
        for j in range(2*i-1):
            print(char,end='')
        print()
printTriangle('@',6)
printTriangle('#',height=7)
printTriangle(char='*')
"""
     @
    @@@
   @@@@@
  @@@@@@@
 @@@@@@@@@
@@@@@@@@@@@
      #
     ###
    #####
   #######
  #########
 ###########
#############
    *
   ***
  *****
 *******
*********
"""

注意:Python要求将带默认值的参数定义在形参列表的最后。

5.2.3 参数列表(个数可变的参数)

很多编程语言都允许定义个数可变的参数,这样可以在调用函数时传入任意多个参数。python当然也不例外,Python允许在形参前面添加以一个星号(*),这样意味着该形参可以接受多个参数值,多个参数被当成元组传入。

#定义了支持参数收集的函数
def text(a,*book):
    print(book)
    #books被当成元组处理
    for b in book:
        print(b)
    #输出整数变量a的值
    print(a)
#调用test()函数
text(5,"疯狂","fkit","Python")
"""
('疯狂', 'fkit', 'Python')
疯狂
fkit
Python
5
"""

从上面的运行结果可以看出,当调用text()函数时,book参数可以传入多个字符串作为参数值。从text()函数体代码来看,参数收集的本质就是一个元组,Python会将传给book参数的多个值收集成一个元组。

Python允许个数可变的形参可以处于形参列表的任意位置(不要求时形参列表的最后一个参数),但Python要求一个函数最多只能带一个支持“普通”参数收集的形参。

#定义了支持参数收集的函数
def text(*book,num):
    print(book)
    #book被当成元组处理
    for b in book:
        print(b)
    print(num)
#调用text()函数
text("Python",'fkit','java','C',num=20)
"""
('Python', 'fkit', 'java', 'C')
Python
fkit
java
C
20
"""

Python还可以收集关键字参数,此时Python会将这种关键字参数收集成字典。为了让Python能收集关键字参数,需要在参数前面添加两个星号。在这种情况下,一个函数可同时包含一个支持“普通”参数收集的参数和一个支持关键字收集的参数。

#定义了支持参数收集的函数
def text(x,y,z=3,*books,**scores):
    print(x,y,z)
    print(books)
    print(scores)
text(1,2,3,'Python','java',语文=89,数学=94)
"""
1 2 3
('Python', 'java')
{'语文': 89, '数学': 94}
"""

最后的两个关键字参数将会被收集成字典。

对于上面方式定义的text()函数,参数z的默认值几乎不能发挥作用。

text(1,2,'Python','java',语文=89,数学=94)
"""
1 2 Python
('java',)
{'语文': 89, '数学': 94}
"""

如果希望让z参数的默认值发挥作用,则需要只传入两个位置参数。

text(1,2,语文=89,数学=94)
"""
1 2 3
()
{'语文': 89, '数学': 94}
"""

5.2.4 逆向参数收集

所谓逆向参数收集,指的是在程序已有列表、元组、字典等对象的前提下,把它们的元素“拆开”后传给函数的参数。

逆向参数收集需要在传入的列表、元组参数之前添加一个星号,在字典参数之前添加两个星号。

def text(name,message):
    print("用户是:",name)
    print("欢迎消息:",message)
my_list=["孙悟空","欢迎来到西游之旅"]
text(*my_list)
"""
用户是: 孙悟空
欢迎消息: 欢迎来到西游之旅
"""

实际上,即使是支持收集的参数,如果程序需要将一个元组传给该函数,那么同样需要使用逆向收集。

def text(name,*nums):
    print("用户是:",name)
    print("nums参数:",nums)
my_list=[1,2,3]
#使用逆向收集,将my_list的元素传给nums参数
text('fkit',*my_list)
"""
用户是: fkit
nums参数: (1, 2, 3)
"""

此外,也可调用如下方式调用text()函数:

#使用逆向收集,将my_list列表的第一个元素传给name参数,剩下元素传给nums参数
text(*my_list)
"""
用户是: 1
nums参数: (2, 3)
"""

如果不使用逆向收集(不在列表参数之前添加*),整个列表将作为一个参数,而不是将元组的元素作为多个参数。

text(*my_list)
#不使用逆向收集,my_list列表整体传给name参数
text(my_list)
"""
用户是: [1, 2, 3]
nums参数: ()
"""

字典也支持逆向收集,字典将会以关键字参数的形式传入。

def bar(book,price,desc):
    print(book,"这本书的价格是:",price)
    print("描述信息:",desc)
my_dict={'price':89,'book':'Python','desc':'这是本系统全面的Python学习图书'}
#按逆向收集的方式将my_dict的多个key-value对传给bar()函数
bar(**my_dict)
"""
Python 这本书的价格是: 89
描述信息 这是本系统全面的Python学习图书
"""

5.2.5 函数的参数传递机制

Python的参数值是如何传入函数的呢?这是由Python函数的传递机制来控制的。Python中函数的参数传递机制都是“值传递”。所谓值传递,就是将实际参数数值的副本(复制品)传入函数,而函数本身不会受到任何影响。

def swap(a,b):
    #下面代码实现a、b变量的值变换
    a,b=b,a
    print("在swap函数里,a的值是",\
          a,";b的值是",b)
a=6
b=9
swap(a,b)
print("在swap函数里,a的值是", \
      a, ";b的值是", b)
"""
在swap函数里,a的值是 9 ;b的值是 6
在swap函数里,a的值是 6 ;b的值是 9
"""

上面程序开始定义了a、b两个局部变量,这两个变量在内存中的存储示意图如下:

当程序执行swap()函数时,系统进入swap()函数,并将主程序中的a、b中的a、b变量作为参数值传入swap()函数,但传入swap()函数的只是a、b的副本,而不是a、b本身。进入swap()函数后,系统中产生了4个变量,这4个变量在内存中的存储示意图如下:

当主程序中调用swap()函数时,系统分别为主程序和swap()函数分配两块栈区,用于保存它们的局部变量。将主程序中的a、b变量作为参数值传入swap()函数,实际上在swap()函数栈区中的a、b变量的值分别赋值给swap()函数栈区中的a、b参数(就是对swap()函数的a、b两个变量进行初始化)。此时,系统存在两个a变量、两个b变量,只是存在于不同的栈区中而已。

程序在swap()函数中交换a、b两个变量的值,实际上是上图中的灰色区域的a、b变量进行交换。交换结束后,输出swap()函数中的a、b变量的值,可以看到a是9,b是6。

主程序栈区中的a、b的值并未发生改变,程序改变的只是swap()函数栈区中a、b的值 ,这就是值传递的实质:当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋给函数中形参的变量,在函数中操作的并不是实际的实参变量。

根据Python的参数传递机制,我们知道:传入函数的只是参数的副本,因此程序在函数中对参数赋值并不影响参数本身。如果参数本身是一个可变的对象(比如列表、字典等),此时虽然Python采用的也是值传递方式,但有很多人可能会对这种类型的参数传递产生一些误会。

def swap(dw):
    #下面代码实现dw的a、b两个元素的值交换
    dw['a'],dw['b']=dw['b'],dw['a']
    print("在swap()函数里,a元素的值是",\
          dw['a'],";b元素的值是",dw['b'])
dw={'a':6,'b':9}
swap(dw)
print("在swap()函数里,a元素的值是", \
      dw['a'], ";b元素的值是", dw['b'])
"""
在swap()函数里,a元素的值是 9 ;b元素的值是 6
在swap()函数里,a元素的值是 9 ;b元素的值是 6
"""

从上面的运行结果来看,在swap()函数中,dw字典的a、b两个元素的值被成功交换。不仅如此,当swap()函数执行结束后,主程序中dw字典的a、b两个元素的值也被交换了。这很容易造成一种错觉:在调用swap()函数时,传入swap()函数的就是dw字典本身,而不是它的复制品。但只是一种错觉。

程序开始创建了一个字典对象,并定义了一个dw引用变量指向字典都西昂,这意味着此时内存中有两个东西:对象本身和指向该对象的引用变量。此时在系统内存中的存储示意图如图:

接下来主程序开始调用swap()函数,在调用swap()函数时,dw变量作为参数传入swap()函数的dw形参,从而完成swap()函数的dw参数初始化。值得指出的时,主程序中的dw是一个引用变量(也就是一个指针),它保存了字典对象的地址值,当把dw的值赋给swap()函数的dw参数后,就是让swap()函数的dw参数也保存这个地址值,即也会引用到同一个字典对象。

         

从上图看出,这种参数传递的方式是不折不扣的值传递方式,系统一样赋值了dw的副本传入swap()函数。但由于dw只是一个引用变量,因此系统复制的是dw,并未复制该字典本身。、

当程序在swap()函数中操作dw参数时,由于dw只是一个引用变量,故实际操作的还是字典对象。此时,不管是操作主程序中的dw变量,还是操作swap()函数里的dw参数,其实操作的都是它们共同引用的字典对象,它们引用的是同一个字典对象。因此,当在swap()函数中交换dw参数所引用字典对象的a、b两个元素的值后,可以看到在主程序中dw变量所引用字典对象的a、b两个元素的值也被交换了。

为了更好地证明主程序中的dw和swap()函数中的dw是两个变量,在swap()函数的最后一行增加如下代码:

#把dw直接赋值为None,让它不再指向任何对象
dw=None

把swap()函数中的dw赋值为None后,再swap()函数中失去了对字典对象的引用,不可再访问该字典对象。但主程序中的dw变量不受任何影响,依然可以引用该字典对象所以依然可以输出字典对象的a、b元素的值。

通过上面介绍可以得出两个结论:

  1. 不管什么类型的参数,再Python函数中对参数直接使用“=”符号赋值是没用的,直接使用“-”符号赋值并不能改变参数。
  2. 如果需要让函数修改某些数据,则可以通过把这些数据包装成列表、字典等可变对象,然后把列表、字典等可变对象作为参数传入函数,在函数中通过列表、字典的方法修改它们,这样才可以改变这些数据。

5.2.6 变量作用域

再程序中定义一个变量时,这个变量是有作用范围的,变量的作用范围称为它的作用域。根据定义变量的位置,变量分为两种:

  1. 局部变量:在函数中定义的变量,包括参数,都被称为局部变量。
  2. 全局变量。在函数外面、全局范围内定义的变量,被称为全局变量。

每个函数在执行时,系统都会为该函数分配一块“临时内存空间”,所有局部变量都被保存在这块临时内存空间内。当函数执行完成后,这块内存空间就被释放了,这些局部变量也就失效了,因此离开函数之后就不能再访问局部变量了。

全局变量意味着它们可以再所有函数内被访问。

不管是在函数的局部范围内还是再全局范围内,都可能存在多个变量,每个变量“持有”该变量的值。从这个角度来看,不管是局部范围还是全局范围,这些变量和它们的值就像一个“看不见”的字典,其中变量名就是字典的key,变量值就是字典的value。

实际上,Python提供了如下三个工具函数来获取指定范围内的“变量字典”。

  1. globals():该函数返回全局范围内所有变量组成的“变量字典”。
  2. locals():该函数当前范围内所有变量组成的“变量字典”。
  3. vars(object):获取在指定对象范围内所有变量组成的“变量字典”。如果不传入object参数,vars()和locals()的作用完全相同。

globals()和locals()看似完全不同,当它们实际上也是有联系的,关于这两个函数的区别和联系大致有以下两点:

  1. locals()总是获取当前局部范围内所有变量组成的“变量字典”,因此,如果在全局范围内(在函数之外)调用locals()函数,同样会获取全局范围内所有变量组成的“变量字典”;而globals()函数无论在哪执行,总会获取全局范围内所有变量组成的“变量字典”。
  2. 一般来说,使用locals()和globals()获取的“变量字典”只应该被访问,不应该被修改。但实际上,不管是使用locals()还是globals()函数获取的全局范围内的“变量字典”,都可以被修改,而这种修改会真正改变全局变量本身;但通过locals()获取的局部范围内的“变量字典”,即使对它修改也不会影响全局变量。
def test():
    age=20
    #直接访问age局部变量
    print(age)#输出20
    #访问函数局部范围内的“变量数组”
    print(locals())#{'age': 20}
    #通过l局部变量范围内的“变量数组”访问age变量
    print(locals()['age'])#20
    #通过locals函数局部范围内的“变量数组”改变age变量的值
    locals()['age']=12
    #再次访问age变量的值
    print("xxx",age)#12
    #通过globals函数修改x全局变量
    globals()['x']=19
x=5
y=20
print(globals())#'x': 5, 'y': 20
#在全局范围内使用locals函数,访问的是全局变量的“变量数组”
print(locals())#'x': 5, 'y': 20
#直接访问x全局变量
print(x)#5
#通过全局变量的“变量数组”访问x全局变量
print(globals()['x'])#5
#通过全局变量的“变量数组”对x全局变量赋值
globals()['x']=39
print(x)#39
#在全局范围内使用locals函数对x全局变量赋值
locals()['x']=99
print(x)#99

提示:在使用globals()或locals()访问全局变量的“变量字典”时,将会看到程序输出的“变量字典”默认包含了很多变量,这些都是Python主程序内置的。

全局变量默认可以在所有函数内被访问,但如果在函数中定义了全局变量同名的变量,此时就会发生局部变量遮蔽全局变量的情形。

name='Charlie'
def test():
    #直接访问name全局变量
    print(name)#Charlie
test()
print(name)#Charlie

如果在上面程序代码改成:

name='Charlie'
def test():
    #直接访问name全局变量
    print(name)#Charlie
    name="孙悟空"
test()
print(name)#Charlie
"""
UnboundLocalError: cannot access local variable 'name' where it is not associated with a value
"""

该错误提示所访问的name变量还未定义。这是什么原因:正是由于程序在test()函数中增加了“name=’孙悟空’”一行代码造成的。

Python语法规定:在函数内部对不存在的变量赋值时,默认就是重新定义新的局部变量。因此这一行代码相当于重新定义了name局部变量,这样name全局变量就被遮蔽了,所以上面程序中会报错。

为了避免这个问题,可以通过两种方式来修改上面程序。

1.访问被遮蔽的全局变量

如果程序希望上面的代码依然能访问name全局变量,且在后面重新定义局部变量_也就是在函数中可访问被遮蔽的全局变量,此时可通过globals()函数来实现.

name='Charlie'
def test():
    #直接访问name全局变量
    print(globals()['name'])#Charlie
    name="孙悟空"
test()
print(name)#Charlie

2.在函数中声明全局变量

为了避免在函数中对全局变量赋值(不是重新定义局部变量),可使用global语句来声明全局变量.因此,可将程序改为:

name='Charlie'
def test():
    #声明name是全局变量,后面的赋值语句不会重新定义局部变量
    global name
    print(globals()['name'])#Charlie
    name="孙悟空"
test()
print(name)#孙悟空

增加了global name”声明之后,程序会把name变量当作全局变量,这意味着test()函数后面对name赋值的语句只是对全局变量赋值,而不是重新定义全局变量。

5.3 局部变量

前面所看到的函数都是在全局范围内定义的,它们都是全局函数。Python还支持在函数体内定义函数,这种被放在函数体内定义的函数称为局部函数。

在默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭函数内有效,其封闭函数也可以返回局部函数,以便程序在其他作用域中使用局部函数。

#定义函数,该函数会包含局部函数
def get_math_func(type,nn):
    #定义一个计算平方的局部函数
    def square(n):
        return n*n
    #定义一个计算立方的局部函数
    def cube(n):
        return n*n*n
    #定义一个计算阶乘的局部函数
    def factorial(n):
        result=1
        for index in range(1,n+1):
            result*=index
        return result
    #调用局部函数
    if type=="square":
        return square(nn)
    elif type=="cube":
        return cube(nn)
    else:
        return factorial(nn)
print(get_math_func("square",3))
print(get_math_func("cube",3))
print(get_math_func("factorial",3))
"""
9
27
6
"""

如果封闭函数没有局部函数,那么局部函数只能在封闭函数内部调用,如上面程序所示。

另外,还会出现一种情况,如果封闭函数将局部函数返回,且程序使用变量保存了封闭函数的返回值,那么这些局部函数的作用域就会被扩大。因此程序完全可以自由地调用它们,就像它们都是全局一样。

局部函数内的变量也会遮蔽它所在函数内的局部变量。

def foo():
    #局部变量name
    name='Charlie'
    def bar():
        #访问bar函数所在foo函数内的name局部变量
        print(name)#UnboundLocalError: cannot access local variable 'name' where it is not associated with a value
        name='孙悟空'
    bar()
foo()

该错误是由局部变量遮蔽局部变量导致的,在bar()函数中定义的name局部变量遮蔽了它所在foo()函数内的name局部变量,因此导致程序中报错。

为了声明bar()函数中的“name=’孙悟空’”赋值语句不是定义新的局部变量,只是访问他所在foo()函数内的name局部变量,Python提供nonlocal关键字,提供nonlocal语句即可声明访问赋值语句只是访问该函数所在函数内的局部变量。将上面程序改写成:

def foo():
    #局部变量name
    name='Charlie'
    def bar():
        nonlocal name
        #访问bar函数所在foo函数内的name局部变量
        print(name)#UnboundLocalError: cannot access local variable 'name' where it is not associated with a value
        name='孙悟空'
    bar()
foo()

增加nonlocal name代码之后,接下来bar()函数中的“name=’孙悟空’”就不再是定义新的局部变量,而是访问它所在函数(foo()函数)内的name局部变量。

提示:nonlocal和前面介绍的global功能大致相似,区别只是global用于声明访问全局变量,而nonlocal用于声明访问当前函数所在函数内的局部变量。

5.4 函数的高级内容

Python的函数是“一等公民”,因此函数本身也是一个对象,函数即可用于赋值,也可用作其他函数的参数,还可作为其他函数的返回值。

5.4.1 使用函数变量

Python的函数也是一种值:所以函数都是function对象,这意味着可以把函数本身赋值给变量,就像把整数、浮点数、列表、元组赋值给变量一样。

当把函数赋值给变量之后,接下来程序也可通过该变量来调用。

#定义一个计算乘方的函数
def pow(base,exponent):
    result =1
    for i in range(1,exponent+1):
        result*=base
    return result
#将pow()函数赋值给my_fun,则my_fun可被当成pow()调用
my_fun=pow
print(my_fun(3,4))#81
#定义一个计算面积的函数
def area(width,height):
    return width*height
#将area函数赋值给my_fun,则my_fun可被当成area使用
my_fun=area
print(my_fun(3,4))#12

提示:其实Python已经内置了很多计算乘方的方法,因此此处pow()函数并没有太大的实际意义,只是作为示范使用。

通过对my_fun变量赋值不同的函数,可以让my_fun再不同的时间指向不同的函数,从而让程序更加灵活。由此可见,使用函数变量的好处是让程序更加灵活。

除此之外,程序还可使用函数作为另一个函数的形参和(或)返回值。

5.4.2 使用函数作为函数形参

有时候需要定义一个函数,该函数的大部分计算逻辑都能确定,但某些处理逻辑暂时无法确定——这意味着某些程序代码需要动态改变,如果希望调用函数时能动态传入这些代码,那么就需要再函数中定义函数形参,这样即可在调用该函数时传入不同的函数作为参数,从而动态改变这段代码。

Python支持像使用其他参数一样使用函数参数。

#定义函数类型的形参,其中fn是一个函数
def map(data,fn):
    result=[]
    #遍历data列表中的每一个元素,并用fn函数对每个元素进行计算
    #然后将计算结果作为新数组的元素
    for e in data:
        result.append(fn(e))
    return result
#定义一个计算平方的函数
def square(n):
    return n*n
#定义一个计算立方的函数
def cube(n):
    return n*n*n
#定义一个计算阶乘的函数
def factorial(n):
    result=1
    for index in range(2,n+1):
        result*=index
    return result
data=[3,4,9,5,8]
print("原数据:",data)
#下面程序代码调用map()函数三次,每次调用传入不同的函数
print("计算数组元素的平方")
print(map(data,square))
print("计算数组元素的立方")
print(map(data,cube))
print("计算数组元素的阶乘")
print(map(data,factorial))
print(type(map))
"""
原数据: [3, 4, 9, 5, 8]
计算数组元素的平方
[9, 16, 81, 25, 64]
计算数组元素的立方
[27, 64, 729, 125, 512]
计算数组元素的阶乘
[6, 24, 362880, 120, 40320]
<class 'function'>
"""

从上面介绍不难看出,通过使用函数作为参数可以调用函数时动态传入函数——实际上就可以动态改变被调用函数的部分代码。

5.4.3 使用函数作为返回值

前面提到,Python还支持使用函数作为其他函数的返回值。

def get_math_func(type):
    # 定义一个计算平方的函数
    def square(n):
        return n * n
    # 定义一个计算立方的函数
    def cube(n):
        return n * n * n
    # 定义一个计算阶乘的函数
    def factorial(n):
        result = 1
        for index in range(2, n + 1):
            result *= index
        return result
    #返回局部函数
    if type=='square':
        return square
    if type=='cube':
        return cube
    else:
        return factorial
#调用get_math_func(),程序返回一个嵌套函数
math_func=get_math_func('cube')
print(math_func(5))#125
math_func=get_math_func('square')
print(math_func(5))#25
math_func=get_math_func('factorial')
print(math_func(5))#120

5.5 局部函数于lambda表达式

lambda表达式是现代编程语言争相引入的一种语法。如果说函数是命名的、方便复用的代码块,那么lambda表达式则是功能更灵活的代码块,它可以在程序中被传递和调用。

5.5.1回顾局部函数

前面学习局部函数是有这样一串代码:

def get_math_func(type,nn):
    #定义一个计算平方的局部函数
    def square(n):
        return n*n
    #定义一个计算立方的局部函数
    def cube(n):
        return n*n*n
    #定义一个计算阶乘的局部函数
    def factorial(n):
        result=1
        for index in range(1,n+1):
            result*=index
        return result
    #调用局部函数
    if type=="square":
        return square(nn)
    elif type=="cube":
        return cube(nn)
    else:
        return factorial(nn)

由于局部函数的作用域默认仅停留在其封闭函数内,因此这三个局部函数的函数名的作用域太有限了——仅仅是在程序的if语句中作为返回值使用。一旦离开了get_math_func(),这三个局部函数的函数名就失去了意义。

既然局部函数的函数名没有太大的意义,俺么就考虑使用lambda表达式来简化局部函数的写法。

5.5.2 使用lambda表达式代替局部函数

如果使用lambda表达式来简化上面的程序,可以改写为如下形式:

def get_math_func(type):
    result=1
    #该函数返回的是lambda表达式
    if type =='square':
        return lambda n:n*n
    elif type =='cube':
        return lambda n:n*n*n
    else:
        return lambda n:(1+n)*n/2
#调用get_math_func(),程序返回一个嵌套函数
math_func=get_math_func("cube")
print(math_func(5))#125
math_func=get_math_func("square")
print(math_func(5))#25
math_func=get_math_func("other")
print(math_func(5))#15.0

lambda表达式语法格式如下:

lambda [parameter_list]:表达式

从上面的语法格式可以看出lambda表达式的几个要点。

  1. lambda表达式必须使用lambda关键字定义。
  2. 在lambda关键字之后、冒号左边的是参数列表,可以没有参数,也可以有多个参数。如果有多个参数,则需要使用逗号隔开,冒号右边是该lambda表达式的返回值。

实际上,lambda表达式的本质就是匿名的、单行函数体的函数。因此lambda表达式可以写成函数的形式。

lambda x,y:x+y

可改写为如下函数形式:

def add(x,y):return x+y

上面定义函数时使用了简化语法:当函数只有一行代码时,可以直接把函数体的代码放在与函数头同一行。

总体来说,函数比lambda表达式适应性更强,lambda表达式只能创建简单的函数对象(它只适合函数体为单行的情形),但lambda表达式依然有如下两个用途:

  1. 对于单行函数,使用lambda表达式可以省去定义函数的过程,让代码更简洁。
  2. 对于不需要多次复用的函数,使用lambda表达式可以在用完之后立即释放,提高了性能。
#传入计算平方的lambda表达式作为参数
x=map(lambda x:x*x,range(8))
print([e for e in x])#[0, 1, 4, 9, 16, 25, 36, 49]
#传入计算平方的lambda表达式作为参数
y=map(lambda x:x*x if x%2==0 else x,range(8))
print([e for e in y])#[0, 1, 4, 3, 16, 5, 36, 7]

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

提刀立码,调参炼丹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值