《最值得收藏的python3语法汇总》,是我为了准备公众号“跟哥一起学python”上面视频教程而写的课件。整个课件将近200页,10w字,几乎囊括了python3所有的语法知识点。
你可以关注这个公众号“跟哥一起学python”,获取对应的视频和实例源码。
这是我和几位老程序员一起维护的个人公众号,全是原创性的干货编程类技术文章,欢迎关注。
命名空间(namespace)是对符号(变量、函数、类等等)名字的一种分组机制,它提供了在项目中避免名字冲突的一种方法。不同组的相同命名符号被视作两个独立的符号,因此隶属于不同命名空间的符号名称可以重复。
命名空间是高级编程语言的一种基本概念,C++、Java等都支持命名空间机制。C语言不支持命名空间,所以C语言的程序员需要自己保证命名在全局范围内不重复。
在Python中,命名空间采用了字典的结构来实现,它记录了符号名称和对象之间的对应关系。
Python有4种命名空间:
需要注意的是,上图中的命名空间并没有从上到下的包含关系,比如:我们不能认为Global-ns是包含它下级的Local-ns的。事实上,所有的命名空间都是独立存在的。
什么是作用域(scope)?
作用域是Python的一块文本区域,这个区域中,命名空间可以被“直接访问”。这里的直接访问指的是试图在命名空间中找到名字的绝对引用(非限定引用)。这里有必要解释下直接引用和间接引用:
直接引用:直接使用名字访问的方式,如name,这种方式尝试在名字空间中搜索名字name。
间接引用:使用形如objname.attrname的方式,即属性引用,这种方式不会在命名空间中搜索名字attrname,而是搜索名字objname,再访问其属性。
上面的四种命名空间的作用域如下:
我们可以看到,这些命名空间的作用域是相互包含的。
当解释器要查找一个符号名字时,它会采用L-E-G-B(由近及远)的顺序,依次在这四种命名空间中去查找。
我们看看下面的例子:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./11/11_1.py
# 命名空间和作用域
x = 1 # 位于Global-NS
def foo():
x = 2 # 位于foo的Local-NS,相对于innerfoo来说,是外层嵌套函数NS,Enclosing-NS
def innerfoo():
x = 3 # 位于Local-NS
print('locals', x)
innerfoo()
print('enclosing function locals ', x)
foo()
print('global ', x)
输出为:
locals 3
enclosing function locals 2
global 1
如果要执行innerfoo函数,查找变量x时,按照LEGB原则,获取到的是Local-NS中的x对应的对象。
这里需要注意的是,Local-NS和Enclosing-NS的概念是相对的。对于innerfoo来说,foo的Local-NS也是innerfoo的Enclosing-NS。
需要特别注意的是,Python中,只有模块、函数、类、推导式等可以产生作用域,而if、for、while等等这些控制语句,则不会产生作用域。这和其他编程语言存在差异。比如下面这个简单的例子:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./11/11_2.py
# if不产生作用域
x = 100
if True:
x = 200
print(x)
这个例子中,x被改变了,最后输出结果为200。所以if语句中使用的x变量就是Glocal-NS的中的名字x。if本身并没有产生一个新的作用域。
理解变量的定义和引用?
我们需要清楚地理解变量的定义和引用这两个行为的区别。
比如: x = 100,这是定义了变量x (define); 而print(x),这是引用了变量x。
对于引用变量的行为,python会采用LEGB原则到命名空间去查找名字,如果找不到则抛出异常;
在Python中,任何符号(变量名、函数名、类名等等)都是需要先定义后才能被引用的。
对于定义变量的行为,python会在对应的命名空间增加名字和对象的映射关系。同时要注意一点,定义行为是优先于引用行为的。
我们再回头看看前面的例子,我们只看函数foo定义的代码片段:
def foo():
x = 2
def innerfoo():
x = 3
print('locals', x)
innerfoo()
print('enclosing function locals ', x)
内层函数innerfoo中第一行代码x=3,它到底是新定义了一个Local-NS的变量名x呢?还是引用了外层函数foo的变量名x呢?根据我们前面提到的原则:定义行为优先于引用行为。所以,解释器会优先认为它是新定义的一个Local-NS变量名x,并添加到命名空间中。下一行print函数引用变量x时,更加LEGB顺序,首先查找到的就是这个新定义的变量名x。
这个例子中的x是一个不可变数据类型,我们再看一个可变数据类型,它的行为会让你感到更加“怪异”!
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./11/11_3.py
# 变量的定义和引用
list_1 = [100, 200]
list_2 = [100, 200]
def foo1():
list_1 = [300] # 这是定义
def foo2():
list_2.append(300) # 这是引用
foo1()
foo2()
print(list_1)
print(list_2)
输出为:
[100, 200]
[100, 200, 300]
List_1和list_2产生的结果是完全不一样的,foo1中是新定义了一个Local-NS的变量list_1,而foo2中是对Global-NS中list_2的引用。
通过前面的例子,我们可以看到,如果在作用域里面使用=号赋值运算符,那么解释器会优先认为是定义行为。那么如果我们要用=号修改外部变量,该怎么办呢?
我们需要使用到global和nonlocal关键字。
Global:告诉解释器,我使用的这个变量是Global-NS里面的变量。
Nonlocal:告诉解释器,我使用的这个变量是外层嵌套函数的变量。
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./11/11_4.py
# global
list_1 = [100, 200]
def foo1():
global list_1 # 声明list_1使用的是全局的
list_1 = [300] # 这是引用,是对list_1的修改
foo1()
print(list_1)
输出为:
[300]
解释器看到global声明后,会在foo1中引用Global-NS中的list_1名字。所以,list_1=[300]不被认为是定义行为,因为已经定义过了。
同样,nonlocal也类似:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./11/11_5.py
# non-local
def foo1():
list_1 = [500]
def foo1_inner():
nonlocal list_1
list_1 = [300] # 这是引用,是对list_1的修改
foo1_inner()
return list_1
print(foo1())
输出为:
[300]
解释器在看到nonlocal声明后,会引用外层函数foo1定义的变量list_1。
Global和nonlocal存在一个重要的区别,nonlocal声明的变量必须在外层函数中已经定义,否则解释器会报错。而global声明的变量如果没有定义,那么它会主动帮你定义一个。
大家在看一些官方手册时,还会提到一个“自由变量(free variable)”的概念。当一个变量被引用的地方不是它定义的作用域时,这个变量就叫做“自由变量”。比如上面例子中全局定义的list_1,被foo函数内部引用了,那么list_1就是一个自由变量。