修改 __main__ 模块的属性访问(名称解析的细节)
Posted
技术标签:
【中文标题】修改 __main__ 模块的属性访问(名称解析的细节)【英文标题】:Modifying attribute access for the __main__ module (the details of name resolution) 【发布时间】:2019-05-17 23:28:08 【问题描述】:从文档中收集的信息
关于name resolution 的文档并不十分清楚。它使用了 scope 和 namespace 这两个术语,但没有准确说明它们是如何生效的以及何时引发 NameError
:
在代码块中使用名称时,将使用最近的封闭范围对其进行解析。对代码块可见的所有此类范围的集合称为代码块的环境。
当根本找不到名称时,会引发
NameError
异常。
但是,这并不能解释搜索名称的确切位置。关于命名空间,我们得到以下信息:
通过搜索全局命名空间(即包含代码块的模块的命名空间)在***命名空间中解析名称,[...]
此外,关于__main__
:
模块的命名空间是在第一次导入模块时自动创建的。脚本的主模块始终称为
__main__
。
This part of the docs 进一步声明
'__main__'
是***代码执行的范围的名称。
相关代码
结合上面的陈述,我想每当在“***脚本环境”(“***命名空间”)中解析一个名字时,这个通过检查sys.modules['__main__']
发生(类似于PEP 562 指出的模块属性访问的工作方式和修改方式)。但是下面的 sn-p 表明情况并非如此:
import sys
class Wrapper:
def __init__(self):
self.main = sys.modules['__main__']
def __getattr__(self, name):
try:
return getattr(self.main, name)
except AttributeError:
return 'Fallback for ""'.format(name)
sys.modules['__main__'] = Wrapper()
print(undefined)
引发NameError: name 'undefined' is not defined
。
另一方面,我们可以通过修改sys.modules['__main__'].__dict__
或使用setattr
来添加名称:
import sys
# Either ...
sys.modules['__main__'].__dict__['undefined'] = 'not anymore'
# Or ...
setattr(sys.modules['__main__'], 'undefined', 'not anymore')
print(undefined) # Works.
所以我怀疑可能是直接检查模块的__dict__
属性(或等效__builtins__.globals
),在模块对象上回避getattr
。然而,扩展上面的例子表明情况并非如此:
import sys
class Wrapper:
def __init__(self):
self.main = sys.modules['__main__']
def __getattr__(self, name):
try:
return getattr(self.main, name)
except AttributeError:
return 'Fallback for ""'.format(name)
@property
def __dict__(self):
class D:
def __contains__(*args):
return True
def __getitem__(__, item):
return getattr(self, item)
return D()
sys.modules['__main__'] = Wrapper()
sys.modules['builtins'].globals = lambda: sys.modules['__main__'].__dict__
print(globals()['undefined']) # Works.
print(undefined) # Raises NameError.
问题
-
作用域和命名空间的确切定义是什么?
如何准确解析名称(采取了哪些步骤以及检查哪些资源以确定名称是否存在)?
名称解析以何种方式涉及范围和命名空间?
为什么上面使用
Wrapper
的示例会失败(虽然它确实适用于“常规”模块属性访问,根据PEP 562)?
【问题讨论】:
【参考方案1】:你的问题很有趣,因为我没有明确的答案,让我们进行一些实验。
首先让我们稍微修改一下代码:
# file main.py
import sys
print(sys.modules['__main__'])
class Wrapper:
def __init__(self):
self.main = sys.modules['__main__']
def __getattr__(self, name):
try:
return getattr(self.main, name)
except AttributeError:
return 'Fallback for ""'.format(name)
sys.modules['__main__'] = Wrapper()
print(sys.modules['__main__'])
print(undefined)
它会打印出来
<module '__main__' from 'main.py'>
<__main__.Wrapper object at 0x000001F87601BE48>
Traceback (most recent call last):
File "main.py", line 15, in <module>
print(undefined)
NameError: name 'undefined' is not defined
所以我们这里仍然有__main__
作为一个模块,Wrapper
类在里面。
文档说:
当从标准输入、脚本或交互式提示中读取时,模块的
__name__
设置为等于__main__
。
这意味着我们的sys.modules['__main__'] = Wrapper()
行是为了替换一个已经加载的模块,用该模块内部的东西(!!)。
OTOH,从 REPL 导入 main.py
(另一种情况是创建 __main__
模块),完全搞砸了一切,所以当时正在发生一些替换。
总结:
据我所知,从正在运行的模块中更改 __main__
需要一些深奥的黑魔法,也许如果我们使用 importlib.reload
并弄乱缓存的模块?
从其他模块执行此操作似乎没问题,但是(示例)会弄乱东西,并且名称解析会中断,即 Wapper 类不会像您认为的那样解析以前的名称。
警察局。
对不起,如果这不是您想要的经验丰富的答案,而且看起来更像是评论。我把它作为一个实验来测试你的假设,也许会找到一些结果。
【讨论】:
"[...] 用该模块内部的东西替换一个已经加载的模块 (!!)。” 我不明白这是怎么回事,它是与您在 “从 REPL 导入 main.py,完全搞砸了一切 [...]” 我不确定你的意思是什么或它是如何相关的,但导入主脚本不会造成任何问题。 “从其他模块执行此操作似乎没问题,但它会弄乱事情,并且名称解析会中断。” 你到底是什么意思?无论如何,整件事与其说是答案,不如说是评论。 编辑澄清 再一次,您的 cmets(仍然)不清楚。具体(再次): 1. “用该模块内部的东西替换已经加载的模块” 这正是您用来修改模块属性访问的方法;那么你在这里提到它是什么意思? 2. “从 REPL 导入main.py
(另一种情况是创建 __main__
模块),完全搞砸了一切” 实际上这是错误的。导入main.py
将创建一个名为'main'
的模块;它不会“弄乱”任何东西。 3. “测试你的假设” OP 中没有假设,那么你实际测试的是什么?
其实这并没有错,可能写得不好,运行 REPL 会创建一个 __main__
模块,导入 main.py
会覆盖 __main__
模块(因为您的示例),而我期待名称解析为了继续工作,它没有。这让我相信,由于模块加载的工作方式,尽可能用包装器(你所做的方式)替换 __main__
可能会很麻烦。您关于 名称解析 无法按说明工作的假设无法证明自己
澄清一下,运行解释器确实会创建一个__main__
模块,但是导入一个名为main.py
的文件将创建一个名为"main"
(而不是"__main__"
)的模块。此外,导入一个模块不会覆盖另一个已加载的同名模块(为此您需要使用importlib.reload
)。因此,即使您调用文件__main__.py
(而不是main.py
)并在解释器中执行import __main__
,也不会发生任何事情,因为已经加载了一个名为__main__
的模块。因此,您声称的内容是错误的,我看不出它如何回答我的问题。以上是关于修改 __main__ 模块的属性访问(名称解析的细节)的主要内容,如果未能解决你的问题,请参考以下文章
python中if __name__ == '__main__': 的解析
Python多处理错误:AttributeError:模块'__main__'没有属性'__spec__'
AttributeError:模块“__main__”没有属性“AverageWordLengthExtractor”
pickle/joblib AttributeError:模块'__main__'在pytest中没有属性'thing'