模块可以具有与对象相同的属性吗?


Answers:


56

只有新式类的实例才能具有属性。通过将其存储在中,可以使Python相信这样的实例是模块sys.modules[thename] = theinstance。因此,例如,您的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()

2
有人尝试过吗?当我将此代码放入一个文件x.py并从另一个文件导入时,然后调用xy会导致AttributeError:'NoneType'对象没有属性'c',因为_M值不存在...
Stephan202

3
的确,代码可以在解释器上工作。但是,当我将其放入文件(例如bowwow.py)并从另一个文件(otherfile.py)导入时,它不再起作用……
Stephan202

4
问:从types.ModuleType@Unknown否则非常相似的答案中可以看出,从实例的类派生有什么特别的优点吗?
martineau

11
只有新式类的实例才能具有属性。这不是原因:模块新型类的实例,因为它们是的实例builtins.module,而自身本身是的实例type(这是新型类的定义)。问题是性能必须在类,而不是实例:如果你这样做f = Foo()f.some_property = property(...),它会以同样的方式失败,因为如果你天真地把它放在一个模块中。解决方案是将其放在类中,但是由于您不希望所有模块都具有该属性,因此可以将其子类化(请参阅未知答案)。
Thanatos

3
@Joe,重新绑定名称后更改globals()(保持键完整但将值重置为Nonesys.modules是Python 2问题-Python 3.4可以正常工作。如果您需要访问Py2中的类对象,请_M._cls = _M在该class语句之后添加(例如,将其等效地存储在其他命名空间中)并按照self._cls需要它的方法进行访问(type(self)可能可以,但是如果您还对进行了任何子类化,则可能不需要_M) 。
亚历克斯·马丁里

54

我这样做是为了正确继承模块的所有属性,并由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)在运行时在包含该类的模块中进行的导入将不会“可见”,等等...
tutuDajuju

1
不必从派生子类types.ModuleType任何(新式)类都可以。您希望在哪里继承哪些特殊的模块属性?
martineau '16

如果原始模块是软件包,并且我想访问原始模块下面的模块怎么办?
kawing-chiu

2
@martineau您将拥有一个模块代表,您可以在__init__实例时指定模块名称,使用时将获得正确的行为isinstance
WIM

@wim:采取的观点,尽管坦率地说似乎没有什么是最重要的IMO。
martineau

34

由于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'

请注意,如果您raise AttributeError__getattr__函数中省略,则表示函数以结束return None,则的module.nosuch值将为None


2
基于此,我添加了另一个答案:stackoverflow.com/a/58526852/2124834

3
这只是财产的一半。没有二传手。
维姆

不幸的是,使工具知道这些属性似乎并不困难(?)(仅在未找到常规成员的情况下才调用getattr
olejorgenb

9

根据约翰·林的回答

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()没有下划线是更常规的做法。您还可以在答案中创建“模块属性设置器”修饰符吗?
约翰·林

2
@JohnLin,我尝试执行您的def thing()建议。问题在于,__getattr__仅因缺少属性而被调用。但是@module_property def thing(): …运行后,由于the_module.thing已定义,因此不会调用getattr。我们需要以某种方式thing在装饰器中注册,然后将其从模块的命名空间中删除。我尝试None从装饰器返回,但随后thing定义为None。一个人可以做,@module_property def thing(): … del thing但我发现比使用thing()函数更糟糕
Ben Mares

好的,我看到没有“模块属性设置器”,也没有“模块__getattribute__”。谢谢。
John Lin

5

一个典型的用例是:使用一些动态属性来丰富(庞大的)现有模块-而无需将所有模块内容转换为类布局。不幸的是,最简单的模块类补丁如sys.modules[__name__].__class__ = MyPropertyModule失败了TypeError: __class__ assignment: only for heap types。因此,需要重新创建模块。

这种方法无需使用Python导入钩子就能做到这一点,只需在模块代码的顶部添加一些序言即可:

# 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


1

一个简单的答案:使用 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类的实例。设置“模块属性”等于在此类的实例上而不是在types.ModuleType类本身上设置属性。有关更多详细信息,请参见此答案

types.ModuleType尽管结果不是很好,但实际上我们可以按如下方式实现属性。我们不能直接修改内置类型,但是可以诅咒它们:

# 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

1
与像@Alex Martelli的答案所示的那样,仅使模块成为真实类的实例相比,这有什么好处?
martineau

1
您说的其他话对我来说毫无意义。从事有关@module_property装饰的工作。一般而言,内置@property装饰器是在定义类时使用的,而不是在创建其实例后使用的,因此我认为对于模块属性也是如此,这与Alex的回答相同—回想一下这个问题“模块可以具有与对象相同的属性吗?”。然而,它可以在后面加上他们,我已经修改了我刚才的片段来说明可以做的一种方式。
martineau

1
Ben:在查看了您的具体示例中的代码之后,我想我知道您现在所得到的。我还认为我最近偶然发现了一种实现类似于模块属性的技术,该技术不需要像Alex的回答中那样用类实例替换模块,尽管目前我不确定是否有办法通过装饰器进行处理-如果我取得任何进展,将会回复您。
martineau

1
好的,这是指向另一个包含核心思想的问题的答案的链接。
martineau

1
好吧,至少在a的情况下,如果定义了属性将不再调用cached_module_property该事实__getattr__()是有帮助的。(类似于functools.cached_property完成的工作)。
martineau
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.