【问题标题】:Can modules have properties the same way that objects can?模块可以像对象一样具有属性吗?
【发布时间】:2010-10-27 04:51:51
【问题描述】:

使用 python 属性,我可以做到这一点

obj.y 

调用一个函数而不是仅仅返回一个值。

有没有办法用模块做到这一点?我有一个我想要的案例

module.y 

调用一个函数,而不是仅仅返回存储在那里的值。

【问题讨论】:

标签: python properties python-module


【解决方案1】:

基于user2124834's answer

import sys
class AttrGeter:
    def __new__(cls, gt):
        if isinstance(gt, cls):
            return gt
        else:
            o = super().__new__(cls)
            o.oldgetattr = gt
            o.funcmap = {}
            return o

    def __call__(self, name):
        name2 = "_" + name
        if name2 in self.funcmap:
            return self.funcmap[name2]()
        else:
            return self.oldgetattr(name)

    def add(self, func):
        self.funcmap[func.__name__] = func


def module_property(func):
    """Decorator to turn module functions into properties.
    Function names must be prefixed with an underscore."""
    module = sys.modules[func.__module__]
    def base_getattr(name):
        raise AttributeError(
            f"module '{module.__name__}' has no attribute '{name}'")
    ag = AttrGeter(getattr(module, '__getattr__', base_getattr))
    module.__getattr__ = ag
    ag.add(func)
    return func

用法(注意开头的下划线),在the_module.py中:

@module_property
def _thing():
    return 'hello'

然后:

import the_module

print(the_module.thing)  # prints 'hello'

我在原始解决方案中使用 dict 而不是嵌套的 function。在一个模块中多次使用装饰器可能会更有效。

【讨论】:

  • 建议您将类重命名为AttrGetter,这样更接近英文拼写。无论如何,必须在某些引用前加上下划线意味着该类的用户必须知道哪些是属性,哪些不是——这至少部分破坏了属性的一个非常重要的方面。
【解决方案2】:

简短回答:使用proxy_tools

proxy_tools 包尝试提供@module_property 功能。

它安装在

pip install proxy_tools

对@Marein 的示例稍作修改,在the_module.py 中我们将

from proxy_tools import module_property

@module_property
def thing():
    print(". ", end='')  # Prints ". " on each invocation
    return 'hello'

现在从另一个脚本,我可以做到

import the_module

print(the_module.thing)
# . hello

意外行为

此解决方案并非没有注意事项。也就是说,the_module.thing不是字符串!它是一个proxy_tools.Proxy 对象,其特殊方法已被覆盖,因此它模仿了一个字符串。以下是一些说明这一点的基本测试:

res = the_module.thing
# [No output!!! Evaluation doesn't occur yet.]

print(type(res))
# <class 'proxy_tools.Proxy'>

print(isinstance(res, str))
# False

print(res)
# . hello

print(res + " there")
# . hello there

print(isinstance(res + "", str))
# . True

print(res.split('e'))
# . ['h', 'llo']

在内部,原函数存储到the_module.thing._Proxy__local

print(res._Proxy__local)
# <function thing at 0x7f729c3bf680>

进一步的想法

老实说,我对为什么模块没有内置此功能感到困惑。我认为问题的症结在于the_moduletypes.ModuleType 类的一个实例。设置“模块属性”相当于在此类的 instance 上设置属性,而不是在 types.ModuleType 类本身上设置属性。详情请见this answer

我们实际上可以在types.ModuleType 上实现属性,如下所示,虽然结果不是很好。我们不能直接修改内置类型,但是可以curse他们:

# python -m pip install forbiddenfruit
from forbiddenfruit import curse
from types import ModuleType
# curse has the same signature as setattr.
curse(ModuleType, "thing2", property(lambda module: f'hi from {module.__name__}'))

这给了我们一个存在于所有模块中的属性。这有点笨拙,因为我们打破了所有模块的设置行为:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute

【讨论】:

  • 这比仅仅让模块成为@Alex Martelli 的回答中所示的真实类的实例更好吗?
  • 你说了一些对我来说没有意义的话。以拥有@module_property 装饰器的业务为例。一般来说,内置的 @property 装饰器是在定义类时使用的,而不是在创建它的实例之后,所以我假设模块属性也是如此,这与亚历克斯的回答一样——回想一下这个问题问“模块可以像对象一样具有属性吗?”。但是,可以在之后添加它们,我已经修改了我之前的 snippet 来说明一种可以完成的方法。
  • Ben:在查看了您具体示例中的代码之后,我想我明白您现在的意思了。我还认为我最近偶然发现了一种实现类似于模块属性的技术,该技术不需要像 Alex 的回答中那样将模块替换为类实例,尽管我目前不确定是否有这样做的方法它通过一个装饰器——如果我有任何进展会回复你。
  • 好的,这里有一个answer 的链接,指向另一个包含核心思想的问题。
  • 好吧,至少在cached_module_property 的情况下,如果属性被定义,__getattr__() 将不再被调用这一事实是有帮助的。 (类似于functools.cached_property 完成的工作)。
【解决方案3】:

基于John Lin's answer:

def module_property(func):
    """Decorator to turn module functions into properties.
    Function names must be prefixed with an underscore."""
    module = sys.modules[func.__module__]

    def base_getattr(name):
        raise AttributeError(
            f"module '{module.__name__}' has no attribute '{name}'")

    old_getattr = getattr(module, '__getattr__', base_getattr)

    def new_getattr(name):
        if f'_{name}' == func.__name__:
            return func()
        else:
            return old_getattr(name)

    module.__getattr__ = new_getattr
    return func

用法(注意前导下划线),在the_module.py:

@module_property
def _thing():
    return 'hello'

然后:

import the_module

print(the_module.thing)  # prints 'hello'

前导下划线是区分属性化函数和原始函数所必需的。我想不出重新分配标识符的方法,因为在装饰器执行期间,它还没有被分配。

请注意,IDE 不会知道该属性存在并且会显示红色波浪。

【讨论】:

  • 太棒了!与类属性@property def x(self): return self._x 相比,我认为不带下划线的def thing() 更常规。你也可以在你的答案中创建一个“模块属性设置器”装饰器吗?
  • @JohnLin,我尝试实施您的def thing() 建议。问题是__getattr__ 只被调用missing attributes。但是在@module_property def thing(): … 运行之后,the_module.thing 被定义,所以 getattr 永远不会被调用。我们需要以某种方式在装饰器中注册thing,然后将其从模块的命名空间中删除。我尝试从装饰器返回None,但随后thing 被定义为None。可以使用@module_property def thing(): … del thing,但我发现这比使用thing() 作为函数更糟糕
  • 好的,我看到没有“模块属性设置器”,也没有“模块__getattribute__”。谢谢。
【解决方案4】:

由于PEP 562 已经在 Python >= 3.7 中实现,现在我们可以这样做了

文件:module.py

def __getattr__(name):
    if name == 'y':
        return 3
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

other = 4

用法:

>>> import module
>>> module.y
3
>>> module.other
4
>>> module.nosuch
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "module.py", line 4, in __getattr__
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
AttributeError: module 'module' has no attribute 'nosuch'

注意,如果你在__getattr__函数中省略了raise AttributeError,则意味着函数以return None结尾,那么module.nosuch将得到None的值。

【讨论】:

  • 基于此,我添加了另一个答案:stackoverflow.com/a/58526852/2124834
  • 这只是一个属性的一半。没有二传手。
  • 不幸的是,让工具知道这些属性似乎并不难(?)(getattr 仅在未找到常规成员时调用)
  • @olejorgenb 尝试在不初始化变量的情况下添加类型提示。 y: int(没有 =3 或类似的东西)。在我的项目中满足mypy
【解决方案5】:

只有新式类的实例可以有属性。您可以通过将其存储在sys.modules[thename] = theinstance 中来使 Python 相信这样的实例是一个模块。因此,例如,您的 m.py 模块文件可能是:

import sys

class _M(object):
    def __init__(self):
        self.c = 0
    def afunction(self):
        self.c += 1
        return self.c
    y = property(afunction)

sys.modules[__name__] = _M()

【讨论】:

  • 其他人试过这个吗?当我将此代码放入一个文件 x.py 并从另一个文件中导入时,调用 x.y 会导致 AttributeError: 'NoneType' object has no attribute 'c',因为 _M 不知何故具有值 None...
  • 确实代码在解释器上工作。但是当我把它放在一个文件中(比如说,bowwow.py)并从另一个文件(otherfile.py)中导入它时,它就不再起作用了......
  • 问:如@Unknown 的其他非常相似的答案所示,从types.ModuleType 派生实例的类是否有任何特殊优势?
  • 只有新式类的实例可以有属性。这不是原因:模块新式类的实例,在它们是builtins.module 的实例,它本身就是type 的实例(这是新式类的定义)。问题是属性必须在类上,而不是实例上:如果你这样做f = Foo()f.some_property = property(...),它会以同样的方式失败,就像你天真地将它放在一个模块中一样。解决方案是将它放在类中,但由于您不希望所有模块都具有该属性,因此您可以进行子类化(参见 Unknown 的答案)。
  • @Joe,globals() 的更改(保持键不变,但将值重置为 None)在 sys.modules 中重新绑定名称时是 Python 2 问题——Python 3.4 有效如预期。如果您需要访问 Py2 中的类对象,请在 class 语句之后添加例如 _M._cls = _M(或将其等效地存储在其他命名空间中)并在需要它的方法中以 self._cls 的形式访问它(type(self) 可能可以,但如果您还对_M 进行任何子类化,则不行。
【解决方案6】:

我这样做是为了正确继承一个模块的所有属性,并被 isinstance() 正确识别

import types

class MyModule(types.ModuleType):
    @property
    def y(self):
        return 5


>>> a=MyModule("test")
>>> a
<module 'test' (built-in)>
>>> a.y
5

然后你可以将它插入到 sys.modules 中:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class

【讨论】:

  • 这似乎只适用于最简单的情况。可能的问题是:(1) 一些导入助手可能还期望其他属性,例如必须手动定义的 __file__,(2) 在包含该类的模块中进行的导入在运行时将不“可见”等。 .
  • 不必从types.ModuleType 派生子类,任何(新式)类都可以。您希望继承哪些特殊模块属性?
  • 如果原始模块是一个包,我想访问原始模块下的模块怎么办?
  • @martineau 你会有一个模块repr,你可以在__init__实例时指定模块名称,使用isinstance时你会得到正确的行为。
  • @wim:得分,但坦率地说,似乎没有一个对 IMO 如此重要。
【解决方案7】:

一个典型的用例是:用一些(很少)动态属性来丰富一个(巨大的)现有模块——而不是将所有模块的东西变成一个类布局。 不幸的是,像sys.modules[__name__].__class__ = MyPropertyModule 这样的最简单的模块类补丁以TypeError: __class__ assignment: only for heap types 失败。所以模块创建需要重新布线。

这种方法不需要 Python 导入钩子,只需在模块代码顶部添加一些 prolog:

# propertymodule.py
""" Module property example """

if '__orgmod__' not in globals():

    # constant prolog for having module properties / supports reload()

    print "PropertyModule stub execution", __name__
    import sys, types
    class PropertyModule(types.ModuleType):
        def __str__(self):
            return "<PropertyModule %r from %r>" % (self.__name__, self.__file__)
    modnew = PropertyModule(__name__, __doc__)
    modnew.__modclass__ = PropertyModule        
    modnew.__file__ = __file__
    modnew.__orgmod__ = sys.modules[__name__]
    sys.modules[__name__] = modnew
    exec sys._getframe().f_code in modnew.__dict__

else:

    # normal module code (usually vast) ..

    print "regular module execution"
    a = 7

    def get_dynval(module):
        return "property function returns %s in module %r" % (a * 4, module.__name__)    
    __modclass__.dynval = property(get_dynval)

用法:

>>> import propertymodule
PropertyModule stub execution propertymodule
regular module execution
>>> propertymodule.dynval
"property function returns 28 in module 'propertymodule'"
>>> reload(propertymodule)   # AFTER EDITS
regular module execution
<module 'propertymodule' from 'propertymodule.pyc'>
>>> propertymodule.dynval
"property function returns 36 in module 'propertymodule'"

注意:像from propertymodule import dynval 这样的东西当然会产生一个冻结的副本——对应于dynval = someobject.dynval

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-13
    • 2013-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-27
    • 2016-03-13
    相关资源
    最近更新 更多