ฉันจะสร้างคลาสที่ได้รับแบบไดนามิกจากคลาสพื้นฐานได้อย่างไร


95

ตัวอย่างเช่นฉันมีคลาสพื้นฐานดังนี้:

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

จากคลาสนี้ฉันได้รับคลาสอื่น ๆ อีกมากมายเช่น

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

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

มีวิธี pythonic ที่ดีในการสร้างคลาสเหล่านั้นแบบไดนามิกโดยการเรียกใช้ฟังก์ชันที่ทำให้คลาสใหม่อยู่ในขอบเขตปัจจุบันของฉันเช่น:

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

เนื่องจากจะมีความคิดเห็นและคำถามว่าทำไมฉันถึงต้องการสิ่งนี้: คลาสที่ได้รับทั้งหมดมีโครงสร้างภายในที่เหมือนกันโดยมีความแตกต่างกันนั่นคือตัวสร้างจะใช้อาร์กิวเมนต์ที่ไม่ได้กำหนดไว้ก่อนหน้า ดังนั้นสำหรับตัวอย่างเช่นMyClassใช้เวลาคำหลักaในขณะที่คอนสตรัคของชั้นTestClassใช้เวลาและbc

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

แต่ไม่ควรใช้ประเภทของคลาสเป็นอาร์กิวเมนต์อินพุตเช่น

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

ฉันทำให้สิ่งนี้ใช้งานได้ แต่ต้องการวิธีอื่นนั่นคือคลาสออบเจ็กต์ที่สร้างขึ้นแบบไดนามิก


เพื่อให้แน่ใจว่าคุณต้องการให้ประเภทของอินสแตนซ์เปลี่ยนไปตามอาร์กิวเมนต์ที่ให้มาหรือไม่? เช่นถ้าฉันให้aมันจะเป็นเสมอMyClassและTestClassจะไม่ใช้a? ทำไมไม่เพียงแค่ประกาศทั้ง 3 อาร์กิวเมนต์BaseClass.__init__()แต่เริ่มต้นทั้งหมดเป็นNone? def __init__(self, a=None, b=None, C=None)เหรอ?
แข่งขัน

ฉันไม่สามารถประกาศอะไรในคลาสพื้นฐานได้เนื่องจากฉันไม่ทราบข้อโต้แย้งทั้งหมดที่ฉันอาจใช้ ฉันอาจมี 30 clas ที่แตกต่างกันโดยมี 5 อาร์กิวเมนต์ที่แตกต่างกันดังนั้นการประกาศ 150 อาร์กิวเมนต์ในคอนสตรัคเตอร์จึงไม่ใช่วิธีแก้ปัญหา
Alex

คำตอบ:


145

บิตของโค้ดนี้ช่วยให้คุณสร้างคลาสใหม่ด้วยชื่อไดนามิกและชื่อพารามิเตอร์ การตรวจสอบพารามิเตอร์ใน__init__ไม่อนุญาตให้ใช้พารามิเตอร์ที่ไม่รู้จักหากคุณต้องการการตรวจสอบอื่น ๆ เช่นประเภทหรือว่าจำเป็นให้เพิ่มตรรกะที่นั่น:

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

และสิ่งนี้ได้ผลเช่น:

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

ผมเห็นคุณจะขอใส่ชื่อแบบไดนามิกในขอบเขตการตั้งชื่อ - ตอนนี้ว่าจะไม่ถือว่าเป็นวิธีที่ดีในหลาม - คุณอาจมีชื่อตัวแปรรู้จักกันในเวลาเขียนโปรแกรมหรือข้อมูล - และชื่อได้เรียนรู้ในการรันไทม์มีมากขึ้น " ข้อมูล "กว่า" ตัวแปร "-

ดังนั้นคุณสามารถเพิ่มชั้นเรียนของคุณลงในพจนานุกรมและใช้จากที่นั่น:

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

และหากการออกแบบของคุณต้องการชื่อที่อยู่ในขอบเขตจริงๆให้ทำเช่นเดียวกัน แต่ใช้พจนานุกรมที่ส่งคืนโดยการglobals() เรียกแทนพจนานุกรมตามอำเภอใจ:

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

(แน่นอนว่าเป็นไปได้ที่ฟังก์ชันโรงงานคลาสจะแทรกชื่อแบบไดนามิกในขอบเขตทั่วโลกของผู้โทร - แต่นั่นเป็นการปฏิบัติที่แย่กว่านั้นและไม่สามารถใช้งานร่วมกันได้กับการใช้งาน Python วิธีการทำเช่นนั้นก็คือการเรียกผู้โทรเข้า เฟรมการดำเนินการผ่านsys._getframe (1)และการตั้งชื่อคลาสในพจนานุกรมส่วนกลางของเฟรมในf_globalsแอตทริบิวต์)

update, tl; dr:คำตอบนี้ได้รับความนิยม แต่ยังคงมีความเฉพาะเจาะจงมากสำหรับเนื้อหาคำถาม คำตอบทั่วไปเกี่ยวกับวิธี "สร้างคลาสที่ได้รับแบบไดนามิกจากคลาสพื้นฐาน" ใน Python เป็นการเรียกง่ายๆในการtypeส่งผ่านชื่อคลาสใหม่ทูเปิลที่มีคลาสพื้นฐาน (es) และ__dict__เนื้อความสำหรับคลาสใหม่ดังนี้:

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

อัปเดต
ใครก็ตามที่ต้องการสิ่งนี้ควรตรวจสอบโครงการผักชีฝรั่งด้วยซึ่งอ้างว่าสามารถดองและเลิกใช้คลาสได้เช่นเดียวกับการดองกับวัตถุธรรมดาและเคยใช้มันในการทดสอบของฉัน


2
ถ้าจำไม่ผิดBaseClass.__init__()จะดีกว่าในฐานะคนทั่วไปsuper(self.__class__).__init__()ซึ่งเล่นได้ดีกว่าเมื่อคลาสใหม่เป็นคลาสย่อย (อ้างอิง: rhettinger.wordpress.com/2011/05/26/super-cons ถือว่า-super )
Eric O Lebigot

@EOL: สำหรับคลาสที่ประกาศแบบคงที่ - แต่เนื่องจากคุณไม่มีชื่อคลาสที่แท้จริงสำหรับฮาร์ดโค้ดเป็นพารามิเตอร์แรกของ Super จึงต้องมีการเต้นรอบ ๆ ลองแทนที่ด้วยsuperด้านบนและสร้างคลาสย่อยของคลาสที่สร้างแบบไดนามิกเพื่อทำความเข้าใจ และในทางกลับกันในกรณีนี้คุณสามารถมี BaseClass เป็น objectfrom __init__ทั่วไปซึ่งจะเรียก
jsbueno

ตอนนี้ฉันมีเวลาดูวิธีแก้ปัญหาที่แนะนำ แต่มันก็ยังไม่ตรงตามที่ฉันต้องการ ประการแรกมันดูเหมือน__init__ของBaseClassเรียกว่ามีอาร์กิวเมนต์หนึ่ง แต่ในความเป็นจริงBaseClass.__init__มักจะใช้รายการโดยพลการของข้อโต้แย้งคำหลัก ประการที่สองวิธีแก้ปัญหาด้านบนตั้งชื่อพารามิเตอร์ที่อนุญาตทั้งหมดเป็นแอตทริบิวต์ซึ่งไม่ใช่สิ่งที่ฉันต้องการ มีข้อโต้แย้งใด ๆ ที่ต้องไปBaseClassแต่สิ่งใดที่ฉันรู้เมื่อสร้างคลาสที่ได้รับ ฉันอาจจะอัปเดตคำถามหรือถามคำถามที่แม่นยำยิ่งขึ้นเพื่อให้ชัดเจนขึ้น
Alex

@jsbueno: ขวาใช้ผมได้รับการกล่าวขวัญให้super() TypeError: must be type, not SubSubClassถ้าผมเข้าใจอย่างถูกต้องนี้มาจากอาร์กิวเมนต์แรกselfของ__init__()ซึ่งเป็นSubSubClassที่typeวัตถุที่คาดว่า: นี้ดูเหมือนว่าเกี่ยวข้องกับความจริงที่super(self.__class__)เป็นวัตถุที่ไม่ถูกผูกซุปเปอร์ มันคืออะไร__init__()วิธี? ฉันไม่แน่ใจว่าวิธีการใดที่อาจต้องใช้อาร์กิวเมนต์ประเภทtypeแรก คุณช่วยอธิบายได้ไหม (หมายเหตุด้านข้าง: super()แนวทางของฉันไม่สมเหตุสมผลที่นี่เพราะ__init__()มีลายเซ็นตัวแปร)
Eric O Lebigot

1
@EOL: ปัญหาใหญ่จริง ๆ แล้วถ้าคุณสร้างคลาสย่อยอื่นของคลาสที่แยกตัวประกอบ: self .__ class__ จะอ้างถึงคลาสย่อยนั้นไม่ใช่คลาสที่เรียกว่า "super" - และคุณจะได้รับการเรียกซ้ำไม่สิ้นสุด
jsbueno

91

type() เป็นฟังก์ชันที่สร้างคลาส (และในคลาสย่อยเฉพาะ):

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

ในการพยายามทำความเข้าใจตัวอย่างนี้โดยใช้ Python 2.7 ฉันได้สิ่งTypeErrorที่กล่าว__init__() takes exactly 2 arguments (1 given)มา ฉันพบว่าการเพิ่มบางสิ่ง (อะไรก็ได้?) เพื่อเติมเต็มช่องว่างก็เพียงพอแล้ว ตัวอย่างเช่นobj = SubClass('foo')รันโดยไม่มีข้อผิดพลาด
DaveL17

นี่เป็นเรื่องปกติเนื่องจากSubClassเป็นคลาสย่อยของBaseClassคำถามและBaseClassรับพารามิเตอร์ ( classtypeซึ่งอยู่'foo'ในตัวอย่างของคุณ)
Eric O Lebigot

@EricOLebigot ฉันสามารถเรียกใช้ BaseClass ที่นี่ได้หรือไม่? ชอบสุด ๆ ?
mikazz

-3

หากต้องการสร้างคลาสที่มีค่าแอตทริบิวต์แบบไดนามิกให้ชำระเงินรหัสด้านล่าง NB. นี่คือตัวอย่างโค้ดในภาษาโปรแกรมไพ ธ อน

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*
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.