Os módulos podem ter propriedades da mesma forma que os objetos?

98

Com propriedades python, posso torná-lo tal que

obj.y 

chama uma função em vez de apenas retornar um valor.

Existe uma maneira de fazer isso com módulos? Eu tenho um caso onde eu quero

module.y 

para chamar uma função, em vez de apenas retornar o valor armazenado lá.

Josh Gibson
fonte
2
Veja __getattr__em um módulo para uma solução mais moderna.
wim

Respostas:

56

Apenas instâncias de classes de novo estilo podem ter propriedades. Você pode fazer o Python acreditar que tal instância é um módulo armazenando-o em sys.modules[thename] = theinstance. Portanto, por exemplo, o arquivo do módulo m.py pode ser:

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()
Alex Martelli
fonte
2
Alguém mais tentou isso? Quando coloco este código em um arquivo x.py e o importo de outro, chamando os resultados de xy em AttributeError: o objeto 'NoneType' não tem atributo 'c', pois _M de alguma forma tem o valor None ...
Stephan202
3
Na verdade, o código funciona no interpretador. Mas quando o coloco em um arquivo (digamos, bowwow.py) e o importo de outro arquivo (otherfile.py), ele não funciona mais ...
Stephan202
4
P: Haveria alguma vantagem particular em derivar a classe da instância, types.ModuleTypeconforme mostrado na resposta muito semelhante de @Inconhecido?
martineau de
11
Apenas instâncias de classes de novo estilo podem ter propriedades. Esta não é a razão: módulos são instâncias de classes de novo estilo, no sentido de que são instâncias de builtins.module, que por si só é uma instância de type(que é a definição de classe de novo estilo). O problema é que as propriedades devem estar na classe, não a instância: se você fizer isso f = Foo(), f.some_property = property(...), vai falhar da mesma maneira como se você colocá-lo ingenuamente em um módulo. A solução é colocá-lo na classe, mas como você não quer que todos os módulos tenham a propriedade, você cria uma subclasse (veja a resposta de Desconhecido).
Thanatos,
3
@Joe, a alteração de globals()(mantendo as chaves intactas, mas redefinindo os valores para None) após a religação do nome sys.modulesé um problema do Python 2 - o Python 3.4 funciona conforme o planejado. Se você precisar acessar o objeto de classe em Py2, adicione, por exemplo, _M._cls = _Mlogo após a classinstrução (ou armazene-o de forma equivalente em algum outro namespace) e acesse-o como self._clsnos métodos que o exigem ( type(self)pode ser OK, mas não se você também fizer qualquer subclasse de _M) .
Alex Martelli,
54

Eu faria isso para herdar adequadamente todos os atributos de um módulo e ser identificado corretamente por isinstance ()

import types

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


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

E então você pode inserir isso em sys.modules:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class
Desconhecido
fonte
Isso parece funcionar apenas para os casos mais simples. Os possíveis problemas são: (1) alguns ajudantes de importação também podem esperar outros atributos, como os __file__que devem ser definidos manualmente, (2) as importações feitas no módulo que contém a classe não serão "visíveis" durante o tempo de execução, etc ...
tutuDajuju
1
Não é necessário derivar uma subclasse de types.ModuleType, qualquer (estilo novo) classe vai fazer. Exatamente quais atributos de módulo especial você espera herdar?
Martineau de
E se o módulo original for um pacote e eu quiser acessar os módulos abaixo do módulo original?
kawing-chiu
2
@martineau Você terá um repr módulo, você pode especificar o nome do módulo quando __init__uma instância e você obterá o comportamento correto ao usar isinstance.
wim
@wim: Pontos aceitos, embora francamente nenhum pareça ser tão importante IMO.
martineau
34

Como o PEP 562 foi implementado em Python> = 3.7, agora podemos fazer isso

arquivo: module.py

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

other = 4

uso:

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

Observe que se você omitir o raise AttributeErrorna __getattr__função, isso significa que a função termina com return None, então o module.nosuchobterá um valor de None.

John Lin
fonte
2
Com base nisso, adicionei outra resposta: stackoverflow.com/a/58526852/2124834
3
Esta é apenas metade de uma propriedade. Sem setter.
wim
Infelizmente, não parece difícil tornar o ferramental ciente de tais atributos (?) ( Getattr só é invocado se nenhum membro regular for encontrado)
olejorgenb
9

Com base na resposta de John Lin :

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

Uso (observe o sublinhado inicial), em the_module.py:

@module_property
def _thing():
    return 'hello'

Então:

import the_module

print(the_module.thing)  # prints 'hello'

O sublinhado inicial é necessário para diferenciar a função de propriedade da função original. Não consegui pensar em uma maneira de reatribuir o identificador, pois durante o tempo de execução do decorador, ele ainda não foi atribuído.

Observe que os IDEs não saberão que a propriedade existe e exibirão ondulações vermelhas.


fonte
Ótimo! Comparado com a propriedade de classe @property def x(self): return self._x, acho que def thing()sem sublinhado é mais convencional. E você também pode criar um decorador "configurador de propriedades do módulo" em sua resposta?
John Lin
2
@JohnLin, tentei implementar sua def thing()sugestão. O problema é que __getattr__só é chamado para atributos ausentes . Mas depois de @module_property def thing(): …executado, the_module.thingé definido, portanto getattr nunca será chamado. Precisamos registrar de alguma forma thingno decorador e, em seguida, excluí-lo do namespace do módulo. Tentei retornar Nonedo decorador, mas thingé definido como None. Pode-se fazer, @module_property def thing(): … del thingmas acho pior do que usar thing()como uma função
Ben Mares
OK, vejo que não há "configurador de propriedade do módulo", nem "módulo __getattribute__". Obrigado.
John Lin
5

Um caso de uso típico é: enriquecer um (enorme) módulo existente com alguns (poucos) atributos dinâmicos - sem transformar todo o material do módulo em um layout de classe. Infelizmente, um patch de classe de módulo mais simples como sys.modules[__name__].__class__ = MyPropertyModulefalha com TypeError: __class__ assignment: only for heap types. Portanto, a criação do módulo precisa ser reconectada.

Essa abordagem faz isso sem ganchos de importação Python, apenas por ter algum prólogo no topo do código do módulo:

# 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)

Uso:

>>> 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'"

Nota: algo como from propertymodule import dynvalirá produzir uma cópia congelada, é claro - correspondendo adynval = someobject.dynval

kxr
fonte
1

Uma resposta curta: use proxy_tools

O proxy_toolspacote tenta fornecer @module_propertyfuncionalidade.

É instalado com

pip install proxy_tools

Usando uma ligeira modificação do exemplo de @Marin, the_module.pycolocamos

from proxy_tools import module_property

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

Agora a partir de outro script, posso fazer

import the_module

print(the_module.thing)
# . hello

Comportamento inesperado

Esta solução possui algumas ressalvas. Ou seja, nãothe_module.thing é uma string ! É um proxy_tools.Proxyobjeto cujos métodos especiais foram substituídos para que imite uma string. Aqui estão alguns testes básicos que ilustram o ponto:

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']

Internamente, a função original é armazenada em the_module.thing._Proxy__local:

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

Pensamentos adicionais

Honestamente, estou perplexo sobre por que os módulos não têm essa funcionalidade incorporada. Acho que o ponto crucial da questão é que the_moduleé uma instância da types.ModuleTypeclasse. Definir uma "propriedade de módulo" equivale a definir uma propriedade em uma instância dessa classe, em vez da types.ModuleTypeprópria classe. Para mais detalhes, veja esta resposta .

Na verdade, podemos implementar propriedades da types.ModuleTypeseguinte maneira, embora os resultados não sejam bons. Não podemos modificar diretamente os tipos integrados, mas podemos amaldiçoá- los:

# 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__}'))

Isso nos dá uma propriedade que existe em todos os módulos. É um pouco difícil de manejar, pois quebramos o comportamento da configuração em todos os módulos:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute
Ben Mares
fonte
1
Como isso é melhor do que apenas tornar o módulo uma instância de uma classe real, conforme mostrado na resposta de @Alex Martelli?
martineau de
1
Você disse outra coisa que não faz sentido para mim. Veja este negócio de ter um @module_propertydecorador. De modo geral, o @propertydecorador embutido é usado quando uma classe é definida, não depois que uma instância dela foi criada, então eu presumiria que o mesmo seria verdadeiro para uma propriedade de módulo e é com a resposta de Alex - lembre-se que esta pergunta pergunta “Os módulos podem ter propriedades da mesma forma que os objetos podem?”. No entanto, é possível adicioná-los depois e modifiquei meu snippet anterior para ilustrar uma maneira que pode ser feita.
martineau
1
Ben: Depois de examinar o código em seu exemplo concreto, acho que entendi o que você quer chegar agora. Também acho que recentemente encontrei uma técnica para implementar algo semelhante às propriedades do módulo que não exige que o módulo seja substituído por uma instância de classe como na resposta de Alex, embora eu não tenha certeza neste ponto se há uma maneira de fazer por meio de um decorador - entrarei em contato se eu fizer algum progresso.
martineau de
1
OK, aqui está um link para uma resposta a outra pergunta que contém a ideia central.
martineau de
1
Bem, pelo menos no caso de a cached_module_property, o fato de que __getattr__()não será mais chamado se o atributo for definido é útil. (semelhante ao que functools.cached_propertyrealiza).
martineau