(1)函数基本
● 函数是第一类对象
Python中万物皆对象,所有对象都是第一类的(first class),函数也不例外,也是第一类对象。既然是对象,那就可以当作普通的对象数据处理,比如:拥有自己的属性、可赋值给一个变量、可放入容器存储、可作为另一个函数的参数或是返回值等等。当你在使用 def 定义函数时,就相当于生成了一个函数对象。
下例中,将3个内置函数放入一个序列,并用迭代器取出后分别调用:
line = 'abc,1,3.14'
fun_list = [str, int, float]
para_list = line.split(',')
obj_list = [f(val) for f, val n zip(fun_list, para_list)]
# obj_list结果为:['abc', 1, 3.14]
● 文档字符串
通常,将函数def后的第一行,作为描述函数用途的“文档字符串”,保存在函数的__doc__属性中。用内置函数 help(函数名) 也可以查看函数的描述文档。
● 可调用类型
可调用类型表示支持函数调用操作的对象,包括:用户定义的函数、内置函数、实例方法、类、提供了可调用接口的实例。可以使用内置函数 callable() 来检查一个对象是否是可调用的。
类都是是可以调用的,调用类时,会自动将入参传递给类的__init__()方法,用以创建一个新实例。
实例对象一般是不可调用的,但是如果这个实例实现了__call__()方法,那么这个实例就可直接调用。例如:若x是某个实例,执行 x(args) 就相当于调用 x.__call__(args) 方法。
● 函数的属性
函数作为一种对象,理论上可以给函数添加任意属性。函数也有一些内部默认的属性,见下列各表。
“内置函数”具有以下属性
| 属性 | 描述 | 
|---|---|
| __doc__ | 文档字符串。 | 
| __name__ | 函数名称。 | 
| __self__ | 与方法相关的实例。 说明:对于像len()这样的内置函数,__self__为None(表明未绑定);而像s.append()这样的内置方法,__self__为列表对象s。 | 
“用户定义函数”具有以下属性
| 属性 | 描述 | 
|---|---|
| __doc__ | 文档字符串。 | 
| __name__ | 函数名称。 | 
| __dict__ | 包含函数属性的字典。 | 
| __code__ | 编译后的代码。 | 
| __defaults__ | 包含默认参数的元组。 | 
| __globals__ | 函数应用时对应的全局命名空间的字典。 | 
| __clousre__ | 闭包(包含与嵌套作用域相关数据的元组) | 
“实例方法”具有以下属性
| 属性 | 描述 | 
|---|---|
| __doc__ | 文档字符串。 | 
| __name__ | 方法名称。 | 
| __class__ | 定义该方法的类。 | 
| __func__ | 实现方法的函数对象 | 
| __self__ | 与方法相关的实例(如果是非绑定方法,则为None) | 
● 绑定与非绑定方法
通过实例调用方法时,有绑定和非绑定两种用法。绑定方法封装了成员函数和一个对应实例,调用绑定方法时,实例会作为第一个参数self自动传递给方法。而非绑定方法仅封装了成员函数,并没有实例,用户在调用非绑定方法时,需要显式地将实例作为第一个参数传递进去。详见下面2个例子
绑定用法(bound method):
class Foo():
    def meth(self, a):
        print(a)
obj = Foo()     # 创建一个实例
m = obj.meth    # 将meth方法绑定到obj这个实例上
m(2)            # 调用这个方法时,Python会自动将obj作为self参数传递给meth()方法
非绑定用法(unbound method):
class Foo():
    def meth(self, a):
        print(a)
obj = Foo()     # 创建一个实例
um = Foo.meth   # 非绑定,仅仅是将这个方法赋值给um,并不需要实例存在
um(obj, 2)      # 调用这个非绑定方法时,用户需要显式地传入obj实例作为第一个参数。
● 匿名函数
使用 lambda 语句可以创建表达式形式的匿名函数,其用途是指定短小的回调函数。语法为:
lambda 参数 : 表达式
lambda匿名函数中不能出现非表达式语句、也不能出现多行语句。
下例定义一个匿名函数:
a = lambda x,y: x*y b = a(2,5) # b的结果为:10
下例在序列的sort()方法中传入lambda匿名函数:
[('b',2),('a',1)].sort(key=lambda x:x[1])  # 结果为 [('a',1),('b',2)]
# 说明:使用匿名函数对列表元素进行了预处理,将原本的元组('a',1)预处理为:取出元组中后一个元素(即:1),所以能够进行排序。
(2)函数参数
● 位置参数、关键字参数、返回值
在调用函数时,传入参数的顺序数量必须与函数定义匹配,否则会引发TypeError异常。如果在调用时指定参数名,这样就不必遵照函数定义中的参数顺序了,这样可大大增加调用时的可读性。这种指定参数名传入的参数叫做关键字参数,一般的未指定参数名的参数叫做位置参数。关键字参数只能放在所有的位置参数后面。
函数参数都是按值传递的,但是如果传递的是对象(即非单纯数字),所谓按值传递就是函数参数仅仅是将对象的地址值复制一下传过去而已,所以在函数中其实是可以改变外面对象中的内容的。一般最好避免使用这种风格,而且在涉及线程和并发的程序中,使用这类函数的效率很低,因为通常需要使用线程锁来防止副作用的影响。
若省略return语句、或单一个return关键字,就会返回 None 对象。
● 参数的默认值
在函数定义时,可以为某些参数指定默认值,这样在调用时可以不提供这个参数了。一旦出现带默认值的参数,此参数后续的参数都必须带默认值,否则会引发SyntaxError异常。
建议不要使用可变对象作为默认值(如空列表),这样可能导致意外的bug,如下例所示:
def fun(x, seq=[]):
    seq.append(x)
    return seq
fun(1)    # 返回[1] 
fun(2)    # 返回[1,2]
fun(3)    # 返回[1,2,3]
上例的本意是若未传入seq参数,则新建一个列表,并将x放入新列表。但是事实上会产生bug。这种情况建议使用seq=None,再在函数中建新列表。
● 单星号 *
在函数定义时,参数前加单星号的意思为:收集其余的位置参数,并将它们放入同一个元组中。这样在函数调用时,用户就可以提供任意多个参数了。如下例所示:
def fun(x, *y):
    print(x)
    print(y)
    
fun('a', 1, 2, 'c')   # 结果为:a 和 (1,2,'c')
fun(3)                # 结果为:3 和 ()
单星号亦可反转使用,即:在调用函数时使用*,自动将一个元组展开为若干个指定名字的关键字参数。
def myadd(x, y):
    return x+y
    
t = (1,2)
myadd(*t)   # 调用时,单星号自动将元组(1,2)展开为 x, y
● 双星号 **
在函数定义时,参数前加双星号的意思为:收集其余的关键字参数,并将它们放入一个字典中。这样在调用函数时,可以传入大量可扩充的配置项作为参数。
def fun(x, **z):
    print(x)
    print(z)
fun(x=1, y=2, z=3)    # 结果为:1 和 {'y':2, 'z':3}    
双星号亦可反转使用,在调用函数时使用**,自动将一个字典拆分为若干个指定名字的关键字参数。
def myadd(x, y):
    return x+y
d = {'x':1, 'y':2}
myadd(**d)    # 调用时,双星号自动将字典d展开为:x=1, y=2
单星号和双星号可组合使用,用于同时收集位置参数和关键字参数,**参数必须出现在*参数的后面。
def fun(*args, **kwargs):
    print(args)
    print(kwargs)
    
fun(1,2,3, x=4,y=5,z=6)
# 结果为:(1,2,3) 和 {'x':4, 'y':5, 'z':6 }
(3)作用域
每次执行一个函数时,就会创建一个新的局部命名空间。这个命名空间(又叫作用域),就像其内部有一个“不可见”的字典,其中包含本函数参数的名称和所有在函数内部定义的局部变量。
除了每个函数有有一个局部作用域以外,还有一个全局作用域。可以通过内置函数locals()和globals()查看局部和全局作用域字典,内建函数vars(obj)可以返回某个实例的作用域字典。
x = 1
def fun():
    y = 2
    print(locals())
    print(globals())
fun()
# locals()的显示为:{'y':2}
# globals()除了显示全局变量x以外,还会显示很多全局默认的变量:
{ 'x': 1,
  'fun': <function fun at 0x000001E5C52EA048>
  '__name__': '__main__',
  '__doc__':  None,
  '__package__': None,
  …… 
}
● 在函数内访问全局变量
Python解释器解析变量时,会先搜素局部作用域,若找不到就会搜索全局作用域,若再找不到就会搜索内置命名空间,如果仍然找不到,就会引发NameError异常。
此种方法虽然可以访问全局变量,但不能对全局变量赋值,若要赋值全局变量,需要使用global关键字。
在函数中修改全局变量:
x = 1
def fun():
    global x
    x = 2
fun()       # 运行fun()后,全局变量 x 的值变为2
函数中局部变量名和全局变量名重复时:
x = 1
def fun():
    x = 2                 # 此处创建一个名为 x 的局部变量
    globals()['x'] = 3    # 使用globals()内置函数,可通过字典的方式直接操作全局变量
fun()       # 运行fun()后,全局变量 x 的值变为3
注意:Python中不支持访问在函数中访问上级调用函数的局部作用域,如下例的代码会引发NameError异常
def fun_inner():
    print(x)
def fun_outer():
    x = 2
    fun_inner()
    
fun_outer()     # 调用时会引发NameError异常,因为在fun_inner()函数中,不能访问外层调用函数fun_outer()的局部作用域中的 x
● 嵌套作用域
Python3支持在嵌套定义的函数中,使用外层定义函数(不是外层调用函数)的局部作用域中的变量,这个称为:动态作用域(dynamic scoping),需要使用nonlocal关键字,如下例所示:
def fun_outer():
    x = 4
    def fun_inner():
        nonlocal x      # 声明绑定到外层定义的x
        x = 5
        print(x)
        
    fun_inner()
    print(x)
    
fun_outer()             #  会先运行fun_inner()中的print语句,打印出5;然后运行外层中的print语句,可看到x确实被修改成了5
(4)递归
函数调用自身称为递归(recursion)。递归最典型的使用场景是,可以把一个大问题分解成重复的小问题来解决,而且这个小问题的解决结构非常简洁。虽然大部分的递归都可以用循环来替代解决,但有时用递归写函数要比循环可读性更高,看起来也更优雅。
用递归计算n的阶乘
def factorial(n):
    if n <= 1: 
        return 1
    else:
        return n * factorial(n-1)
用递归实现序列的二分法查找:
def search(seq, num, lower, upper):
    if lower == upper:
        return upper
    else:
        middle = (lower + upper) // 2
        if num > seq[middle]:
            return search(eq, num, middle+1, upper)
        else:
            return search(seq, num, lower, middle)
            
seq = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
seq.sort()
search(seq, num=2, lower=0, upper=9)    # 使用二分法查找比起遍历查找,可以很快地找到num