Bir sınıf nasıl dekore edilir?


129

Python 2.5'te, bir sınıfı süsleyen bir dekoratör yaratmanın bir yolu var mı? Özellikle, bir sınıfa üye eklemek ve kurucuyu o üye için bir değer alacak şekilde değiştirmek için bir dekoratör kullanmak istiyorum.

Aşağıdakine benzer bir şey aranıyor ('sınıf Foo:' üzerinde sözdizimi hatası olan:

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

Sanırım gerçekte peşinde olduğum şey Python'da C # arayüzü gibi bir şey yapmanın bir yolu. Sanırım paradigmamı değiştirmem gerekiyor.

Yanıtlar:


80

Ana hatlarıyla belirttiğiniz yaklaşım yerine bir alt sınıf düşünmek isteyebileceğiniz fikrini ikinci kez görüyorum. Ancak, özel senaryonuzu bilmeden, YMMV :-)

Düşündüğün şey bir metasınıf. __new__Bir metaclass fonksiyon sınıfı oluşturulmadan önce o zaman yeniden olabilir sınıfı, tam önerilen tanımı geçirilir. O zaman kurucudan yeni bir tane alabilirsiniz.

Misal:

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

Yapıcıyı değiştirmek belki biraz dramatik, ancak dil bu tür derin iç gözlem ve dinamik modifikasyon için destek sağlıyor.


Teşekkür ederim, aradığım şey bu. Herhangi bir sayıda başka sınıfı, hepsinin belirli bir üyesi olacak şekilde değiştirebilen bir sınıf. Sınıfların ortak bir ID sınıfından miras almamasının nedenleri, sınıfların ID olmayan versiyonlarının yanı sıra ID versiyonlarına sahip olmak istememdir.
Robert Gowland

Python2.5 veya daha eski sürümlerde metasınıflar bunun gibi şeyler yapmanın en iyi yoluydu, ancak günümüzde çok daha basit olan sınıf dekoratörlerini (Steven'ın cevabına bakın) sıklıkla kullanabilirsiniz.
Jonathan Hartley

203

Sınıf dekoratörlerinin probleminize doğru çözüm olup olmadığı sorusunun yanı sıra:

Python 2.6 ve üzeri sürümlerde, @-sözdizimine sahip sınıf dekoratörleri vardır, böylece şunları yazabilirsiniz:

@addID
class Foo:
    pass

Eski sürümlerde, bunu başka bir şekilde yapabilirsiniz:

class Foo:
    pass

Foo = addID(Foo)

Bununla birlikte, bunun işlev dekoratörleri ile aynı şekilde çalıştığını ve dekoratörün, örnekte yaptığınız şey olmayan yeni (veya değiştirilmiş orijinal) sınıfı döndürmesi gerektiğini unutmayın. AddID dekoratörü şöyle görünür:

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

Daha sonra, yukarıda açıklandığı gibi Python sürümünüz için uygun sözdizimini kullanabilirsiniz.

Ama başkalarıyla, eğer geçersiz kılmak istiyorsanız mirasın daha uygun olduğu konusunda hemfikirim __init__.


2
... soru özellikle Python 2.5'ten bahsediyor olsa da :-)
Jarret Hardie

5
Satır karışıklığı için üzgünüz, ancak kod örnekleri yorumlarda kesinlikle harika değil ...: def addID (orijinal_sınıf): original_class .__ orig__init__ = original_class .__ init__ def init __ (self, * argümanlar, ** kws): print "decorator" self.id = 9 original_class .__ orig__init __ (self, * args, ** kws) original_class .__ init = init return original_class @addID sınıfı Foo: def __init __ (self): print "Foo" a = Foo () print a.id
Gerald Senarclens de Grancy

2
@Steven, @Gerald Cevabınızı güncellemek eğer, onun kodunu okumak için çok daha kolay olurdu, doğru;)
Gün

3
@Day: Hatırlatma için teşekkürler, yorumları daha önce fark etmemiştim. Ancak: Maksimum özyineleme derinliğini Geralds koduyla da aşıyorum. Çalışır bir versiyon yapmayı deneyeceğim ;-)
Steven

1
@Day ve @Gerald: üzgünüm, sorun Gerald'ın koduyla ilgili değildi, yorumdaki karıştırılmış kod yüzünden batırdım ;-)
Steven

20

Sınıfları dinamik olarak tanımlayabileceğinizi kimse açıklamadı. Böylece, bir alt sınıfı tanımlayan (ve geri döndüren) bir dekoratörünüz olabilir:

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'de kullanılabilen (bunu 2.6+ sürümünde neden devam etmeniz gerektiğini açıklayan Blckknght'ın yorumu) şöyle:

class Foo:
    pass

FooId = addId(Foo)

Ve Python 3'te bunun gibi (ancak super()sınıflarınızda kullanmaya dikkat edin ):

@addId
class Foo:
    pass

Böylece pastanızı alıp yiyebilirsiniz - miras ve dekoratörler!


4
Bir dekoratörde alt sınıflandırma Python 2.6+ için tehlikelidir çünkü superorijinal sınıftaki çağrıları bozar . Eğer Foobir yöntem adında vardı foodenen super(Foo, self).foo()isim, çünkü sonsuz özyineleme olur Foodekoratör tarafından döndürülen alt sınıfa bağlı, (herhangi bir isimle erişilebilir değil) orijinal sınıfı. Python 3'ün argümansız super()bu sorunu ortadan kaldırır (aynı derleyici sihrinin onun çalışmasına izin verdiğini varsayıyorum). Sınıfı farklı bir adla manuel olarak dekore ederek de sorunu çözebilirsiniz (Python 2.5 örneğinde yaptığınız gibi).
Blckknght

1
ha. teşekkürler, hiçbir fikrim yoktu (python 3 kullanıyorum). bir yorum ekleyecek.
andrew cooke

13

Bu iyi bir uygulama değil ve bu yüzden bunu yapacak bir mekanizma yok. İstediğinizi başarmanın doğru yolu kalıtımdır.

Sınıf belgelerine bir göz atın .

Küçük bir örnek:

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)

Böylelikle Boss'un sahip olduğu her şey Employee, kendi __init__metodu ve kendi üyeleri ile de olur.


3
Sanırım istediğim, Boss'un içerdiği sınıftan agnostik olmasını sağlamaktı. Yani Boss özelliklerini uygulamak istediğim onlarca farklı sınıf olabilir. Bu düzinelerce sınıfın Boss'tan miras kalmasına izin mi verdim?
Robert Gowland

5
@Robert Gowland: Python'un birden fazla mirasa sahip olmasının nedeni budur. Evet, çeşitli ebeveyn sınıflarından çeşitli yönleri miras almalısınız.
S.Lott

7
@ S.Lott: Genelde çoklu kalıtım kötü bir fikirdir, çok fazla kalıtım seviyesi bile kötüdür. Çoklu mirastan uzak durmanızı tavsiye edeceğim.
mpeterson

5
mpeterson: Python'daki çoklu kalıtım bu yaklaşımdan daha mı kötü? Python'un çoklu mirasının nesi yanlış?
Arafangion

2
@Arafangion: Çoklu miras genellikle bir sorun uyarısı olarak kabul edilir. Karmaşık hiyerarşiler ve takip edilmesi zor ilişkiler yaratır. Sorunlu alanınız çoklu kalıtıma uygunsa (hiyerarşik olarak modellenebilir mi?), Bu birçokları için doğal bir seçimdir. Bu, çoklu mirasa izin veren tüm diller için geçerlidir.
Morten Jensen

6

Kalıtımın, ortaya çıkan sorun için daha uygun olduğuna katılıyorum.

Bu soruyu dekorasyon derslerinde gerçekten kullanışlı buldum, hepinize teşekkürler.

Python 2.7'de (ve orijinal işlevin docstringini koruyan @wraps , vb.)

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

Genellikle dekoratörünüze parametreler eklemek istersiniz:

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

Çıktılar:

@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

Onları daha kısa bulduğum için bir işlev dekoratörü kullandım. İşte bir sınıfı dekore etmek için bir sınıf:

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

Bu parantezleri kontrol eden ve yöntemler dekore edilmiş sınıfta yoksa çalışan daha sağlam bir sürüm:

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Çekler dekoratör parantez olmadan kullanılamaz edilmediğini. Varsa, dekore edilen sınıf msgdekoratörün parametresine geçirilir ve bu da bir AssertionError.

@decorate_ifyalnızca decoratoreğer conditiondeğerlendirilirse uygulanır True.

getattr, callableTesti ve @decorate_ifeğer dekoratör kırmak kalmaması kullanılan foo()yöntem sınıf dekore edilen mevcut değil.


4

Aslında burada bir sınıf dekoratörünün oldukça iyi bir uygulaması var:

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

Aslında bunun oldukça ilginç bir uygulama olduğunu düşünüyorum. Süslediği sınıfı alt sınıflara ayırdığı için, isinstanceçekler gibi şeylerde tam olarak bu sınıf gibi davranacaktır .

Ek bir faydası vardır: __init__özel bir django Formundaki ifadede değişiklik veya ekleme yapılması alışılmadık bir durum değildir, bu self.fieldsnedenle değişikliklerin söz konusu sınıf için tümüyle çalıştırıldıktan sonraself.fields gerçekleşmesi daha iyidir .__init__

Çok zeki.

Bununla birlikte, sınıfınızda dekorasyonun kurucuyu değiştirmesini istiyorsunuz ki bu bir sınıf dekoratörü için iyi bir kullanım durumu olduğunu düşünmüyorum.


0

İşte bir sınıfın parametrelerini döndürme sorusuna cevap veren bir örnek. Dahası, kalıtım zincirine hala saygı duyar, yani yalnızca sınıfın kendisinin parametreleri döndürülür. İşlev get_paramsbasit bir örnek olarak eklenmiştir, ancak inceleme modülü sayesinde başka işlevler de eklenebilir.

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

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.