Dapatkah seorang dekorator metode instance mengakses kelas?

109

Saya memiliki sesuatu yang kira-kira seperti berikut ini. Pada dasarnya saya perlu mengakses kelas metode contoh dari dekorator yang digunakan pada metode contoh dalam definisinya.

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

Kode apa adanya memberikan:

AttributeError: objek 'function' tidak memiliki atribut 'im_class'

Saya menemukan pertanyaan / jawaban yang serupa - Python decorator membuat fungsi lupa bahwa itu milik kelas dan Dapatkan kelas dalam dekorator Python - tetapi ini bergantung pada solusi yang mengambil contoh pada saat run-time dengan mengambil parameter pertama. Dalam kasus saya, saya akan memanggil metode berdasarkan informasi yang dikumpulkan dari kelasnya, jadi saya tidak sabar menunggu panggilan masuk.

Carl G
sumber

Jawaban:

68

Jika Anda menggunakan Python 2.6 atau yang lebih baru, Anda dapat menggunakan dekorator kelas, mungkin sesuatu seperti ini (peringatan: kode belum diuji).

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

Penghias metode menandai metode sebagai salah satu yang menarik dengan menambahkan atribut "use_class" - fungsi dan metode juga merupakan objek, sehingga Anda dapat melampirkan metadata tambahan padanya.

Setelah kelas dibuat, dekorator kelas kemudian memeriksa semua metode dan melakukan apa pun yang diperlukan pada metode yang telah ditandai.

Jika Anda ingin semua metode terpengaruh maka Anda dapat meninggalkan metode dekorator dan hanya menggunakan dekorator kelas.

Dave Kirby
sumber
2
Terima kasih, saya pikir ini adalah rute yang harus ditempuh. Hanya satu baris kode tambahan untuk kelas apa pun yang ingin saya gunakan dekorator ini. Mungkin saya bisa menggunakan metaclass kustom dan melakukan pemeriksaan yang sama ini saat baru ...?
Carl G
3
Siapa pun yang mencoba menggunakan ini dengan metode statis atau metode kelas akan ingin membaca PEP ini: python.org/dev/peps/pep-0232 Tidak yakin itu mungkin karena Anda tidak dapat menetapkan atribut pada metode kelas / statis dan saya pikir mereka melahap atribut fungsi kustom apa pun saat diterapkan ke fungsi.
Carl G
Hanya apa yang saya cari, untuk ORM berbasis DBM saya ... Terima kasih, kawan.
Coyote21
Anda harus menggunakan inspect.getmro(cls)untuk memproses semua kelas dasar di dekorator kelas untuk mendukung pewarisan.
schlamar
1
oh, sebenarnya ini terlihat seperti inspectpenyelamatan stackoverflow.com/a/1911287/202168
Anentropic
16

Sejak python 3.6 Anda dapat menggunakan object.__set_name__untuk mencapai ini dengan cara yang sangat sederhana. Doc menyatakan bahwa __set_name__"dipanggil pada saat pemilik kelas pemilik dibuat". Berikut ini contohnya:

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)

Perhatikan bahwa itu dipanggil pada waktu pembuatan kelas:

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

Jika Anda ingin mengetahui lebih banyak tentang bagaimana kelas dibuat dan khususnya kapan tepatnya __set_name__dipanggil, Anda dapat merujuk ke dokumentasi tentang "Membuat objek kelas" .

tyrion
sumber
1
Bagaimana tampilan menggunakan dekorator dengan parameter? Misalnya@class_decorator('test', foo='bar')
luckydonald
2
@luckydonald Anda dapat melakukan pendekatan serupa dengan dekorator biasa yang suka berargumen . def decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Punya
Wah terima kasih banyak. Tidak tahu tentang __set_name__meskipun saya telah menggunakan Python 3.6+ untuk waktu yang lama.
kawing-chiu
Ada satu kelemahan dari metode ini: pemeriksa statis tidak memahami ini sama sekali. Mypy akan berpikir bahwa helloitu bukan metode, melainkan objek bertipe class_decorator.
kawing-chiu
@ kawing-chiu Jika tidak ada yang berhasil, Anda dapat menggunakan if TYPE_CHECKINGto define class_decoratorsebagai dekorator normal yang mengembalikan tipe yang benar.
tyrion
15

Seperti yang ditunjukkan orang lain, kelas belum dibuat pada saat dekorator dipanggil. Namun , dimungkinkan untuk membuat anotasi objek fungsi dengan parameter dekorator, lalu menghias ulang fungsi dalam metode metaclass __new__. Anda harus mengakses __dict__atribut fungsi secara langsung, setidaknya bagi saya, func.foo = 1menghasilkan AttributeError.

Mark Visser
sumber
6
setattrharus digunakan daripada mengakses__dict__
schlamar
7

Seperti yang disarankan Mark:

  1. Setiap dekorator disebut SEBELUM kelas dibangun, jadi tidak diketahui oleh dekorator.
  2. Kami dapat menandai metode ini dan membuat proses pasca yang diperlukan nanti.
  3. Kami memiliki dua opsi untuk pasca-pemrosesan: secara otomatis di akhir definisi kelas atau di suatu tempat sebelum aplikasi berjalan. Saya lebih suka opsi 1 menggunakan kelas dasar, tetapi Anda dapat mengikuti pendekatan ke-2 juga.

Kode ini menunjukkan bagaimana ini dapat bekerja menggunakan pasca-pemrosesan otomatis:

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

Hasil keluarannya:

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}

Perhatikan bahwa dalam contoh ini:

  1. Kami dapat membuat anotasi fungsi apa pun dengan parameter arbitrer apa pun.
  2. Setiap kelas memiliki metode tereksposnya sendiri.
  3. Kami juga dapat mewarisi metode yang terbuka.
  4. metode dapat diganti karena fitur pemaparan diperbarui.

Semoga ini membantu

asterio gonzalez
sumber
4

Seperti yang ditunjukkan Semut, Anda tidak bisa mendapatkan referensi ke kelas dari dalam kelas. Namun, jika Anda tertarik untuk membedakan antara kelas yang berbeda (tidak memanipulasi objek tipe kelas yang sebenarnya), Anda dapat meneruskan string untuk setiap kelas. Anda juga dapat meneruskan parameter lain apa pun yang Anda suka ke dekorator menggunakan dekorator bergaya kelas.

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

Cetakan:

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

Juga, lihat halaman Bruce Eckel tentang dekorator.

Ross Rogers
sumber
Terima kasih telah mengonfirmasi kesimpulan menyedihkan saya bahwa ini tidak mungkin. Saya juga bisa menggunakan string yang sepenuhnya memenuhi syarat modul / kelas ('module.Class'), menyimpan string sampai semua kelas terisi penuh, kemudian mengambil kelas sendiri dengan import. Itu sepertinya cara yang sangat tidak KERING untuk menyelesaikan tugas saya.
Carl G
Anda tidak perlu menggunakan kelas untuk dekorator semacam ini: pendekatan idiomatiknya adalah menggunakan satu tingkat tambahan fungsi bertingkat di dalam fungsi dekorator. Namun, jika Anda memilih kelas, akan lebih baik jika tidak menggunakan kapitalisasi dalam nama kelas untuk membuat dekorasi itu sendiri terlihat "standar", yaitu @decorator('Bar')sebagai lawan @Decorator('Bar').
Erik Kaplun
4

Apa yang dilakukan oleh flask-classy adalah membuat cache sementara yang disimpannya pada metode, kemudian menggunakan sesuatu yang lain (fakta bahwa Flask akan mendaftarkan kelas menggunakan registermetode kelas) untuk membungkus metode tersebut.

Anda dapat menggunakan kembali pola ini, kali ini menggunakan metaclass sehingga Anda dapat menggabungkan metode ini pada waktu impor.

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

Di kelas sebenarnya (Anda bisa melakukan hal yang sama menggunakan metaclass):

@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

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

charlax.dll
sumber
itu pola yang berguna, tetapi ini tidak mengatasi masalah dekorator metode yang dapat merujuk ke kelas induk dari metode yang diterapkan
padanya
Saya memperbarui jawaban saya menjadi lebih eksplisit bagaimana ini dapat berguna untuk mendapatkan akses ke kelas pada waktu impor (yaitu menggunakan metaclass + caching param dekorator pada metode).
charlax
3

Masalahnya adalah ketika dekorator dipanggil kelas tersebut belum ada. Coba ini:

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

Program ini akan menampilkan:

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

Seperti yang Anda lihat, Anda harus mencari cara lain untuk melakukan apa yang Anda inginkan.

Semut Aasma
sumber
ketika seseorang mendefinisikan suatu fungsi, fungsi tersebut belum ada, tetapi ia dapat memanggil fungsi tersebut secara rekursif dari dalam dirinya sendiri. Saya rasa ini adalah fitur bahasa khusus untuk fungsi dan tidak tersedia untuk kelas.
Carl G
DGGenuine: Fungsi ini hanya dipanggil, dan dengan demikian fungsi mengakses dirinya sendiri, hanya setelah itu dibuat sepenuhnya. Dalam hal ini, kelas tidak bisa diselesaikan saat dekorator dipanggil, karena kelas harus menunggu hasil dekorator, yang akan disimpan sebagai salah satu atribut kelas.
u0b34a0f6ae
3

Berikut contoh sederhananya:

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"

Outputnya adalah:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec
nicodjimenez.dll
sumber
1

Ini adalah pertanyaan lama tetapi muncul di Venesia. http://venusian.readthedocs.org/en/latest/

Tampaknya memiliki kemampuan untuk menghias metode dan memberi Anda akses ke kelas dan metode saat melakukannya. Perhatikan bahwa menelepon setattr(ob, wrapped.__name__, decorated)bukanlah cara khas menggunakan venusian dan agak mengalahkan tujuan.

Either way ... contoh di bawah ini selesai dan harus dijalankan.

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()
eric.frederich
sumber
1

Fungsi tidak tahu apakah itu metode pada titik definisi, ketika kode dekorator dijalankan. Hanya ketika diakses melalui pengenal kelas / instance, ia mungkin mengetahui kelas / instansinya. Untuk mengatasi batasan ini, Anda dapat menghias dengan objek deskriptor untuk menunda kode dekorasi yang sebenarnya hingga waktu akses / panggilan:

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)

Ini memungkinkan Anda untuk mendekorasi metode individu (statis | kelas):

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

Sekarang Anda dapat menggunakan kode dekorator untuk introspeksi ...

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

... dan untuk mengubah perilaku fungsi:

>>> 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'}
aurzenligl.dll
sumber
Sayangnya, pendekatan ini secara fungsional setara dengan Will McCutchen 's jawabannya sama-sama tidak berlaku . Baik jawaban ini dan itu mendapatkan kelas yang diinginkan pada waktu pemanggilan metode daripada waktu dekorasi metode , seperti yang disyaratkan oleh pertanyaan awal. Satu-satunya cara yang masuk akal untuk mendapatkan kelas ini pada waktu yang cukup awal adalah dengan melakukan introspeksi terhadap semua metode pada waktu definisi kelas (misalnya, melalui dekorator kelas atau metaclass). </sigh>
Cecil Curry
1

Seperti yang ditunjukkan oleh jawaban lain, dekorator adalah hal yang berfungsi dengan baik, Anda tidak dapat mengakses kelas tempat metode ini dimiliki karena kelas tersebut belum dibuat. Namun, tidak masalah menggunakan dekorator untuk "menandai" fungsi dan kemudian menggunakan teknik metaclass untuk menangani metode tersebut nanti, karena pada __new__tahap tersebut, kelas telah dibuat oleh metaclass-nya.

Berikut ini contoh sederhananya:

Kami menggunakan @fielduntuk menandai metode sebagai bidang khusus dan menanganinya di metaclass.

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": ...}
ospider
sumber
Ada kesalahan ketik di baris ini: if inspect.isfunction(v) and getattr(k, "is_field", False):seharusnya begitu getattr(v, "is_field", False).
EvilTosha
0

Anda akan memiliki akses ke kelas objek tempat metode tersebut dipanggil dalam metode dekorasi yang harus dikembalikan oleh dekorator Anda. Seperti:

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

Menggunakan kelas ModelA Anda, inilah yang dilakukannya:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>
Will McCutchen
sumber
1
Terima kasih, tetapi ini persis solusi yang saya rujuk dalam pertanyaan saya yang tidak berhasil untuk saya. Saya mencoba menerapkan pola pengamat menggunakan dekorator dan saya tidak akan pernah dapat memanggil metode dalam konteks yang benar dari operator pengamatan saya jika saya tidak memiliki kelas di beberapa titik saat menambahkan metode ke operator pengamatan. Mendapatkan kelas pada pemanggilan metode tidak membantu saya memanggil metode dengan benar sejak awal.
Carl G
Whoa, maaf atas kemalasan saya karena tidak membaca seluruh pertanyaan Anda.
Will McCutchen
0

Saya hanya ingin menambahkan contoh saya karena ia memiliki semua hal yang dapat saya pikirkan untuk mengakses kelas dari metode dekorasi. Ini menggunakan deskriptor seperti yang disarankan @tyrion. Dekorator dapat mengambil argumen dan meneruskannya ke deskriptor. Itu bisa menangani baik metode di kelas atau fungsi tanpa kelas.

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")
    
djiao
sumber