__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
Matt Joiner
fonte
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.

wim
fonte
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.

Ethan Furman
fonte
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__])
Håvard S
fonte
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".

S.Lott
fonte
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.

Matt Anderson
fonte
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.

entristecer
fonte
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")
Martineau
fonte
-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')
drr
fonte
1
Você está respondendo uma pergunta diferente aqui.
Marius Gedminas