带有默认值的参数必须位于不含默认值的参数后面

函数默认值

函数默认参数

  1. def printme(name,age=15 ):
  2. print (name,age)
  3. print(printme('name',233))
  4. >>> printme('name',233)
  5. name 233

保持好习惯,函数传入值时尽量 带上变量名,易读的同时也不用管参数传入的顺序了

  1. def printme(name='zhangsan',age=15 ):
  2. print (name,age)
  3. print(printme(age=23))
  4. >>> zhangsan 23

一些坑

  1. >>> def add_test(L=[]):
  2. L.append('end')
  3. return L
  4. >>> add_test([1,2,3])
  5. [1, 2, 3, 'end']
  6. >>> add_test()
  7. ['end']
  8. >>> add_test()
  9. ['end', 'end']
  10. >>> add_test()
  11. ['end', 'end', 'end']

很多初学者很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了’END’后的list。
原因解释如下:
Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None这个不变对象来实现:

  1. def add_end(L=None):
  2. if L is None:
  3. L = []
  4. L.append('END')
  5. return L

现在,无论调用多少次,都不会有问题:

  1. >>> add_end()
  2. ['END']
  3. >>> add_end()
  4. ['END']

为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一 点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

函数是否会改变传入变量的值?

  • 对于不可变类型的参数,函数无法改变其值,如python标准数据类型中的字符串、数字、元组
  • 对可变数据类型的参数,函数可以改变其值,如python标准数据类型中的列表、字典、集合

例子:

  1. >>> def change_nothing(var):
  2. var="changed"
  3. >>> def change_mabe(var):
  4. var.append('new value')
  5. #传入参数为字符串,param1的值不会改变
  6. >>> param1="hello"
  7. >>> change_nothing(param1)
  8. >>> param1
  9. 'hello'
  10. #传入参数为字符串,param1的值不会改变
  11. >>> param2=["value"]
  12. >>> change_mabe(param2)
  13. >>> param2
  14. ['value', 'new value']

递归函数

https://www.liaoxuefeng.com/wiki/1016959663602400/1017268131039072
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:

fact(n)=n!=1\times2\times3\times\cdot\cdot\cdot\times(n-1)\times n=(n-1)!\times n=fact(n-1)\times nfact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n

所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

于是,fact(n)用递归的方式写出来就是:

  1. def fact(n):
  2. if n==1:
  3. return 1
  4. return n * fact(n - 1)

上面就是一个递归函数。可以试试:

  1. >>> fact(1)
  2. 1
  3. >>> fact(5)
  4. 120
  5. >>> fact(100)
  6. 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

如果我们计算fact(5),可以根据函数定义看到计算过程如下:

  1. ===> fact(5)
  2. ===> 5 * fact(4)
  3. ===> 5 * (4 * fact(3))
  4. ===> 5 * (4 * (3 * fact(2)))
  5. ===> 5 * (4 * (3 * (2 * fact(1))))
  6. ===> 5 * (4 * (3 * (2 * 1)))
  7. ===> 5 * (4 * (3 * 2))
  8. ===> 5 * (4 * 6)
  9. ===> 5 * 24
  10. ===> 120

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000)

  1. >>> fact(1000)
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. File "<stdin>", line 4, in fact
  5. ...
  6. File "<stdin>", line 4, in fact
  7. RuntimeError: maximum recursion depth exceeded in comparison

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:

  1. def fact(n):
  2. return fact_iter(n, 1)
  3. def fact_iter(num, product):
  4. if num == 1:
  5. return product
  6. return fact_iter(num - 1, num * product)

可以看到,return fact_iter(num - 1, num * product)仅返回递归函数本身,num - 1num * product在函数调用前就会被计算,不影响函数调用。

fact(5)对应的fact_iter(5, 1)的调用如下:

  1. ===> fact_iter(5, 1)
  2. ===> fact_iter(4, 5)
  3. ===> fact_iter(3, 20)
  4. ===> fact_iter(2, 60)
  5. ===> fact_iter(1, 120)
  6. ===> 120

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。

小结

使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。

针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。

Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。