クラスを飾る方法は?


129

Python 2.5では、クラスを装飾するデコレータを作成する方法はありますか?具体的には、デコレーターを使用してクラスにメンバーを追加し、そのメンバーの値を取るようにコンストラクターを変更します。

次のようなものを探します( 'class 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()

私が本当に求めているのは、PythonでC#インターフェイスのようなものを実行する方法だと思います。パラダイムを変える必要があると思います。

回答:


80

あなたが概説したアプローチの代わりにサブクラスを検討したいと思うかもしれないという考えを私は2番目に述べます。ただし、特定のシナリオを知らないYMMV :-)

あなたが考えているのはメタクラスです。__new__メタクラスの関数には、クラスの提案された完全な定義が渡され、クラスが作成される前にそれを書き換えることができます。その時点で、コンストラクターを新しいものに置き換えることができます。

例:

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

コンストラクタを置き換えることはおそらく少し劇的ですが、言語はこの種の深いイントロスペクションと動的な変更をサポートします。


ありがとう、それが私が探しているものです。それらがすべて特定のメンバーを持つように、他のクラスをいくつでも変更できるクラス。クラスに共通のIDクラスを継承させない理由は、IDバージョンだけでなく、クラスの非IDバージョンも必要だからです。
Robert Gowland

メタクラスは、以前はPython2.5以前でこのようなことを行うための頼りになる方法でしたが、今日では、はるかに単純なクラスデコレーター(Stevenの回答を参照)を使用することができます。
Jonathan Hartley

203

クラスデコレータが問題の正しい解決策であるかどうかという質問は別として、

Python 2.6以降では、@構文を使用したクラスデコレータがあるため、次のように記述できます。

@addID
class Foo:
    pass

古いバージョンでは、別の方法で行うことができます。

class Foo:
    pass

Foo = addID(Foo)

ただし、これは関数デコレータの場合と同じように機能し、デコレータは新しい(または変更された元の)クラスを返す必要があることに注意してください。これは、例では行っていることではありません。addIDデコレーターは次のようになります。

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

次に、上記のように、Pythonバージョンに適切な構文を使用できます。

しかし、オーバーライドしたい場合は、継承の方が適しているということには同意します__init__


2
...質問では具体的にPython 2.5について言及していますが:-)
ジャレットハーディー

5
linesepの混乱で申し訳ありませんが、コードサンプルはコメントで厳密に優れていません...:def addID(original_class):original_class .__ orig__init__ = original_class .__ init__ def init __(self、* args、** kws):print "decorator" self.id = 9 original_class .__ orig__init __(self、* args、** kws)original_class .__ init = init return original_class @addID class Foo:def __init __(self):print "Foo" a = Foo()print a.id
ジェラルドセナクレンドグランシー2010

2
@スティーブン、@ジェラルドは正しいです。もしあなたの答えを更新できれば、彼のコードを読む方がずっと簡単になります;)

3
@Day:リマインダーをありがとう、私はこれまでコメントに気づかなかった。ただし、Geraldsコードでも最大再帰深度を超えています。私は試してみて、動作するバージョンを作成します;-)
スティーブン

1
@Dayと@Gerald:申し訳ありませんが、問題はGeraldのコードではありませんでした。コメント内のコードが壊れているため、失敗しました;-)
Steven

20

クラスを動的に定義できると誰も説明していません。したがって、サブクラスを定義(および返す)するデコレータを使用できます。

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

これは次のようにPython 2で使用できます(2.6+以降でこれを継続する必要がある理由を説明するBlckknghtからのコメント)。

class Foo:
    pass

FooId = addId(Foo)

そして、このようなPython 3では(ただしsuper()、クラスでの使用には注意してください):

@addId
class Foo:
    pass

だからあなたはあなたのケーキ持ってそれ食べることができます-継承デコレータ!


4
Python 2.6以降では、デコレータでのサブクラス化は危険superです。元のクラスの呼び出しが中断されるためです。そのFoo名前のメソッドfooが呼び出されたsuper(Foo, self).foo()場合、名前Fooは元のクラス(どの名前からもアクセスできない)ではなく、デコレータによって返されるサブクラスにバインドされるため、無限に再帰します。Python 3の引数なしsuper()はこの問題を回避します(私はそれがまったく機能することを可能にする同じコンパイラマジックを介して仮定します)。(Python 2.5の例で行ったように)別の名前でクラスを手動で装飾することで問題を回避することもできます。
Blckknght 2013

1
ええと。おかげで、私にはわかりませんでした(私はpython 3を使用しています)。コメントを追加します。
Andrew Cooke 2013

13

それは良い習慣ではなく、そのためにそれを行うメカニズムはありません。あなたが望むものを達成する正しい方法は継承です。

見てくださいクラスのドキュメントを

小さな例:

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)

このようにしEmployeeて、ボスはすべてを持っています__init__。彼自身の方法とメンバーもいます。


3
私が欲しかったのは、それが含んでいるクラスにボスにとらわれないことでした。つまり、Bossの機能を適用したいクラスが何十とあるかもしれません。これらの12のクラスにボスから継承させることはできますか?
Robert Gowland

5
@Robert Gowland:Pythonに多重継承があるのはそのためです。はい、さまざまな親クラスからさまざまな側面を継承する必要があります。
S.Lott

7
@ S.Lott:一般に、多重継承は悪い考えです。継承のレベルが高すぎても悪いことです。多重継承から離れることをお勧めします。
mpeterson 2009年

5
mpeterson:Pythonの多重継承はこのアプローチよりも悪いですか?Pythonの多重継承の何が問題になっていますか?
アラファンギオン2009

2
@Arafangion:多重継承は通常、問題の警告サインであると考えられています。これは、複雑な階層とわかりにくい関係を作成します。問題のドメインが多重継承に適している場合(階層的にモデル化できるか?)、それは多くの人にとって自然な選択です。これは、マルチ継承を許可するすべての言語に適用されます。
Morten Jensen

6

私は継承が提起された問題により適していることに同意します。

私はこの質問が本当に便利であると感じましたが、クラスの装飾に感謝します。

Python 2.7で継承がどのように影響するか(そして、元の関数のdocstringを維持する@wrapsなど)など、他の回答に基づく別の例をいくつか示します。

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

多くの場合、デコレータにパラメータを追加する必要があります。

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

出力:

@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

関数のデコレータを使用しました。簡潔にするためです。クラスを装飾するクラスは次のとおりです。

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

これらの括弧をチェックし、装飾されたクラスにメソッドが存在しない場合に機能する、より堅牢なバージョン:

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

assertデコレータは括弧なしで使用されていないことを確認します。含まれている場合は、装飾されているクラスがmsgデコレータのパラメータに渡され、が発生しAssertionErrorます。

@decorate_ifdecoratorifがにcondition評価される場合にのみ適用されTrueます。

getattrcallableテスト、および@decorate_if場合デコレータが壊れないように使用されているfoo()方法が飾られているクラスに存在しません。


4

実際には、ここにクラスデコレータのかなり良い実装があります。

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

これは実際にはかなり興味深い実装だと思います。デコレートするクラスをサブクラス化するため、isinstanceチェックなどの点ではこのクラスとまったく同じように動作します。

これは、追加の利点があります:それはのために珍しくない__init__フォームへの変更や追加を行うことがDjangoのカスタム中の文self.fieldsの変更には、のためにそれが良いですので、self.fields発生するのすべて__init__問題のクラスに対して実行されました。

非常に賢い。

ただし、クラスでは実際に装飾でコンストラクタを変更する必要がありますが、これはクラスデコレータの良いユースケースではないと思います。


0

これは、クラスのパラメーターを返すという質問に答える例です。さらに、継承のチェーンも尊重します。つまり、クラス自体のパラメーターのみが返されます。この関数get_paramsは簡単な例として追加されていますが、inspectモジュールを使用して他の機能を追加できます。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.