[Python] Python系列(二)之Python函数

Posted by 胡伟煌 on 2017-09-10

1. 内置函数

python的内置参数详见以下链接,也可以通过help(function_name)来查看具体函数的使用帮助。

http://docs.python.org/3/library/functions.html#abs

Built-in Functions
abs() dict() help() min() setattr()
all() dir() hex() next() slice()
any() divmod() id() object() sorted()
ascii() enumerate() input() oct() staticmethod()
bin() eval() int() open() str()
bool() exec() isinstance() ord() sum()
bytearray() filter() issubclass() pow() super()
bytes() float() iter() print() tuple()
callable() format() len() property() type()
chr() frozenset() list() range() vars()
classmethod() getattr() locals() repr() zip()
compile() globals() map() reversed() __import__()
complex() hasattr() max() round()
delattr() hash() memoryview() set()

调用函数需要知道函数名和参数,如果传入的参数不对,会报TypeError的错误并且给出错误信息。

1.1 数据类型转换函数

​ 数据类型转换函数可以对数据类型进行转换,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False

1.2 函数引用

函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

1
2
3
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

2. 定义函数

2.1 函数定义

​ 在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。如果没有return语句,函数执行完毕后也会返回结果,只是结果为Nonereturn None可以简写为return

1
2
3
4
5
def my_abs(x):
if x >= 0:
return x
else:
return -x

2.2 空函数

如果想定义一个什么事也不做的空函数,可以用pass语句:

1
2
def nop():
pass

pass语句主要用来暂时让程序运行不报错,待后续再补充函数内容。同理,pass还可以用在其他语句里,比如:

1
2
if age >= 18:
pass

2.3 参数检查

​ 调用函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError。如果要对参数类型进行检查,可以使用内置函数isinstance()实现。例如:

1
2
3
4
5
6
7
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x

2.4 多返回值

1
2
3
4
5
6
import math

def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny

​ 实际上python的多返回值是一个tuple。只不过按位置依次赋值。

1
2
3
4
5
6
7
8
#多返回值
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0
#返回值为tuple
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)

3. 函数的参数

​ 定义函数的时候,把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值,而函数内部的复杂逻辑被封装起来,调用者无需了解。

3.1 位置参数

​ 位置参数,即传入的参数有位置顺序区分。例如以下的xn根据位置不同,接收对应位置的传入参数。

1
2
3
4
5
6
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s

3.2 默认参数

​ 默认参数,即对某个参数设置默认值,如果没有传入该参数则使用默认值,如果有传入该参数则使用传入值。

  • 必须参数在前,默认参数在后
  • 一般将变化小的参数设置为默认参数,变化大的设置为必选参数
1
2
3
4
5
6
7
8
9
10
11
12
#函数定义
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
#函数调用
>>> power(5)
25
>>> power(5, 3)
125

有多个默认参数时,可以按顺序提供默认参数,也可以指定默认参数的参数名传值而不按参数顺序。例如:

1
2
3
4
5
6
7
8
9
10
#函数定义
def enroll(name, gender, age=6, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)
#按顺序提供默认参数,即7表示age的值
enroll('Bob', 'M', 7)
#按参数名提供默认参数,即city值,而没有age的值
enroll('Adam', 'M', city='Tianjin')

注意事项:默认参数必须指向不可变对象,例如整数,字符串,None等。不能指向可变对象,例如list。

1
2
3
4
5
6
7
8
9
10
11
#错误示例,默认参数为可变对象list
add_end(L=[]):
L.append('END')
return L
#每次调用都会在list中添加元素
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

以上例子中,Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

可改为指向None这个不可变对象来避免该问题。例如:

1
2
3
4
5
6
7
8
9
10
11
#默认参数指向不可变对象None
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
#每次调用恢复原默认值
>>> add_end()
['END']
>>> add_end()
['END']

3.3 可变参数

可变参数,即待传入的参数的个数是可变的,定义时在参数前面加个*,例如*number。可变参数在函数调用时实际上是一个tuple。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#定义可变参数 *number
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
#函数调用
>>> calc(1, 2)
5
>>> calc()
0
#已存在list待传入
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14
#通过list方式出传入可变参数
>>> nums = [1, 2, 3]
>>> calc(*nums)
14

3.4 关键字参数

关键字参数,即可以传入0个或任意个含参数名的参数,关键字参数在函数调用时实际上是一个dict。

关键参数定义时在参数前面加**,例如**kw

1
2
3
4
5
6
7
8
9
10
11
12
13
#定义关键字参数
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
#传入必选参数
>>> person('Michael', 30)
name: Michael age: 30 other: {}
#传入可选的关键字参数
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
#使用dict方式传入关键字参数
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

通过dict方式传入实际上是传入一份拷贝,函数参数的改变并不会影响到函数外原dict的值。

3.5 命名关键字参数

通过对关键字参数命名的方式限制只能传入指定名字的关键字参数,而不能传入其他参数。命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。例如:

1
2
3
4
5
6
#定义命名关键字参数
def person(name, age, *, city, job):
print(name, age, city, job)
#命名关键字参数的调用
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

命名关键字参数必须传入参数名,如果没有传入参数名,调用将报错。

1
2
3
4
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given

命名关键字参数可以有缺省值,即默认参数。

1
2
3
4
5
6
#命名关键字参数的缺省值
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
#函数调用
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

3.6 参数组合

必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数可以组合使用。但参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

1
2
3
4
5
6
7
8
9
10
11
12
#参数组合的定义
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

#参数组合的调用
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

通过tuple和dict调用参数组合函数:

1
2
3
4
5
6
7
8
9
10
#f1
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
#f2
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。

4. 递归函数

4.1 递归函数的定义

递归函数即函数在内部调用函数本身(自己调用自己)。

递归的关键点:

  • 递归结束的临界值,通常使用if语句来判断临界值
  • 递归体的递归逻辑

例如,以下是fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x n 的递归函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#递归函数的定义
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
#计算5的阶乘,递归函数调用细节
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

4.2 栈溢出问题

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

例如:

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

4.3 尾递归

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

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#尾递归函数的定义
def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

#计算5的阶乘,递归函数调用细节
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120

尾递归调用时,如果做了优化,栈不会增长,无论多少次调用也不会导致栈溢出。但实际上大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,因此当没有优化的情况下任何递归函数都存在栈溢出的问题。

本文参考:廖雪峰Python教程

推荐文章