インスタンスメソッドのデコレータはクラスにアクセスできますか?


108

こんにちは、次のようなものがあります。基本的に、インスタンスメソッドの定義で使用されるデコレータからインスタンスメソッドのクラスにアクセスする必要があります。

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

コードは現状のまま:

AttributeError: 'function' object has no attribute 'im_class'

私は同様の質問/回答を見つけました-Pythonデコレーターは、関数がクラスに属していることを関数に忘れさせ、Pythonデコレーターのクラス取得します -しかし、これらは実行時に最初のパラメーターをスナッチしてインスタンスを取得する回避策に依存しています。私の場合、クラスから収集した情報に基づいてメソッドを呼び出すので、呼び出しが来るのを待つことができません。

回答:


68

Python 2.6以降を使用している場合は、おそらく次のようなクラスデコレータを使用できます(警告:テストされていないコード)。

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

メソッドデコレータは、「use_class」属性を追加することで、メソッドを重要なものとしてマークします。関数とメソッドもオブジェクトであるため、追加のメタデータをそれらにアタッチできます。

クラスが作成された後、クラスデコレータはすべてのメソッドを実行し、マークされたメソッドで必要なことをすべて実行します。

すべてのメソッドに影響を与える場合は、メソッドデコレータを省略して、クラスデコレータを使用します。


2
ありがとうこれは行くべき道だと思います。このデコレータを使用したいクラスのコードを1行追加するだけです。たぶん、カスタムメタクラスを使用して、これと同じチェックを新しい ...
カールG

3
staticmethodまたはclassmethodでこれを使用しようとする人は誰でもこのPEPを読みたいと思います:python.org/dev/peps/pep-0232 クラス/静的メソッドに属性を設定できないので、それは不可能だと思いますカスタム関数の属性が関数に適用されている場合は、それらを設定します。
カールG

DBMベースのORMについて、私が探していたものだけです。
Coyote21

を使用inspect.getmro(cls)して、クラスデコレータですべての基本クラスを処理し、継承をサポートする必要があります。
シュラマー2013年

1
ああ、実際にはそれはinspect救助に似ていますstackoverflow.com/a/1911287/202168
Anentropic

16

Python 3.6以降object.__set_name__では、これを使用して非常に簡単な方法でこれを実現できます。文書には、__set_name__「所有するクラスの所有者が作成されたときに呼び出される」と記載されています。次に例を示します。

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with owner, i.e.
        print(f"decorating {self.fn} and using {owner}")
        self.fn.class_name = owner.__name__

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

クラスの作成時に呼び出されることに注意してください。

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42

クラスの作成方法、特にがいつ__set_name__呼び出されるかについて詳しく知りたい場合は、「クラスオブジェクトの作成」ドキュメントを参照してください。


1
パラメータでデコレータを使用する場合、どのように見えますか?例えば@class_decorator('test', foo='bar')
luckydonald

2
@luckydonald 引数を取る通常のデコレータと同じようにアプローチできます。ただ持っているdef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
マットエディング

わぁ、ありがとうございました。__set_name__私は長い間Python 3.6+を使用してきましたが、知りませんでした。
kawing-chiu

この方法には1つの欠点があります。静的チェッカーはこれをまったく理解しません。Mypy helloはそれをメソッドではなく、タイプのオブジェクトであると考えますclass_decorator
kawing-chiu

@ kawing-chiu他に何も機能しない場合は、を使用しif TYPE_CHECKINGclass_decorator、正しいタイプを返す通常のデコレーターとして定義できます。
タイリオン

15

他の人が指摘したように、クラスはデコレータが呼び出された時点では作成されていません。ただし、関数オブジェクトにデコレータパラメータで注釈を付け、メタクラスの__new__メソッドで関数を再装飾することは可能です。__dict__少なくとも私にとってfunc.foo = 1は、AttributeErrorが発生したため、関数の属性に直接アクセスする必要があります。


6
setattrアクセスの代わりに使用する必要があります__dict__
schlamar 2013年

7

マークが示唆するように:

  1. クラスが構築される前にデコレータが呼び出されるため、デコレータには不明です。
  2. タグ付けできますこれらのメソッドにて、必要な後処理を後で行うます。
  3. 後処理には2つのオプションがあります。1つはクラス定義の最後で、もう1つはアプリケーションが実行される前の場所です。私は基本クラスを使用する1番目のオプションを好みますが、2番目のアプローチを使用することもできます。

このコードは、自動後処理を使用してこれがどのように機能するかを示しています。

def expose(**kw):
    "Note that using **kw you can tag the function with any parameters"
    def wrap(func):
        name = func.func_name
        assert not name.startswith('_'), "Only public methods can be exposed"

        meta = func.__meta__ = kw
        meta['exposed'] = True
        return func

    return wrap

class Exposable(object):
    "Base class to expose instance methods"
    _exposable_ = None  # Not necessary, just for pylint

    class __metaclass__(type):
        def __new__(cls, name, bases, state):
            methods = state['_exposed_'] = dict()

            # inherit bases exposed methods
            for base in bases:
                methods.update(getattr(base, '_exposed_', {}))

            for name, member in state.items():
                meta = getattr(member, '__meta__', None)
                if meta is not None:
                    print "Found", name, meta
                    methods[name] = member
            return type.__new__(cls, name, bases, state)

class Foo(Exposable):
    @expose(any='parameter will go', inside='__meta__ func attribute')
    def foo(self):
        pass

class Bar(Exposable):
    @expose(hide=True, help='the great bar function')
    def bar(self):
        pass

class Buzz(Bar):
    @expose(hello=False, msg='overriding bar function')
    def bar(self):
        pass

class Fizz(Foo):
    @expose(msg='adding a bar function')
    def bar(self):
        pass

print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)

print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)

出力は次のようになります。

Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}

この例では、次のことに注意してください。

  1. 任意の関数を任意のパラメーターで注釈できます。
  2. 各クラスには、独自の公開メソッドがあります。
  3. 公開されたメソッドも継承できます。
  4. 公開機能が更新されると、メソッドをオーバーライドできます。

お役に立てれば


4

Antが示すように、クラス内からクラスへの参照を取得することはできません。ただし、(実際のクラス型オブジェクトを操作するのではなく)異なるクラスを区別したい場合は、各クラスに文字列を渡すことができます。クラススタイルのデコレーターを使用して、他のパラメーターをデコレーターに渡すこともできます。

class Decorator(object):
    def __init__(self,decoratee_enclosing_class):
        self.decoratee_enclosing_class = decoratee_enclosing_class
    def __call__(self,original_func):
        def new_function(*args,**kwargs):
            print 'decorating function in ',self.decoratee_enclosing_class
            original_func(*args,**kwargs)
        return new_function


class Bar(object):
    @Decorator('Bar')
    def foo(self):
        print 'in foo'

class Baz(object):
    @Decorator('Baz')
    def foo(self):
        print 'in foo'

print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()

プリント:

before instantiating Bar()
calling b.foo()
decorating function in  Bar
in foo

また、デコレータに関するBruce Eckelのページも参照してください。


これは不可能だという私の憂鬱な結論を確認してくれてありがとう。また、モジュール/クラスを完全に修飾した文字列( 'module.Class')を使用し、クラスがすべて完全に読み込まれるまで文字列を保存してから、インポートして自分でクラスを取得することもできます。それは私の仕事を成し遂げるためにひどく乾燥しない方法のようです。
カールG

この種のデコレータにクラスを使用する必要はありません。慣用的なアプローチは、デコレータ関数内でネストされた関数の1つの追加レベルを使用することです。ただし、クラスを使用する場合は、クラス名に大文字を使用せずに、装飾自体を「標準」に見えるようにするには、つまり@decorator('Bar')とは対照的@Decorator('Bar')です。
エリックカプルン2012

4

フラスコ上品が、それは方法に保存することを一時的にキャッシュを作成しているん、それは何か他のもの(フラスコAを使用してクラスを登録するという事実を使用していますregisterに実際にメソッドをラップしたクラスメソッドを)。

今回はメタクラスを使用してこのパターンを再利用できるため、インポート時にメソッドをラップできます。

def route(rule, **options):
    """A decorator that is used to define custom routes for methods in
    FlaskView subclasses. The format is exactly the same as Flask's
    `@app.route` decorator.
    """

    def decorator(f):
        # Put the rule cache on the method itself instead of globally
        if not hasattr(f, '_rule_cache') or f._rule_cache is None:
            f._rule_cache = {f.__name__: [(rule, options)]}
        elif not f.__name__ in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

実際のクラス(メタクラスを使用して同じことを行うことができます):

@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
             trailing_slash=None):

    for name, value in members:
        proxy = cls.make_proxy_method(name)
        route_name = cls.build_route_name(name)
        try:
            if hasattr(value, "_rule_cache") and name in value._rule_cache:
                for idx, cached_rule in enumerate(value._rule_cache[name]):
                    # wrap the method here

ソース:https : //github.com/apiguy/flask-classy/blob/master/flask_classy.py


それが適用されるということは、有益なパターンだが、これはメソッドの親クラスを参照することができるという方法デコレータの問題に対処していない
Anentropic

インポート時にクラスにアクセスするためにこれがどのように役立つかをより明確にするために回答を更新しました(つまり、メタクラスを使用して+メソッドのデコレータパラメータをキャッシュします)。
charlax 14

3

問題は、デコレータが呼び出されたときにクラスがまだ存在しないことです。これを試して:

def loud_decorator(func):
    print("Now decorating %s" % func)
    def decorated(*args, **kwargs):
        print("Now calling %s with %s,%s" % (func, args, kwargs))
        return func(*args, **kwargs)
    return decorated

class Foo(object):
    class __metaclass__(type):
        def __new__(cls, name, bases, dict_):
            print("Creating class %s%s with attributes %s" % (name, bases, dict_))
            return type.__new__(cls, name, bases, dict_)

    @loud_decorator
    def hello(self, msg):
        print("Hello %s" % msg)

Foo().hello()

このプログラムは出力します:

Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World

ご覧のように、自分がやりたいことを行うには別の方法を考えなければなりません。


関数を定義するとき、関数はまだ存在していませんが、関数をそれ自体から再帰的に呼び出すことができます。これは関数固有の言語機能であり、クラスでは使用できないと思います。
Carl G

DGGenuine:関数は呼び出されるだけであり、完全に作成された後にのみ、関数にアクセスします。この場合、クラスはデコレーターの結果を待つ必要があるため、デコレーターが呼び出されたときにクラスを完了できません。デコレーターの結果はクラスの属性の1つとして保管されます。
u0b34a0f6ae 2010年

3

以下に簡単な例を示します。

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

出力は次のとおりです。

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec

1

これは古い質問ですが、金星に出くわしました。 http://venusian.readthedocs.org/en/latest/

メソッドを装飾し、クラスとメソッドの両方にアクセスできるようにする機能があるようです。呼び出しsetattr(ob, wrapped.__name__, decorated)はvenusianを使用する一般的な方法ではなく、目的を多少損なうことに注意してください。

いずれにしても...以下の例は完全であり、実行する必要があります。

import sys
from functools import wraps
import venusian

def logged(wrapped):
    def callback(scanner, name, ob):
        @wraps(wrapped)
        def decorated(self, *args, **kwargs):
            print 'you called method', wrapped.__name__, 'on class', ob.__name__
            return wrapped(self, *args, **kwargs)
        print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
        setattr(ob, wrapped.__name__, decorated)
    venusian.attach(wrapped, callback)
    return wrapped

class Foo(object):
    @logged
    def bar(self):
        print 'bar'

scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])

if __name__ == '__main__':
    t = Foo()
    t.bar()

1

関数は、デコレータコードが実行されるとき、定義ポイントでそれがメソッドであるかどうかを認識しません。クラス/インスタンス識別子を介してアクセスされた場合にのみ、クラス/インスタンスを認識できます。この制限を克服するには、記述子オブジェクトで装飾して、アクセス/呼び出し時間まで実際の装飾コードを遅らせることができます。

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        func = self.func.__get__(obj, type_)
        print('accessed %s.%s' % (type_.__name__, func.__name__))
        return self.__class__(func, type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

これにより、個々の(静的|クラス)メソッドを装飾できます。

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

    @decorated
    @staticmethod
    def bar(a, b):
        pass

    @decorated
    @classmethod
    def baz(cls, a, b):
        pass

class Bar(Foo):
    pass

これで、イントロスペクションにデコレータコードを使用できます...

>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz

...および関数の動作を変更する場合:

>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}

悲しいことに、このアプローチは、Will McCutchen同様に適用できない答えと機能的に同等です。これとその回答の両方が、元の質問で必要とされるように、メソッド装飾時ではなくメソッド呼び出し時に目的のクラスを取得します。十分早い時期にこのクラスを取得するための唯一の合理的な手段は、クラス定義時にすべてのメソッドを内省することです(たとえば、クラスデコレータまたはメタクラスを介して)。</sigh>
セシルカレー

1

他の答えが指摘しているように、デコレータは関数のようなものであり、クラスがまだ作成されていないため、このメソッドが属するクラスにアクセスできません。ただし、デコレータを使用して関数を「マーク」し、メタクラス手法を使用して後でメソッドを処理することはまったく問題__new__ありません。その段階で、クラスはそのメタクラスによって作成されているためです。

以下に簡単な例を示します。

私たちは、使用@fieldの特別なフィールドとしてメソッドをマークし、メタクラスでそれに対処します。

def field(fn):
    """Mark the method as an extra field"""
    fn.is_field = True
    return fn

class MetaEndpoint(type):
    def __new__(cls, name, bases, attrs):
        fields = {}
        for k, v in attrs.items():
            if inspect.isfunction(v) and getattr(k, "is_field", False):
                fields[k] = v
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)
        attrs["_fields"] = fields

        return type.__new__(cls, name, bases, attrs)

class EndPoint(metaclass=MetaEndpoint):
    pass


# Usage

class MyEndPoint(EndPoint):
    @field
    def foo(self):
        return "bar"

e = MyEndPoint()
e._fields  # {"foo": ...}

この行にタイプミスif inspect.isfunction(v) and getattr(k, "is_field", False):がありgetattr(v, "is_field", False)ます。代わりに入力してください。
EvilTosha

0

デコレータが返す必要があるデコレートされたメソッドでメソッドが呼び出されているオブジェクトのクラスにアクセスできます。そのようです:

def decorator(method):
    # do something that requires view's class
    def decorated(self, *args, **kwargs):
        print 'My class is %s' % self.__class__
        method(self, *args, **kwargs)
    return decorated

ModelAクラスを使用すると、次のようになります。

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>

1
おかげで、これはまさに私の質問で参照した解決策であり、私にとってはうまくいきません。私はデコレーターを使用してオブザーバーパターンを実装しようとしています。観測ディスパッチャーにメソッドを追加するときに、ある時点でクラスがないと、観測ディスパッチャーから正しいコンテキストでメソッドを呼び出すことができません。メソッド呼び出し時にクラスを取得しても、そもそもメソッドを正しく呼び出すのに役立ちません。
Carl G

おっと、あなたの質問全体を読んでいないので、私の怠惰をごめんなさい。
Will McCutchen
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.