Bagaimana cara menghias kelas?

129

Dalam Python 2.5, apakah ada cara untuk membuat dekorator yang menghiasi kelas? Secara khusus, saya ingin menggunakan dekorator untuk menambahkan anggota ke kelas dan mengubah konstruktor untuk mengambil nilai bagi anggota itu.

Mencari sesuatu seperti berikut (yang memiliki kesalahan sintaks pada '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()

Saya kira apa yang benar-benar saya cari adalah cara untuk melakukan sesuatu seperti antarmuka C # dengan Python. Saya perlu mengubah paradigma saya, saya kira.

Robert Gowland
sumber

Jawaban:

80

Saya akan mendukung gagasan bahwa Anda mungkin ingin mempertimbangkan subkelas alih-alih pendekatan yang telah Anda uraikan. Namun, tidak mengetahui skenario spesifik Anda, YMMV :-)

Apa yang Anda pikirkan adalah metaclass. The __new__fungsi dalam metaclass dilewatkan definisi yang diusulkan penuh kelas, yang kemudian dapat menulis ulang sebelum kelas dibuat. Anda dapat, pada saat itu, mengeluarkan konstruktor untuk yang baru.

Contoh:

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

Mengganti konstruktor mungkin sedikit dramatis, tetapi bahasa memang memberikan dukungan untuk introspeksi mendalam dan modifikasi dinamis.

Jarret Hardie
sumber
Terima kasih, itulah yang saya cari. Kelas yang dapat memodifikasi sejumlah kelas lain sehingga mereka semua memiliki anggota tertentu. Alasan saya untuk tidak memiliki kelas yang diwarisi dari kelas ID umum adalah bahwa saya ingin memiliki versi non-ID dari kelas serta versi ID.
Robert Gowland
Dulu metaclasses adalah cara untuk melakukan hal-hal seperti ini di Python2.5 atau lebih tua, tetapi saat ini Anda sangat sering dapat menggunakan dekorator kelas (lihat jawaban Steven), yang jauh lebih sederhana.
Jonathan Hartley
203

Terlepas dari pertanyaan apakah dekorator kelas adalah solusi tepat untuk masalah Anda:

Dalam Python 2.6 dan lebih tinggi, ada dekorator kelas dengan @ -syntax, sehingga Anda dapat menulis:

@addID
class Foo:
    pass

Dalam versi yang lebih lama, Anda dapat melakukannya dengan cara lain:

class Foo:
    pass

Foo = addID(Foo)

Namun perlu dicatat bahwa ini berfungsi sama seperti untuk dekorator fungsi, dan bahwa dekorator harus mengembalikan kelas baru (atau modifikasi asli), yang bukan apa yang Anda lakukan dalam contoh. Dekorator addID akan terlihat seperti ini:

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

Anda kemudian dapat menggunakan sintaks yang sesuai untuk versi Python Anda seperti dijelaskan di atas.

Tetapi saya setuju dengan orang lain bahwa warisan lebih cocok jika Anda ingin menimpanya __init__.

Steven
sumber
2
... meskipun pertanyaannya secara spesifik menyebutkan Python 2.5 :-)
Jarret Hardie
5
Maaf atas kesalahan linesep, tetapi contoh kode tidak sepenuhnya bagus dalam komentar ...: def addID (original_class): original_class .__ orig__init__ = original_class .__ init__ def init __ (sendiri, * 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 __ (mandiri): cetak "Foo" a = Foo () cetak a.id
Gerald Senarclens de Grancy
2
@ Sebelas, @Gerald benar, jika Anda dapat memperbarui jawaban Anda, akan jauh lebih mudah untuk membaca kode-nya;)
Hari
3
@ Hari: terima kasih atas pengingatnya, saya belum memperhatikan komentar sebelumnya. Namun: Saya mendapatkan kedalaman rekursi maksimum yang terlampaui dengan kode Geralds juga. Saya akan mencoba dan membuat versi yang berfungsi ;-)
Steven
1
@Day dan @Gerald: maaf, masalah bukan pada kode Gerald, saya mengacaukan karena kode yang rusak dalam komentar ;-)
Steven
20

Tidak ada yang menjelaskan bahwa Anda dapat mendefinisikan kelas secara dinamis. Jadi, Anda dapat memiliki dekorator yang mendefinisikan (dan mengembalikan) subkelas:

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

Yang dapat digunakan dalam Python 2 (komentar dari Blckknght yang menjelaskan mengapa Anda harus terus melakukan ini dalam 2.6+) seperti ini:

class Foo:
    pass

FooId = addId(Foo)

Dan dengan Python 3 seperti ini (tapi hati-hati menggunakannya super()di kelas Anda):

@addId
class Foo:
    pass

Jadi Anda dapat memiliki kue dan memakannya - warisan dan dekorator!

andrew cooke
sumber
4
Subkelas di dekorator berbahaya di Python 2.6+, karena merusak superpanggilan di kelas aslinya. Jika Fooada metode bernama fooyang memanggilnya super(Foo, self).foo()akan berulang tanpa batas, karena nama Footerikat ke subclass yang dikembalikan oleh dekorator, bukan kelas asli (yang tidak dapat diakses dengan nama apa pun). Argumen Python 3 super()menghindari masalah ini (saya berasumsi melalui sihir kompiler yang sama yang memungkinkannya bekerja sama sekali). Anda juga dapat mengatasi masalah dengan mendekorasi kelas secara manual dengan nama yang berbeda (seperti yang Anda lakukan pada contoh Python 2.5).
Blckknght
1
Hah. terima kasih, saya tidak tahu (saya menggunakan python 3). akan menambahkan komentar.
andrew cooke
13

Itu bukan praktik yang baik dan tidak ada mekanisme untuk melakukan itu karena itu. Cara yang tepat untuk mencapai apa yang Anda inginkan adalah warisan.

Lihatlah dokumentasi kelas .

Sedikit contoh:

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)

Dengan cara ini Boss memiliki segalanya Employee, dengan __init__metode dan anggota sendiri.

mpeterson
sumber
3
Saya kira yang saya inginkan adalah memiliki Bos agnostik dari kelas yang dikandungnya. Artinya, mungkin ada puluhan kelas berbeda yang ingin saya terapkan fitur Boss. Apakah saya pergi dengan memiliki selusin kelas yang diwarisi dari Boss?
Robert Gowland
5
@ Robert Gowland: Itu sebabnya Python memiliki banyak warisan. Ya, Anda harus mewarisi berbagai aspek dari berbagai kelas induk.
S.Lott
7
@ S.Lott: Secara umum pewarisan berganda adalah ide yang buruk, bahkan terlalu banyak tingkat warisan juga buruk. Saya akan merekomendasikan Anda untuk menjauh dari banyak warisan.
mpeterson
5
mpeterson: Apakah multiple inheritance in python lebih buruk daripada pendekatan ini? Apa yang salah dengan multiple inheritance python?
Arafangion
2
@Arafangion: Warisan berganda umumnya dianggap sebagai tanda peringatan adanya masalah. Itu membuat hierarki yang kompleks dan hubungan yang sulit diikuti. Jika domain masalah Anda cocok dengan multi inheritance, (dapatkah itu dimodelkan secara hierarkis?) Itu adalah pilihan alami bagi banyak orang. Ini berlaku untuk semua bahasa yang memungkinkan multi-warisan.
Morten Jensen
6

Saya setuju warisan lebih cocok untuk masalah yang diajukan.

Saya menemukan pertanyaan ini sangat berguna pada kelas dekorasi, terima kasih semua.

Berikut adalah beberapa contoh lain, berdasarkan jawaban lain, termasuk bagaimana pewarisan mempengaruhi hal-hal di Python 2.7, (dan @wraps , yang memelihara dokumentasi fungsi asli, dll.):

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

Seringkali Anda ingin menambahkan parameter ke dekorator Anda:

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

Output:

@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

Saya telah menggunakan fungsi dekorator, karena saya menemukan mereka lebih ringkas. Berikut kelas untuk mendekorasi kelas:

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

Versi yang lebih kuat yang memeriksa tanda kurung itu, dan berfungsi jika metode tidak ada pada kelas yang didekorasi:

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

The assertcek yang dekorator belum digunakan tanpa tanda kurung. Jika sudah, maka kelas yang sedang didekorasi dilewatkan ke msgparameter dekorator, yang menimbulkan AssertionError.

@decorate_ifhanya berlaku decoratorjika conditiondievaluasi True.

The getattr, callabletes, dan @decorate_ifdigunakan sehingga dekorator tidak pecah jika foo()metode tidak ada pada kelas yang dihiasi.

Chris
sumber
4

Sebenarnya ada penerapan dekorator kelas yang cukup baik di sini:

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

Sebenarnya saya pikir ini adalah implementasi yang cukup menarik. Karena subclass kelas yang didekorasi, ia akan berperilaku persis seperti kelas ini dalam hal-hal seperti isinstancecek.

Ini memiliki manfaat tambahan: itu tidak biasa untuk __init__pernyataan dalam Formulir django kustom untuk membuat modifikasi atau penambahan self.fieldssehingga lebih baik untuk perubahan self.fieldsterjadi setelah semua __init__telah berjalan untuk kelas yang bersangkutan.

Sangat pintar.

Namun, di kelas Anda, Anda benar-benar ingin dekorasi mengubah konstruktor, yang menurut saya bukan kasus yang baik untuk dekorator kelas.

Jordan Reiter
sumber
0

Berikut adalah contoh yang menjawab pertanyaan mengembalikan parameter kelas. Selain itu, masih menghormati rantai warisan, yaitu hanya parameter kelas itu sendiri yang dikembalikan. Fungsi get_paramsditambahkan sebagai contoh sederhana, tetapi fungsi lain dapat ditambahkan berkat modul inspeksi.

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']
Patricio Astudillo
sumber