函数是一等对象

编程语言中的函数就是对一段代码逻辑的封装,它是面向过程编程的基本单元。Python 是面向对象的语言,但是也对函数式编程提供了部分支持,并且,函数是 Python 里面的一等对象,即:函数在运行时被创建;能作为变量赋值;能作为参数传递;能作为结果返回。
基于函数是一等对象,如果一个函数接受另外的函数作为参数,或者把另外的函数作为结果返回,那么该函数便是高阶函数。在函数式编程范式中,最为人熟知便是mapfilterreduce,不过在 Python 中,它们都有更好的替代方案,即列表推导式和生成器表达式,使用后者,代码逻辑更加清晰,性能更好。
Python 内置库里也有 operator 和 functools 这样的函数式包,operator 主要为多个算术运算符提供了对应的函数,functools 提供了一系列高阶函数,如 reduce、partial 等。partial 用于生产偏函数,偏函数是指基于一个函数创建一个新的可调用对象,把原函数的某些参数固定,像 flask 的 request 等对象就是用 partial 生成的。

函数的创建

1.def关键字
2.匿名函数lambda
3.动态创建,即使用字符串形式的函数表达式,通过编译等步骤最终生成函数对象,适合写框架等场景

def gen_function(function_express):
    module_code = compile(function_express, '', 'exec')
    function_code = [c for c in module_code.co_consts if isinstance(c, types.CodeType)][0]
    return types.FunctionType(function_code, builtins.__dict__)

4.还有一种广义上的可调用对象,只需要实现__call__特殊方法,任何 Python 对象都可以表现得像函数

函数参数

我非常喜欢的 Python 特性之一是它极为灵活的参数处理机制,Python 里面共有位置参数、默认参数、可变参数、关键字参数、命名关键字参数,使用者可以灵活组合,而且可以配合拆包使用。
1.位置参数,def test(a),a 就是位置参数
2.默认参数,def test(a=1),a 就是默认参数
3.可变参数,def test(*a),此时传入的参数个位是可变的,并且会在函数内部组装成一个元祖,配合元祖拆包的话又可以很方便的还原成单个参数
4.关键字参数,def test(**a),和可变参数不同的是可以传入带参数名的参数,并且会组装成一个 dict
5.命名关键字参数,def test(a, *, b),此时,b 作为命名关键字参数,调用 test 函数的时候,只接受参数名为 b 的关键字参数,也就是把关键字参数参数名定死了
其实对于任何函数,都可以根据func(*args, **kw)的形式去调用,简洁优雅

函数内省

众所周知,Python 里面一切皆对象,函数也不例外,它是function对象的实例。
函数对象有很多属性,可以使用dir函数探知,其中很多都是 Python 对象共有的,但函数对象肯定会有自己的专业属性,下面这个表格简单记录了一下。
| 名称 | 类型 | 说明 |
|:-:|:-:|:-:|
aannotations|dict| 参数和返回值的注解
call|method-wrapper| 实现可调用对象协议,即 () 运算符
closure|tuple| 函数闭包,即自由变量的绑定
code|code| 编译成字节码的函数元数据和函数定义体
defaults|tuple| 形式参数的默认值
get|method-wrapper| 实现只读描述符协议
globals|dict| 函数所在模块中的全局变量
kwdefaults|dict| 仅限关键字形式参数的默认值
name|str| 函数名称
qulname|str| 函数的限定名称
要说明一点,虽然对象信息很全,但是里面信息组织的方式并不是便利的,比如函数对象的defaults属性保存着位置参数和关键字参数的默认值,然而,参数名称却在code属性中。这里我们可以借助inspect模块。
Python3 提供了一种新的句法,用于为函数的参数和返回值附加元数据,信息便保存在__annotations__属性里,例如def test(a:str,b:'int>0'=4) -> str:。注解只是让代码逻辑更清晰,元数据可以提供给 IDE、框架等工具去使用,但是对解释器没有任何意义。

闭包

闭包是指延伸了自己作用域的函数,如果一个函数可以访问定义体之外定义的非全局变量,那么它就是闭包。
Python 的变量作用域遵循 LEGB 原则。L 指local,局部作用域;E 指enclosing,嵌套作用域;G 指glocal,全局作用域;B 指bulitin,内置作用域。有如下一个函数体:

def test():
    l = 0

    def inner(a):
        l += 1
        return a * l

    return inner

此时,l叫做自由变量,它没有在inner的作用域内,而inner访问了l,所以inner就是一个闭包,那么 Python 中是怎么实现这样的机制的呢?
函数对象有一个__code__属性,__code__.co_freevars就保存着自由变量的名称,它是一个元祖;还有一个__closure__属性,里面保存着若干cell对象,它也是一个元祖,这两个元祖中的元素一一对应,类似于键值对,而cell对象的cell_contents属性便保存着自由变量的值。这样在调用inner的时候就能使用这些自由变量了。
但前面的test定义其实是有问题的,问题在哪儿呢?其实在函数体内,一旦给变量赋值,那么解释器便认为该变量是局部变量innerl+=1等于l=l+1,而且l是不可变类型,未赋值先引用,所以会报错UnboundLocalError
Python3 引入了nonlocal来解决这个问题,它的作用是把变量标记为自由变量,如果自由变量更新新值,闭包中的绑定也会随之更新。那么 Python2 要怎么解决呢?把l变成 dict、list 等可变对象就可以了。

装饰器及实现

装饰器就是一种特殊的闭包,他只接受函数作为参数,严格来说,装饰器只是一种语法糖,它把被装饰的函数替换成其他函数,简洁地扩展函数功能。以下两种写法是等价的。

@decorate
def target(): 
    pass

def target():
    pass
target = decorate(target)

还有,遇到嵌套的装饰器不要慌,只要遵循着由下往上看,把下当成一个整体,下是上的参数,装饰器的结构便能理清楚了。例如如下的结构,就等价于target=decorator1(decorator2(a)(decorator3(target)))

@decorator1
@decorator2(a)
@decorator3
def target():
    pass

装饰器是在导入时运行的,被装饰的函数在被调用时才运行,这点要尤为注意,否则在使用装饰器时会出现难以察觉的 bug。
装饰器的实现需要注意的是,被装饰的函数其实是装饰器返回的一个闭包,被装饰的函数对象原先的信息都没了,为了后续的调试等,一定要使用functools.wraps来加一个装饰,装饰装装饰。


↙↙↙阅读原文可查看相关链接,并与作者交流