【问题标题】:How to override module name (__name__)?如何覆盖模块名称(__name__)?
【发布时间】:2021-11-06 14:17:58
【问题描述】:

我在单独的文件中有两个类,a.pyb.py

# a.py
import logging

LOG = logging.getLogger(__name__)

class A:
    def name(self):
        # Do something
        LOG.debug("Did something") # LOG reads __name__ and print it.

我无法以任何方式修改 a.py

# b.py
import a

class B(A):
    pass

日志输出为:

DEBUG:<TIME> called in a, Did something

我想更改__name__,以便子类的调用应该记录called in b

我的项目将其模块名称记录为输出的一部分。但是,当我继承一个类并调用父类方法时,我不知道它是从AB 调用的,因为它只显示父类的模块名称。我怎样才能改变它?或者,有什么其他方法可以避免这种情况?

【问题讨论】:

  • 当然你应该指的是self.__name__而不是__name__
  • @smci。不,OP 想要模块命名空间
  • @MadPhysicist 谢谢。 @smci 我想在不更改读取模块的__name__ 的日志记录的情况下解决。
  • 似乎试图改变作为python模块导入系统一部分的__name__是获得这样的功能的不好方法。为什么不直接在实例或类上使用属性?
  • @马克。我同意。但是您可以谨慎地更改其他内容。我的回答提出了一个不直接违反最小惊喜的替代方案。

标签: python class oop global-variables python-module


【解决方案1】:

有一种方法可以做到,但它比您想象的要复杂。

__name__ 这个名字从何而来?答案是它是一个全局变量。这是意料之中的,因为全局命名空间是模块命名空间。例如:

>>> globals()['__name__']
'__main__'

如果您在 python 的 Data Model 文档的 Standard Type Hierarchy 中查找可调用对象,您会发现函数的 __globals__ 属性是只读的。这意味着函数A.name 将始终在模块a 的命名空间中查找其全局属性(而不是模块命名空间本身是只读的)。

更改函数全局变量的唯一方法是复制函数对象。这已在 Stack Overflow 上至少讨论过几次:

复制类也出现了:

解决方案 1

我已经在一个名为haggis 的实用程序库中实现了这两种技术。具体来说,haggis.objects.copy_class 会做你想做的事:

from haggis.objects import copy_class
import a

class B(copy_class(A, globals(), __name__)):
    pass

这将复制A。所有功能代码和非功能属性将被直接引用。但是,函数对象将应用更新后的 __globals____module__ 属性。

这种方法的主要问题是它略微破坏了您预期的继承层次结构。从功能上讲,您不会看到差异,但 issubclass(b.B, a.A) 将不再适用。

解决方案 2(可能是最好的)

我可以看到复制 all A 的方法并通过这样做破坏继承层次结构会有点麻烦。幸运的是,还有另一种方法haggis.objects.copy_func,它可以帮助您将name 方法从A 中复制出来:

from haggis.objects import copy_func
import a

class B(A):
    name = copy_func(A.name, globals(), __name__)

这个解决方案更健壮一些,因为复制类比在 python 中复制函数要复杂得多。这相当有效,因为name 的两个版本实际上将共享相同的代码对象:只有元数据(包括__globals__)会有所不同。

解决方案 3

如果你可以稍微改变A,你就有一个更简单的解决方案。 python 中的每个(普通)类都有一个__module__ 属性,它是定义它的模块的__name__ 属性。

您可以将A 重写为

class A:
    def name(self):
        # Do something
        print("Did in %s" % self.__module__)

使用self.__module__ 代替__name__ 大致类似于使用type(self) 代替__class__

解决方案 4

另一个简单的想法是正确覆盖A.name

class B(A):
    def name(self):
        # Do something
        LOG.debug("Did something")

当然,如果# Do something 是一项未在其他地方实现的复杂任务,我当然可以看出这将如何运作。直接从A 中复制函数对象可能效率更高。

【讨论】:

  • 感谢您的详细解决方案。因为我无法更改a.py(文中添加),根据你的回答,我必须将A中的所有方法复制到B。
  • @osflw。是的,但我已经为此编写了函数,它不是很长:github.com/madphysicist/haggis/blob/…
  • @osflw。您可以避免复制 all 方法。问题是 A 没有实现您想要的灵活性,因此您可以重新实现它,或者解决它。你没有选择权。
  • @osflw。我已经更新了我的答案。你只需要复制一种方法。这就像一种更有效的覆盖形式:代码对象将被共享,并且几乎只更改了 __globals____module__ 属性,这正是您要寻找的。​​span>
  • 谢谢,我明白了。这很有帮助。我认为最快的解决方案是覆盖使用 LOG 的方法。
【解决方案2】:

解决办法:

尝试使用inspect.stack

import inspect
class A:
    def __init__(self):
        pass
    def name(self):
        # Do something
        print("Did in %s" % inspect.stack()[1].filename.rpartition('/')[-1][:-3])

现在运行b.py 会输出:

Did in b

说明:

inspect 可以检查 python 脚本中的活动对象,如docs 中所述:

返回调用者堆栈的帧记录列表。返回列表中的第一个条目代表调用者;最后一项表示堆栈上最外层的调用。

它还提到它会输出NamedTuple,例如:

FrameInfo(frame, filename, lineno, function, code_context, index)

所以我们可以通过索引该元组的第二个元素来获取实时文件名,这将给出完整路径,因此我们必须将其拆分路径并使用 str.rpartition 从完整路径中提取文件名。

编辑:

不编辑a.py,只能试试:

b.py:

import inspect
exec(inspect.getsource(__import__('a')))
class B(A):
    pass

它会给出:

Did in b

【讨论】:

  • 你走在正确的轨道上:使用定义类的属性而不是 A,但是有一种更简单的方法可以做到这一点。并不是说我原来的解决方案特别简单:)
  • @MadPhysicist 嗯,inspect?
  • 查看我的更新答案。与self.__module__。这是对A 的一个微不足道的更改
  • @MadPhysicist 用self.__module__ 测试,但它给了__main__
  • 因为您没有像 OP 那样将类放入单独的文件中。 self.__module__ 将是 __name__ 没有硬编码。
猜你喜欢
  • 1970-01-01
  • 2021-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多