Могут ли модули иметь свойства так же, как объекты?


100

С помощью свойств Python я могу сделать так, чтобы

obj.y 

вызывает функцию, а не просто возвращает значение.

Есть ли способ сделать это с помощью модулей? У меня есть случай где я хочу

module.y 

для вызова функции, а не просто для возврата значения, хранящегося там.


2
См. __getattr__Модуль для более современного решения.
wim

Ответы:


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 каким-то образом имеет значение None ...
Stephan202

3
Действительно, код работает на интерпретаторе. Но когда я помещаю его в файл (скажем, bowwow.py) и импортирую из другого файла (otherfile.py), он больше не работает ...
Stephan202

4
Вопрос: Будет ли какое-либо конкретное преимущество (-я) в получении класса экземпляра из того, types.ModuleTypeчто показано в очень похожем ответе @ Unknown?
Мартино

11
Только экземпляры классов нового стиля могут иметь свойства. Причина не в этом: модули являются экземплярами классов нового стиля, в том смысле, что они являются экземплярами builtins.module, которые сами по себе являются экземпляром type(что является определением класса нового стиля). Проблема заключается в том, что свойства должны быть в классе, а не например: если вы это сделаете f = Foo(), f.some_property = property(...)он будет не в состоянии точно так же , как если бы вы наивный поставить его в модуле. Решение состоит в том, чтобы поместить его в класс, но поскольку вы не хотите, чтобы все модули обладали этим свойством, вы создаете подкласс (см. Ответ Неизвестного).
Thanatos

3
@Joe, изменение globals()(сохранение ключей без изменений, но сброс значений None) при повторной привязке имени является проблемой sys.modulesPython 2 - Python 3.4 работает по назначению. Если вам нужен доступ к объекту класса в Py2, добавьте, например, _M._cls = _Mсразу после classоператора (или сохраните его эквивалентным образом в каком-либо другом пространстве имен) и получите доступ к нему, как self._clsв методах, требующих этого ( type(self)может быть в порядке, но не если вы также выполняете какое-либо подклассирование _M) .
Alex Martelli

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

Что делать, если исходный модуль является пакетом, и я хочу получить доступ к модулям, находящимся под исходным модулем?
kawing-chiu

2
@martineau У вас будет репортер модуля, вы можете указать имя модуля при __init__создании экземпляра, и вы получите правильное поведение при использовании isinstance.
wim

@wim: Очки приняты, хотя, честно говоря, ни один из них не кажется таким важным, ИМО.
Мартино,

35

Поскольку 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
Это только половина собственности. Нет сеттера.
wim

К сожалению, не сложно сделать так, чтобы инструменты знали о таких атрибутах (?) ( 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()в качестве функции
Бен Марес

Хорошо, я вижу, что нет ни «установщика свойств модуля», ни «модуля __getattribute__». Спасибо.
Джон Лин

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_moduleэто экземпляр types.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?
Мартино,

1
Вы сказали еще кое-что, что для меня не имеет смысла. Возьмем, к @module_propertyпримеру, декоратора. Вообще говоря, встроенный @propertyдекоратор используется, когда класс определен, а не после создания его экземпляра, поэтому я предполагаю, что то же самое будет верно для свойства модуля, и это с ответом Алекса - напомним, этот вопрос задает «Могут ли модули иметь свойства так же, как объекты?». Однако это можно добавить их после этого и я изменил мой предыдущий фрагмент кода , чтобы проиллюстрировать способ , который может быть сделано.
Мартино,

1
Бен: После просмотра кода в вашем конкретном примере, я думаю, что понимаю, к чему вы сейчас клоните. Я также думаю, что недавно наткнулся на метод реализации чего-то похожего на свойства модуля, который не требует замены модуля экземпляром класса, как в ответе Алекса, хотя на данный момент я не уверен, есть ли способ сделать это через декоратор - вернусь к вам, если я добьюсь прогресса.
Мартино,

1
Хорошо, вот ссылка на ответ на другой вопрос, содержащий основную идею.
Мартино,

1
Что ж, по крайней мере, в случае a cached_module_property, тот факт, что __getattr__()он больше не будет вызываться, если атрибут будет определен, полезен. (аналогично тому, что происходит functools.cached_property).
Мартино
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.