Bir örnek yönteminin dekoratörü sınıfa erişebilir mi?


109

Aşağıdakine benzer bir şeyim var. Temel olarak, bir örnek yönteminin sınıfına, tanımındaki örnek yönteminde kullanılan bir dekoratörden erişmem gerekiyor.

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

Olduğu gibi kod şunları verir:

AttributeError: 'function' nesnesinin 'im_class' niteliği yok

Benzer soru / cevaplar buldum - Python dekoratörü, fonksiyonun bir sınıfa ait olduğunu unutmasını ve Python dekoratöründe Get sınıfını unutturuyor - ancak bunlar, ilk parametreyi yakalayarak örneği çalışma zamanında yakalayan bir geçici çözüme dayanıyor. Benim durumumda, yöntemi sınıfından toplanan bilgilere dayanarak çağıracağım, bu yüzden bir çağrının gelmesini bekleyemem.

Yanıtlar:


68

Python 2.6 veya üzerini kullanıyorsanız, bir sınıf dekoratörü kullanabilirsiniz, belki bunun gibi bir şey (uyarı: test edilmemiş kod).

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

Yöntem dekoratörü, bir "use_class" özniteliği ekleyerek yöntemi ilgi çekici olarak işaretler - işlevler ve yöntemler de nesnelerdir, böylece bunlara ek meta veriler ekleyebilirsiniz.

Sınıf oluşturulduktan sonra, sınıf dekoratörü tüm yöntemleri gözden geçirir ve işaretlenen yöntemlerde ne gerekiyorsa onu yapar.

Tüm yöntemlerin etkilenmesini istiyorsanız, yöntem tasarımcısını dışarıda bırakabilir ve yalnızca sınıf dekoratörünü kullanabilirsiniz.


2
Teşekkürler Sanırım gidilecek yol bu. Bu dekoratörü kullanmak istediğim herhangi bir sınıf için fazladan bir kod satırı. Belki özel bir metasınıf kullanabilir ve aynı kontrolü yeni sırasında da yapabilirim ...?
Carl G

3
Bunu statik yöntemle veya sınıf yöntemiyle kullanmaya çalışan herkes bu PEP'i okumak isteyecektir: python.org/dev/peps/pep-0232 Bunun mümkün olduğundan emin değilim çünkü bir sınıf / statik yöntemde bir öznitelik ayarlayamazsınız ve bence bir işleve uygulandıklarında herhangi bir özel işlev özelliğini yükseltin.
Carl G

Tam aradığım şey, DBM tabanlı ORM'm için ... Teşekkürler dostum.
Coyote21

inspect.getmro(cls)Devralmayı desteklemek için sınıf dekoratöründeki tüm temel sınıfları işlemek için kullanmalısınız .
schlamar

1
oh aslında benziyor inspectkurtarma için stackoverflow.com/a/1911287/202168
Anentropic

16

Python 3.6'dan beri bunu object.__set_name__çok basit bir şekilde gerçekleştirmek için kullanabilirsiniz . Doküman __set_name__, "sahip sınıf sahibi yaratıldığında çağrılır" ifadesini belirtir . İşte bir örnek:

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)

Sınıf oluşturma sırasında çağrıldığına dikkat edin:

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

Sınıfların nasıl oluşturulduğu ve özellikle tam olarak ne zaman __set_name__çağrıldığı hakkında daha fazla bilgi edinmek istiyorsanız , "Sınıf nesnesi oluşturma" konusundaki belgelere bakabilirsiniz .


1
Dekoratörü parametrelerle kullanmak nasıl görünür? Örneğin@class_decorator('test', foo='bar')
luckydonald

2
@luckydonald Buna argüman alan normal dekoratörlere benzer şekilde yaklaşabilirsiniz . Sadece vardef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding

Vay canına, çok teşekkür ederim. __set_name__Uzun süredir Python 3.6+ kullanmama rağmen bilmiyordum .
kawing-chiu

Bu yöntemin bir dezavantajı vardır: statik denetleyici bunu hiç anlamıyor. Mypy bunun hellobir yöntem olmadığını, bunun yerine bir tür nesnesi olduğunu düşünecektir class_decorator.
kawing-chiu

@ kawing-chiu Başka hiçbir şey işe yaramazsa , doğru türü döndüren normal bir dekoratör olarak if TYPE_CHECKINGtanımlamak için bir kullanabilirsiniz class_decorator.
tyrion

15

Diğerlerinin de belirttiği gibi, dekoratör çağrıldığında sınıf yaratılmadı. Bununla birlikte , işlev nesnesine dekoratör parametreleriyle açıklama eklemek, ardından meta sınıfın __new__yönteminde işlevi yeniden dekore etmek mümkündür . __dict__En azından benim için func.foo = 1bir AttributeError ile sonuçlandığından , fonksiyonun özelliğine doğrudan erişmeniz gerekecek .


6
setattrerişim yerine kullanılmalı__dict__
schlamar

7

Mark'ın önerdiği gibi:

  1. Herhangi bir dekoratör, BEFORE sınıfı oluşturulur, bu nedenle dekoratör tarafından bilinmemektedir.
  2. Biz yapabilirsiniz etiketlemek bu yöntemleri ve daha sonra gerekli sonrası işlemi yapmak.
  3. İşlem sonrası için iki seçeneğimiz var: sınıf tanımının sonunda otomatik olarak veya uygulama çalışmadan önce bir yerde. Temel sınıf kullanarak 1. seçeneği tercih ederim, ancak 2. yaklaşımı da takip edebilirsiniz.

Bu kod, bunun otomatik sonradan işlemeyi kullanarak nasıl çalışabileceğini gösterir:

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

Çıktı şunları verir:

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}

Bu örnekte şunu unutmayın:

  1. Herhangi bir işleve herhangi bir keyfi parametre ile açıklama ekleyebiliriz.
  2. Her sınıfın kendi açık yöntemleri vardır.
  3. Maruz kalan yöntemleri de miras alabiliriz.
  4. yöntem, açığa çıkarma özelliği güncellendiğinden geçersiz kılabilir.

Bu yardımcı olur umarım


4

Karıncaların da belirttiği gibi, sınıfın içinden sınıfa bir referans alamazsınız. Bununla birlikte, farklı sınıflar arasında ayrım yapmakla ilgileniyorsanız (gerçek sınıf türü nesnesini değiştirmekle değil), her sınıf için bir dize geçirebilirsiniz. Ayrıca, sınıf stili dekoratörleri kullanarak dekoratöre istediğiniz diğer parametreleri de iletebilirsiniz.

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

Baskılar:

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

Ayrıca Bruce Eckel'in dekoratörler hakkındaki sayfasına bakın.


Bunun mümkün olmadığına dair iç karartıcı sonucumu teyit ettiğiniz için teşekkürler. Ayrıca modülü / sınıfı ('module.Class') tam olarak nitelendiren bir dize kullanabilir, dizeleri tüm sınıflar tam olarak yüklenene kadar depolayabilir, ardından sınıfları içe aktarmayla kendim alabilirim. Bu, görevimi yerine getirmenin ne yazık ki kuru bir yolu gibi görünüyor.
Carl G

Bu tür bir dekoratör için bir sınıf kullanmanıza gerek yoktur: deyimsel yaklaşım, dekoratör işlevi içinde fazladan bir iç içe geçmiş işlev düzeyi kullanmaktır. Bununla birlikte, sınıflara girerseniz, dekorasyonun kendisini "standart", yani @decorator('Bar')aksine olarak göstermek için sınıf adında büyük harf kullanmamak daha iyi olabilir @Decorator('Bar').
Erik Kaplun

4

Ne şişeyi-klas o yönteme depolayan geçici bir önbellek oluşturmak olduğunu mu, o zaman başka bir şey (Şişe bir yardımıyla sınıflara kayıt gerçeğini kullanır registeraslında yöntemini sarar sınıf yöntemi).

Bu modeli, bu kez bir metasınıf kullanarak yeniden kullanabilirsiniz, böylece yöntemi içe aktarma zamanında kaydırabilirsiniz.

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

Gerçek sınıfta (bir metasınıf kullanarak aynısını yapabilirsiniz):

@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

Kaynak: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py


bu kullanışlı bir model, ancak bu, bir yöntem dekoratörünün uygulandığı yöntemin ana sınıfına başvurma sorununu ele
almıyor

Cevabımı, bunun içeri aktarma zamanında sınıfa erişim sağlamak için nasıl yararlı olabileceğini daha açık hale getirecek şekilde güncelledim (yani bir metasınıf kullanarak + yöntem üzerinde dekoratör parametresini önbelleğe alma).
charlax

3

Sorun, dekoratör çağrıldığında sınıfın henüz mevcut olmamasıdır. Bunu dene:

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

Bu programın çıktısı:

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

Gördüğünüz gibi, istediğinizi yapmanın farklı bir yolunu bulmanız gerekecek.


biri bir işlevi tanımladığında, işlev henüz mevcut değildir, ancak işlevi kendi içinden özyinelemeli olarak çağırabilir. Sanırım bu, işlevlere özgü bir dil özelliğidir ve sınıflar için mevcut değildir.
Carl G

DGGenuine: İşlev yalnızca çağrılır ve bu nedenle işlev, yalnızca tamamen oluşturulduktan sonra kendisine erişir. Bu durumda, sınıf, dekoratör çağrıldığında tamamlanamaz, çünkü sınıfın, sınıfın niteliklerinden biri olarak saklanacak olan dekoratörün sonucunu beklemesi gerekir.
u0b34a0f6ae

3

İşte basit bir örnek:

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"

Çıktı:

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

1

Bu eski bir soru ama karşımıza bir venüslü. http://venusian.readthedocs.org/en/latest/

Yöntemleri dekore etme ve bunu yaparken hem sınıfa hem de yönteme erişmenizi sağlama yeteneğine sahip gibi görünüyor. Bu setattr(ob, wrapped.__name__, decorated)çağrının venüslü kullanmanın tipik yolu olmadığını ve bir şekilde amacını bozduğunu unutmayın.

Her iki durumda da ... aşağıdaki örnek tamamlanmıştır ve çalışmalıdır.

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

İşlev, dekoratör kodu çalıştığında, tanım noktasında bir yöntem olup olmadığını bilmez. Yalnızca sınıf / örnek tanımlayıcı aracılığıyla erişildiğinde, sınıfını / örneğini bilebilir. Bu sınırlamanın üstesinden gelmek için, gerçek dekorasyon kodunu erişim / çağrı zamanına kadar geciktirmek için tanımlayıcı nesneyle dekore edebilirsiniz:

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)

Bu, bireysel (statik | sınıf) yöntemleri dekore etmenize olanak tanır:

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

Artık iç gözlem için dekoratör kodunu kullanabilirsiniz ...

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

... ve işlev davranışını değiştirmek için:

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

Ne yazık ki, bu yaklaşım kod şuna eşdeğerdir Will McCutchen 'ın eşit uygulanamaz cevap . Hem bu hem de bu yanıt , orijinal sorunun gerektirdiği gibi, yöntem dekorasyon zamanı yerine yöntem çağrısı zamanında istenen sınıfı elde eder . Bu sınıfı yeterince erken bir zamanda elde etmenin tek makul yolu, sınıf tanımlama zamanında (örneğin, bir sınıf dekoratörü veya meta sınıf aracılığıyla) tüm yöntemler üzerinden iç gözlem yapmaktır. </sigh>
Cecil Curry

1

Diğer yanıtların da belirttiği gibi, dekoratör işlev-benzeri bir şeydir, sınıf henüz oluşturulmadığı için bu yöntemin ait olduğu sınıfa erişemezsiniz. Bununla birlikte, işlevi "işaretlemek" için bir dekoratör kullanmak ve daha sonra yöntemle uğraşmak için metasınıf tekniklerini kullanmak tamamen uygundur, çünkü __new__aşamada sınıf kendi meta sınıfı tarafından yaratılmıştır.

İşte basit bir örnek:

Biz kullanmak @fieldözel bir alan olarak şeklini işaretleyip metaclass onunla uğraşmak.

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": ...}

Bu satırda bir yazım hatası var: if inspect.isfunction(v) and getattr(k, "is_field", False):onun getattr(v, "is_field", False)yerine olmalı .
EvilTosha

0

Dekoratörünüzün dönmesi gereken dekore edilmiş yöntemde, yöntemin çağrıldığı nesnenin sınıfına erişebileceksiniz. Şöyle:

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 sınıfınızı kullanarak, bunun yaptığı şudur:

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

1
Teşekkürler ama sorumda bahsettiğim çözüm benim için işe yaramıyor. Dekoratörleri kullanarak bir gözlemci modeli uygulamaya çalışıyorum ve yöntemi gözlem dağıtıcısına eklerken bir noktada sınıfa sahip değilsem, yöntemi gözlem dağıtıcımdan doğru bağlamda asla arayamayacağım. Sınıfı yöntem çağrısı üzerine almak, ilk etapta yöntemi doğru şekilde çağırmama yardımcı olmuyor.
Carl G

Whoa, tüm sorunuzu okumadığım için tembelliğime üzüldüm.
Will McCutchen

0

Sınıfa dekore edilmiş yöntemden erişmek için düşünebildiğim her şeye sahip olduğu için örneğimi eklemek istiyorum. @Tyrion'un önerdiği gibi bir tanımlayıcı kullanır. Dekoratör argümanlar alabilir ve bunları tanımlayıcıya iletebilir. Hem bir sınıftaki bir yöntemle hem de sınıfsız bir işlevle başa çıkabilir.

import datetime as dt
import functools

def dec(arg1):
    class Timed(object):
        local_arg = arg1
        def __init__(self, f):
            functools.update_wrapper(self, f)
            self.func = f

        def __set_name__(self, owner, name):
            # doing something fancy with owner and name
            print('owner type', owner.my_type())
            print('my arg', self.local_arg)

        def __call__(self, *args, **kwargs):
            start = dt.datetime.now()
            ret = self.func(*args, **kwargs)
            time = dt.datetime.now() - start
            ret["time"] = time
            return ret
        
        def __get__(self, instance, owner):
            from functools import partial
            return partial(self.__call__, instance)
    return Timed

class Test(object):
    def __init__(self):
        super(Test, self).__init__()

    @classmethod
    def my_type(cls):
        return 'owner'

    @dec(arg1='a')
    def decorated(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)
        return dict()

    def call_deco(self):
        self.decorated("Hello", world="World")

@dec(arg1='a function')
def another(*args, **kwargs):
    print(args)
    print(kwargs)
    return dict()

if __name__ == "__main__":
    t = Test()
    ret = t.call_deco()
    another('Ni hao', world="shi jie")
    
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.