Python 中使用
def
语句创建函数,其一般的格式如下所示:
def name(arg1, arg2, ...,argN):
statements
下面这段代码定义了一个简单函数。
def repeator(s, n):
result = s * n
print(result)
这段代码仅仅是对函数的定义,并没有调用执行。这条定义语句运行后会新建一个名为
repeator
的变量名,其类型为function
,即函数。
def repeator(s, n):
result = s * n
print(result)
print(type(repeator))
与内置函数一样,定义完函数后,可以通过函数名调用执行。
def repeator(s, n):
result = s * n
print(result)
repeator('嗷', 3)
在很多情况下,函数需要将计算的结果返回到调用处。在这类函数的函数体中,通常包含一条
return
语句:
def name(arg1, arg2, ...,argN):
statements
return value
在创建函数时, 没有在函数体中添加
return
语句,Python 也会默默地在函数体最后添加一条return None
。
def repeator(s, n):
result = s * n
print(result)
value = repeator('嗷', 3)
print(value)
函数如果以返回值来输出:
def repeator(s, n):
result = s * n
return result
print(repeator('嗷', 3))
在 Python 中, 还允许在函数中返回多个值。 只需将返回值以逗号隔开, 放在
return
关键字后面即可。
def calculator(m, n):
return m+n, m-n, m*n, m/n
i, j = 2,
r1, r2, r3, r4 = calculator(i, j)
print(f'{i} 和 {j} 的加减乘除运算结果是:{r1},{r2},{r3},{r4}')
在这里总结一下函数调用的四个步骤:
- 程序执行到函数调用时,在调用处暂停,等待函数执行完毕;
- 将实参赋值给函数的形参;
- 执行函数体中的语句;
- 调用结束后,回到调用前暂停处继续执行,如果函数体中执行了
return
语句,return
关键字后的值会返回到暂停处,供程序使用,否则函数返回None
值。
参数的传递过程,实际上是一个赋值的过程。在调用函数时,调用者的实际参数自动赋值给函数的形式参数变量。
def avg(m, n):
return (m + n) /2
print(avg(5, 2))
目前我们所学习的不可变类型包括:整型、浮点型、字符串和元组,可变类型有:列表、字典和集合等。这些都可以作为参数的类型。但参数在函数中使用时,这两种类型的表现有所不同。
下面的代码调用时,传递的是不可变类型的参数:
def priceChanger(p):
p = p + 10
print('改变后的价格:{:.2f}'.format(p))
price = 10.8
priceChanger(price)
print(price)
在使用可变参数时,函数体中可以改变参数的元素:
def contentChanger(name_list):
name_list[0], name_list[1] = name_list[1], name_list[0]
print('函数中的 name_list:', name_list)
language_name = ['C', 'Python']
contentChanger(language_name)
print('调用函数后的 language_name:', language_name)
因此,在使用可变类型参数时需要特别注意,如果在函数中修改了参数的元素,这种修改会影响调用者的变量。 如果想消除这种影响,可以使用列表
copy
方法或者使用分片操作创建新列表。
位置参数是调用函数为形参赋值的一种默认方式。实参与形参按照从左到右的位置顺序依次赋值。
def myMinus(num1, num2):
return num1 - num2
print(myMinus(5, 2))
赋值顺序改变将得到不同的结果。
def myMinus(num1, num2):
return num1 - num2
print(myMinus(2, 5))
为了避免位置参数赋值带来的混乱,Python 允许调用函数时通过关键字参数的形式指定形参与实参的对应关系。 调用者使用
name=value
的形式来指定函数中的哪个形参接受某个值:
def myMinus(num1, num2):
return num1 - num2
print(myMinus(num1=5, num2=2))
print(myMinus(num2=2, num1=5))
在函数定义时,可以为参数指定值。这样当函数调用者没有提供对应参数值时,就可以使用指定的默认值。 指定默认参数值在 Python 的函数中广泛存在。例如,打印函数
print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file: a file-like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
可以看到,
sep
、end
、file
和flush
参数都指定了默认值。
print('C', 'C++', 'Java', 'Python')
如果调用时指定了
sep
参数的值,则会使用该值来连接每个打印的值。
print('C', 'C++', 'Java', 'Python', sep='_')
在定义函数时, 为形参指定默认值, 就可以让该形参在调用时变为可选:
def myMod(x, y=2):
return x % y
print(myMod(13,4))
print(myMod(13))
Python 允许在定义函数时使用单星号
*
来收集位置参数,双星号**
收集关键字参数。
单个星号将一组可变数量的位置参数组合成参数值的元组。在函数内部可以通过访问元组中的每个元素来使用参数。
def m_value(*values):
max_value = max(values)
min_value = min(values)
print(f'最大值: {max_value}, 最小值: {min_value}')
m_value(8, 6, 7, 4, 3, 9)
针对形参的关键字参数赋值形式, 利用 Python 定义函数时, 在形参前面加上双星号
**
来定义收集关键字参数的形参。此时形参是字典类型。
def f(**info):
if 'name' not in info.keys():
print('必须拥有名称信息。')
else:
print(info['name'] + '的诞生年份:' + info.get('time', '不详'))
f(name = 'C', time = '1972')
f(name = 'Python')
在调用函数时,实参也可以使用
*
和**
语法。此时不是收集参数,正好相反, 实参前加上*
或**
执行的是参数解包。 通常来说, 在列表、元组等类型的实参值前加上*
, 将这些类型的元素解包成位置参数的形式;在字典类型的实参值前加上**
,将字典的元组解包成关键字参数的形式。
当调用者的数据存储在列表中时, 可以通过在列表前加上
*
对列表解包来实现位置参数形式的调用。
当调用者的数据存储在字典中时, 可以通过在字典前加上
**
对字典解包来实现关键字参数形式的调用。
def f(name, time = '不详'):
if name and len(name) > 0:
print(name + '的诞生年份:' + str(time))
else:
print('必须拥有名称信息。')
info1 = ['C', '1972']
print(*info1)
info2 = {'name':'Python', 'time':'1989'}
print(**info2)
Python 中规定每个变量都有它的作用域, 即变量只有在作用域范围内才是可见可用的。 作用域能避免程序代码中的名称冲突,在一个函数中定义的变量名称不会干扰另外一个函数内的变量。这有助于使函数更加独立。
根据作用域范围的大小,可以将作用域分为全局作用域和局部作用域。
局部变量仅仅在局部作用域内可用。在局部作用域之外,该变量是不可见的。 如果变量是在函数体内被创建(首次赋值)的,这个变量就只能在该函数体内使用,是这个函数体的局部变量。 函数执行结束后, 局部变量被销毁。函数的参数作为一类特殊的变量,是在函数调用时首先被创建并赋值的。参数 也是局部变量。
def avg(x, y):
avg_price = (x + y) / 2
print(avg_price)
avg(10.0, 20.0)
print(avg_price)
相对于局部变量,在所有函数之外创建的变量,在整个文件范围内都是可见的。也就是说,在所有 def 外被首次赋值的变量,对于整个文件来说是全局的。
对于交互式命令行下输入的代码,可以看成是在一个临时文件中。因此, 在交互式命令行下, 在所有函数体之外定义的变量, 在整个交互过程中都是可见的, 直到被删除或者交互窗口关闭。
rate = 3
def f(x):
return x * rate
print(f(4))
在变量使用过程中,有时不可避免地存在局部变量和全局变量同名的情况。在这种情况下,在局部作用域内,可见的是局部变量,全局变量被暂时隐藏起来。
rate = 3
def f(x):
if x > 10:
rate = 5
else:
rate = 10
return x * rate
print(rate)
print(f(4))
关键字
global
语句通常放在函数体的开始部分,用于申明变量为全局变量。其语法如下:
global variable_names
rate = 3
def f(x):
global rate
if x > 10:
rate = 5
else:
rate = 10
return x * rate
print(rate)
print(f(4))
Python 中提供了一项非常有用的功能:利用
lambda
函数来替代def
, 创建一个临时简单函数。 请注意, 与def
语句不同,lambda
是一个表达式。 这就使得lambda
能够出现在函数调用的参数中。而def
语句则不能作为参数传递给函数。
lambda <args>: <expression>
lambda
表达式创建一个函数,它会返回函数本身。 这也是lambda
被称为匿名函数的原因,它本身是没有名称的。如果需要在后面代码中使用该匿名函数,也可以将它赋值给一个变量。这个变量的类型就是一个函数。
getSecondItem = lambda x: x[1]
print(type(getSecondItem))
print(getSecondItem(['C','1972']))
下面列举一些 Python 中经常用到匿名函数的场景。
对于列表的
sort()
方法,可以通过key
来传入一个函数,sort()
方法将利用这个函数对列表每个元素的返回结果来排序。
language = [('C', '1972'), ('C++', '1979'),
('Java', '1995'), ('Python', '1989')]
language.sort(key=lambda x:x[1])
for name, time in language:
print(name, time)
map()
函数, 可以对序列中的每个元素应用某个内置函数, 并把函数结果收集起来,构成一个可迭代的map
对象。 除了内置函数,常常会使用一些自定义的匿名函数来对序列元素进行映射。
language = [('C', '1972'), ('C++', '1979'),
('Java', '1995'), ('Python', '1989')]
language_map = map(lambda x:x[0], language)
language_name = list(language_map)
print(language_name)
与
map
函数类似,filter
函数也可以接受一个返回结果为布尔值的函数和可迭代对象作为实参。其作用是将可迭代对象中每一个元素都应用到传入的函数中,并将函数返回为True
的元素添加到结果中,即对可迭代对象中的元素进行过滤。
language = [('C', '1972'), ('C++', '1979'),
('Java', '1995'), ('Python', '1989')]
language_filter = filter(lambda x:int(x[1]) < 1990, language)
language_list = list(language_filter)
print(language_list)
递归是一种广泛应用算法。它能够把一个大型复杂的问题转化为一个与原问题相似的较小规模的问题来求解,用非常简洁的方法来解决重要问题。就像一个人站在装满镜子的房间中,看到的影像就是递归的结果。递归在数学和计算机应用上非常强大,能够非常简洁的解决重要问题。程序设计中,通过函数定义中调用函数自身的方式来实现递归。
数学上有个经典的递归例子叫阶乘,阶乘通常定义为:
n ! = n ∗ ( n − 1 ) ∗ ( n − 2 ) . . . ∗ 2 ∗ 1 n! = n * (n-1) * (n-2)... * 2 * 1 n!=n∗(n−1)∗(n−2)...∗2∗1
这个关系给出了另一种方式表达阶乘的方式:
n ! = { 1 n=0 n ∗ ( n − 1 ) ! n>0 n! = \begin{cases} 1 & \text{n=0} \\ n*(n-1)! & \text{n>0} \end{cases} n!={1n∗(n−1)!n=0n>0
阶乘的例子揭示了递归的2个关键特征:
(1)存在一个或多个基例,基例不需要再次递归,它是确定的表达式;
(2)所有递归链要以一个或多个基例结尾。
根据用户输入的整数 n, 计算并输出 n 的阶乘值:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
num = int(input("请输入一个整数: "))
print(f'{num}的阶乘为:{factorial(num)}')
基例有时不止一个,可能有多个。
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)。以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
在数学上,斐波纳契数列以如下被以递推的方法定义: F ( 1 ) = 1 , F ( 2 ) = 1 , F ( n ) = F ( n − 1 ) + F ( n − 2 ) ( n > = 3 , n ∈ N ) F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3, n∈N) F(1)=1,F(2)=1,F(n)=F(n−1)+F(n−2)(n>=3,n∈N)
这个数列从第3项开始,每一项都等于前两项之和。
编写程序,用户输入正整数 n,输出斐波那契数列的前 n 项:
def fibo(i):
if i in (0,1):
return 1
else:
return fibo(i-1) + fibo(i-2)
num = int(input('请输入一个大于 3 的正整数 :'))
print('\n斐波那契数列的前 {} 项为:'.format(num))
for i in range(1, num+1):
print(fibo(i), end=' ')
试试打印出前50项。
如此之慢的原因是什么?
每次在计算第i项值时,都需要递归调用直到
fibo(0)
,也就是说像fibo(0)
,fibo(1)
,fibo(2)
,fibo(3)
被计算了无数次,如果我们能在第一次计算出来后就存储下来,以供后面使用,会不会快些?
让我们使用字典改进一下:
calculate_dic = {1: 1, 2: 1}
def fibByDic(n):
if n not in calculate_dic:
new_value = fibByDic(n-1) + fibByDic(n-2)
calculate_dic[n] = new_value
return calculate_dic[n]
num = int(input('请输入一个大于3的正整数:'))
print('\n斐波那契数列的前{}项为:'.format(num))
for i in range(1, num + 1):
print(fibByDic(i), end=' ')
和简单的递归相比较,速度是否快到让你怀疑人生?所以,有的时候算法很重要。程序设计可以让你的工作由几天节约至几个小时,好的算法可能可以让你的程序运行时间从几个小时节约至几秒钟。