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("函数外部 a =", a ," b =", b)
运行上面程序,将看到如下运行结果:
swap函数里,a = 9 b = 6
函数外部 a = 6 b = 9
从上面的运行结果来看,在 swap() 函数里,经过交换形参 a 和 b 的值,它们的值分别变成了 9 和 6,但函数外部变量 a 和 b 的值依然是 6 和 9。这也证实了,swap() 函数的参数传递机制,采用的是值传递,函数内部使用的形参 a 和 b,和实参 a、b 没有任何关系。
swap() 函数中形参 a 和 b,各自分别是实参 a、b 的复制品。
如果读者依旧不是很理解,下面通过示意图来说明上面程序的执行过程。
上面程序开始定义了 a、b 两个局部变量,这两个变量在内存中的存储示意图如图 1 所示。
图 1 主栈区中 a、b 变量存储示意图
当程序执行 swap() 函数时,系统进入 swap() 函数,并将主程序中的 a、b 变量作为参数值传入 swap() 函数,但传入 swap() 函数的只是 a、b 的副本,而不是 a、b 本身。进入 swap() 函数后,系统中产生了 4 个变量,这 4 个变量在内存中的存储示意图如图 2 所示。
图 2 主栈区的变量作为参数值传入 swap() 函数后存储示意图
当在主程序中调用 swap() 函数时,系统分别为主程序和 swap() 函数分配两块栈区,用于保存它们的局部变量。将主程序中的 a、b 变量作为参数值传入 swap() 函数,实际上是在 swap() 函数栈区中重新产生了两个变量 a、b,并将主程序栈区中 a、b 变量的值分别赋值给 swap() 函数栈区中的 a、b 参数(就是对 swap() 函数的 a、b 两个变量进行初始化)。此时,系统存在两个 a 变量、两个 b 变量,只是存在于不同的栈区中而己。
程序在 swap() 函数中交换 a、b 两个变量的值,实际上是对图 2 中灰色区域的 a、b 变量进行交换。交换结束后,输出 swap() 函数中 a、b 变量的值,可以看到 a 的值为 9,b 的值为 6,此时在内存中的存储示意图如图 3 所示。
图 3 swap() 函数中 a、b 交换之后的存储示意图
对比图 3 与图 1,可以看到两个示意图中主程序栈区中 a、b 的值并未有任何改变,程序改变的只是 swap() 函数栈区中 a、b 的值。这就是值传递的实质:当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋给函数的形参变量,在函数中操作的并不是实际的实参变量。
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("外部 dw 字典中,a =", dw['a']," b =",dw['b'])
运行上面程序,将看到如下运行结果:
swap 函数里,a = 9 b = 6
外部 dw 字典中,a = 9 b = 6
从上面的运行结果来看,在 swap() 函数里,dw 字典的 a、b 两个元素的值被交换成功。不仅如此,当 swap() 函数执行结束后,主程序中 dw 字典的 a、b 两个元素的值也被交换了。
注意,这里这很容易造成一种错觉,读者可能认为,在此 swap() 函数中,使用 dw 字典,就是外界的 dw 字典本身,而不是他的复制品。这只是一种错觉,实际上,引用传递的底层实现,依旧使用的是值传递的方式。下面还是结合示意图来说明程序的执行过程。
程序开始创建了一个字典对象,并定义了一个 dw 引用变量(其实就是一个指针)指向字典对象,这意味着此时内存中有两个东西:对象本身和指向该对象的引用变量。此时在系统内存中的存储示意图如图 4 所示:
图 4 主程序创建了字典对象后存储示意图
接下来主程序开始调用 swap() 函数,在调用 swap() 函数时,dw 变量作为参数传入 swap() 函数,这里依然采用值传递方式:把主程序中 dw 变量的值赋给 swap() 函数的 dw 形参,从而完成 swap() 函数的 dw 参数的初始化。值得指出的是,主程序中的 dw 是一个引用变量(也就是一个指针),它保存了字典对象的地址值,当把 dw 的值赋给 swap() 函数的 dw 参数后,就是让 swap() 函数的 dw 参数也保存这个地址值,即也会引用到同一个字典对象。图 5 显示了 dw 字典传入 swap() 函数后的存储示意图。
图 5 dw 字典传入 swap() 函数后存储示意图
从图 5 来看,这种参数传递方式是不折不扣的值传递方式,系统一样复制了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 变量不再指向任何对象,程序其他地方没有任何改变。主程序调用 swap() 函数后,再次访问 dw 变量的 a、b 两个元素,依然可以输出 9、6。可见,主程序中的 dw 变量没有受到任何影响。实际上,当在 swap() 函数中增加“dw =None”代码后,在内存中的存储示意图如图 6 所示。
图 6 将 swap() 函数中的 dw 赋值为 None 后存储示意图
从图 6 来看,把 swap() 函数中的 dw 赋值为 None 后,在 swap() 函数中失去了对字典对象的引用,不可再访问该字典对象。但主程序中的 dw 变量不受任何影响,依然可以引用该字典对象,所以依然可以输出字典对象的 a、b 元素的值。
通过上面介绍可以得出如下两个结论:
- 不管什么类型的参数,在 Python 函数中对参数直接使用“=”符号赋值是没用的,直接使用“=”符号赋值并不能改变参数。
- 如果需要让函数修改某些数据,则可以通过把这些数据包装成列表、字典等可变对象,然后把列表、字典等可变对象作为参数传入函数,在函数中通过列表、字典的方法修改它们,这样才能改变这些数据。
什么是位置参数,Python位置参数
位置参数,有时也称必备参数,指的是必须按照正确的顺序将实际参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置都必须和定义函数时保持一致。
实参和形参数量必须一致
在调用函数,指定的实际参数的数量,必须和形式参数的数量一致(传多传少都不行),否则 Python 解释器会抛出 TypeError 异常,并提示缺少必要的位置参数。
例如:
def girth(width , height): return 2 \* (width + height) #调用函数时,必须传递 2 个参数,否则会引发错误 print(girth(3))
运行结果为:
Traceback (most recent call last): File “C:\Users\mengma\Desktop\1.py”, line 4, in print(girth(3)) TypeError: girth() missing 1 required positional argument: ‘height’
可以看到,抛出的异常类型为 TypeError,具体是指 girth() 函数缺少一个必要的 height 参数。
同样,多传参数也会抛出异常:
def girth(width , height): return 2 \* (width + height) #调用函数时,必须传递 2 个参数,否则会引发错误 print(girth(3,2,4))
运行结果为:
Traceback (most recent call last): File “C:\Users\mengma\Desktop\1.py”, line 4, in print(girth(3,2,4)) TypeError: girth() takes 2 positional arguments but 3 were given
通过 TypeErroe 异常信息可以知道,girth() 函数本只需要 2 个参数,但是却传入了 3 个参数。
实参和形参位置必须一致
在调用函数时,传入实际参数的位置必须和形式参数位置一一对应,否则会产生以下 2 种结果:
- 抛出 TypeError 异常
当实际参数类型和形式参数类型不一致,并且在函数种,这两种类型之间不能正常转换,此时就会抛出 TypeError 异常。
例如:
def area(height,width): return height\*width/2 print(area("C语言中文网",3))
输出结果为:
Traceback (most recent call last): File “C:\Users\mengma\Desktop\1.py”, line 3, in print(area(“C语言中文网”,3)) File “C:\Users\mengma\Desktop\1.py”, line 2, in area return height*width/2 TypeError: unsupported operand type(s) for /: ‘str’ and ‘int’
以上显示的异常信息,就是因为字符串类型和整形数值做除法运算。
2. 产生的结果和预期不符
调用函数时,如果指定的实际参数和形式参数的位置不一致,但它们的数据类型相同,那么程序将不会抛出异常,只不过导致运行结果和预期不符。
例如,设计一个求梯形面积的函数,并利用此函数求上底为 4cm,下底为 3cm,高为 5cm 的梯形的面积。但如果交互高和下低参数的传入位置,计算结果将导致错误:
def area(upper_base,lower_bottom,height): return (upper_base+lower_bottom)\*height/2 print("正确结果为:",area(4,3,5)) print("错误结果为:",area(4,5,3))
运行结果为:
正确结果为: 17.5
错误结果为: 13.5
因此,在调用函数时,一定要确定好位置,否则很有可能产生类似示例中的这类错误,还不容易发现。
Python函数关键字参数及用法
目前为止,我们使用函数时所用的参数都是位置参数,即传入函数的实际参数必须与形式参数的数量和位置对应。而本节将介绍的关键字参数,则可以避免牢记参数位置的麻烦,令函数的调用和参数传递更加灵活方便。
关键字参数是指使用形式参数的名字来确定输入的参数值。通过此方式指定函数实参时,不再需要与形参的位置完全一致,只要将参数名写正确即可。
因此,Python 函数的参数名应该具有更好的语义,这样程序可以立刻明确传入函数的每个参数的含义。
例如,在下面的程序中就使用到了关键字参数的形式给函数传参:
def dis\_str(str1,str2): print("str1:",str1) print("str2:",str2) #位置参数 dis_str("http://c.biancheng.net/python/","http://c.biancheng.net/shell/") #关键字参数 dis_str("http://c.biancheng.net/python/",str2="http://c.biancheng.net/shell/") dis_str(str2="http://c.biancheng.net/python/",str1="http://c.biancheng.net/shell/")
程序执行结果为:
str1: http://c.biancheng.net/python/
str2: http://c.biancheng.net/shell/
str1: http://c.biancheng.net/python/
str2: http://c.biancheng.net/shell/
str1: http://c.biancheng.net/shell/
str2: http://c.biancheng.net/python/
可以看到,在调用有参函数时,既可以根据位置参数来调用,也可以使用关键字参数(程序中第 8 行)来调用。在使用关键字参数调用时,可以任意调换参数传参的位置。
当然,还可以像第 7 行代码这样,使用位置参数和关键字参数混合传参的方式。但需要注意,混合传参时关键字参数必须位于所有的位置参数之后。也就是说,如下代码是错误的:
# 位置参数必须放在关键字参数之前,下面代码错误 dis_str(str1="http://c.biancheng.net/python/","http://c.biancheng.net/shell/")
Python 解释器会报如下错误:
SyntaxError: positional argument follows keyword argument
Python函数默认参数设置(超级详细)
我们知道,在调用函数时如果不指定某个参数,Python 解释器会抛出异常。为了解决这个问题,Python 允许为参数设置默认值,即在定义函数时,直接给形式参数指定一个默认值。这样的话,即便调用函数时没有给拥有默认值的形参传递参数,该参数可以直接使用定义函数时设置的默认值。
Python 定义带有默认值参数的函数,其语法格式如下:
def 函数名(…,形参名,形参名=默认值):
代码块
注意,在使用此格式定义函数时,指定有默认值的形式参数必须在所有没默认值参数的最后,否则会产生语法错误。
下面程序演示了如何定义和调用有默认参数的函数:
#str1没有默认参数,str2有默认参数 def dis\_str(str1,str2 = "http://c.biancheng.net/python/"): print("str1:",str1) print("str2:",str2) dis_str("http://c.biancheng.net/shell/") dis_str("http://c.biancheng.net/java/","http://c.biancheng.net/golang/")
运行结果为:
str1: http://c.biancheng.net/shell/
str2: http://c.biancheng.net/python/
str1: http://c.biancheng.net/java/
str2: http://c.biancheng.net/golang/
上面程序中,dis_str() 函数有 2 个参数,其中第 2 个设有默认参数。这意味着,在调用 dis_str() 函数时,我们可以仅传入 1 个参数,此时该参数会传给 str1 参数,而 str2 会使用默认的参数,如程序中第 6 行代码所示。
当然在调用 dis_str() 函数时,也可以给所有的参数传值(如第 7 行代码所示),这时即便 str2 有默认值,它也会优先使用传递给它的新值。
同时,结合关键字参数,以下 3 种调用 dis_str() 函数的方式也是可以的:
dis_str(str1 = "http://c.biancheng.net/shell/") dis_str("http://c.biancheng.net/java/",str2 = "http://c.biancheng.net/golang/") dis_str(str1 = "http://c.biancheng.net/java/",str2 = "http://c.biancheng.net/golang/")
再次强调,当定义一个有默认值参数的函数时,有默认值的参数必须位于所有没默认值参数的后面。因此,下面例子中定义的函数是不正确的:
#语法错误 def dis\_str(str1="http://c.biancheng.net/python/",str2,str3): pass
显然,str1 设有默认值,而 str2 和 str3 没有默认值,因此 str1 必须位于 str2 和 str3 之后。
有读者可能会问,对于自己自定义的函数,可以轻易知道哪个参数有默认值,但如果使用 Python 提供的内置函数,又或者其它第三方提供的函数,怎么知道哪些参数有默认值呢?
Pyhton 中,可以使用“函数名.defaults”查看函数的默认值参数的当前值,其返回值是一个元组。以本节中的 dis_str() 函数为例,在其基础上,执行如下代码:
print(dis_str.__defaults__)
程序执行结果为:
(‘http://c.biancheng.net/python/’,)
Python函数可变参数(*args,**kwargs)详解
Python 在定义函数时也可以使用可变参数,即允许定义参数个数可变的函数。这样当调用该函数时,可以向其传入任意多个参数。
可变参数,又称不定长参数,即传入函数中的实际参数可以是任意多个。Python 定义可变参数,主要有以下 2 种形式。
1) 可变参数:形参前添加一个 ‘*’
此种形式的语法格式如下所示:
*args
args 表示创建一个名为 args 的空元组,该元组可接受任意多个外界传入的非关键字实参。
下面程序演示了如何定义一个参数可变的函数:
# 定义了支持参数收集的函数 def dis\_str(home, \*str) : print(home) # 输出str元组中的元素 print("str=",str) for s in str : print(s) #可传入任何多个参数 dis_str("http://c.biancheng.net","http://c.biancheng.net/python/","http://c.biancheng.net/shell/")
程序执行结果为:
http://c.biancheng.net str= (‘http://c.biancheng.net/python/’, ‘http://c.biancheng.net/shell/’) http://c.biancheng.net/python/ http://c.biancheng.net/shell/
上面程序中,dis_str() 函数的最后一个参数就是 str 元组,这样在调用该函数时,除了前面位置参数接收对应位置的实参外,其它非关键字参数都会由 str 元组接收。
当然,可变参数并不一定必须为最后一个函数参数,例如修改 dis_str() 函数为:
# 定义了支持参数收集的函数 def dis\_str(\*str,home) : print(home) # 输出str元组中的元素 print("str=",str) for s in str : print(s) dis_str("http://c.biancheng.net","http://c.biancheng.net/python/",home="http://c.biancheng.net/shell/")
可以看到,str 可变参数作为 dis_str() 函数的第一个参数。但需要注意的是,在调用该函数时,必须以关键字参数的形式给普通参数传值,否则 Python 解释器会把所有参数都优先传给可变参数,如果普通参数没有默认值,就会报错。
也就是说,下面代码调用上面的 dia_str() 函数,是不对的:
dis_str("http://c.biancheng.net","http://c.biancheng.net/python/","http://c.biancheng.net/shell/")
Python 解释器会提供如下报错信息:
TypeError: dis_str() missing 1 required keyword-only argument: ‘home’
翻译过来就是我们没有给 home 参数传值。当然,如果 home 参数有默认参数,则此调用方式是可行的。
2) 可变参数:形参前添加两个’*’
这种形式的语法格式如下:
**kwargs
**kwargs 表示创建一个名为 kwargs 的空字典,该字典可以接收任意多个以关键字参数赋值的实际参数。
例如如下代码:
# 定义了支持参数收集的函数 def dis\_str(home,\*str,\*\*course) : print(home) print(str) print(course) #调用函数 dis_str("C语言中文网",\ "http://c.biancheng.net",\ "http://c.biancheng.net/python/",\ shell教程="http://c.biancheng.net/shell/",\ go教程="http://c.biancheng.net/golang/",\ java教程="http://c.biancheng.net/java/")
程序输出结果为:
C语言中文网
(‘http://c.biancheng.net’, ‘http://c.biancheng.net/python/’) {‘shell教程’: ‘http://c.biancheng.net/shell/’, ‘go教程’: ‘http://c.biancheng.net/golang/’, ‘java教程’: ‘http://c.biancheng.net/java/’}
上面程序在调用 dis_str() 函数时,第 1 个参数传递给 home 参数,第 2、3 个非关键字参数传递给 str 元组,最后 2 个关键字参数将由 course 字典接收。
注意,*args 可变参数的值默认是空元组,**kwargs 可变参数的值默认是空字典。因此,在调用具有可变参数的函数时,不一定非要给它们传值。以调用 dis_str(home, *str, **course) 为例,下面的调用方式也是正确的:
dis_str(home="http://c.biancheng.net/shell/")
程序执行结果为:
http://c.biancheng.net/shell/ () {}
Python逆向参数收集详解(进阶必读)
前面章节中介绍了,Python 支持定义具有可变参数的函数,即该函数可以接收任意多个参数,其中非关键字参数会集中存储到元组参数(*args)中,而关键字参数则集中存储到字典参数(**kwargs)中,这个过程可称为参数收集。
不仅如此,Python 还支持逆向参数收集,即直接将列表、元组、字典作为函数参数,Python 会将其进行拆分,把其中存储的元素按照次序分给函数中的各个形参。
在以逆向参数收集的方式向函数参数传值时,Pyhon 语法规定,当传入列表或元组时,其名称前要带一个 * 号,当传入字典时,其名称前要带有 2 个 * 号。
举个例子:
def dis\_str(name,add) : print("name:",name) print("add",add) data = ["Python教程","http://c.biancheng.net/python/"] #使用逆向参数收集方式传值 dis_str(\*data)
程序执行结果为:
name: Python教程
add http://c.biancheng.net/python/
再举个例子:
def dis\_str(name,add) : print("name:",name) print("add:",add) data = {'name':"Python教程",'add':"http://c.biancheng.net/python/"} #使用逆向参数收集方式传值 dis_str(\*\*data)
程序执行结果为:
name: Python教程
add: http://c.biancheng.net/python/
此外,以逆向参数收集的方式,还可以给拥有可变参数的函数传参,例如:
def dis\_str(name,\*add) : print("name:",name) print("add:",add) data = ["http://c.biancheng.net/python/",\ "http://c.biancheng.net/shell/",\ "http://c.biancheng.net/golang/"] #使用逆向参数收集方式传值 dis_str("Python教程",\*data)
程序执行结果为:
name: Python教程 add: (‘http://c.biancheng.net/python/’, ‘http://c.biancheng.net/shell/’, ‘http://c.biancheng.net/golang/’)
上面程序中,也同样可以用逆向参数收集的方式给 name 参数传值,只需要将 “python教程” 放到 data 列表中第一个位置即可。也就是说,上面程序中,以下面代码调用 dis_str() 函数的方式也是可行的:
data = ["Python教程",\ "http://c.biancheng.net/python/",\ "http://c.biancheng.net/shell/",\ "http://c.biancheng.net/golang/"] #使用逆向参数收集方式传值 dis_str(\*data)
执行此程序,会发现其输出结果和上面一致。
再次强调,如果使用逆向参数收集的方式,必须注意 * 号的添加。以逆向收集列表为例,如果传参时其列表名前不带 * 号,则 Python 解释器会将整个列表作为参数传递给一个参数。例如:
def dis\_str(name,\*add) : print("name:",name) print("add:",add) data = ["Python教程",\ "http://c.biancheng.net/python/",\ "http://c.biancheng.net/shell/",\ "http://c.biancheng.net/golang/"] dis_str(data)
程序执行结果为:
name: [‘Python教程’, ‘http://c.biancheng.net/python/’, ‘http://c.biancheng.net/shell/’, ‘http://c.biancheng.net/golang/’] add: ()
Python None(空值)及用法
在 Python 中,有一个特殊的常量 None(N 必须大写)。和 False 不同,它不表示 0,也不表示空字符串,而表示没有值,也就是空值。
这里的空值并不代表空对象,即 None 和 []、“” 不同:
None is []
False
None is “”
False
None 有自己的数据类型,我们可以在 IDLE 中使用 type() 函数查看它的类型,执行代码如下:
type(None)
可以看到,它属于 NoneType 类型。
需要注意的是,None 是 NoneType 数据类型的唯一值(其他编程语言可能称这个值为 null、nil 或 undefined),也就是说,我们不能再创建其它 NoneType 类型的变量,但是可以将 None 赋值给任何变量。如果希望变量中存储的东西不与任何其它值混淆,就可以使用 None。
除此之外,None 常用于 assert、判断以及函数无返回值的情况。举个例子,在前面章节中我们一直使用 print() 函数输出数据,其实该函数的返回值就是 None。因为它的功能是在屏幕上显示文本,根本不需要返回任何值,所以 print() 就返回 None。
spam = print(‘Hello!’)
Hello!
None == spam
True
另外,对于所有没有 return 语句的函数定义,Python 都会在末尾加上 return None,使用不带值的 return 语句(也就是只有 return 关键字本身),那么就返回 None。
Python return函数返回值详解
到目前为止,我们创建的函数都只是对传入的数据进行了处理,处理完了就结束。但实际上,在某些场景中,我们还需函数将处理的结果反馈回来,就好像主管向下级员工下达命令,让其去打印文件,员工打印好文件后并没有完成任务,还需要将文件交给主管。
Python中,用 def 语句创建函数时,可以用 return 语句指定应该返回的值,该返回值可以是任意类型。需要注意的是,return 语句在同一函数中可以出现多次,但只要有一个得到执行,就会直接结束函数的执行。
函数中,使用 return 语句的语法格式如下:
return [返回值]
其中,返回值参数可以指定,也可以省略不写(将返回空值 None)。
【例 1】
def add(a,b): c = a + b return c #函数赋值给变量 c = add(3,4) print(c) #函数返回值作为其他函数的实际参数 print(add(3,4))
运行结果为:
7
7
本例中,add() 函数既可以用来计算两个数的和,也可以连接两个字符串,它会返回计算的结果。
通过 return 语句指定返回值后,我们在调用函数时,既可以将该函数赋值给一个变量,用变量保存函数的返回值,也可以将函数再作为某个函数的实际参数。
【例 2】
def isGreater0(x): if x > 0: return True else: return False print(isGreater0(5)) print(isGreater0(0))
运行结果为:
True
False
可以看到,函数中可以同时包含多个 return 语句,但需要注意的是,最终真正执行的做多只有 1 个,且一旦执行,函数运行会立即结束。
以上实例中,我们通过 return 语句,都仅返回了一个值,但其实通过 return 语句,可以返回多个值,读者可以阅读《Python函数返回多个值》一节做详细了解。
Python函数返回多个值的方法(入门必读)
通常情况下,一个函数只有一个返回值,实际上 Python 也是如此,只不过 Python 函数能以返回列表或者元组的方式,将要返回的多个值保存到序列中,从而间接实现返回多个值的目的。
因此,实现 Python 函数返回多个值,有以下 2 种方式:
- 在函数中,提前将要返回的多个值存储到一个列表或元组中,然后函数返回该列表或元组;
- 函数直接返回多个值,之间用逗号( , )分隔,Python 会自动将多个值封装到一个元组中,其返回值仍是一个元组。
下面程序演示了以上 2 种实现方法:
def retu\_list() : add = ["http://c.biancheng.net/python/",\ "http://c.biancheng.net/shell/",\ "http://c.biancheng.net/golang/"] return add def retu\_tuple() : return "http://c.biancheng.net/python/",\ "http://c.biancheng.net/golang/",\ "http://c.biancheng.net/golang/" print("retu\_list = ",retu_list()) print("retu\_tuple = ",retu_tuple())
程序执行结果为:
retu_list = [‘http://c.biancheng.net/python/’, ‘http://c.biancheng.net/shell/’, ‘http://c.biancheng.net/golang/’] retu_tuple = (‘http://c.biancheng.net/python/’, ‘http://c.biancheng.net/golang/’, ‘http://c.biancheng.net/golang/’)
在此基础上,我们可以利用 Python 提供的序列解包功能,之间使用对应数量的变量,直接接收函数返回列表或元组中的多个值。这里以 retu_list() 为例:
def retu\_list() : add = ["http://c.biancheng.net/python/",\ "http://c.biancheng.net/shell/",\ "http://c.biancheng.net/golang/"] return add pythonadd,shelladd,golangadd = retu_list() print("pythonadd=",pythonadd) print("shelladd=",shelladd) print("golangadd=",golangadd)
程序执行结果为:
pythonadd= http://c.biancheng.net/python/
shelladd= http://c.biancheng.net/shell/
golangadd= http://c.biancheng.net/golang/
Python函数递归(带实例演示)
一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。
递归函数不是 Python 语言的专利,C/C++、Java、C#、JavaScript、PHP 等其他编程语言也都支持递归函数。
下面我们通过一个实例,看看递归函数到底是如何运作的。
有这样一个数学题。己知有一个数列:f(0) = 1,f(1) = 4,f(n + 2) = 2*f(n+ 1) +f(n),其中 n 是大于 0 的整数,求 f(10) 的值。这道题可以使用递归来求得。下面程序将定义一个 fn() 函数,用于计算 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)的结果是:", fn(10))
在上面的 fn() 函数体中再次调用了 fn() 函数,这就是函数递归。注意在 fn() 函数体中调用 fn 的形式:
return 2 * fn(n - 1) + fn(n - 2)
对于 fn(10),即等于 2fn(9)+fn(8),其中 fn(9) 又等于 2fn(8)+fn(7)……依此类推,最终会计算到 fn(2) 等于 2*fn(1)+fn(0),即 fn(2) 是可计算的,这样递归带来的隐式循环就有结束的时候,然后一路反算回去,最后就可以得到 fn(10) 的值。
仔细看上面递归的过程,当一个函数不断地调用它自身时,必须在某个时刻函数的返回值是确定的,即不再调用它自身:否则,这种递归就变成了无穷递归,类似于死循环。因此,在定义递归函数时有一条最重要的规定: 递归一定要向已知方向进行。
例如,如果把上面数学题改为如此。己知有一个数列:f(20)=1,f(21)=4,f(n + 2)=2*f(n+1)+f(n),其中 n 是大于 0 的整数,求 f(10) 的值。那么 f(10) 的函数体应该改为如下形式:
def fn(n) : if n == 20 : return 1 elif n == 21 : return 4 else : # 函数中调用它自身,就是函数递归 return fn(n + 2) - 2\*fn(n + 1)
从上面的 fn() 函数来看,当程序要计算 fn(10) 的值时,fn(10) 等于 fn(12)-2fn(11),而 fn(11) 等于 fn(13)-2fn(12)……依此类推,直到 fn(19) 等于 fn(21)-2fn(20),此时就可以得到 fn(19) 的值,然后依次反算到 fn(10) 的值。这就是递归的重要规则:对于求 fn(10) 而言,如果 fn(0) 和 fn(1) 是已知的,则应该采用 fn(n)=2fn(n-1)+fn(n-2) 的形式递归,因为小的一端已知;如果 fn(20) 和 fn(21) 是已知的,则应该采用 fn(n)=fn(n+2)-2*fn(n+1) 的形式递归,因为大的一端已知。
递归是非常有用的,例如程序希望遍历某个路径下的所有文件,但这个路径下的文件夹的深度是未知的,那么就可以使用递归来实现这个需求。系统可定义一个函数,该函数接收一个文件路径作为参数,该函数可遍历出当前路径下的所有文件和文件路径,即在该函数的函数体中再次调用函数自身来处理该路径下的所有文件路径。
总之,只要在一个函数的函数体中调用了函数自身,就是函数递归。递归一定要向已知方向进行。
Python变量作用域(全局变量和局部变量)
所谓作用域(Scope),就是变量的有效范围,就是变量可以在哪个范围以内使用。有些变量可以在整段代码的任意位置使用,有些变量只能在函数内部使用,有些变量只能在 for 循环内部使用。
变量的作用域由变量的定义位置决定,在不同位置定义的变量,它的作用域是不一样的。本节我们只讲解两种变量,局部变量和全局变量。
Python局部变量
在函数内部定义的变量,它的作用域也仅限于函数内部,出了函数就不能使用了,我们将这样的变量称为局部变量(Local Variable)。
要知道,当函数被执行时,Python 会为其分配一块临时的存储空间,所有在函数内部定义的变量,都会存储在这块空间中。而在函数执行完毕后,这块临时存储空间随即会被释放并回收,该空间中存储的变量自然也就无法再被使用。
举个例子:
def demo(): add = "http://c.biancheng.net/python/" print("函数内部 add =",add)demo()print("函数外部 add =",add)
程序执行结果为:
函数内部 add = http://c.biancheng.net/python/
Traceback (most recent call last):
File “C:\Users\mengma\Desktop\file.py”, line 6, in
print(“函数外部 add =”,add)
NameError: name ‘add’ is not defined
可以看到,如果试图在函数外部访问其内部定义的变量,Python 解释器会报 NameError 错误,并提示我们没有定义要访问的变量,这也证实了当函数执行完毕后,其内部定义的变量会被销毁并回收。
值得一提的是,函数的参数也属于局部变量,只能在函数内部使用。例如:
def demo(name,add): print("函数内部 name =",name) print("函数内部 add =",add)demo("Python教程","http://c.biancheng.net/python/")print("函数外部 name =",name)print("函数外部 add =",add)
程序执行结果为:
函数内部 name = Python教程
函数内部 add = http://c.biancheng.net/python/
Traceback (most recent call last):
File “C:\Users\mengma\Desktop\file.py”, line 7, in
print(“函数外部 name =”,name)
NameError: name ‘name’ is not defined
由于 Python 解释器是逐行运行程序代码,由此这里仅提示给我“name 没有定义”,实际上在函数外部访问 add 变量也会报同样的错误。
Python全局变量
除了在函数内部定义变量,Python 还允许在所有函数的外部定义变量,这样的变量称为全局变量(Global Variable)。
和局部变量不同,全局变量的默认作用域是整个程序,即全局变量既可以在各个函数的外部使用,也可以在各函数内部使用。
定义全局变量的方式有以下 2 种:
- 在函数体外定义的变量,一定是全局变量,例如:
add = "http://c.biancheng.net/shell/" def text(): print("函数体内访问:",add) text() print('函数体外访问:',add)
运行结果为:
函数体内访问: http://c.biancheng.net/shell/
函数体外访问: http://c.biancheng.net/shell/
- 在函数体内定义全局变量。即使用 global 关键字对变量进行修饰后,该变量就会变为全局变量。例如:
def text(): global add add= "http://c.biancheng.net/java/" print("函数体内访问:",add) text() print('函数体外访问:',add)
运行结果为:
函数体内访问: http://c.biancheng.net/java/
函数体外访问: http://c.biancheng.net/java/
注意,在使用 global 关键字修饰变量名时,不能直接给变量赋初值,否则会引发语法错误。
获取指定作用域范围中的变量
在一些特定场景中,我们可能需要获取某个作用域内(全局范围内或者局部范围内)所有的变量,Python 提供了以下 3 种方式:
1) globals()函数
globals() 函数为 Python 的内置函数,它可以返回一个包含全局范围内所有变量的字典,该字典中的每个键值对,键为变量名,值为该变量的值。
举个例子:
#全局变量 Pyname = "Python教程" Pyadd = "http://c.biancheng.net/python/" def text(): #局部变量 Shename = "shell教程" Sheadd= "http://c.biancheng.net/shell/" print(globals())
程序执行结果为:
{ … , ‘Pyname’: ‘Python教程’, ‘Pyadd’: ‘http://c.biancheng.net/python/’, …}
注意,globals() 函数返回的字典中,会默认包含有很多变量,这些都是 Python 主程序内置的,读者暂时不用理会它们。
可以看到,通过调用 globals() 函数,我们可以得到一个包含所有全局变量的字典。并且,通过该字典,我们还可以访问指定变量,甚至如果需要,还可以修改它的值。例如,在上面程序的基础上,添加如下语句:
print(globals()['Pyname']) globals()['Pyname'] = "Python入门教程" print(Pyname)
程序执行结果为:
Python教程
Python入门教程
2) locals()函数
locals() 函数也是 Python 内置函数之一,通过调用该函数,我们可以得到一个包含当前作用域内所有变量的字典。这里所谓的“当前作用域”指的是,在函数内部调用 locals() 函数,会获得包含所有局部变量的字典;而在全局范文内调用 locals() 函数,其功能和 globals() 函数相同。
举个例子:
#全局变量 Pyname = "Python教程" Pyadd = "http://c.biancheng.net/python/" def text(): #局部变量 Shename = "shell教程" Sheadd= "http://c.biancheng.net/shell/" print("函数内部的 locals:") print(locals()) text() print("函数外部的 locals:") print(locals())
程序执行结果为:
函数内部的 locals:
{‘Sheadd’: ‘http://c.biancheng.net/shell/’, ‘Shename’: ‘shell教程’}
函数外部的 locals:
{… , ‘Pyname’: ‘Python教程’, ‘Pyadd’: ‘http://c.biancheng.net/python/’, … }
当使用 locals() 函数获取所有全局变量时,和 globals() 函数一样,其返回的字典中会默认包含有很多变量,这些都是 Python 主程序内置的,读者暂时不用理会它们。
注意,当使用 locals() 函数获得所有局部变量组成的字典时,可以向 globals() 函数那样,通过指定键访问对应的变量值,但无法对变量值做修改。例如:
#全局变量 Pyname = "Python教程" Pyadd = "http://c.biancheng.net/python/" def text(): #局部变量 Shename = "shell教程" Sheadd= "http://c.biancheng.net/shell/" print(locals()['Shename']) locals()['Shename'] = "shell入门教程" print(Shename) text()
程序执行结果为:
shell教程
shell教程
显然,locals() 返回的局部变量组成的字典,可以用来访问变量,但无法修改变量的值。
3) vars(object)
vars() 函数也是 Python 内置函数,其功能是返回一个指定 object 对象范围内所有变量组成的字典。如果不传入object 参数,vars() 和 locals() 的作用完全相同。
由于目前读者还未学习 Python 类和对象,因此初学者可先跳过该函数的学习,等学完 Python 类和对象之后,再回过头来学习该函数。
举个例子:
#全局变量 Pyname = "Python教程" Pyadd = "http://c.biancheng.net/python/" class Demo: name = "Python 教程" add = "http://c.biancheng.net/python/" print("有 object:") print(vars(Demo)) print("无 object:") print(vars())
程序执行结果为:
有 object:
{… , ‘name’: ‘Python 教程’, ‘add’: ‘http://c.biancheng.net/python/’, …} 无 object: {… , ‘Pyname’: ‘Python教程’, ‘Pyadd’: ‘http://c.biancheng.net/python/’, … }
Python如何在函数中使用同名的全局变量?
《Python局部变量和全局变量》一节中提到,全局变量可以在程序中任何位置被访问甚至修改,但是,当函数中定义了和全局变量同名的局部变量时,那么在当前函数中,无论是访问还是修改该同名变量,操作的都是局部变量,而不再是全局变量。
当函数内部的局部变量和函数外部的全局变量同名时,在函数内部,局部变量会“遮蔽”同名的全局变量。
有读者可能并不能完全理解上面这段话,没关系,这里举个实例:
name = "Python教程" def demo (): #访问全局变量 print(name) demo()
程序执行结果为:
Python教程
上面程序中,第 4 行直接访问 name 变量,这是允许的。在上面程序的基础上,在函数内部添加一行代码,如下所示:
name = "Python教程" def demo (): #访问全局变量 print(name) name = "shell教程" demo()
执行此程序,Python 解释器报如下错误:
UnboundLocalError: local variable ‘name’ referenced before assignment
该错误直译过来的意思是:所访问的 name 变量还未定义。这是什么原因呢?就是我们添加第 5 行代码导致的。
Python 语法规定,在函数内部对不存在的变量赋值时,默认就是重新定义新的局部变量。上面程序中,第 5 行就定义了一个新的 name 局部变量,由于该局部变量名和全局变量名 name 同名,局部 name 变量就会“遮蔽”全局 name 变量,再加上局部变量 name 在 print(name) 后才被初始化,违反了“先定义后使用”的原则,因此程序会报错。
那么,如果就是想在函数中访问甚至修改被“遮蔽”的变量,怎么办呢?可以采取以下 2 中方法:
- 直接访问被遮蔽的全局变量。如果希望程序依然能访问 name 全局变量,且在函数中可重新定义 name 局部变量,也就是在函数中可以访问被遮蔽的全局变量,此时可通过 globals() 函数来实现,将上面程序改为如下形式即可:
name = "Python教程" def demo (): #通过 globals() 函数访问甚至修改全局变量 print(globals()['name']) globals()['name']="Java教程" #定义局部变量 name = "shell教程" demo() print(name)
程序执行结果为:
Python教程
Java教程
2. 在函数中声明全局变量。为了避免在函数中对全局变量赋值(不是重新定义局部变量),可使用 global 语句来声明全局变量。因此,可将程序改为如下形式:
name = "Python教程" def demo (): global name #访问全局name变量 print(name) #修改全局name变量的值 name = "shell教程" demo() print(name)
程序执行结果为:
Python教程
shell教程
增加了“global name”声明之后,程序会把 name 变量当成全局变量,这意味着 demo() 函数后面对 name 赋值的语句只是对全局变量赋值,而不是重新定义局部变量。
Python局部函数及用法(包含nonlocal关键字)
通过前面的学习我们知道,Python 函数内部可以定义变量,这样就产生了局部变量,有读者可能会问,Python 函数内部能定义函数吗?答案是肯定的。Python 支持在函数内部定义函数,此类函数又称为局部函数。
那么,局部函数有哪些特征,在使用时需要注意什么呢?接下来就给读者详细介绍 Python 局部函数的用法。
首先,和局部变量一样,默认情况下局部函数只能在其所在函数的作用域内使用。举个例子:
#全局函数 def outdef (): #局部函数 def indef(): print("http://c.biancheng.net/python/") #调用局部函数 indef() #调用全局函数 outdef()
程序执行结果为:
http://c.biancheng.net/python/
就如同全局函数返回其局部变量,就可以扩大该变量的作用域一样,通过将局部函数作为所在函数的返回值,也可以扩大局部函数的使用范围。例如,修改上面程序为:
#全局函数 def outdef (): #局部函数 def indef(): print("调用局部函数") #调用局部函数 return indef #调用全局函数 new_indef = outdef() 调用全局函数中的局部函数 new_indef()
程序执行结果为:
调用局部函数
因此,对于局部函数的作用域,可以总结为:如果所在函数没有返回局部函数,则局部函数的可用范围仅限于所在函数内部;反之,如果所在函数将局部函数作为返回值,则局部函数的作用域就会扩大,既可以在所在函数内部使用,也可以在所在函数的作用域中使用。
以上面程序中的 outdef() 和 indef() 为例,如果 outdef() 不将 indef 作为返回值,则 indef() 只能在 outdef() 函数内部使用;反之,则 indef() 函数既可以在 outdef() 函数内部使用,也可以在 outdef() 函数的作用域,也就是全局范围内使用。
有关函数返回函数,更详细的讲解,可阅读《Python函数高级方法》一节。
另外值得一提的是,如果局部函数中定义有和所在函数中变量同名的变量,也会发生“遮蔽”的问题。例如:
#全局函数 def outdef (): name = "所在函数中定义的 name 变量" #局部函数 def indef(): print(name) name = "局部函数中定义的 name 变量" indef() #调用全局函数 outdef()
执行此程序,Python 解释器会报如下错误:
UnboundLocalError: local variable ‘name’ referenced before assignment