第七章 函数

 
       很多代码放在一个python文件中显然不合适。可以将代码根据功能不同放在不同的python文件中,然后通过导入模块的方式进行重用。不过由于导入模块时,python解析器会引用模块中的所有代码,但实际的情况中大多数只会引用某段代码。这就引出了函数。

       从本质上说。函数就是将一段代码封装起来,然后被其他python程序重用。如果不主动调用函数,代码是不会执行的。既然其他程序要调用函数,那么就会涉及到数据交换,数据的交换就是数据的输入/输出。为了完成这项工作,函数有两个技能:参数和返回值。外部程序要调用函数就和访问变量一样,需要有个名字,即函数名。如果给函数下一个定义的话,那么函数就是:一个拥有名称、参数和返回值的代码块。

7.1 懒惰是重用的基石

例如,现在要编写一个斐波那契数列的程序。要求程序是动态的,且不能要求用户去修改python源代码,可以计算任何长度的斐波那契数列。在程序中可以多次的重用这段代码。

fibs = [0,1]
for i in range(10):
    fibs.append(fibs[-2] + fibs[-1])
print(fibs)
def fibs(n):
    result = [0,1]
    for i in range(n - 2):
        result.append(result[-2] + result[-1])
    return result

7.2 函数基础

7.2.1 创建函数

函数是可以调用的。函数名、函数参数、返回值是函数的三个重要元素。其中函数名是必须的,函数参数和返回值是可选的。定义函数要使用def语句,例如

def greet(name):
	return 'hello {}'.format(name)

由于python是动态语言,所以函数参数与返回值都不需要事先指定数据类型,函数参数就直接写参数名即可,如果参数有多个,中间用逗号","分隔。如果函数有返回值,直接调用return语句返回即可。return语句可以返回任何东西,一个值,一个变量,或是另一个函数的返回值。如果函数没有返回值,可以直接省略return语句。

print(greet("李宁"))
print(greet("马云"))

计算斐波那契数列的例子:

# 定义计算斐波那契数列的函数
def fibs(n):
	# 定义列表初始值
    result = [0,1]
    # 循环调用,计算结果保存到result列表中
    for i in range(n - 2):
        result.append(result[-2] + result[-1])
    # 返回计算结果
    return result

# 循环调用函数,返回结果
while True:
    value = input("请输入一个整数:")
    if value == ":exit":
        break;
    n = int(value)
    print(fibs(n))

7.2.2 为函数添加文档注释

python支持单行注释和多行注释,前者使用井号“#”,后者使用三个单引号或双引号将多行注释内容括起来。对于函数来说,还可以使用另外一种注释:文档注释

不管单行注释还是多行注释,在程序编译后,这些注释都会被编译器去掉。而文档注释作为程序的一部分,通过代码可以动态获取这些注释。文档注释有一个很重要的作用就是让函数或类具有自描述功能。通过一些工具可以生成API帮助文档。

为函数添加文档注释需要在def语句的下一行用一对单引号或双引号将注释括起来。可以使用“doc”函数属性获取add函数的文档注释。还可以直接使用help函数直接获取add函数的文档注释。

def add(x,y):
    "计算两个数的和"
    return x + y

print(add.__doc__)
print("----------------")
help(add)
print("----------------")

7.2.3 没有返回值的函数

并不是所有的函数都有返回值。有些函数只需要在内部处理一些逻辑,例如函数需要输出,可以在函数内部通过print函数输出信息。如果函数没有返回值,不使用return语句就可以了,或者使用return语句,但是return语句后边什么也不跟。后者情况主要是从函数的任意深度的代码中直接跳出函数。

如果函数没有返回值,那么使用print函数调用此函数,会输出None,这个值表示没有值。

def test(flag):
    print("这是在函数中打印的信息")
    if flag:
    	# 可以跳出函数任意深度,停止后边代码的执行
        return 
    print("这行信息只有在flag为False是才会输出")

test(False)
print("----------")
# 返回None值
returnValue = test(True)
print(returnValue)

7.3 函数参数

写在def语句中函数名后边圆括号中的参数称为形参,而调用函数时指定的参数称为实参。形参对于调用者来说是透明的,叫什么都无所谓,与调用者无关。这个形参是在函数内部使用的,函数外部并不可见。

7.3.1 改变参数的值

如果将一个变量作为实参传入函数,并在函数内部改变这个形参的值,那么结果怎样?

x = 20
s = "世界你好"

def test(xx,ss):
	xx = 40
	ss = "hello world"

test(x,s)
print(x,s)

执行结果并没有改变:说明对于数值类型、字符串类型等一些简单类型,在函数内部修改变量的值,不会影响到原始变量的值,也就是说,函数内部的变量实际上是x和s的一个副本。
在这里插入图片描述
等价于如下代码段:

x = 20
s = "世界你好"

# 副本,等价于函数作用
x1 = x
s1 = s
x1 = 40
s1 = "hello world"

print(x,s)

再来看看变量x和变量y是字典和列表类型的时候。

x = {"a":30,"b":20}
y = ["a","b","c"]

def test(xx,yy):
	xx["a"] = 100
	yy[1] = "abcd"

test(x,y)
print(x,y)

# 执行结果 {'b': 20, 'a': 100} ['a', 'abcd', 'c']

在函数内部修改字典和列表变量的值,是可以影响外部x变量和y变量的。如果传递的变量类型是数值、字符串、布尔等类型,那么就是值传递。如果传递的变量类型是序列、对象等复合类型,就是引用传递

值传递就是在传递时将自身复制一份,而在函数内部接触到的参数实际上是传递给函数的变量的副本,修改副本的值自然不会影响到原始变量。而像序列、对象这样的复合类型的变量,在传入函数时,实际上也是将其复制了一份,但复制的不是变量中的数据,而是变量的引用。因为这些复合类型在内存中是用一块连续或不连续的内存空间保存,想要找到这些复合类型的数据,必须得到这些内存空间的首地址,而这个首地址就是复合类型数据的引用。因此,将复合类型的变量传入函数,复制的是内存空间的首地址,而不是首地址指向的内存空间本身。引用传递在函数内部修改内存空间中的数据,自然会影响到函数外部变量的值。要保留函数的修改痕迹,那么就要向函数传入复合类型的变量。

# 未使用函数抽象的代码
data = {}
data["d"] = {}
data["names"] = []
data["products"] = []
print("请输入字典数据,key和value之间用逗号分隔")
dictStr = input(":")
# 将dictStr拆分成key和value的两个列表
list = dictStr.split(",")
keys = []
values = []
for i in range(len(list)):
    # key
    if i % 2 == 0:
        keys.append(list[i])
    else:
        values.append(list[i])
# 将key和value的两个列表合并成一个字典,并利用update方法(更新或追加)到data["d"]后面
data["d"].update(dict(zip(keys,values)))

# extend方法可以追加到尾部
print("请输入姓名,多个姓名之间用逗号分隔")
nameStr = input(":")
names = nameStr.split(",")
data["names"].extend(names)


print("请输入产品,多个产品之间用逗号分隔")
productStr = input(":")
products = productStr.split(",")
data["products"].extend(products)


for key in data.keys():
    print(key,":",data[key])

执行结果

请输入字典数据,key和value之间用逗号分隔
:1,2,3,4,5,6
请输入姓名,多个姓名之间用逗号分隔
:11,22,33,44,55,66
请输入产品,多个产品之间用逗号分隔
:111,222,333,444,555,666
names : ['11', '22', '33', '44', '55', '66']
products : ['111', '222', '333', '444', '555', '666']
d : {'5': '6', '3': '4', '1': '2'}

如果要对多个字典进行同样的操作,这样显得太麻烦了,而且会造成代码几大的冗余。下面对代码做一些抽象。

# 初始化函数
def init(data):
	data["d"] = {}
	data["names"] = []
	data["products"] = []

# 数据采集函数,flag为True将字符串转化为列表,flag为False将字符串转化为字典
# msg表示提示文本
def inputListOrDict(flag,msg):
	print(msg)
	inputStr = input(":")
	# 拆分成列表
	list = inputStr.split(",")
	if flag:
		return list
	keys = []
	values = []
	result = {}
	for i in range(len(list)):
    # key
		if i % 2 == 0:
			keys.append(list[i])
		else:
			values.append(list[i])
	#返回字典
	return dict(zip(keys,values))

#输出字典中的数据
def outDict(data):
	for key in data.keys():
	    print(key,":",data[key])
# 导入上边的三个函数所在文件dataman.py
from dataman import *

data1 = {}
data2 = {}
init(data1)
init(data2)
data1["d"].update(inputListOrDict(False, "请输入字典数据,key和value之间用逗号分隔"))
data1["names"].extend(inputListOrDict(True, "请输入姓名,多个姓名之间用逗号分隔"))
data1["products"].extend(inputListOrDict(True, "请输入产品,多个产品之间用逗号分隔"))

data2["d"].update(inputListOrDict(False, "请输入字典数据,key和value之间用逗号分隔"))
data2["names"].extend(inputListOrDict(True, "请输入姓名,多个姓名之间用逗号分隔"))
data2["products"].extend(inputListOrDict(True, "请输入产品,多个产品之间用逗号分隔"))

outDict(data1)
outDict(data2)

执行结果

请输入字典数据,key和value之间用逗号分隔
:1,2,3,4,5,6
请输入姓名,多个姓名之间用逗号分隔
:11,22,33,44,55,66
请输入产品,多个产品之间用逗号分隔
:111,222,333,444,555,666
请输入字典数据,key和value之间用逗号分隔
:1111,2222,3333,4444,5555,6666
请输入姓名,多个姓名之间用逗号分隔
:11111,22222,33333,44444,55555,66666
请输入产品,多个产品之间用逗号分隔
:111111,222222,333333,444444,555555,666666

d : {'1': '2', '5': '6', '3': '4'}
names : ['11', '22', '33', '44', '55', '66']
products : ['111', '222', '333', '444', '555', '666']
d : {'5555': '6666', '1111': '2222', '3333': '4444'}
names : ['11111', '22222', '33333', '44444', '55555', '66666']
products : ['111111', '222222', '333333', '444444', '555555', '666666']

7.3.2 关键字参数与默认值

到目前为止,函数的参数位置很重要,因为在调用函数时,传递实参都是按照形参的定义顺序传递的。记不清参数的位置,这种情况会经常发生。为了抵消这种相关性,调用函数时可以使用关键字指定参数,这种参数被称为关键字参数

那么函数参数的关键字是什么呐?其实就是函数形参的名字。

示例1:不管实参位置如何变化,都不影响函数传参使用。

def greet(name,greeting):
    return "问候语:{} 姓名:{}".format(greeting,name)

print(greet(name = "李宁",greeting = "Hello"))
print(greet(greeting = "Hello",name = "李宁"))
问候语:Hello 姓名:李宁
问候语:Hello 姓名:李宁

示例2:关键字参数与位置参数混合使用。混合使用时,关键字参数必须放在位置参数后面,否则会抛出异常。

def greet(name,greeting):
    return "问候语:{} 姓名:{}".format(greeting,name)

print(greet("李宁",greeting = "Hello"))
print(greet(name = "李宁",greeting = "Hello"))

# 抛出异常:print(greet("Hello",name = "李宁"))
# 抛出异常:print(greet(greeting = "Hello","李宁"))
问候语:Hello 姓名:李宁
问候语:Hello 姓名:李宁

示例3:如果参数过多,或在特定的场景,大多数参数都使用某个固定的值即可,那么可以为函数的形参指定默认值。

def greet(name = "李宁",greeting = "Hello"):
    return "问候语:{} 姓名:{}".format(greeting,name)

print(greet())
print(greet(name = "李宁110",greeting = "Hello"))
问候语:Hello 姓名:李宁
问候语:Hello 姓名:李宁

其他示例4:

def sub1(m, n):
    return m - n

# 使用位置参数传递参数值:16
print(sub1(20,4))
# 使用位置参数传递参数值:-16
print(sub1(4,20))
# 使用关键字参数传递参数值:16
print(sub1(m = 20, n = 4))
# 使用关键字参数传递参数值:16
print(sub1(n = 4, m = 20))

def sub2(m = 100, n = 50):
    return m - n

# 使用默认形参指定的值:50    
print(sub2())
# 使用位置参数传递参数值:24
print(sub2(45,21))
# 使用位置参数、关键字参数传递参数值:41
print(sub2(53, n = 12))
# 使用关键字参数、默认参数传递参数值:-23
print(sub2(n = 123))
# 使用关键字参数传递参数值:399
print(sub2(m = 542,n = 143))
# 抛出异常,位置参数与关键字参数重复定义
print(sub2(53, m = 12))  

注意:

  • 关键字参数必须写在位置参数的后面
  • 只能将位置参数还未设置的参数作为关键字参数指定

7.3.3 可变参数

前边已经多次使用过print函数,这个函数可以接收任意多个参数,在输出到控制台时,会将输出的参数之间加上空格。像print函数这样可以传递任意多个参数的形式称为可变参数。定义函数的可变参数需要在形参前面加一个星号(*)。

def greet(*params):
    print(params)

greet("李宁",11,True,-1)

# 输出:('李宁', 11, True, -1)

可变参数可以接收任意多个参数,而且参数的类型也可以是任意的。从输出的结果来看,可变参数在函数内部是以元组的形式体现的,所以在函数内部可以像使用元组一样使用可变参数中的具体参数值。

def greet(*params):
    for param in params:
        print("<" + str(param) + ">",end = " ")

greet("李宁",11,True,-1)

# 输出:<李宁> <11> <True> <-1> 

如果在函数中,即有普通参数,也有可变参数,通常可变参数会放在最后。

def greet(value,*params):
    print("[" + value + "]")
    for param in params:
        print("<" + str(param) + ">",end = " ")

greet("李宁",11,True,-1,0.4)

'''
[李宁]
<11> <True> <-1> <0.4>
'''

其实可变参数也可以放在函数的中间或最前面,只是在调用函数时,可变参数后面的普通参数要使用关键字参数形式传递参数值。

def greet(value1,*params,value2,value3=666666):
    print("[" + value1 + "]")
    for param in params:
        print("<" + str(param) + ">",end = " ")
    print("{},{}".format(value2,value3))

greet("李宁",11,True,-1,0.4,value2=100,value3=1000)
# 抛出异常:greet("李宁",value2=100,value3=1000,11,True,-1,0.4)
# 抛出异常:greet(value2=100,value3=1000,"李宁",11,True,-1,0.4)
# 抛出异常:greet("李宁",11,value2=100,True,value3=1000,-1,0.4)
# 或者不指定参数默认值,调用时也不指定,均会抛出异常

注意:调用函数时,关键字参数必须在位置参数后面,可变参数必须在位置参数或关键字参数或默认参数的后面。

# 定义一个累加的函数
def addNumbers(*numbers):
    result = 0
    for number in numbers:
        result += number
    return result
	
print(addNumbers(1,2,3,4,5))
print("--------------")

# 定义一个加、减、乘、除的函数
def calculator(type, *numbers):
    result = 0
    if type == "add":
        for number in numbers:
            result += number
    elif type == "sub":
        result = numbers[0]
        for i in range(1, len(numbers)):
            result -= numbers[i]
    elif type == "mul":
        result = 1
        for number in numbers:
            result *= number
    else:
        result = numbers[0]
        for i in range(1, len(numbers)):
            result /= numbers[i]
    return result

print(calculator("add",1,2,3,4,5,6))
print(calculator("sub",1234,44,54,12,57))
print(calculator("mul",1,2,3,4,5,6,7))
print(calculator("div",100,2,5))
print("--------------")


# 定义一个指定ratio倍数的函数
def calculator1(type, *numbers, ratio):
	# 为可变参数值传入一个可变参数值,也需要在变量前边加星号(*)
	# 将序列作为函数参数值
    return calculator(type, *numbers) * ratio
    
print(calculator1("add",1,2,3,4,5,6,ratio = 3))
print(calculator1("sub",1234,44,54,12,57,ratio = 2))
print(calculator1("mul",1,2,3,4,5,6,7,ratio = 4))
print(calculator1("div",100,2,5,ratio = 4))
print("--------------")

# 定义一个默认指定ratio=4的倍数的函数
def calculator2(type, *numbers, ratio = 4):
    return calculator(type, *numbers) * ratio
print(calculator2("add",1,2,3,4,5,6))

7.3.4 将序列作为函数的参数值

函数的参数可以是任何类型,自然也包括序列(元组、列表、字典等)。此处并不是直接将序列作为单个值传入函数,而是将序列中的每个元素单独作为函数的参数值,相当把序列拆开进行传值。

def greet(value1,value2,value3="666666"):
    print("[" + value1 , value2,value3 + "]")

list = ["100","1000","55555"]

# 一般传值方式
greet(value1= "100",value2="1000")

# 将列表或元组中的元素作为单个参数值传递给函数,需要在实参前面加星号(*),
# 此处列表或元组中的元素个数必须与函数定义的个数相同,除非函数也定义一个可变参数
greet(*list)
def greet(*value1):
    for s in value1:
        print("<{}>".format(s),end=' ')
        print()
list = ["100","1000","55555"]

# 将列表作为一个序列传入
greet(*list)
# 将字符串作为一个序列传入
print('-------------------')
greet(*"abcdef")
print('-------------------')
greet(*[123456,654321])

从上边可以看出,不仅可以将列表变量前边加星号后传入函数,也可以将列表值前边加星号传入函数。如果将字符串前边加星号,那么会将字符串看作字符的序列进行拆分。

不仅元组、列表可以拆分后传入函数,字典也可以这么做。

# 定义的函数,调用字典时,要使用两个星号,
# 而列表、元组 使用一个星号 == 可变参数,参数个数的角度
def greet(**value1):
    for s in value1.items():
        print("<{} = {}>".format(s[0],s[1]))
        print()
dict = {"a":"100","b":"1000","c":"55555"}

# 将字典作为一个序列传入,字典元素作为单个参数传入时,使用两个星号
greet(**dict)
# 将字典值作为一个序列传入
print('-------------------')
greet(**{"111":"222","333":"444"})
print('-------------------')

如果定义字典相关函数时,参数未加两个星号,那么再调用该函数时,也不能加两个星号。

# 定义函数,调用字典时,要使用两个星号,而列表、元组 使用一个星号
def greet(value1):
    for s in value1.items():
        print("<{} = {}>".format(s[0],s[1]))
        print()
dict = {"a":"100","b":"1000","c":"55555"}

# 将字典作为一个序列传入,字典元素作为单个参数传入时,使用两个星号
greet(dict)
# 将字典值作为一个序列传入
print('-------------------')
greet({"111":"222","333":"444"})
print('-------------------')

相关示例:

def add1(x,y,z):
    return x + y + z
print(add1(1,2,3))

# 可以用列表或元组,参数个数必须为3
list = [2,3,4]   
print(add1(*list))
# 可以用字典,参数个数必须为3
dict = {'x':100, 'y':200, 'z':12}
print(add1(**dict))

# 定义元组、列表可变参数
def add2(*numbers):
    result = 0
    for number in numbers:
        result += number
    return result
print(add2(1,2,3,4,5))
print(add2(*list))

# 定义字典可变参数
def add3(**numbers):
    result = 0
    for item in numbers.items():
       result += item[1] 
    return result

print(add3(**dict))

# 一种简略的字典引用方式
def add4(numbers):
    result = 0
    for item in numbers.items():
       result += item[1] 
    return result

print(add4(dict))

7.4 作用域

作用域就是变量、函数、类等python语言元素是否可见的范围。直接在python文件的顶层定义的变量、函数都属于全局作用域,而在函数中定义的变量属于函数本身的局部作用域。在局部作用域定义的变量,在上一层作用域是不可见的。

# 全局变量
x = 1
def fun1():
    # 局部变量
    x = 30
fun1()

# 结果:1
print(x)

当然在局部作用域中也可以访问上一层作用域中的变量,但不能在局部作用域中定义同名的变量。

# 全局变量
x = 123
def fun1():
    # 全局变量
    print(x)

# 结果:123
fun1()

一旦本层作用域中定义了同名的上一层作用中的变量,那么上层变量就相对本层变量隐藏起来。

# 全局变量
x = 123
def fun1():
    # 自动隐藏上层同名变量
    x = 666666
    print(x)

# 结果:666666
fun1()

可能你会问先访问全局变量,再访问局部变量,其实这样也是不行的。

# 全局变量
x = 123
def fun1():
    # 会抛出异常
    print(x)
    x = 666666

# 结果:会抛出异常,在为x赋值之前就已经使用了
fun1()

在python语言中,函数支持嵌套,也就是说,可以在一个函数中定义另一个函数,并且可以直接返回函数本身。

x = 1
def fun1():
    x = 12
    def fun12():
        print(x)
        print("fun12")
    return fun12
# 结果:调用了fun1中嵌套的fun12函数
fun1()()

#  等价上边函数
x = 1
def fun1():
    x = 12
    def fun12():
        print(x)
        print("fun12")
    return fun12()
fun1()

7.5 递归

所谓递归,就是在函数内部调用自身。在执行过程中python解析器会利用栈(stack)处理递归函数返回的数据。所以递归函数的一个必要条件是要有终止条件,否则栈就会溢出。通过递归可以实现很多经典的算法,如阶乘、斐波那契数列等。

# 计算阶乘的递归函数
def jc(n):
	# 终止条件
    if n == 0 or n == 1:
        return 1
    else:
		# 递归调用
        return n * jc(n - 1)
print(jc(10))

# 计算斐波那契数列的递归函数
def fibonacci(n):
	# 终止条件
    if n == 1:
        return 0
	# 终止条件
    elif n == 2:
        return 1
    else:
		# 递归调用
        return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10))
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值