修改 __main__ 模块的属性访问(名称解析的细节)

Posted

技术标签:

【中文标题】修改 __main__ 模块的属性访问(名称解析的细节)【英文标题】:Modifying attribute access for the __main__ module (the details of name resolution) 【发布时间】:2019-05-17 23:28:08 【问题描述】:

从文档中收集的信息

关于name resolution 的文档并不十分清楚。它使用了 scopenamespace 这两个术语,但没有准确说明它们是如何生效的以及何时引发 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中if __name__ == '__main__': 的解析

Python多处理错误:AttributeError:模块'__main__'没有属性'__spec__'

AttributeError:模块“__main__”没有属性“AverageWordLengthExtractor”

pickle/joblib AttributeError:模块'__main__'在pytest中没有属性'thing'