__getattr__ em um módulo


127

Como implementar o equivalente a __getattr__em uma classe, em um módulo?

Exemplo

Ao chamar uma função que não existe nos atributos estaticamente definidos de um módulo, desejo criar uma instância de uma classe nesse módulo e chamar o método nele com o mesmo nome que falhou na pesquisa de atributos no módulo.

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

# note this function is intentionally on the module, and not the class above
def __getattr__(mod, name):
    return getattr(A(), name)

if __name__ == "__main__":
    # i hope here to have my __getattr__ function above invoked, since
    # salutation does not exist in the current namespace
    salutation("world")

Que dá:

matt@stanley:~/Desktop$ python getattrmod.py 
Traceback (most recent call last):
  File "getattrmod.py", line 9, in <module>
    salutation("world")
NameError: name 'salutation' is not defined

2
Provavelmente irei com a resposta do luto, pois funciona em todas as circunstâncias (embora seja um pouco confuso e poderia ser melhor). Harvard S e S Lott têm boas respostas limpas, mas não são soluções práticas.
— Matt Joiner

1
Você não está nem mesmo acessando um atributo, então está pedindo duas coisas diferentes ao mesmo tempo. Portanto, a principal questão é qual você deseja. Deseja salutationexistir no espaço de nomes global ou local (que é o que o código acima está tentando fazer) ou deseja uma pesquisa dinâmica de nomes quando você acessa um ponto em um módulo? São duas coisas diferentes.
— Lennart Regebro

Pergunta interessante, como você chegou a isso?
— 21711 Chris Wesseling

1
possível duplicata de Autoload em Python
— Piotr Dobrogost

1
__getattr__em módulos é suportado no Python 3.7
— Fumito Hamamura

Respostas:


41

Há um tempo atrás, Guido declarou que todas as pesquisas de métodos especiais em classes de novo estilo ignoram __getattr__e__getattribute__ . Os métodos Dunder já haviam trabalhado nos módulos - você poderia, por exemplo, usar um módulo como um gerenciador de contexto simplesmente definindo __enter__e __exit__antes que esses truques quebrassem .

Recentemente, alguns recursos históricos retornaram, o módulo __getattr__entre eles e, portanto, o hack existente (um módulo que se substitui por uma classe no sys.modulesmomento da importação) não é mais necessário.

No Python 3.7+, você apenas usa a maneira óbvia. Para personalizar o acesso ao atributo em um módulo, defina uma __getattr__função no nível do módulo que aceite um argumento (nome do atributo) e retorne o valor calculado ou aumente AttributeError:

# my_module.py

def __getattr__(name: str) -> Any:
    ...

Isso também permitirá ganchos nas importações "de", ou seja, você pode retornar objetos gerados dinamicamente para instruções como from my_module import whatever.

Em uma nota relacionada, juntamente com o módulo getattr, você também pode definir uma __dir__função no nível do módulo para responder dir(my_module). Veja PEP 562 para detalhes.


1
Se eu criar dinamicamente um módulo via m = ModuleType("mod")e definir m.__getattr__ = lambda attr: return "foo"; no entanto, quando corro from mod import foo, recebo TypeError: 'module' object is not iterable.
— weberc2 14/01

@ weberc2: Faça isso m.__getattr__ = lambda attr: "foo", mais você precisa definir uma entrada para o módulo sys.modules['mod'] = m. Depois, não há erro com from mod import foo.
— martineau 14/02

wim: Você também pode obter valores computados dinamicamente - como ter uma propriedade no nível do módulo - permitindo que se escreva my_module.whateverpara invocá-la (após um import my_module).
— martineau 14/02

115

Há dois problemas básicos que você está enfrentando aqui:

  1. __xxx__ métodos são pesquisados ​​apenas na classe
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) significa que qualquer solução também precisaria acompanhar qual módulo estava sendo examinado; caso contrário, cada módulo teria o comportamento de substituição de instância; e (2) significa que (1) nem sequer é possível ... pelo menos não diretamente.

Felizmente, sys.modules não é exigente quanto ao que existe lá, então um wrapper funcionará, mas apenas para acesso ao módulo (ou seja import somemodule; somemodule.salutation('world'), para acesso ao mesmo módulo, você praticamente precisará retirar os métodos da classe de substituição e adicioná-los a globals()cada método personalizado na classe (eu gosto de usar .export()) ou com uma função genérica (como as que já estão listadas como respostas). Um aspecto a ser lembrado: se o wrapper estiver criando uma nova instância a cada vez e a solução global não estiver, você acaba com um comportamento sutilmente diferente, e não consegue usar os dois ao mesmo tempo - é um ou outro.


Atualizar

Partida Guido van Rossum :

Na verdade, existe um hack ocasionalmente usado e recomendado: um módulo pode definir uma classe com a funcionalidade desejada e, no final, substituir-se em sys.modules por uma instância dessa classe (ou pela classe, se você insistir , mas isso geralmente é menos útil). Por exemplo:

# module foo.py

import sys

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>

sys.modules[__name__] = Foo()

Isso funciona porque o mecanismo de importação está ativando ativamente esse hack e, como sua etapa final, retira o módulo real dos sys.modules, após carregá-lo. (Não é por acaso. O hack foi proposto há muito tempo e decidimos que gostávamos o suficiente para apoiá-lo nas máquinas de importação.)

Portanto, a maneira estabelecida de realizar o que você deseja é criar uma única classe no seu módulo e, como o último ato do módulo, substituir sys.modules[__name__]por uma instância da sua classe - e agora você pode jogar com __getattr__/ __setattr__/ __getattribute__conforme necessário.


Nota 1 : Se você usar essa funcionalidade, qualquer outra coisa no módulo, como globais, outras funções etc., será perdida quando a sys.modulesatribuição for feita - portanto, verifique se tudo o que é necessário está dentro da classe de substituição.

Nota 2 : Para dar suporte, from module import *você deve ter __all__definido na classe; por exemplo:

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>
    __all__ = list(set(vars().keys()) - {'__module__', '__qualname__'})

Dependendo da sua versão do Python, pode haver outros nomes para omitir __all__. O set()pode ser omitido se a compatibilidade Python 2 não é necessário.


3
Isso funciona porque o mecanismo de importação está ativando ativamente esse hack e, como sua etapa final, retira o módulo real dos sys.modules, após carregá- lo. Ele é mencionado em algum lugar nos documentos?
— Piotr Dobrogost 30/09/12

4
Agora me sinto mais à vontade usando este corte, considerando-o "semi-sancionada" :)
— Mark Nunberg

3
Esta é fazer as coisas screwy, como fazer import sysdar Nonepara sys. Eu estou supondo que este hack não é sancionada em Python 2.
— asmeurer

3
@asmeurer: Para entender o motivo disso (e uma solução), consulte a pergunta Por que o valor de __name__ está mudando após a atribuição a sys.modules [__ name__]? .
— 21414 martineau

1
@ qarma: Eu me lembro de algumas melhorias mencionadas que permitiriam que os módulos python participassem mais diretamente do modelo de herança de classe, mas mesmo assim esse método ainda funciona e é suportado.
— precisa

48

Isso é um hack, mas você pode agrupar o módulo com uma classe:

class Wrapper(object):
  def __init__(self, wrapped):
    self.wrapped = wrapped
  def __getattr__(self, name):
    # Perform custom logic here
    try:
      return getattr(self.wrapped, name)
    except AttributeError:
      return 'default' # Some sensible default

sys.modules[__name__] = Wrapper(sys.modules[__name__])

10
agradável e sujo: D
— Matt Joiner

Isso pode funcionar, mas provavelmente não é uma solução para o problema real do autor.
— DasIch 15/03/10

12
"Pode funcionar" e "provavelmente não" não é muito útil. É um truque / truque, mas funciona e resolve o problema colocado pela pergunta.
— Håvard S

6
Embora isso funcione em outros módulos que importam seu módulo e acessam atributos inexistentes, não funcionará no exemplo de código real aqui. Acessar globals () não passa por sys.modules.
— Marius Gedminas

5
Infelizmente, isso não funciona para o módulo atual ou provavelmente para itens acessados ​​após um import *.
— Matt Joiner

19

Geralmente não fazemos dessa maneira.

O que fazemos é isso.

class A(object):
....

# The implicit global instance
a= A()

def salutation( *arg, **kw ):
    a.salutation( *arg, **kw )

Por quê? Para que a instância global implícita seja visível.

Por exemplo, observe o randommódulo, que cria uma instância global implícita para simplificar levemente os casos de uso em que você deseja um gerador de números aleatórios "simples".


Se você for realmente ambicioso, poderá criar a classe, percorrer todos os seus métodos e criar uma função no nível do módulo para cada método.
— Paul Fisher

@ Paul Fisher: Pelo problema, a classe já existe. Expor todos os métodos da classe pode não ser uma boa ideia. Geralmente esses métodos expostos são métodos de "conveniência". Nem todos são apropriados para a instância global implícita.
— S.Lott

13

Semelhante ao que o @ Håvard S propôs, em um caso em que eu precisava implementar alguma mágica em um módulo (como __getattr__), eu definiria uma nova classe que herda types.ModuleTypee a coloca sys.modules(provavelmente substituindo o módulo em que meu costume ModuleTypefoi definido).

Veja o __init__.pyarquivo principal do Werkzeug para uma implementação bastante robusta disso.


8

Isso é tolice, mas ...

import types

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

    def farewell(self, greeting, accusative):
         print greeting, accusative

def AddGlobalAttribute(classname, methodname):
    print "Adding " + classname + "." + methodname + "()"
    def genericFunction(*args):
        return globals()[classname]().__getattribute__(methodname)(*args)
    globals()[methodname] = genericFunction

# set up the global namespace

x = 0   # X and Y are here to add them implicitly to globals, so
y = 0   # globals does not change as we iterate over it.

toAdd = []

def isCallableMethod(classname, methodname):
    someclass = globals()[classname]()
    something = someclass.__getattribute__(methodname)
    return callable(something)


for x in globals():
    print "Looking at", x
    if isinstance(globals()[x], (types.ClassType, type)):
        print "Found Class:", x
        for y in dir(globals()[x]):
            if y.find("__") == -1: # hack to ignore default methods
                if isCallableMethod(x,y):
                    if y not in globals(): # don't override existing global names
                        toAdd.append((x,y))


for x in toAdd:
    AddGlobalAttribute(*x)


if __name__ == "__main__":
    salutation("world")
    farewell("goodbye", "world")

Isso funciona iterando sobre todos os objetos no espaço para nome global. Se o item for uma classe, ele itera sobre os atributos da classe. Se o atributo puder ser chamado, ele será adicionado ao namespace global como uma função.

Ele ignora todos os atributos que contêm "__".

Eu não usaria isso no código de produção, mas deve começar.


2
Prefiro a resposta de Håvard S à minha, pois parece muito mais limpa, mas isso responde diretamente à pergunta, conforme solicitado.
— luto

Isso é muito mais próximo do que eu acabei aceitando. É um pouco confuso, mas funciona corretamente com globals () dentro do mesmo módulo.
— Matt Joiner

1
Parece-me que esta resposta não é exatamente o que foi solicitado, e foi "Ao chamar uma função que não existe nos atributos estaticamente definidos de um módulo" porque está fazendo seu trabalho incondicionalmente e adicionando todos os métodos de classe possíveis. Isso pode ser corrigido usando um invólucro de módulo que faz apenas AddGlobalAttribute()quando há um nível de módulo AttributeError- tipo o inverso da lógica do @ Håvard S. Se eu tiver uma chance, testarei isso e adicionarei minha própria resposta híbrida, mesmo que o OP já tenha aceitado essa resposta.
— martineau

Atualize para o meu comentário anterior. Agora entendo que é muito difícil (impossível?) Interceptar NameErrorexceções para o espaço de nome global (módulo) - o que explica por que essa resposta adiciona chamadas para todas as possibilidades encontradas no espaço para nome global atual para cobrir todos os casos possíveis com antecedência.
— 27410 martineau

5

Aqui está minha humilde contribuição - um leve embelezamento da resposta altamente cotada do @ Håvard S, mas um pouco mais explícita (portanto, pode ser aceitável para o @ S.Lott, embora provavelmente não seja bom o suficiente para o OP):

import sys

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

class Wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, name):
        try:
            return getattr(self.wrapped, name)
        except AttributeError:
            return getattr(A(), name)

_globals = sys.modules[__name__] = Wrapper(sys.modules[__name__])

if __name__ == "__main__":
    _globals.salutation("world")

-3

Crie seu arquivo de módulo que possui suas classes. Importe o módulo. Execute getattrno módulo que você acabou de importar. Você pode fazer uma importação dinâmica usando__import__ e puxando o módulo de sys.modules.

Aqui está o seu módulo some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

E em outro módulo:

import some_module

Foo = getattr(some_module, 'Foo')

Fazendo isso dinamicamente:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')

1
Você está respondendo uma pergunta diferente aqui.
— Marius Gedminas
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.