def 是什么

python 中实现一个自定义的函数,以 def 开头。类比 c,golang 这种静态类型的语言,有的是以 func 开头,有的直接省略类似的「关键字」,直接写函数签名。学习Python 的一个很大的误区,就是我们认为 def 也是Python 中定义函数的一个关键字。在Python 中,def 不是一个关键字,而是一个可执行的语句。如果不考虑类,一般来说,函数都实现在某一个模块中,那么当这个模块被导入的时候,def 语句就会自动执行,创建一个函数对象,并且把这个对象赋值给对应的函数名。也就是说,函数的定义和普通变量的定义是没有区别的,函数名仅仅是 def 语句创建出来的函数对象的一个引用而已。

python 中使用 def 语句创建的函数对象,并不要求每一个都要有返回值或者显式的 return 调用。仅当你需要这个函数的返回值的时候,才使用 return 进行返回,否则,函数默认返回一个 None 对象。既然Python 是一个动态类型的语言且没有所谓的「编译」,「链接」阶段,那么也就是说在一个模块被执行之前,某一个函数变量名具体引用了哪一个函数对象我们是不清楚的,这个关系是在模块运行的时候才能够确定的。并且,如果函数内部有一些明显的运行时的 Bug,在 def 语句执行的时候也不会去检测,只有在调用者调用这个函数发生错误的时候才能够清楚。 å 所以,在Python 中,函数变量和其他任何变量没有区别。def 是一个可执行语句,并不是一个关键字。

函数中的多态思想

如果是有编程基础的人,对多态这个词应该不难理解。我是这样理解的

多态,就是某一个操作的意义取决于被操作的对象类型

一个最直接的例子就是,C++中,子类和父类有一个函数签名相同的方法。定义一个父类类型的指针,当这个指针指向父类对象或者子类对象的时候,执行的同名方法是两个不同类中所分别定义的;或者在 Java 中,「+」这个操作既可以用于字符串的链接,也可以用于数字的四则运算。python 则比 java 还要方便,因为它是一种动态类型的语言,python 的世界中只有对象和引用的区别,没有数据类型之分,一切事物都是对象,被某个变量引用。所以在编写Python 的函数中,尽量不要去用类似「type」等方法检测参数的类型,这其实就是一种典型的用静态语言的思维在使用动态语言。仔细想想Python 的一个便捷之处就是在编程的时候不需要考虑数据类型,一旦某个函数内的某个操作是传递进来的参数所不具有的,让其抛出一个异常也是正确的选择。

所以,只要是在函数内部对参数做的操作,被传递进来的对象都支持,这个函数就能够正常工作,这和参数的数据类型是没关系的。

作用域

作用域这个词,从字面意思上来理解,就是某个事物起作用的区域。在编程语言中,通常指某个变量的生存周期。某个变量的作用域和它第一次被赋值的位置是相关的。

  • 非嵌套函数内部:本地作用域
  • 嵌套函数内部:非本地作用域
  • 模块内部(文件内部,函数外部,包括Python 内置模块):全局作用域

本地作用域与全局作用域

python 中的全局作用域都是要和模块一起提出才是有意义的,这里的「全局」是指模块内全局,模块和模块之间,也就是文件和文件之间是完全隔离开的。一个我们在开发过程中所常见的现象就是某个模块的全局变量对外是作为这个模块的某个属性被使用的。本地作用域一般指调用函数所构造的一个作用域,本地作用域一般和函数调用相关。从操作系统的层面上来讲,每一个函数的调用都会开辟一块栈的空间用来存放函数调用的上下文或者一些本地变量,再结合我们一般说作用域都是和变量的生命周期联系在一起,就不难发现,每一次对函数调用,都会创建一个新的本地作用域。

各个作用域之前可能会存在一个作用域屏蔽的问题,比如在某一个函数内定义的某一个变量和函数外部的某一个变量同名,那么在函数内部对这个变量的更改将不会影响到函数外部的变量。这个现象看起来很像是一个「屏蔽」的效果,函数内的变量屏蔽的外部的。但其实这和解析变量的规则是有关的,后面的段落将会介绍相关的细节。

另外,要谨记的一点就是,只有在发生变量赋值的时候,才会涉及到作用域变化的问题。原地对一个对象的修改是不涉及到任何作用域变更的问题的。原因有以下几点:

1.Python 中,变量和对象是不同的两个概念。变量通常指的是引用变量,指向内存中实际存在的一个对象 2. 作用域这一概念是针对变量的,而不是针对对象的

举一个简单的例子,函数外定义一个 list 对象并且赋值给一个变量 x。那么在函数中,如果执行 x.append 操作,影响的是全局作用域中的变量 x 所指向的list对象。x 这个变量并没有发生「更改」类型的操作。但是如果在函数内部执行了 x=y 这种操作,那么就视为在函数的本地作用域内定义了一个新的变量 x,它屏蔽了全局作用域中的同名变量。

LEGB 变量解析原则

当我们使用一个变量的时候,自然得找到它出现并且被赋值的位置。然而,可以使用的有效的变量一般都是在其有效的作用域内。所以,变量的查找和解析与作用域是分不开的。Python 中解析变量的规则称作为「LEGB」原则。

「LEGB」是四个作用域简称的组合,在解析变量的时候也会依次按照顺序进行查找,只要查找到一处就停下来

  • L:本地作用域
  • E:非本地作用域
  • G:全局作用域
  • B:内置作用域

上述四个作用域,应该有两个是大家比较迷惑的地方。非本地作用域和内置作用域。

非本地作用域

非本地作用域出现的场景是发生有函数嵌套的时候。「非本地」是对于被嵌套的函数而言。一个被嵌套函数的内部可以被称作是本地作用域,但是在这个函数和嵌套他的函数之间这一块区域被称作是非本地作用域。非本地作用域除了必须要发生函数嵌套行为之外,嵌套的层次也会影响到非本地作用域的大小,python 在进行变量解析的时候,会追踪到任意的嵌套层次。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> def foo():
...     x = 1
...     def foo2():
...         def foo3():
...             nonlocal x
...             x = 3
...             return x
...         return foo3
...     return foo2
...
>>> a = foo()()()
>>> a
3

如上面的代码所示,foo3在解析 x 这个变量的时候,一直查找到了嵌套的第二层。也就是说,对于foo3的非本地作用域有foo2和foo1两层这么大,尤其是我们使用了 nonlocal 这个关键字,让上面这段实例更加有说服力,因为 nonlocal 关键字的出现,证明我们不但要对非本地作用域的变量进行读取,还会有修改操作。

内置作用域

内置作用域其实本质上来说就是全局作用域。之前已经说过,python 当中的全局作用域是和文件(模块)相关的。那么内置作用域,很显然是和Python 的内置模块相关。python 2.x 版本中内置模块的名字是 buildin,Python 3.x 版本中内置模块的名字是*builtins*。说白了,这两个内置模块也是两个不同的文件,比如一些我们常用的Python 的内置函数,有可能就是这个模块当中的一个全局作用域当中的一个变量。那么也就是说,当我们想引用Python 给我们提供的某些内置功能的时候,除了可以依赖「LEGB」原则,还可以直接通过 import 内置的模块进行显示调用。

修改外部作用域的变量

既然作用域会被氛围很多种,尤其是对本地作用域和全局作用域来讲,因为有了变量隔离机制,所在在本地作用域中对同名变量的读取和修改并不会影响到全局变量。那么,如果我们现在在本地作用域中,不但要读取全局作用域中的变量,还要修改它,这个时候就需要 global 这个关键字了。

global 语句

global 这个语句一般后面会带着一个变量名。它是目前为止,我们所见到的第一个Python 当中具有声明作用的语句。global声明的是一个命名空间,或者说是作用域也可以。例如,global a,声明了 a 这个变量的作用域或者说是命名空间是全局的。一般来说,如果本地作用域和全局作用域没有重名的变量且不会对全局变量做修改的话,任何一个本地作用域内都是可以直接读取本模块内的全局变量的。但是如果是有重名的情况,为了确保我们在本地能够读取和修改的是全局变量,就需要在使用之前用 global 语句来声明这个变量的命名空间。

这里需要注意的是,global 语句只是起到了一个声明的作用,如果 global 后面跟着一个并不存在的变量名,在函数定义的时候,并不会发生报错, 只有当函数被调用的时候,执行逻辑到了 global 语句所在位置的时候,才会抛出异常。包括在定义函数的时候使用了一个不存在的变量也会有相同的结果, 但是这一切的前提都是在 global 语句出现之后,本地作用域内没有对这个全局变量进行赋值操作。一旦有赋值操作,将会自动创建这个变量。

这应该是算作Python 这类动态语言一个「不好」的地方,由于在运行之前并不会做一些常规的语法上的检查就导致了很多错误只有在运行的时候才会暴露出来。不过这也恰恰印证了一点,我们在学Python 的时候,还是需要摒弃一些使用静态语言时的一些习惯。前面也说到了,python 是有意不做一些校验的,猜测它这么做是因为做多了这种校验会让代码变的复杂,灵活性降低。有的时候,直接抛出异常,也未尝不是一个好的错误处理方式。学习和使用动态语言的时候,思维还是要有所转变。

嵌套函数

嵌套函数从字面意思上来讲就是一个函数套着一个函数,这种形式在各个语言的「闭包」中出现的最多。伴随着嵌套函数的一个比较重要的概念是嵌套作用域,也就是我们之前说的「LEGB」原则中的 E。如果在一个被嵌套的函数中,想要读取并且修改上层函数的某一个变量,则需要在自己的函数体内声明这个变量为「nonlocal」,python 会自动寻找到离他最近嵌套层次中的同名变量。

工厂函数(闭包 or 装饰器)

工厂函数其实也就是我们常说的的闭包函数,闭包的概念之前是有讲过的,它是一个能够保存嵌套作用域变量的函数。也就是说,即使最后闭包函数的外层嵌套作用域都不存在了,但是在它的内部还是能够访问到之前的变量,这是目前为止我们遇到的,除了类的属性之外能够保存状态的最好的办法。之所以不提全局变量,是因为这个东西使用起来还是有很多坑的,需要考虑很多东西,一旦有了并发等场景就更加复杂,当然,还有默认的可变参数也可以实现这个功能,但是都不推荐使用。闭包函数在 python 中一个典型的应用就是装饰器,但是除了代码编写上还没有发现有什么特别的好处。

Ps: 这里有一个比较小的 tips 需要提到:在 python 中一个函数体中可以调用一个在它之后才定义的函数。但是在这个函数调用之前,它所使用的函数都必须已经定义。由此我们可以知道,在 python 中 def 语句只是把函数体中的东西统一的构造除了一个对象然后让一个变量去引用它,至于这个函数体本身有没有问题,还是得执行起来才能够知道。

lambda表达式

lambda 看起来好像是一个匿名的函数,但本质上它仍然只是一个表达式,一个可以构造函数的表达式。如果这个表达式出现在 def 定义的函数中,无意中就创造了一个嵌套函数模型。在之前没有嵌套作用域概念的时候,想让 lambda 表达式生成的函数访问所在嵌套层次函数内的变量,只能通过默认参数传递的方式进行,但是现在,可以不用多写任何一行代码依赖 python 本身所提供的对嵌套作用域的支持就可以实现这个效果。

嵌套函数的陷阱

嵌套函数一个最容易被忽略的陷阱就是在函数被嵌套在了一个循环中,并且嵌套函数中还使用了循环变量。如在生成一个函数列表的情况下,这个陷阱导致的直接后果就是,函数列表中的函数内所保存的循环变量的值都是循环范围中最后的那一个值,为什么呢?

  1. 循环变量也是一个引用,在循环范围内每次引用一个不同的值
  2. 函数体中的逻辑只有在函数执行的时候才执行,def 语句并不会执行内部逻辑

出现这种情况,最直接的解决问题的思想就是将每一次的循环变量所引用的值都分别保存在嵌套函数的内部,每一个函数都会创建一个自己的本地作用域,所以将循环变量赋值给嵌套函数的参数,即可实现我们所期望的效果。

nonlocal

nonlocal 关键字从 Python3.x 开始提供,它允许在嵌套函数中读取和修改嵌套作用域中的变量。nonlocal 的出现,在变量解析的过程中直接跳过了本地作用域这一阶段,从嵌套作用域开始。它与「global」相同,「global」是在整个模块中寻找想要操作的变量,而「nonlocal」是在本地作用域与全局作用域之前的嵌套作用域中进行查找。但是,「global」所声明的变量如果在全局作用域内找不到,还可以去内置作用域找,但是「nonlocal」不行,它只能在嵌套作用域内进行查找。