Bagaimana saya bisa secara dinamis membuat kelas turunan dari kelas dasar

95

Misalnya saya memiliki kelas dasar sebagai berikut:

class BaseClass(object):
    def __init__(self, classtype):
        self._type = classtype

Dari kelas ini saya mendapatkan beberapa kelas lain, misalnya

class TestClass(BaseClass):
    def __init__(self):
        super(TestClass, self).__init__('Test')

class SpecialClass(BaseClass):
    def __init__(self):
        super(TestClass, self).__init__('Special')

Apakah ada cara yang bagus dan pythonic untuk membuat kelas-kelas itu secara dinamis dengan panggilan fungsi yang menempatkan kelas baru ke dalam ruang lingkup saya saat ini, seperti:

foo(BaseClass, "My")
a = MyClass()
...

Karena akan ada komentar dan pertanyaan mengapa saya membutuhkan ini: Semua kelas turunan memiliki struktur internal yang sama persis dengan perbedaannya, bahwa konstruktor mengambil sejumlah argumen yang sebelumnya tidak ditentukan. Jadi, misalnya, MyClassmengambil kata kunci asementara konstruktor kelas TestClassmengambil bdan c.

inst1 = MyClass(a=4)
inst2 = MyClass(a=5)
inst3 = TestClass(b=False, c = "test")

Tetapi mereka TIDAK PERNAH menggunakan tipe kelas sebagai argumen input seperti

inst1 = BaseClass(classtype = "My", a=4)

Saya mendapatkan ini untuk bekerja tetapi lebih suka cara lain, yaitu objek kelas yang dibuat secara dinamis.

Alex
sumber
Hanya untuk memastikan, Anda ingin tipe instance berubah bergantung pada argumen yang diberikan? Seperti jika saya memberikannya, aitu akan selalu MyClassdan TestClasstidak akan pernah mengambil a? Mengapa tidak mendeklarasikan semua 3 argumen BaseClass.__init__()tetapi default semuanya None? def __init__(self, a=None, b=None, C=None)?
Acattle
Saya tidak dapat mendeklarasikan apa pun di kelas dasar, karena saya tidak tahu semua argumen yang mungkin saya gunakan. Saya mungkin memiliki 30 klas berbeda dengan masing-masing 5 argumen berbeda, jadi mendeklarasikan 150 argumen dalam konstruktor bukanlah solusi.
Alex

Jawaban:

145

Sedikit kode ini memungkinkan Anda membuat kelas baru dengan nama dinamis dan nama parameter. Verifikasi parameter __init__tidak mengizinkan parameter yang tidak diketahui, jika Anda memerlukan verifikasi lain, seperti tipe, atau wajib, cukup tambahkan logika di sana:

class BaseClass(object):
    def __init__(self, classtype):
        self._type = classtype

def ClassFactory(name, argnames, BaseClass=BaseClass):
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            # here, the argnames variable is the one passed to the
            # ClassFactory call
            if key not in argnames:
                raise TypeError("Argument %s not valid for %s" 
                    % (key, self.__class__.__name__))
            setattr(self, key, value)
        BaseClass.__init__(self, name[:-len("Class")])
    newclass = type(name, (BaseClass,),{"__init__": __init__})
    return newclass

Dan ini berfungsi seperti ini, misalnya:

>>> SpecialClass = ClassFactory("SpecialClass", "a b c".split())
>>> s = SpecialClass(a=2)
>>> s.a
2
>>> s2 = SpecialClass(d=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __init__
TypeError: Argument d not valid for SpecialClass

Saya melihat Anda meminta untuk memasukkan nama dinamis dalam lingkup penamaan - sekarang, itu tidak dianggap sebagai praktik yang baik dengan Python - Anda memiliki nama variabel, yang dikenal pada waktu pengkodean, atau data - dan nama yang dipelajari dalam runtime lebih banyak " data "daripada" variabel "-

Jadi, Anda bisa menambahkan kelas Anda ke kamus dan menggunakannya dari sana:

name = "SpecialClass"
classes = {}
classes[name] = ClassFactory(name, params)
instance = classes[name](...)

Dan jika desain Anda benar-benar membutuhkan nama yang akan dimasukkan dalam cakupan, lakukan hal yang sama, tetapi gunakan kamus yang ditampilkan oleh globals() panggilan alih-alih kamus acak:

name = "SpecialClass"
globals()[name] = ClassFactory(name, params)
instance = SpecialClass(...)

(Memang mungkin bagi fungsi pabrik kelas untuk memasukkan nama secara dinamis pada cakupan global pemanggil - tetapi itu praktik yang lebih buruk lagi, dan tidak kompatibel di seluruh implementasi Python. Cara melakukannya adalah dengan mendapatkan pemanggil bingkai eksekusi, melalui sys._getframe (1) dan menetapkan nama kelas dalam kamus global bingkai dalam f_globalsatributnya).

update, tl; dr: Jawaban ini telah menjadi populer, masih sangat spesifik untuk badan pertanyaan. Jawaban umum tentang bagaimana "secara dinamis membuat kelas turunan dari kelas dasar" dengan Python adalah panggilan sederhana untuk typemeneruskan nama kelas baru, tupel dengan baseclass (es) dan __dict__tubuh untuk kelas baru -seperti ini:

>>> new_class = type("NewClassName", (BaseClass,), {"new_method": lambda self: ...})

memperbarui
Siapa pun yang membutuhkan ini juga harus memeriksa proyek dill - ia mengklaim dapat membuat kelas acar dan membongkar seperti yang dilakukan acar pada objek biasa, dan telah hidup untuk itu dalam beberapa pengujian saya.

jsbueno
sumber
2
Jika saya ingat dengan benar, BaseClass.__init__()akan lebih baik sebagai yang lebih umum super(self.__class__).__init__(), yang bermain lebih baik ketika kelas-kelas baru disubkelas. (Referensi: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Eric O Lebigot
@EOL: Ini akan untuk kelas yang dideklarasikan secara statis - tetapi karena Anda tidak memiliki nama kelas yang sebenarnya untuk hardcode sebagai parameter pertama ke Super, itu akan membutuhkan banyak kerumitan. Coba ganti dengan di superatas dan buat subclass dari kelas yang dibuat secara dinamis untuk memahaminya; Dan, di sisi lain, dalam hal ini Anda dapat memiliki baseclass sebagai objek umum yang akan dipanggil __init__.
jsbueno
Sekarang saya punya waktu untuk melihat solusi yang disarankan, tetapi itu bukan yang saya inginkan. Pertama, sepertinya __init__dari BaseClassdipanggil dengan satu argumen, tetapi pada kenyataannya BaseClass.__init__selalu mengambil daftar argumen kata kunci yang berubah-ubah. Kedua, solusi di atas menetapkan semua nama parameter yang diizinkan sebagai atribut, yang bukan yang saya inginkan. Setiap argumen HARUS pergi ke BaseClass, tapi yang mana yang saya tahu saat membuat kelas turunan. Saya mungkin akan memperbarui pertanyaan atau mengajukan pertanyaan yang lebih tepat untuk membuatnya lebih jelas.
Alex
@jsbueno: Benar, menggunakan yang super()saya sebutkan memberi TypeError: must be type, not SubSubClass. Jika saya mengerti benar, ini berasal dari argumen pertama selfdari __init__(), yang merupakan SubSubClassdi mana typeobjek diharapkan: ini tampaknya berkaitan dengan fakta super(self.__class__)adalah objek yang super terikat. Apa __init__()metodenya? Saya tidak yakin metode mana yang membutuhkan tipe argumen pertama type. Bisakah Anda menjelaskan? (Catatan tambahan: super()pendekatan saya memang tidak masuk akal, di sini, karena __init__()memiliki tanda tangan variabel.)
Eric O Lebigot
1
@EOL: Masalah utamanya adalah jika Anda membuat subkelas lain dari kelas faktorisasi: self .__ class__ akan merujuk ke subkelas tersebut, bukan kelas di mana "super" dipanggil - dan Anda mendapatkan rekursi tak terbatas.
jsbueno
91

type() adalah fungsi yang membuat kelas (dan di sub-kelas tertentu):

def set_x(self, value):
    self.x = value

SubClass = type('SubClass', (BaseClass,), {'set_x': set_x})
# (More methods can be put in SubClass, including __init__().)

obj = SubClass()
obj.set_x(42)
print obj.x  # Prints 42
print isinstance(obj, BaseClass)  # True
Eric O Lebigot
sumber
Saat mencoba memahami contoh ini menggunakan Python 2.7, saya mendapatkan jawaban TypeErroryang mengatakan __init__() takes exactly 2 arguments (1 given). Saya menemukan bahwa menambahkan sesuatu (apa saja?) Untuk mengisi kekosongan sudah cukup. Misalnya, obj = SubClass('foo')berjalan tanpa kesalahan.
DaveL17
Ini normal, karena SubClassmerupakan sub-kelas dari BaseClassdalam pertanyaan dan BaseClassmengambil parameter ( classtype, yang ada 'foo'dalam contoh Anda).
Eric O Lebigot
@EricOLebigot Dapatkah saya menelepon ke init dari BaseClass di sini? suka dengan super?
mikazz
-3

Untuk membuat kelas dengan nilai atribut dinamis, periksa kode di bawah ini. NB. Ini adalah potongan kode dalam bahasa pemrograman python

def create_class(attribute_data, **more_data): # define a function with required attributes
    class ClassCreated(optional extensions): # define class with optional inheritance
          attribute1 = adattribute_data # set class attributes with function parameter
          attribute2 = more_data.get("attribute2")

    return ClassCreated # return the created class

# use class

myclass1 = create_class("hello") # *generates a class*
AdefemiGreat
sumber