最近在夯实python基础,又把自己没弄懂的翻出来再攻克一下——python多态,顺藤摸瓜,就到了类的属性查找,其中涉及python名称和对象,再进而到了作用域和命名空间,最然用python也有几年了,也自诩(简历上)python基础扎实者,但自知对这个在python文档“入门教程”一节的内容,还没有完完全全的掌握,再读一遍又有新知,感觉了解的清楚一些了,在此记录一小节。
最近也是在读《如何阅读一本书》,用上了书中的方法,作用域与命名空间一节虽然不长,但自我感觉后面还是比较难度的,像这种没有领会句子或词的意思,没有“与作者达成共识”的,就要反反复复来读,结合上下文,这也是书中说的唯一行之有效的方法。
名称与对象
9.1. 名称和对象
对象之间相互独立,多个名称(在多个作用域内)可以绑定到同一个对象。 其他语言称之为别名。Python 初学者通常不容易理解这个概念,处理数字、字符串、元组等不可变基本类型时,可以不必理会。 但是,对涉及可变对象,如列表、字典等大多数其他类型的 Python 代码的语义,别名可能会产生意料之外的效果。这样做,通常是为了让程序受益,因为别名在某些方面就像指针。例如,传递对象的代价很小,因为实现只传递一个指针;如果函数修改了作为参数传递的对象,调用者就可以看到更改 — 无需 Pascal 用两个不同参数的传递机制。
这段其实就是在说一个指针的概念,(以C语言的角度理解)一个对象可以有多个指针指向,这里要注意的就是对象的immutable和mutable的区别,immutable对象有数字、字符串和元组,这些对象改变其值就会创建新的对象(地址、id()也改变),但mutable对象可以在地址不变的情况下就可以改变其取值(id())不变,这个会在传递对象使用的时候有影响。
Python作用域和命名空间
namespace (命名空间)是一个从名字到对象的映射。 当前大部分命名空间都由 Python 字典实现,但一般情况下基本不会去关注它们(除了要面对性能问题时),而且也有可能在将来更改。 下面是几个命名空间的例子:存放内置函数的集合(包含
abs()
这样的函数,和内建的异常等);模块中的全局名称;函数调用中的局部名称。 从某种意义上说,对象的属性集合也是一种命名空间的形式。 关于命名空间的重要一点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义一个maximize
函数而不会产生混淆 — 模块的用户必须在其前面加上模块名称。
命名空间由python字典实现,是一个从名字到对象的映射(这个我理解就是上一节中名称——对象的意思),不同命名空间中的名称没有关系。
顺便说明一下,我把任何跟在一个点号之后的名称都称为 属性 — 例如,在表达式
z.real
中,real
是对象z
的一个属性。按严格的说法,对模块中名称的引用属于属性引用:在表达式modname.funcname
中,modname
是一个模块对象而funcname
是它的一个属性。在此情况下在模块的属性和模块中定义的全局名称之间正好存在一个直观的映射:它们共享相同的命名空间! 1属性可以是只读或者可写的。如果为后者,那么对属性的赋值是可行的。模块属性是可写的,你可以写
modname.the_answer = 42
。可写的属性同样可以用del
语句删除。例如,del modname.the_answer
将会从名为modname
的对象中移除the_answer
属性。
点号之后的名称称为属性,属性是可读或可写的,可写属性可以使用del语句从对象中删除(模块也是对象)
命名空间在不同时刻被创建,拥有不同的生存期。包含内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。模块的全局命名空间在模块定义被读入时创建;通常,模块命名空间也会持续到解释器退出。被解释器的顶层调用执行的语句,从一个脚本文件读取或交互式地读取,被认为是
__main__
模块调用的一部分,因此它们拥有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块被称作builtins
。)一个函数的本地命名空间在这个函数被调用时创建,并在函数返回或抛出一个不在函数内部处理的错误时被删除。(事实上,比起描述到底发生了什么,忘掉它更好。)当然,每次递归调用都会有它自己的本地命名空间。
命名空间拥有不同的生存期,包含内置名称的命名空间在python解释器启动时创建一直持续到解释器退出,模块的全局命名空间在import时创建,通常也会持续到解释器退出。
这里这个__main__
模块比较迷,查了一些资料,这个可以认为是我们在用python modulename
(命令行方式)执行脚本时预先加载的一个模块,而我们的模块则是__main__
模块调用的一部分,所以在我们的模块中__name__
属性才是__main__
,我们可以使用__name__ == __main__
的方式判断是否从命令行执行脚本而不是导入。
一个 作用域 是一个命名空间可直接访问的 Python 程序的文本区域。 这里的 “可直接访问” 意味着对名称的非限定引用会尝试在命名空间中查找名称。
虽然作用域是静态地确定的,但它们会被动态地使用。 在执行期间的任何时刻,会有 3 或 4 个命名空间可被直接访问的嵌套作用域:
- 最先搜索的最内部作用域包含局部名称
- 从最近的封闭作用域开始搜索的任何封闭函数的作用域包含非局部名称,也包括非全局名称
- 倒数第二个作用域包含当前模块的全局名称
- 最外面的作用域(最后搜索)是包含内置名称的命名空间
这个比较关键,列出了嵌套作用域的结构:
-
最先搜索的最内部作用域包含局部名称
最内层函数内部
-
从最近的封闭作用域开始搜索的任何封闭函数的作用域包含非局部名称,也包括非全局名称
从最内层到最外倒数第二个作用域之间各个封闭函数的作用域
-
倒数第二个作用域包含当前模块的全局名称
倒数第二个作用域,也就是当前模块的全局名称的作用域
-
最外面的作用域(最后搜索)是包含内置名称的命名空间
最外层作用域,在本模块之外的(上帝作用域),包含内置名称的
【这里应该有一张图片】(形象展示各个作用域)
如果一个名称被声明为全局变量,则所有引用和赋值将直接指向包含该模块的全局名称的中间作用域。 要重新绑定在最内层作用域以外找到的变量,可以使用
nonlocal
语句声明为非本地变量。 如果没有被声明为非本地变量,这些变量将是只读的(尝试写入这样的变量只会在最内层作用域中创建一个 新的 局部变量,而同名的外部变量保持不变)。通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。 在函数以外,局部作用域将引用与全局作用域相一致的命名空间:模块的命名空间。 类定义将在局部命名空间内再放置另一个命名空间。
重要的是应该意识到作用域是按字面文本来确定的:在一个模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。 另一方面,实际的名称搜索是在运行时动态完成的 — 但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析! (事实上,局部变量已经是被静态确定了。)
Python 的一个特殊规定是这样的 – 如果不存在生效的
global
或nonlocal
语句 – 则对名称的赋值总是会进入最内层作用域。 赋值不会复制数据 — 它们只是将名称绑定到对象。 删除也是如此:语句del x
会从局部作用域所引用的命名空间中移除对x
的绑定。 事实上,所有引入新名称的操作都是使用局部作用域:特别地,import
语句和函数定义会在局部作用域中绑定模块或函数名称。
global
语句可被用来表明特定变量生存于全局作用域并且应当在其中被重新绑定;nonlocal
语句表明特定变量生存于外层作用域中并且应当在其中被重新绑定。
这一段就是在讲global和nonlocal的作用,当一个名称被声明为全局变量时,将直接绑定到模块的全局作用域,被声明为非本地变量时,将在外层作用域中寻找(应该是优先最近一层)。
当局部作用域不存在时,且没有global和nonlocal的声明,对一个变量赋值总会在最内层创建新的变量——将名称绑定到对象。语句del x
可以从局部命名空间中移除对x的绑定。
作用域和命名空间的示例
def scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
输出:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
- do_local在局部作用域中对spam赋值,不会影响到scope_test中的spam,所以输出test spam
- do_nonlocal中声明spam为nonlocal的,所以在外层作用域中寻找,最后绑定到scope_test中的spam,所以对spam赋值就改变了scope_test中的spam,输出nonlocal spam
- do_global直接将spam绑定到全局作用域,当前全局作用域没有spam,所以会创建一个新的变量,值为global spam,但是print语句不是在do_global的作用域中,所以输出还是在上一步被赋值了的nonlocal spam
- 最后在scope_test中print,此时在全局作用域中,而上一步已在全局作用域中创建了spam,所以输出global spam
同名的类和实例名称
在文档中:同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例。做实验如下:
class A:
var1 = "class var 1"
def __init__(self) -> None:
self.var1 = "instance var 1"
a = A()
print(a.var1) # instance var 1
del a.var1
print(a.var1) # class var 1
a.var1 = "instance var 11"
print(a.var1) # instance var 11
A.var1 = "class var 11"
print(a.var1) # instance var 11
print(A.var1) # class var 11
验证同名的类和实例属性,确实会优先选择实例属性。也验证了,不同命名空间中的名称互不影响,在实例中创建var1属性时并不会导致类属性var1消失。
python属性查找还是比较复杂的,而属性查找又与descriptor相关,descriptor又与类相关,,,所以下一篇写descriptor。