Como decorar uma aula?

129

No Python 2.5, existe uma maneira de criar um decorador que decora uma classe? Especificamente, quero usar um decorador para adicionar um membro a uma classe e alterar o construtor para obter um valor para esse membro.

Procurando algo como o seguinte (que possui um erro de sintaxe na 'classe Foo:':

def getId(self): return self.__id

class addID(original_class):
    def __init__(self, id, *args, **kws):
        self.__id = id
        self.getId = getId
        original_class.__init__(self, *args, **kws)

@addID
class Foo:
    def __init__(self, value1):
        self.value1 = value1

if __name__ == '__main__':
    foo1 = Foo(5,1)
    print foo1.value1, foo1.getId()
    foo2 = Foo(15,2)
    print foo2.value1, foo2.getId()

Eu acho que o que realmente estou procurando é uma maneira de fazer algo como uma interface C # em Python. Eu preciso mudar meu paradigma, suponho.

Robert Gowland
fonte

Respostas:

80

Gostaria de destacar a noção de que você pode considerar uma subclasse em vez da abordagem que você descreveu. No entanto, sem conhecer seu cenário específico, YMMV :-)

O que você está pensando é uma metaclasse. A __new__função em uma metaclasse recebe a definição proposta completa da classe, que pode ser reescrita antes da criação da classe. Você pode, naquele momento, sub-criar o construtor para um novo.

Exemplo:

def substitute_init(self, id, *args, **kwargs):
    pass

class FooMeta(type):

    def __new__(cls, name, bases, attrs):
        attrs['__init__'] = substitute_init
        return super(FooMeta, cls).__new__(cls, name, bases, attrs)

class Foo(object):

    __metaclass__ = FooMeta

    def __init__(self, value1):
        pass

A substituição do construtor é talvez um pouco dramática, mas a linguagem fornece suporte para esse tipo de introspecção profunda e modificação dinâmica.

Jarret Hardie
fonte
Obrigado, é o que estou procurando. Uma classe que pode modificar qualquer número de outras classes, de forma que todas elas tenham um membro específico. Minhas razões para não ter as classes herdadas de uma classe de ID comum é que eu quero ter versões sem ID das classes, bem como versões de ID.
Robert Gowland
Metaclasses costumavam ser o caminho a seguir para fazer coisas como essa no Python2.5 ou mais antigo, mas hoje em dia você pode frequentemente usar decoradores de classe (veja a resposta de Steven), que são muito mais simples.
276166 Jonathan
203

Além da questão de saber se os decoradores da turma são a solução certa para o seu problema:

No Python 2.6 e superior, existem decoradores de classe com o @ -syntax, para que você possa escrever:

@addID
class Foo:
    pass

Nas versões mais antigas, você pode fazer de outra maneira:

class Foo:
    pass

Foo = addID(Foo)

Observe, no entanto, que isso funciona da mesma forma que para os decoradores de funções e que o decorador deve retornar a nova classe (ou original modificado), que não é o que você está fazendo no exemplo. O decorador addID ficaria assim:

def addID(original_class):
    orig_init = original_class.__init__
    # Make copy of original __init__, so we can call it without recursion

    def __init__(self, id, *args, **kws):
        self.__id = id
        self.getId = getId
        orig_init(self, *args, **kws) # Call the original __init__

    original_class.__init__ = __init__ # Set the class' __init__ to the new one
    return original_class

Você pode usar a sintaxe apropriada para sua versão do Python, conforme descrito acima.

Mas concordo com os outros que a herança é mais adequada se você deseja substituir __init__.

Steven
fonte
2
... mesmo que a pergunta menciona especificamente Python 2.5 :-)
Jarret Hardie
5
Desculpe pela bagunça de linha, mas os exemplos de código não são muito bons nos comentários ...: def addID (classe_ original): classe_ original .__ orig__init__ = classe_ original .__ init__ def init __ (self, * args, ** kws): print "decorator" self.id = 9 classe_ original .__ orig__init __ (self, * args, ** kws) classe_ original .__ init = init retorna classe_add original @ classe ADID Foo: def __init __ (self): print "Foo" a = Foo () print a.id
Gerald Senarclens de Grancy
2
@ Steven, @Gerald é certa, se você poderia atualizar sua resposta, seria muito mais fácil de ler o seu código;)
Dia
3
@ Day: obrigado pelo lembrete, eu não tinha notado os comentários antes. No entanto: também obtenho a profundidade máxima de recursão excedida com o código Geralds. Vou tentar fazer uma versão de trabalho ;-)
Steven
1
@Day e @Gerald: sorry, assuntos foi não com o código de Gerald, eu errei por causa do código mutilado no comentário ;-)
Steven
20

Ninguém explicou que você pode definir dinamicamente classes. Assim, você pode ter um decorador que define (e retorna) uma subclasse:

def addId(cls):

    class AddId(cls):

        def __init__(self, id, *args, **kargs):
            super(AddId, self).__init__(*args, **kargs)
            self.__id = id

        def getId(self):
            return self.__id

    return AddId

Que pode ser usado no Python 2 (o comentário de Blckknght, que explica por que você deve continuar fazendo isso no 2.6+) assim:

class Foo:
    pass

FooId = addId(Foo)

E no Python 3 como este (mas tenha cuidado para usar super()em suas classes):

@addId
class Foo:
    pass

Então você pode ter seu bolo e comê-lo - herança e decoradores!

andrew cooke
fonte
4
Subclassificar em um decorador é perigoso no Python 2.6 ou superior, porque interrompe as superchamadas na classe original. Se Footivesse um método nomeado fooque o chamou, super(Foo, self).foo()ele recorreria infinitamente, porque o nome Fooestá vinculado à subclasse retornada pelo decorador, não à classe original (que não é acessível por nenhum nome). O Python 3 sem argumentos super()evita esse problema (suponho que através da mesma mágica de compilador que permite que ele funcione). Você também pode solucionar o problema decorando manualmente a classe com um nome diferente (como no exemplo do Python 2.5).
precisa saber é o seguinte
1
Hã. obrigado, eu não tinha idéia (eu uso python 3). adicionará um comentário.
Andrew cooke
13

Essa não é uma boa prática e não há mecanismo para fazer isso por causa disso. O caminho certo para realizar o que você quer é herança.

Dê uma olhada na documentação da classe .

Um pequeno exemplo:

class Employee(object):

    def __init__(self, age, sex, siblings=0):
        self.age = age
        self.sex = sex    
        self.siblings = siblings

    def born_on(self):    
        today = datetime.date.today()

        return today - datetime.timedelta(days=self.age*365)


class Boss(Employee):    
    def __init__(self, age, sex, siblings=0, bonus=0):
        self.bonus = bonus
        Employee.__init__(self, age, sex, siblings)

Dessa forma, Boss tem tudo o que Employeetem, com também seu próprio __init__método e membros.

mpeterson
fonte
3
Acho que o que eu queria era ter o chefe agnóstico da classe que ela contém. Ou seja, pode haver dezenas de classes diferentes nas quais eu quero aplicar os recursos do Boss. Estou ficando com essas dúzias de classes herdadas de Boss?
Robert Gowland
5
@Robert Gowland: É por isso que o Python tem herança múltipla. Sim, você deve herdar vários aspectos de várias classes pai.
245/09 S.Lott
7
@ S.Lott: Em geral, herança múltipla é uma má idéia, mesmo níveis demais de herança também são ruins. Vou recomendar que você fique longe de herança múltipla.
31909 mpeterson #
5
mpeterson: A herança múltipla em python é pior que essa abordagem? O que há de errado com a herança múltipla do python?
Arafangion 04/08/09
2
@Arafangion: A herança múltipla é geralmente considerada um sinal de alerta de problemas. Isso cria hierarquias complexas e relacionamentos difíceis de seguir. Se o domínio do problema se presta bem à herança múltipla (pode ser modelado hierarquicamente?), É uma escolha natural para muitos. Isso se aplica a todos os idiomas que permitem herança múltipla.
Morten Jensen
6

Concordo que a herança se encaixa melhor no problema apresentado.

Achei essa pergunta realmente útil nas aulas de decoração, obrigado a todos.

Aqui estão outros exemplos, com base em outras respostas, incluindo como a herança afeta as coisas no Python 2.7 (e @wraps , que mantém a documentação original da função, etc.):

def dec(klass):
    old_foo = klass.foo
    @wraps(klass.foo)
    def decorated_foo(self, *args ,**kwargs):
        print('@decorator pre %s' % msg)
        old_foo(self, *args, **kwargs)
        print('@decorator post %s' % msg)
    klass.foo = decorated_foo
    return klass

@dec  # No parentheses
class Foo...

Muitas vezes você deseja adicionar parâmetros ao seu decorador:

from functools import wraps

def dec(msg='default'):
    def decorator(klass):
        old_foo = klass.foo
        @wraps(klass.foo)
        def decorated_foo(self, *args ,**kwargs):
            print('@decorator pre %s' % msg)
            old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)
        klass.foo = decorated_foo
        return klass
    return decorator

@dec('foo decorator')  # You must add parentheses now, even if they're empty
class Foo(object):
    def foo(self, *args, **kwargs):
        print('foo.foo()')

@dec('subfoo decorator')
class SubFoo(Foo):
    def foo(self, *args, **kwargs):
        print('subfoo.foo() pre')
        super(SubFoo, self).foo(*args, **kwargs)
        print('subfoo.foo() post')

@dec('subsubfoo decorator')
class SubSubFoo(SubFoo):
    def foo(self, *args, **kwargs):
        print('subsubfoo.foo() pre')
        super(SubSubFoo, self).foo(*args, **kwargs)
        print('subsubfoo.foo() post')

SubSubFoo().foo()

Saídas:

@decorator pre subsubfoo decorator
subsubfoo.foo() pre
@decorator pre subfoo decorator
subfoo.foo() pre
@decorator pre foo decorator
foo.foo()
@decorator post foo decorator
subfoo.foo() post
@decorator post subfoo decorator
subsubfoo.foo() post
@decorator post subsubfoo decorator

Eu usei um decorador de funções, pois os acho mais concisos. Aqui está uma aula para decorar uma aula:

class Dec(object):

    def __init__(self, msg):
        self.msg = msg

    def __call__(self, klass):
        old_foo = klass.foo
        msg = self.msg
        def decorated_foo(self, *args, **kwargs):
            print('@decorator pre %s' % msg)
            old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)
        klass.foo = decorated_foo
        return klass

Uma versão mais robusta que verifica esses parênteses e funciona se os métodos não existirem na classe decorada:

from inspect import isclass

def decorate_if(condition, decorator):
    return decorator if condition else lambda x: x

def dec(msg):
    # Only use if your decorator's first parameter is never a class
    assert not isclass(msg)

    def decorator(klass):
        old_foo = getattr(klass, 'foo', None)

        @decorate_if(old_foo, wraps(klass.foo))
        def decorated_foo(self, *args ,**kwargs):
            print('@decorator pre %s' % msg)
            if callable(old_foo):
                old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)

        klass.foo = decorated_foo
        return klass

    return decorator

As assertverificações de que o decorador não foi usado sem parênteses. Se tiver, a classe que está sendo decorada é passada para o msgparâmetro do decorador, que gera um AssertionError.

@decorate_ifaplica somente o decoratorif é conditionavaliado como True.

The getattr, callabletest e @decorate_ifsão usados ​​para que o decorador não quebre se o foo()método não existir na classe que está sendo decorada.

Chris
fonte
4

Na verdade, há uma implementação muito boa de um decorador de classe aqui:

https://github.com/agiliq/Django-parsley/blob/master/parsley/decorators.py

Na verdade, acho que essa é uma implementação bastante interessante. Como subclasses da classe que decora, ele se comportará exatamente como essa classe em coisas como isinstancecheques.

Ele tem um benefício adicional: não é incomum que a __init__declaração em um formulário django personalizado faça modificações ou acréscimos, self.fieldspor isso é melhor que as alterações self.fieldsocorram após todos os__init__ tiver sido executado para a classe em questão.

Muito esperto.

No entanto, em sua classe, você realmente deseja que a decoração altere o construtor, o que não acho que seja um bom caso de uso para um decorador de classe.

Jordan Reiter
fonte
0

Aqui está um exemplo que responde à pergunta de retornar os parâmetros de uma classe. Além disso, ainda respeita a cadeia de herança, ou seja, apenas os parâmetros da própria classe são retornados. A função get_paramsé adicionada como um exemplo simples, mas outras funcionalidades podem ser adicionadas graças ao módulo de inspeção.

import inspect 

class Parent:
    @classmethod
    def get_params(my_class):
        return list(inspect.signature(my_class).parameters.keys())

class OtherParent:
    def __init__(self, a, b, c):
        pass

class Child(Parent, OtherParent):
    def __init__(self, x, y, z):
        pass

print(Child.get_params())
>>['x', 'y', 'z']
Patricio Astudillo
fonte