metaclasses ใน Python คืออะไร


คำตอบ:


2869

เมตาคลาสเป็นคลาสของคลาส คลาสจะกำหนดพฤติกรรมของอินสแตนซ์ของคลาส (เช่นวัตถุ) ในขณะที่ metaclass กำหนดว่าคลาสจะทำงานอย่างไร คลาสเป็นตัวอย่างของ metaclass

ในขณะที่อยู่ใน Python คุณสามารถใช้ callables ตามอำเภอใจสำหรับ metaclasses (เช่นJerubแสดง) วิธีที่ดีกว่าคือทำให้คลาสจริง typeเป็น metaclass ปกติใน Python typeเป็นคลาสและเป็นประเภทของตัวเอง คุณจะไม่สามารถสร้างบางสิ่งบางอย่างเช่นtypePython ใหม่ได้ แต่ Python จะโกงนิดหน่อย เพื่อสร้าง metaclass ของคุณเองในหลามจริงๆเพียงแค่คุณต้องการที่จะ typesubclass

เมตาคลาสมักใช้เป็นคลาสโรงงาน เมื่อคุณสร้างวัตถุโดยการเรียกคลาส Python สร้างคลาสใหม่ (เมื่อดำเนินการคำสั่ง 'คลาส') โดยการเรียกเมตาคลาส เมื่อรวมกับวิธีปกติ__init__และ__new__เมธอด metaclasses จึงอนุญาตให้คุณทำ 'สิ่งพิเศษ' เมื่อสร้างคลาสเช่นการลงทะเบียนคลาสใหม่ด้วยรีจิสทรีหรือแทนที่คลาสด้วยสิ่งอื่นทั้งหมด

เมื่อclassคำสั่งถูกดำเนินการ Python จะเรียกใช้งานเนื้อหาของclassคำสั่งก่อนว่าเป็นบล็อกปกติของรหัส เนมสเปซที่เป็นผลลัพธ์ (dict) เก็บคุณสมบัติของ class-to-be metaclass ถูกกำหนดโดยการดูที่ baseclasses ของ class-to-be (metaclasses ถูกสืบทอด), ที่__metaclass__แอ็ตทริบิวต์ของ class-to-be (ถ้ามี) หรือ__metaclass__ตัวแปรโกลบอล เมตาคลาสจะถูกเรียกด้วยชื่อเบสและคุณลักษณะของคลาสเพื่อสร้างอินสแตนซ์

อย่างไรก็ตาม metaclasses จะกำหนดประเภทของชั้นเรียนไม่ใช่เฉพาะโรงงานเท่านั้นดังนั้นคุณสามารถทำอะไรกับมันได้มากขึ้น ตัวอย่างเช่นคุณสามารถกำหนดวิธีการปกติใน metaclass เมตาคลาสของเมธอดเหล่านี้เป็นเหมือนคลาสเมธอดที่สามารถเรียกใช้บนคลาสได้โดยไม่มีอินสแตนซ์ แต่ก็ไม่เหมือนคลาสเมธอดที่ไม่สามารถเรียกใช้บนอินสแตนซ์ของคลาสได้ type.__subclasses__()เป็นตัวอย่างของวิธีการในtypemetaclass นอกจากนี้คุณยังสามารถกำหนดปกติ 'วิเศษ' วิธีการเช่น__add__, __iter__และ__getattr__เพื่อให้การดำเนินการหรือการเปลี่ยนแปลงวิธีการที่จะทำงานระดับ

นี่คือตัวอย่างโดยรวมของบิตและชิ้นส่วน:

def make_hook(f):
    """Decorator to turn 'foo' method into '__foo__'"""
    f.is_hook = 1
    return f

class MyType(type):
    def __new__(mcls, name, bases, attrs):

        if name.startswith('None'):
            return None

        # Go over attributes and see if they should be renamed.
        newattrs = {}
        for attrname, attrvalue in attrs.iteritems():
            if getattr(attrvalue, 'is_hook', 0):
                newattrs['__%s__' % attrname] = attrvalue
            else:
                newattrs[attrname] = attrvalue

        return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

    def __init__(self, name, bases, attrs):
        super(MyType, self).__init__(name, bases, attrs)

        # classregistry.register(self, self.interfaces)
        print "Would register class %s now." % self

    def __add__(self, other):
        class AutoClass(self, other):
            pass
        return AutoClass
        # Alternatively, to autogenerate the classname as well as the class:
        # return type(self.__name__ + other.__name__, (self, other), {})

    def unregister(self):
        # classregistry.unregister(self)
        print "Would unregister class %s now." % self

class MyObject:
    __metaclass__ = MyType


class NoneSample(MyObject):
    pass

# Will print "NoneType None"
print type(NoneSample), repr(NoneSample)

class Example(MyObject):
    def __init__(self, value):
        self.value = value
    @make_hook
    def add(self, other):
        return self.__class__(self.value + other.value)

# Will unregister the class
Example.unregister()

inst = Example(10)
# Will fail with an AttributeError
#inst.unregister()

print inst + inst
class Sibling(MyObject):
    pass

ExampleSibling = Example + Sibling
# ExampleSibling is now a subclass of both Example and Sibling (with no
# content of its own) although it will believe it's called 'AutoClass'
print ExampleSibling
print ExampleSibling.__mro__

13
class A(type):pass<NEWLINE>class B(type,metaclass=A):pass<NEWLINE>b.__class__ = b
pppery

20
ppperry เห็นได้ชัดว่าเขาหมายถึงคุณไม่สามารถสร้างประเภทโดยไม่ต้องใช้ประเภทตัวเองเป็น metaclass ซึ่งยุติธรรมพอจะพูดได้
Holle van

3
ไม่ควรยกเลิกการลงทะเบียน () ถูกเรียกโดยอินสแตนซ์ของคลาสตัวอย่าง?
Ciasto piekarz

5
โปรดทราบว่า__metaclass__ไม่รองรับ Python 3 ในการใช้ Python 3 class MyObject(metaclass=MyType)ดูpython.org/dev/peps/pep-3115และคำตอบด้านล่าง
BlackShift

2
เอกสารอธิบายวิธี metaclass ถูกเลือก metaclass นั้นไม่ได้รับการถ่ายทอดมามากพอสมควร หากคุณระบุ metaclass จะต้องเป็นประเภทย่อยของ metaclass คลาสพื้นฐานแต่ละรายการ มิฉะนั้นคุณจะใช้ metaclass คลาสพื้นฐานที่เป็นประเภทย่อยของ metaclass คลาสพื้นฐานอื่น ๆ โปรดทราบว่าเป็นไปได้ที่ไม่พบ metaclass ที่ถูกต้องและข้อกำหนดจะล้มเหลว
chepner

6817

คลาสเป็นวัตถุ

ก่อนที่จะทำความเข้าใจกับ metaclasses คุณจะต้องเรียนรู้หลักใน Python และ Python มีความคิดที่แปลกประหลาดมากเกี่ยวกับคลาสที่ยืมมาจากภาษา Smalltalk

ในภาษาส่วนใหญ่ชั้นเรียนเป็นเพียงส่วนหนึ่งของรหัสที่อธิบายถึงวิธีการสร้างวัตถุ นั่นเป็นความจริงใน Python เช่นกัน:

>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

แต่คลาสมีมากกว่านั้นใน Python คลาสเป็นวัตถุด้วย

ใช่วัตถุ

ทันทีที่คุณใช้คำสำคัญclassPython จะเรียกใช้งานมันและสร้าง OBJECT การเรียนการสอน

>>> class ObjectCreator(object):
...       pass
...

สร้างในหน่วยความจำวัตถุที่มีชื่อ "ObjectCreator"

วัตถุนี้ (ชั้น) เป็นตัวเองความสามารถในการสร้างวัตถุ (อินสแตนซ์) และนี่คือเหตุผลที่มันเป็นชั้นเรียน

แต่ก็ยังเป็นวัตถุดังนั้น:

  • คุณสามารถกำหนดให้ตัวแปร
  • คุณสามารถคัดลอก
  • คุณสามารถเพิ่มคุณสมบัติเข้าไป
  • คุณสามารถผ่านมันเป็นพารามิเตอร์ฟังก์ชันได้

เช่น:

>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

การสร้างคลาสแบบไดนามิก

เนื่องจากคลาสเป็นวัตถุคุณจึงสามารถสร้างมันได้ทันทีเหมือนกับวัตถุใด ๆ

ก่อนอื่นคุณสามารถสร้างคลาสในฟังก์ชันโดยใช้class:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>

แต่มันไม่ได้เป็นแบบไดนามิกเพราะคุณยังต้องเขียนทั้งชั้นด้วยตัวเอง

เนื่องจากคลาสเป็นวัตถุจึงต้องถูกสร้างโดยบางสิ่ง

เมื่อคุณใช้classคำสำคัญ Python จะสร้างวัตถุนี้โดยอัตโนมัติ แต่เช่นเดียวกับสิ่งของส่วนใหญ่ใน Python มันทำให้คุณสามารถทำมันเองได้

จำฟังก์ชั่นได้typeหรือไม่ ฟังก์ชั่นเก่าที่ดีที่ช่วยให้คุณรู้ว่าวัตถุชนิดใด:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

มันtypeมีความสามารถที่แตกต่างกันโดยสิ้นเชิงมันสามารถสร้างคลาสได้ทันที typeสามารถนำคำอธิบายของคลาสเป็นพารามิเตอร์และส่งคืนคลาสได้

(ฉันรู้ว่ามันงี่เง่าที่ฟังก์ชั่นเดียวกันสามารถมีการใช้งานสองแบบที่แตกต่างกันโดยสิ้นเชิงตามพารามิเตอร์ที่คุณส่งไปมันเป็นปัญหาเนื่องจากความเข้ากันได้ย้อนหลังใน Python)

type ทำงานด้วยวิธีนี้:

type(name, bases, attrs)

ที่ไหน:

  • name: ชื่อของคลาส
  • bases: tuple ของคลาส parent (สำหรับการสืบทอดสามารถว่างได้)
  • attrs: พจนานุกรมที่มีชื่อและค่าของแอตทริบิวต์

เช่น:

>>> class MyShinyClass(object):
...       pass

สามารถสร้างได้ด้วยตนเองด้วยวิธีนี้:

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>

คุณจะสังเกตเห็นว่าเราใช้ "MyShinyClass" เป็นชื่อของคลาสและเป็นตัวแปรเพื่อเก็บการอ้างอิงคลาส พวกเขาอาจแตกต่างกัน แต่ไม่มีเหตุผลที่จะทำให้สิ่งต่าง ๆ ซับซ้อน

typeยอมรับพจนานุกรมเพื่อกำหนดคุณสมบัติของชั้นเรียน ดังนั้น:

>>> class Foo(object):
...       bar = True

สามารถแปลเป็น:

>>> Foo = type('Foo', (), {'bar':True})

และใช้เป็นคลาสปกติ:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True

และแน่นอนคุณสามารถสืบทอดจากมันได้ดังนั้น:

>>>   class FooChild(Foo):
...         pass

อยากจะเป็น:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar is inherited from Foo
True

ในที่สุดคุณจะต้องเพิ่มวิธีการในชั้นเรียนของคุณ เพียงกำหนดฟังก์ชั่นที่มีลายเซ็นที่ถูกต้องและกำหนดเป็นแอททริบิวต์

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

และคุณสามารถเพิ่มวิธีการมากขึ้นหลังจากที่คุณสร้างคลาสแบบไดนามิกเช่นเดียวกับการเพิ่มวิธีการไปยังวัตถุคลาสที่สร้างขึ้นตามปกติ

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

คุณเห็นว่าเรากำลังจะไปไหน: ใน Python คลาสเป็นวัตถุและคุณสามารถสร้างคลาสได้ทันทีแบบไดนามิก

นี่คือสิ่งที่ Python ทำเมื่อคุณใช้คำหลักclassและทำได้โดยใช้ metaclass

metaclasses คืออะไร (ในที่สุด)

Metaclasses เป็น 'สิ่ง' ที่สร้างคลาส

คุณกำหนดคลาสเพื่อสร้างวัตถุใช่ไหม

แต่เราเรียนรู้ว่าคลาส Python เป็นวัตถุ

metaclasses เป็นสิ่งที่สร้างวัตถุเหล่านี้ พวกมันคือคลาสของคลาสคุณสามารถนึกภาพได้ด้วยวิธีนี้:

MyClass = MetaClass()
my_object = MyClass()

คุณเคยเห็นที่typeให้คุณทำสิ่งนี้:

MyClass = type('MyClass', (), {})

เป็นเพราะฟังก์ชั่นtypeนั้นเป็นเมตาคลาสจริงๆ typeเป็น metaclass Python ใช้เพื่อสร้างคลาสทั้งหมดที่อยู่เบื้องหลัง

ตอนนี้คุณสงสัยว่าทำไมมันถึงเขียนเป็นตัวพิมพ์เล็กTypeใช่ไหม?

ฉันเดาว่ามันเป็นเรื่องของความสอดคล้องกับstrคลาสที่สร้างวัตถุสตริงและintคลาสที่สร้างวัตถุจำนวนเต็ม typeเป็นเพียงคลาสที่สร้างวัตถุคลาส

คุณจะเห็นว่าโดยการตรวจสอบ__class__คุณลักษณะ

ทุกอย่างและฉันหมายถึงทุกสิ่งเป็นวัตถุใน Python นั่นรวมถึง ints, สตริง, ฟังก์ชั่นและชั้นเรียน พวกเขาทั้งหมดเป็นวัตถุ และทั้งหมดถูกสร้างขึ้นจากชั้นเรียน:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

ตอนนี้สิ่งที่เป็น__class__ของใด ๆ__class__?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

ดังนั้น metaclass เป็นเพียงสิ่งที่สร้างวัตถุคลาส

คุณสามารถเรียกมันว่า 'โรงงานคลาส' หากคุณต้องการ

type เป็น metaclass ที่ใช้ใน Python แต่แน่นอนว่าคุณสามารถสร้าง metaclass ของคุณเองได้

__metaclass__แอตทริบิวต์

ใน Python 2 คุณสามารถเพิ่ม__metaclass__แอตทริบิวต์เมื่อคุณเขียนคลาส (ดูหัวข้อถัดไปสำหรับไวยากรณ์ Python 3):

class Foo(object):
    __metaclass__ = something...
    [...]

ถ้าคุณทำเช่นนั้นงูใหญ่จะใช้ metaclass Fooเพื่อสร้างชั้นเรียน

ระวังมันยุ่งยาก

คุณเขียนclass Foo(object)ก่อน แต่คลาสอ็อบเจ็กต์Fooยังไม่ได้สร้างในหน่วยความจำ

Python จะค้นหา__metaclass__คำจำกัดความของคลาส Fooหากพบก็จะใช้มันเพื่อสร้างชั้นวัตถุ ถ้าไม่มีก็จะใช้ typeในการสร้างชั้นเรียน

อ่านหลาย ๆ ครั้ง

เมื่อคุณทำ:

class Foo(Bar):
    pass

Python ทำสิ่งต่อไปนี้:

มี__metaclass__แอตทริบิวต์Fooหรือไม่

ถ้าใช่สร้างวัตถุในหน่วยความจำชั้นเรียน (ฉันกล่าวว่าวัตถุชั้น, อยู่กับฉันที่นี่) ที่มีชื่อโดยใช้สิ่งที่อยู่ในFoo__metaclass__

ถ้า Python หาไม่เจอ__metaclass__มันจะมองหา__metaclass__ที่ MODULE level และพยายามทำแบบเดียวกัน (แต่สำหรับคลาสที่ไม่ได้รับสิ่งใดโดยเฉพาะคลาสแบบเก่า)

จากนั้นหากไม่พบสิ่งใด__metaclass__เลยมันจะใช้เมตาคลาสBarของตัวเอง (ซึ่งเป็นพาเรนต์แรก) (ซึ่งอาจเป็นค่าเริ่มต้นtype) เพื่อสร้างคลาสวัตถุ

ระวังที่นี่ว่า__metaclass__จะไม่ได้รับการถ่ายทอดแอตทริบิวต์, metaclass ของผู้ปกครอง ( Bar.__class__) จะเป็น หากBarใช้__metaclass__แอตทริบิวต์ที่สร้างBarด้วยtype()(และไม่type.__new__()) คลาสย่อยจะไม่รับการทำงานนั้น

ตอนนี้คำถามใหญ่ก็คือคุณจะใส่__metaclass__อะไรลงไป

คำตอบคือ: สิ่งที่สามารถสร้างคลาสได้

และสามารถสร้างชั้นเรียนได้อย่างไร typeหรือสิ่งใดก็ตามที่ subclasses หรือใช้มัน

Metaclasses ใน Python 3

ไวยากรณ์ในการตั้งค่า metaclass มีการเปลี่ยนแปลงใน Python 3:

class Foo(object, metaclass=something):
    ...

เช่น__metaclass__ไม่มีการใช้งานแอตทริบิวต์อีกต่อไปเพื่อสนับสนุนอาร์กิวเมนต์ของคำหลักในรายการของคลาสพื้นฐาน

พฤติกรรมของ metaclasses แต่อยู่ส่วนใหญ่เหมือนกัน

สิ่งหนึ่งที่เพิ่มให้กับ metaclasses ใน python 3 ก็คือคุณสามารถส่งแอตทริบิวต์เป็นอาร์กิวเมนต์คำหลักไปยัง metaclass ได้เช่น:

class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2):
    ...

อ่านหัวข้อด้านล่างเพื่อดูว่าหลามจัดการอย่างไร

metaclasses ที่กำหนดเอง

วัตถุประสงค์หลักของ metaclass คือการเปลี่ยนคลาสโดยอัตโนมัติเมื่อสร้างขึ้น

คุณมักจะทำสิ่งนี้สำหรับ API ที่คุณต้องการสร้างคลาสที่ตรงกับบริบทปัจจุบัน

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

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

โชคดีที่__metaclass__สามารถเรียกได้ว่าจริง ๆ แล้วมันไม่จำเป็นต้องเป็นคลาสที่เป็นทางการ (ฉันรู้ว่าบางสิ่งที่มี 'คลาส' ในชื่อไม่จำเป็นต้องเป็นคลาสรูปที่ไป ... แต่มันมีประโยชน์)

ดังนั้นเราจะเริ่มต้นด้วยตัวอย่างง่ายๆโดยใช้ฟังก์ชั่น

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attrs):
    """
      Return a class object, with the list of its attribute turned
      into uppercase.
    """
    # pick up any attribute that doesn't start with '__' and uppercase it
    uppercase_attrs = {
        attr if attr.startswith("__") else attr.upper(): v
        for attr, v in future_class_attrs.items()
    }

    # let `type` do the class creation
    return type(future_class_name, future_class_parents, uppercase_attrs)

__metaclass__ = upper_attr # this will affect all classes in the module

class Foo(): # global __metaclass__ won't work with "object" though
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

ตรวจสอบกัน:

>>> hasattr(Foo, 'bar')
False
>>> hasattr(Foo, 'BAR')
True
>>> Foo.BAR
'bip'

ทีนี้ลองทำแบบเดียวกัน แต่ใช้คลาสจริงสำหรับ metaclass:

# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
    # __new__ is the method called before __init__
    # it's the method that creates the object and returns it
    # while __init__ just initializes the object passed as parameter
    # you rarely use __new__, except when you want to control how the object
    # is created.
    # here the created object is the class, and we want to customize it
    # so we override __new__
    # you can do some stuff in __init__ too if you wish
    # some advanced use involves overriding __call__ as well, but we won't
    # see this
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in future_class_attrs.items()
        }
        return type(future_class_name, future_class_parents, uppercase_attrs)

ลองเขียนข้างบนอีกครั้ง แต่ด้วยชื่อตัวแปรที่สั้นและสมจริงกว่าตอนนี้เรารู้แล้วว่าพวกมันหมายถึงอะไร:

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return type(clsname, bases, uppercase_attrs)

clsคุณอาจสังเกตเห็นอาร์กิวเมนต์พิเศษ ไม่มีอะไรพิเศษเกี่ยวกับมัน: __new__รับคลาสที่กำหนดไว้เสมอเป็นพารามิเตอร์แรก เช่นเดียวกับที่คุณมีselfสำหรับวิธีการทั่วไปที่ได้รับอินสแตนซ์เป็นพารามิเตอร์แรกหรือคลาสที่กำหนดสำหรับวิธีการเรียน

แต่นี่ไม่ใช่ OOP ที่เหมาะสม พวกเราเรียกร้องโดยตรงและเราจะไม่เอาชนะหรือโทรของผู้ปกครองtype __new__มาทำสิ่งนั้นแทน:

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return type.__new__(cls, clsname, bases, uppercase_attrs)

เราสามารถทำให้มันสะอาดยิ่งขึ้นโดยใช้superซึ่งจะทำให้การรับมรดกง่ายขึ้น (เพราะใช่คุณสามารถมี metaclasses สืบทอดจาก metaclasses สืบทอดจากประเภท):

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return super(UpperAttrMetaclass, cls).__new__(
            cls, clsname, bases, uppercase_attrs)

โอ้และในงูหลาม 3 ถ้าคุณโทรด้วยอาร์กิวเมนต์คำหลักเช่นนี้:

class Foo(object, metaclass=MyMetaclass, kwarg1=value1):
    ...

มันแปลสิ่งนี้ใน metaclass เพื่อใช้งาน:

class MyMetaclass(type):
    def __new__(cls, clsname, bases, dct, kwargs1=default):
        ...

แค่นั้นแหละ. ไม่มีอะไรเพิ่มเติมเกี่ยวกับ metaclasses

เหตุผลที่อยู่เบื้องหลังความซับซ้อนของรหัสที่ใช้ metaclasses ไม่ใช่เพราะ metaclasses เป็นเพราะคุณมักจะใช้ metaclasses ในการทำสิ่งที่บิดเบี้ยวโดยอาศัยวิปัสสนาจัดการวิปัสสนา vars เช่น__dict__ฯลฯ

จริง ๆ แล้ว metaclasses มีประโยชน์อย่างยิ่งในการทำเวทย์มนตร์ดำและดังนั้นสิ่งที่ซับซ้อน แต่ด้วยตัวเองมันง่าย:

  • สกัดกั้นการสร้างชั้นเรียน
  • ปรับเปลี่ยนคลาส
  • ส่งคืนคลาสที่แก้ไข

ทำไมคุณถึงใช้คลาส metaclasses แทนฟังก์ชั่น

เนื่องจาก__metaclass__สามารถยอมรับ callable ใด ๆ ทำไมคุณจะใช้คลาสเนื่องจากเห็นได้ชัดว่าซับซ้อนมากขึ้น

มีสาเหตุหลายประการ:

  • ความตั้งใจชัดเจน เมื่อคุณอ่านUpperAttrMetaclass(type)คุณจะรู้ว่าสิ่งที่จะทำตาม
  • คุณสามารถใช้ OOP Metaclass สามารถสืบทอดจาก metaclass แทนที่เมธอด parent Metaclasses ยังสามารถใช้ metaclasses
  • คลาสย่อยของคลาสจะเป็นอินสแตนซ์ของ metaclass ของมันหากคุณระบุ metaclass-class แต่ไม่ใช่กับ metaclass-function
  • คุณสามารถจัดโครงสร้างรหัสของคุณได้ดีขึ้น คุณไม่เคยใช้ metaclasses สำหรับสิ่งที่เล็กน้อยตามตัวอย่างข้างต้น มันมักจะเป็นสิ่งที่ซับซ้อน การมีความสามารถในการทำหลายวิธีและจัดกลุ่มในคลาสเดียวนั้นมีประโยชน์มากในการทำให้โค้ดอ่านง่ายขึ้น
  • คุณสามารถขอได้ที่__new__, และ__init__ __call__ซึ่งจะช่วยให้คุณทำสิ่งต่าง ๆ แม้ว่าโดยปกติคุณสามารถทำได้ทั้งหมดใน__new__บางคนก็ใช้งานได้สะดวก__init__กว่า
  • สิ่งเหล่านี้เรียกว่า metaclasses แล้ว! มันต้องมีความหมายอะไรบางอย่าง!

ทำไมคุณต้องใช้ metaclasses

ตอนนี้คำถามใหญ่ ทำไมคุณถึงใช้คุณสมบัติที่ทำให้เกิดข้อผิดพลาดที่คลุมเครือ

ปกติแล้วคุณจะไม่:

Metaclasses เป็นเวทย์มนตร์ที่ผู้ใช้ 99% ไม่ควรกังวล หากคุณสงสัยว่าคุณต้องการพวกเขาหรือไม่คุณไม่ (คนที่ต้องการให้พวกเขารู้จริง ๆ ว่าพวกเขาต้องการพวกเขาและไม่ต้องการคำอธิบายเกี่ยวกับสาเหตุ)

Python Guru Tim Peters

กรณีการใช้งานหลักสำหรับ metaclass กำลังสร้าง API ตัวอย่างทั่วไปของเรื่องนี้คือ Django ORM อนุญาตให้คุณกำหนดบางอย่างเช่นนี้:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

แต่ถ้าคุณทำสิ่งนี้:

person = Person(name='bob', age='35')
print(person.age)

มันจะไม่ส่งคืนIntegerFieldวัตถุ มันจะส่งคืนintและยังสามารถนำโดยตรงจากฐานข้อมูล

สิ่งนี้เป็นไปได้เพราะmodels.Modelกำหนด__metaclass__และใช้เวทย์มนตร์ที่จะเปลี่ยนคำสั่งที่Personคุณเพิ่งกำหนดด้วยคำสั่งง่ายๆให้กลายเป็นเบ็ดซับซ้อนไปยังเขตข้อมูลฐานข้อมูล

Django ทำให้บางอย่างดูซับซ้อนโดยเปิดเผย API ง่าย ๆ และใช้ metaclasses สร้างรหัสจาก API นี้เพื่อทำงานจริงที่อยู่เบื้องหลัง

คำสุดท้าย

ก่อนอื่นคุณจะรู้ว่าคลาสเป็นวัตถุที่สามารถสร้างอินสแตนซ์ได้

ในความเป็นจริงแล้วคลาสเป็นกรณีของตัวเอง ของ metaclasses

>>> class Foo(object): pass
>>> id(Foo)
142630324

ทุกอย่างเป็นวัตถุใน Python และเป็นอินสแตนซ์ของคลาสหรืออินสแตนซ์ของ metaclasses

typeยกเว้น

typeเป็นจริง metaclass ของตัวเอง นี่ไม่ใช่สิ่งที่คุณสามารถทำซ้ำใน Python บริสุทธิ์และทำได้โดยการโกงเล็กน้อยในระดับการใช้งาน

ประการที่สอง metaclasses มีความซับซ้อน คุณอาจไม่ต้องการใช้มันสำหรับการเปลี่ยนคลาสอย่างง่าย ๆ คุณสามารถเปลี่ยนคลาสได้โดยใช้สองเทคนิคที่แตกต่างกัน:

99% ของเวลาที่คุณต้องการเปลี่ยนคลาสคุณจะดีขึ้นเมื่อใช้สิ่งเหล่านี้

แต่ 98% ของเวลาคุณไม่จำเป็นต้องเปลี่ยนชั้นเรียนเลย


30
ปรากฏว่าใน Django models.Modelนั้นไม่ได้ใช้__metaclass__แต่เป็นการclass Model(metaclass=ModelBase):อ้างอิงModelBaseคลาสที่ทำเวทเวทมนตร์ดังกล่าวข้างต้น โพสต์ที่ยอดเยี่ยม! นี่คือแหล่งที่มาของ Django: github.com/django/django/blob/master/django/db/models/ …
Max Goodridge

15
<< ระวังที่นี่ว่า__metaclass__แอตทริบิวต์จะไม่ถูกสืบทอด metaclass ของ parent ( Bar.__class__) จะเป็น หากBarใช้__metaclass__คุณลักษณะที่สร้างขึ้นBarด้วยtype()(และไม่ใช่type.__new__()) คลาสย่อยจะไม่รับพฤติกรรมนั้น >> - คุณ / ใครสักคนช่วยอธิบายเนื้อเรื่องนี้ให้ลึกขึ้นหน่อยได้ไหม?
petrux

15
@MaxGoodridge นั่นคือไวยากรณ์ของ Python 3 สำหรับ metaclasses ดูPython 3.6 Data model VS Python 2.7 Data model
TBBle

2
Now you wonder why the heck is it written in lowercase, and not Type?- ดีเพราะมีการใช้งานใน C - เป็นเหตุผลเดียวกันที่ defaultdict เป็นตัวพิมพ์เล็กในขณะที่ OrderedDict (ใน python 2) เป็น CamelCase ปกติ
Mr_and_Mrs_D

15
มันเป็นคำตอบของชุมชน wiki (ดังนั้นผู้ที่แสดงความคิดเห็นด้วยการแก้ไข / ปรับปรุงอาจพิจารณาแก้ไขความคิดเห็นของพวกเขาเป็นคำตอบหากพวกเขามั่นใจว่าถูกต้อง)
Brōtsyorfuzthrāx

403

หมายเหตุคำตอบนี้มีไว้สำหรับ Python 2.x ตามที่เขียนในปี 2008 metaclasses จะแตกต่างกันเล็กน้อยใน 3.x

Metaclasses เป็นซอสลับที่ทำให้ 'คลาส' ทำงาน metaclass เริ่มต้นสำหรับวัตถุสไตล์ใหม่ที่เรียกว่า 'type'

class type(object)
  |  type(object) -> the object's type
  |  type(name, bases, dict) -> a new type

Metaclasses ใช้เวลา 3 args ' name ', ' ฐาน ' และ ' dict '

นี่คือที่ลับเริ่มต้น ค้นหาตำแหน่งฐานและ dict ที่มาจากการกำหนดคลาสตัวอย่าง

class ThisIsTheName(Bases, Are, Here):
    All_the_code_here
    def doesIs(create, a):
        dict

ให้กำหนด metaclass ที่จะแสดงให้เห็นว่า ' class: ' เรียกมันอย่างไร

def test_metaclass(name, bases, dict):
    print 'The Class Name is', name
    print 'The Class Bases are', bases
    print 'The dict has', len(dict), 'elems, the keys are', dict.keys()

    return "yellow"

class TestName(object, None, int, 1):
    __metaclass__ = test_metaclass
    foo = 1
    def baz(self, arr):
        pass

print 'TestName = ', repr(TestName)

# output => 
The Class Name is TestName
The Class Bases are (<type 'object'>, None, <type 'int'>, 1)
The dict has 4 elems, the keys are ['baz', '__module__', 'foo', '__metaclass__']
TestName =  'yellow'

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

def init_attributes(name, bases, dict):
    if 'attributes' in dict:
        for attr in dict['attributes']:
            dict[attr] = None

    return type(name, bases, dict)

class Initialised(object):
    __metaclass__ = init_attributes
    attributes = ['foo', 'bar', 'baz']

print 'foo =>', Initialised.foo
# output=>
foo => None

หมายเหตุว่าพฤติกรรมมายากลที่Initialisedกำไรโดยมี metaclass init_attributesไม่ได้ส่งผ่านไปยัง subclass Initialisedของ

นี่คือตัวอย่างที่ชัดเจนยิ่งขึ้นแสดงให้เห็นว่าคุณสามารถ subclass 'type' เพื่อสร้าง metaclass ที่ดำเนินการได้อย่างไรเมื่อสร้างคลาส มันค่อนข้างยุ่งยาก:

class MetaSingleton(type):
    instance = None
    def __call__(cls, *args, **kw):
        if cls.instance is None:
            cls.instance = super(MetaSingleton, cls).__call__(*args, **kw)
        return cls.instance

class Foo(object):
    __metaclass__ = MetaSingleton

a = Foo()
b = Foo()
assert a is b

169

คนอื่น ๆ อธิบายว่า metaclasses ทำงานอย่างไรและเข้ากันได้อย่างไรกับระบบประเภท Python นี่คือตัวอย่างของสิ่งที่พวกเขาสามารถใช้สำหรับ ในกรอบการทดสอบที่ฉันเขียนฉันต้องการติดตามลำดับที่มีการกำหนดชั้นเรียนเพื่อที่ฉันจะได้สร้างอินสแตนซ์ของพวกเขาในลำดับนี้ในภายหลัง ฉันพบว่าทำได้ง่ายที่สุดโดยใช้ metaclass

class MyMeta(type):

    counter = 0

    def __init__(cls, name, bases, dic):
        type.__init__(cls, name, bases, dic)
        cls._order = MyMeta.counter
        MyMeta.counter += 1

class MyType(object):              # Python 2
    __metaclass__ = MyMeta

class MyType(metaclass=MyMeta):    # Python 3
    pass

อะไรก็ตามที่เป็นคลาสย่อยจากMyTypeนั้นจะได้รับแอตทริบิวต์ class _orderที่บันทึกลำดับที่กำหนดคลาส


ขอบคุณสำหรับตัวอย่าง คุณไม่พบว่าทำไมนี้ง่ายกว่าที่สืบทอดจาก MyBase ซึ่ง__init__(self)กล่าวว่าtype(self)._order = MyBase.counter; MyBase.counter += 1?
Michael Gundlach

1
ฉันต้องการให้คลาสตัวเองไม่ใช่อินสแตนซ์ของพวกเขา
kindall

ถูกต้อง ขอบคุณ รหัสของฉันจะรีเซ็ตแอตทริบิวต์ของ MyType ในทุกอินสแตนซ์และจะไม่ตั้งค่าแอตทริบิวต์หากไม่มีการสร้างอินสแตนซ์ของ MyType อุ่ย (และคุณสมบัติระดับสามารถทำงานได้ แต่แตกต่างจาก metaclass มันไม่มีสถานที่ที่ชัดเจนในการจัดเก็บที่เคาน์เตอร์)
Michael Gundlach

1
นี่เป็นตัวอย่างที่น่าสนใจอย่างยิ่งไม่ใช่อย่างน้อยจะมีใครเห็นว่าทำไม metaclass ถึงได้รับสิ่งนี้เพื่อแก้ปัญหาเฉพาะหน้า OTOH ฉันพยายามที่จะเชื่อว่าทุกคนจะต้องยกตัวอย่างวัตถุตามลำดับชั้นเรียนของพวกเขา: ฉันคิดว่าเราต้องใช้คำพูดของคุณเพื่อสิ่งนั้น :)
ไมค์สัตว์ฟันแทะ

159

หนึ่งการใช้งานสำหรับ metaclasses คือการเพิ่มคุณสมบัติและวิธีการใหม่ให้กับอินสแตนซ์โดยอัตโนมัติ

ตัวอย่างเช่นถ้าคุณดูที่รุ่น Djangoนิยามของพวกเขาจะดูสับสนเล็กน้อย ดูเหมือนว่าคุณจะกำหนดคุณสมบัติคลาสเท่านั้น:

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

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


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

119

ฉันคิดว่าคำแนะนำ ONLamp สำหรับการเขียนโปรแกรม metaclass นั้นเขียนได้ดีและให้คำแนะนำที่ดีกับหัวข้อแม้ว่าจะมีอายุหลายปีแล้วก็ตาม

http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html (เก็บถาวรที่https://web.archive.org/web/20080206005253/http://www.onlamp com / pub / a / python / 2003/04/17 / metaclasses.html )

กล่าวโดยย่อ: คลาสเป็นพิมพ์เขียวสำหรับการสร้างอินสแตนซ์ metaclass เป็นพิมพ์เขียวสำหรับการสร้างคลาส จะเห็นได้ง่ายว่าในคลาส Python จะต้องเป็นออบเจ็กต์ชั้นหนึ่งด้วยเพื่อเปิดใช้งานลักษณะการทำงานนี้

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

สิ่งที่เหลือพูดคือ: ถ้าคุณไม่รู้ว่า metaclasses คืออะไรความน่าจะเป็นที่คุณไม่ต้องการคือ 99%


109

metaclasses คืออะไร? คุณใช้มันเพื่ออะไร

TLDR: metaclass instantiates และกำหนดพฤติกรรมสำหรับคลาสเช่นเดียวกับคลาส instantiates และกำหนดพฤติกรรมสำหรับอินสแตนซ์

pseudocode:

>>> Class(...)
instance

ข้างต้นควรดูคุ้นเคย แล้วมันClassมาจากไหน? มันเป็นตัวอย่างของ metaclass (เช่น pseudocode):

>>> Metaclass(...)
Class

ในโค้ดจริงเราสามารถส่ง metaclass เริ่มต้นtypeได้ทุกสิ่งที่เราต้องการในการยกตัวอย่างคลาสและเราได้รับคลาส:

>>> type('Foo', (object,), {}) # requires a name, bases, and a namespace
<class '__main__.Foo'>

วางมันแตกต่างกัน

  • คลาสคืออินสแตนซ์เนื่องจาก metaclass เป็นคลาส

    เมื่อเรายกตัวอย่างวัตถุเราจะได้รับอินสแตนซ์:

    >>> object()                          # instantiation of class
    <object object at 0x7f9069b4e0b0>     # instance

    เมื่อเรากำหนดคลาสอย่างชัดเจนด้วย metaclass เริ่มต้นtypeเราจะยกตัวอย่าง:

    >>> type('Object', (object,), {})     # instantiation of metaclass
    <class '__main__.Object'>             # instance
  • อีกทางหนึ่งคลาสเป็นตัวอย่างของ metaclass:

    >>> isinstance(object, type)
    True
  • วิธีที่สามเมตาคลาสคือคลาสของคลาส

    >>> type(object) == type
    True
    >>> object.__class__
    <class 'type'>

เมื่อคุณเขียนคำจำกัดความของคลาสแล้ว Python จะเรียกใช้มันจะใช้ metaclass เพื่อสร้างอินสแตนซ์ของออบเจ็กต์คลาส (ซึ่งจะใช้เพื่อสร้างอินสแตนซ์ของคลาสนั้น)

เช่นเดียวกับที่เราสามารถใช้คำจำกัดความของคลาสเพื่อเปลี่ยนวิธีการทำงานของอินสแตนซ์ของวัตถุที่กำหนดเองเราสามารถใช้คำจำกัดความของคลาส metaclass เพื่อเปลี่ยนวิธีการทำงานของวัตถุคลาส

พวกเขาจะใช้อะไรได้บ้าง? จากเอกสาร :

การใช้ศักยภาพสำหรับ metaclasses นั้นไร้ขอบเขต แนวคิดบางอย่างที่สำรวจรวมถึงการบันทึกการตรวจสอบอินเตอร์เฟสการมอบหมายอัตโนมัติการสร้างคุณสมบัติอัตโนมัติพร็อกซีเฟรมเวิร์กและการล็อค / การซิงโครไนซ์ทรัพยากรอัตโนมัติ

อย่างไรก็ตามโดยทั่วไปผู้ใช้ควรหลีกเลี่ยงการใช้ metaclasses เว้นแต่จำเป็นจริงๆ

คุณใช้ metaclass ทุกครั้งที่คุณสร้างคลาส:

เมื่อคุณเขียนคำจำกัดความของคลาสเช่นเช่นนี้

class Foo(object): 
    'demo'

คุณยกตัวอย่างวัตถุคลาส

>>> Foo
<class '__main__.Foo'>
>>> isinstance(Foo, type), isinstance(Foo, object)
(True, True)

เหมือนกับการเรียกใช้ฟังก์ชันที่typeมีอาร์กิวเมนต์ที่เหมาะสมและกำหนดผลลัพธ์ให้กับตัวแปรของชื่อนั้น:

name = 'Foo'
bases = (object,)
namespace = {'__doc__': 'demo'}
Foo = type(name, bases, namespace)

โปรดทราบบางสิ่งจะถูกเพิ่มเข้า__dict__ในเนมสเปซโดยอัตโนมัติ:

>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, 
'__module__': '__main__', '__weakref__': <attribute '__weakref__' 
of 'Foo' objects>, '__doc__': 'demo'})

metaclasstypeของวัตถุที่เราสร้างขึ้นในทั้งสองกรณีคือ

(หมายเหตุด้านข้างเกี่ยวกับเนื้อหาของคลาส__dict__: __module__มีเพราะคลาสจะต้องทราบว่าถูกกำหนดไว้ที่ใด __dict__และ__weakref__อยู่ที่นั่นเพราะเราไม่ได้กำหนด__slots__- ถ้าเรากำหนด__slots__เราจะประหยัดพื้นที่ในบิตในกรณีเช่น เราสามารถไม่อนุญาต__dict__และ__weakref__ยกเว้นพวกเขาตัวอย่างเช่น:

>>> Baz = type('Bar', (object,), {'__doc__': 'demo', '__slots__': ()})
>>> Baz.__dict__
mappingproxy({'__doc__': 'demo', '__slots__': (), '__module__': '__main__'})

... แต่ฉันพูดนอกเรื่อง)

เราสามารถขยายtypeความหมายเหมือนกับคลาสอื่น ๆ :

นี่คือค่าเริ่มต้น__repr__ของคลาส:

>>> Foo
<class '__main__.Foo'>

__repr__หนึ่งในสิ่งที่มีคุณค่ามากที่สุดที่เราสามารถทำได้โดยเริ่มต้นในการเขียนวัตถุงูใหญ่คือการให้ด้วยดี เมื่อเราเรียกว่าhelp(repr)เราได้เรียนรู้ว่ามีการทดสอบที่ดีสำหรับการ__repr__ที่ยังต้องมีการทดสอบเพื่อความเท่าเทียมกัน obj == eval(repr(obj))- การใช้งานอย่างง่ายดังต่อไปนี้__repr__และ__eq__สำหรับอินสแตนซ์ของคลาสประเภทคลาสของเราทำให้เรามีการสาธิตที่อาจปรับปรุงให้ดีขึ้นเกี่ยวกับค่าเริ่มต้น__repr__ของคลาส:

class Type(type):
    def __repr__(cls):
        """
        >>> Baz
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        >>> eval(repr(Baz))
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        """
        metaname = type(cls).__name__
        name = cls.__name__
        parents = ', '.join(b.__name__ for b in cls.__bases__)
        if parents:
            parents += ','
        namespace = ', '.join(': '.join(
          (repr(k), repr(v) if not isinstance(v, type) else v.__name__))
               for k, v in cls.__dict__.items())
        return '{0}(\'{1}\', ({2}), {{{3}}})'.format(metaname, name, parents, namespace)
    def __eq__(cls, other):
        """
        >>> Baz == eval(repr(Baz))
        True            
        """
        return (cls.__name__, cls.__bases__, cls.__dict__) == (
                other.__name__, other.__bases__, other.__dict__)

ดังนั้นเมื่อเราสร้างวัตถุด้วย metaclass นี้__repr__เสียงสะท้อนบนบรรทัดคำสั่งจะให้ภาพที่น่าเกลียดน้อยกว่าค่าเริ่มต้น:

>>> class Bar(object): pass
>>> Baz = Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
>>> Baz
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})

ด้วยการ__repr__กำหนดที่ดีสำหรับอินสแตนซ์ของคลาสเรามีความสามารถที่แข็งแกร่งในการดีบักโค้ดของเรา อย่างไรก็ตามการตรวจสอบเพิ่มเติมด้วยeval(repr(Class))ไม่น่าเป็นไปได้มาก (เนื่องจากฟังก์ชั่นจะค่อนข้างเป็นไปไม่ได้ที่จะประเมินจากค่าเริ่มต้น__repr__ )

การใช้งานที่คาดหวัง: __prepare__เนมสเปซ

ตัวอย่างเช่นหากเราต้องการทราบว่าวิธีการใดของคลาสที่สร้างขึ้นเราสามารถจัดเตรียม dict ที่สั่งซื้อเป็นเนมสเปซของคลาส เราจะทำเช่นนี้__prepare__ซึ่งจะคืนค่าเนมสเปซสำหรับคลาสถ้ามันถูกใช้ใน Python 3 :

from collections import OrderedDict

class OrderedType(Type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        return OrderedDict()
    def __new__(cls, name, bases, namespace, **kwargs):
        result = Type.__new__(cls, name, bases, dict(namespace))
        result.members = tuple(namespace)
        return result

และการใช้งาน:

class OrderedMethodsObject(object, metaclass=OrderedType):
    def method1(self): pass
    def method2(self): pass
    def method3(self): pass
    def method4(self): pass

และตอนนี้เรามีบันทึกลำดับที่สร้างวิธีการเหล่านี้ (และคุณสมบัติคลาสอื่น ๆ ):

>>> OrderedMethodsObject.members
('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4')

หมายเหตุตัวอย่างนี้ดัดแปลงจากเอกสารประกอบ - enumใหม่ในไลบรารีมาตรฐานทำสิ่งนี้

ดังนั้นสิ่งที่เราทำคือยกตัวอย่าง metaclass โดยการสร้างคลาส เราสามารถปฏิบัติต่อเมตาคลาสได้เหมือนชั้นเรียนอื่น ๆ มันมีวิธีการแก้ปัญหาการสั่งซื้อ:

>>> inspect.getmro(OrderedType)
(<class '__main__.OrderedType'>, <class '__main__.Type'>, <class 'type'>, <class 'object'>)

และมันมีค่าประมาณที่ถูกต้องrepr(ซึ่งเราไม่สามารถประเมินได้อีกต่อไปหากเราไม่สามารถหาวิธีที่จะเป็นตัวแทนของฟังก์ชันของเราได้):

>>> OrderedMethodsObject
OrderedType('OrderedMethodsObject', (object,), {'method1': <function OrderedMethodsObject.method1 at 0x0000000002DB01E0>, 'members': ('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4'), 'method3': <function OrderedMet
hodsObject.method3 at 0x0000000002DB02F0>, 'method2': <function OrderedMethodsObject.method2 at 0x0000000002DB0268>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'OrderedMethodsObject' objects>, '__doc__': None, '__d
ict__': <attribute '__dict__' of 'OrderedMethodsObject' objects>, 'method4': <function OrderedMethodsObject.method4 at 0x0000000002DB0378>})

78

Python 3 อัพเดท

มีวิธีการสำคัญสองวิธี (ในจุดนี้) ใน metaclass:

  • __prepare__และ
  • __new__

__prepare__ช่วยให้คุณสามารถกำหนดแผนที่ที่กำหนดเอง (เช่นOrderedDict) เพื่อใช้เป็นเนมสเปซในขณะที่กำลังสร้างคลาส คุณต้องส่งคืนอินสแตนซ์ของเนมสเปซที่คุณเลือก หากคุณไม่ได้ใช้งาน__prepare__ปกติdictจะใช้

__new__ รับผิดชอบในการสร้าง / ดัดแปลงชั้นเรียนจริง

metaclass ที่ไม่ต้องทำอะไรเพิ่มเติมต้องการ:

class Meta(type):

    def __prepare__(metaclass, cls, bases):
        return dict()

    def __new__(metacls, cls, bases, clsdict):
        return super().__new__(metacls, cls, bases, clsdict)

ตัวอย่างง่ายๆ:

สมมติว่าคุณต้องการบางรหัสตรวจสอบง่ายที่จะทำงานบนแอตทริบิวต์ของคุณ - เหมือนว่ามันจะต้องเป็นหรือint strคลาสของคุณจะมีลักษณะดังนี้:

class Person:
    weight = ValidateType('weight', int)
    age = ValidateType('age', int)
    name = ValidateType('name', str)

อย่างที่คุณเห็นคุณต้องทำซ้ำชื่อของแอตทริบิวต์สองครั้ง สิ่งนี้ทำให้พิมพ์ผิดไปได้พร้อมกับข้อบกพร่องที่น่ารำคาญ

metaclass ธรรมดาสามารถจัดการกับปัญหานั้นได้:

class Person(metaclass=Validator):
    weight = ValidateType(int)
    age = ValidateType(int)
    name = ValidateType(str)

นี่คือลักษณะที่ metaclass จะดูเหมือน (ไม่ได้ใช้__prepare__เนื่องจากไม่จำเป็น):

class Validator(type):
    def __new__(metacls, cls, bases, clsdict):
        # search clsdict looking for ValidateType descriptors
        for name, attr in clsdict.items():
            if isinstance(attr, ValidateType):
                attr.name = name
                attr.attr = '_' + name
        # create final class and return it
        return super().__new__(metacls, cls, bases, clsdict)

ตัวอย่างการรันของ:

p = Person()
p.weight = 9
print(p.weight)
p.weight = '9'

ผลิต:

9
Traceback (most recent call last):
  File "simple_meta.py", line 36, in <module>
    p.weight = '9'
  File "simple_meta.py", line 24, in __set__
    (self.name, self.type, value))
TypeError: weight must be of type(s) <class 'int'> (got '9')

หมายเหตุ : ตัวอย่างนี้ง่ายพอที่จะทำได้ด้วยเครื่องมือตกแต่งชั้นเรียน แต่สันนิษฐานว่า metaclass จริงจะทำอะไรได้มากกว่า

คลาส 'ValidateType' สำหรับการอ้างอิง:

class ValidateType:
    def __init__(self, type):
        self.name = None  # will be set by metaclass
        self.attr = None  # will be set by metaclass
        self.type = type
    def __get__(self, inst, cls):
        if inst is None:
            return self
        else:
            return inst.__dict__[self.attr]
    def __set__(self, inst, value):
        if not isinstance(value, self.type):
            raise TypeError('%s must be of type(s) %s (got %r)' %
                    (self.name, self.type, value))
        else:
            inst.__dict__[self.attr] = value

ว้าวนี่เป็นคุณสมบัติใหม่ที่ยอดเยี่ยมที่ฉันไม่รู้ว่ามีอยู่ใน Python 3 ขอบคุณสำหรับตัวอย่าง !!
Rich Lysakowski ระดับปริญญาเอก

โปรดทราบว่าตั้งแต่ python 3.6 คุณสามารถใช้__set_name__(cls, name)ใน descriptor ( ValidateType) เพื่อตั้งชื่อใน descriptor ( self.nameและในกรณีนี้ด้วยself.attr) สิ่งนี้ถูกเพิ่มเข้ามาโดยไม่จำเป็นต้องดำน้ำใน metaclasses สำหรับกรณีการใช้งานทั่วไปที่เฉพาะเจาะจงนี้ (ดู PEP 487)
ลา

68

บทบาทของเมตา__call__()คลาส' เมธอดเมื่อสร้างอินสแตนซ์ของคลาส

หากคุณเคยเขียนโปรแกรม Python มานานกว่าสองสามเดือนคุณจะสะดุดกับรหัสที่มีลักษณะดังนี้:

# define a class
class SomeClass(object):
    # ...
    # some definition here ...
    # ...

# create an instance of it
instance = SomeClass()

# then call the object as if it's a function
result = instance('foo', 'bar')

หลังเป็นไปได้เมื่อคุณใช้__call__()วิธีการเวทมนต์ในชั้นเรียน

class SomeClass(object):
    # ...
    # some definition here ...
    # ...

    def __call__(self, foo, bar):
        return bar + foo

__call__()วิธีการจะเรียกเมื่ออินสแตนซ์ของชั้นใช้เป็น callable แต่ดังที่เราเห็นจากคำตอบก่อนหน้าคลาสนั้นเป็นอินสแตนซ์ของ metaclass ดังนั้นเมื่อเราใช้คลาสเป็น callable (เช่นเมื่อเราสร้างตัวอย่างของคลาส) เราเรียก__call__()เมธอดคลาสของ metaclass ณ จุดนี้โปรแกรมเมอร์ Python ส่วนใหญ่สับสนเล็กน้อยเพราะได้รับแจ้งว่าเมื่อสร้างอินสแตนซ์เช่นนี้instance = SomeClass()คุณกำลังเรียก__init__()ใช้เมธอด บางคนที่เคยขุดบิตลึกรู้ว่าก่อนที่จะมี__init__() __new__()ทีนี้วันนี้มีการเปิดเผยความจริงอีกชั้นก่อนที่จะ__new__()มี metaclass '__call__()

ลองศึกษาวิธีการโทรโซ่จากมุมมองของการสร้างตัวอย่างของชั้นเรียนโดยเฉพาะ

นี่คือ metaclass ที่บันทึกช่วงเวลาก่อนที่อินสแตนซ์จะถูกสร้างขึ้นและช่วงเวลาที่จะส่งคืน

class Meta_1(type):
    def __call__(cls):
        print "Meta_1.__call__() before creating an instance of ", cls
        instance = super(Meta_1, cls).__call__()
        print "Meta_1.__call__() about to return instance."
        return instance

นี่คือคลาสที่ใช้ metaclass นั้น

class Class_1(object):

    __metaclass__ = Meta_1

    def __new__(cls):
        print "Class_1.__new__() before creating an instance."
        instance = super(Class_1, cls).__new__(cls)
        print "Class_1.__new__() about to return instance."
        return instance

    def __init__(self):
        print "entering Class_1.__init__() for instance initialization."
        super(Class_1,self).__init__()
        print "exiting Class_1.__init__()."

และตอนนี้เรามาสร้างตัวอย่างของ Class_1

instance = Class_1()
# Meta_1.__call__() before creating an instance of <class '__main__.Class_1'>.
# Class_1.__new__() before creating an instance.
# Class_1.__new__() about to return instance.
# entering Class_1.__init__() for instance initialization.
# exiting Class_1.__init__().
# Meta_1.__call__() about to return instance.

สังเกตว่ารหัสข้างต้นไม่ได้ทำอะไรมากกว่าการบันทึกงาน แต่ละวิธีมอบหมายงานที่แท้จริงให้กับการนำไปใช้งานของผู้ปกครองจึงทำให้พฤติกรรมเริ่มต้น เนื่องจากtypeเป็นMeta_1ของผู้ปกครองชั้นเรียน ( typeเป็น metaclass ผู้ปกครองเริ่มต้น) และเมื่อพิจารณาจากลำดับการสั่งซื้อของการส่งออกดังกล่าวข้างต้นขณะนี้เรามีเงื่อนงำเป็นสิ่งที่จะดำเนินการหลอกของtype.__call__():

class type:
    def __call__(cls, *args, **kwarg):

        # ... maybe a few things done to cls here

        # then we call __new__() on the class to create an instance
        instance = cls.__new__(cls, *args, **kwargs)

        # ... maybe a few things done to the instance here

        # then we initialize the instance with its __init__() method
        instance.__init__(*args, **kwargs)

        # ... maybe a few more things done to instance here

        # then we return it
        return instance

เราจะเห็นได้ว่าเมตาคลาสของ__call__()เมธอดนั้นเป็นวิธีที่เรียกว่าอันดับแรก จากนั้นก็สร้างได้รับมอบหมายจากอินสแตนซ์ในชั้นเรียนของวิธีการและการเริ่มต้นที่จะอินสแตนซ์ของ__new__() __init__()นอกจากนี้ยังเป็นสิ่งที่ส่งคืนอินสแตนซ์ในที่สุด

จากด้านบนมันเกิดขึ้นที่ metaclass ' __call__()ยังได้รับโอกาสในการตัดสินใจว่าจะเรียกClass_1.__new__()หรือไม่Class_1.__init__()ก็จะทำในที่สุด ในระหว่างการดำเนินการมันสามารถคืนวัตถุที่ไม่ได้สัมผัสโดยวิธีใดวิธีหนึ่งเหล่านี้ ยกตัวอย่างวิธีการนี้ให้กับรูปแบบซิงเกิล:

class Meta_2(type):
    singletons = {}

    def __call__(cls, *args, **kwargs):
        if cls in Meta_2.singletons:
            # we return the only instance and skip a call to __new__()
            # and __init__()
            print ("{} singleton returning from Meta_2.__call__(), "
                   "skipping creation of new instance.".format(cls))
            return Meta_2.singletons[cls]

        # else if the singleton isn't present we proceed as usual
        print "Meta_2.__call__() before creating an instance."
        instance = super(Meta_2, cls).__call__(*args, **kwargs)
        Meta_2.singletons[cls] = instance
        print "Meta_2.__call__() returning new instance."
        return instance

class Class_2(object):

    __metaclass__ = Meta_2

    def __new__(cls, *args, **kwargs):
        print "Class_2.__new__() before creating instance."
        instance = super(Class_2, cls).__new__(cls)
        print "Class_2.__new__() returning instance."
        return instance

    def __init__(self, *args, **kwargs):
        print "entering Class_2.__init__() for initialization."
        super(Class_2, self).__init__()
        print "exiting Class_2.__init__()."

ลองสังเกตสิ่งที่เกิดขึ้นเมื่อพยายามสร้างวัตถุชนิดซ้ำ ๆ Class_2

a = Class_2()
# Meta_2.__call__() before creating an instance.
# Class_2.__new__() before creating instance.
# Class_2.__new__() returning instance.
# entering Class_2.__init__() for initialization.
# exiting Class_2.__init__().
# Meta_2.__call__() returning new instance.

b = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

c = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

a is b is c # True

นี่เป็นส่วนเสริมที่ดีของคำตอบที่ยอมรับก่อนหน้านี้ มันมีตัวอย่างสำหรับตัวแปลงสัญญาณระดับกลางที่จะเคี้ยว
Rich Lysakowski ระดับปริญญาเอก

56

เมตาคลาสเป็นคลาสที่บอกวิธีสร้างคลาสอื่น (บางอัน)

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

#!/usr/bin/env python

# Copyright (C) 2013-2014 Craig Phillips.  All rights reserved.

# This requires some explaining.  The point of this metaclass excercise is to
# create a static abstract class that is in one way or another, dormant until
# queried.  I experimented with creating a singlton on import, but that did
# not quite behave how I wanted it to.  See now here, we are creating a class
# called GsyncOptions, that on import, will do nothing except state that its
# class creator is GsyncOptionsType.  This means, docopt doesn't parse any
# of the help document, nor does it start processing command line options.
# So importing this module becomes really efficient.  The complicated bit
# comes from requiring the GsyncOptions class to be static.  By that, I mean
# any property on it, may or may not exist, since they are not statically
# defined; so I can't simply just define the class with a whole bunch of
# properties that are @property @staticmethods.
#
# So here's how it works:
#
# Executing 'from libgsync.options import GsyncOptions' does nothing more
# than load up this module, define the Type and the Class and import them
# into the callers namespace.  Simple.
#
# Invoking 'GsyncOptions.debug' for the first time, or any other property
# causes the __metaclass__ __getattr__ method to be called, since the class
# is not instantiated as a class instance yet.  The __getattr__ method on
# the type then initialises the class (GsyncOptions) via the __initialiseClass
# method.  This is the first and only time the class will actually have its
# dictionary statically populated.  The docopt module is invoked to parse the
# usage document and generate command line options from it.  These are then
# paired with their defaults and what's in sys.argv.  After all that, we
# setup some dynamic properties that could not be defined by their name in
# the usage, before everything is then transplanted onto the actual class
# object (or static class GsyncOptions).
#
# Another piece of magic, is to allow command line options to be set in
# in their native form and be translated into argparse style properties.
#
# Finally, the GsyncListOptions class is actually where the options are
# stored.  This only acts as a mechanism for storing options as lists, to
# allow aggregation of duplicate options or options that can be specified
# multiple times.  The __getattr__ call hides this by default, returning the
# last item in a property's list.  However, if the entire list is required,
# calling the 'list()' method on the GsyncOptions class, returns a reference
# to the GsyncListOptions class, which contains all of the same properties
# but as lists and without the duplication of having them as both lists and
# static singlton values.
#
# So this actually means that GsyncOptions is actually a static proxy class...
#
# ...And all this is neatly hidden within a closure for safe keeping.
def GetGsyncOptionsType():
    class GsyncListOptions(object):
        __initialised = False

    class GsyncOptionsType(type):
        def __initialiseClass(cls):
            if GsyncListOptions._GsyncListOptions__initialised: return

            from docopt import docopt
            from libgsync.options import doc
            from libgsync import __version__

            options = docopt(
                doc.__doc__ % __version__,
                version = __version__,
                options_first = True
            )

            paths = options.pop('<path>', None)
            setattr(cls, "destination_path", paths.pop() if paths else None)
            setattr(cls, "source_paths", paths)
            setattr(cls, "options", options)

            for k, v in options.iteritems():
                setattr(cls, k, v)

            GsyncListOptions._GsyncListOptions__initialised = True

        def list(cls):
            return GsyncListOptions

        def __getattr__(cls, name):
            cls.__initialiseClass()
            return getattr(GsyncListOptions, name)[-1]

        def __setattr__(cls, name, value):
            # Substitut option names: --an-option-name for an_option_name
            import re
            name = re.sub(r'^__', "", re.sub(r'-', "_", name))
            listvalue = []

            # Ensure value is converted to a list type for GsyncListOptions
            if isinstance(value, list):
                if value:
                    listvalue = [] + value
                else:
                    listvalue = [ None ]
            else:
                listvalue = [ value ]

            type.__setattr__(GsyncListOptions, name, listvalue)

    # Cleanup this module to prevent tinkering.
    import sys
    module = sys.modules[__name__]
    del module.__dict__['GetGsyncOptionsType']

    return GsyncOptionsType

# Our singlton abstract proxy class.
class GsyncOptions(object):
    __metaclass__ = GetGsyncOptionsType()

43

เวอร์ชัน tl; dr

type(obj)ฟังก์ชั่นที่คุณได้รับชนิดของวัตถุ

type()ของชั้นเป็นของmetaclass

วิธีใช้ metaclass:

class Foo(object):
    __metaclass__ = MyMetaClass

typeเป็น metaclass ของตัวเอง คลาสของคลาสเป็นเมตาคลาส - ร่างกายของคลาสเป็นอาร์กิวเมนต์ที่ส่งผ่านไปยังเมตาคลาสที่ใช้ในการสร้างคลาส

ที่นี่คุณสามารถอ่านเกี่ยวกับวิธีใช้ metaclasses เพื่อปรับแต่งโครงสร้างของคลาส


42

typeเป็นจริงmetaclass- คลาสที่สร้างคลาสอื่น ส่วนใหญ่เป็นคลาสย่อยของmetaclass รับชั้นเป็นอาร์กิวเมนต์ครั้งแรกและให้สามารถเข้าถึงวัตถุชั้นโดยมีรายละเอียดดังกล่าวข้างล่าง:typemetaclassnew

>>> class MetaClass(type):
...     def __init__(cls, name, bases, attrs):
...         print ('class name: %s' %name )
...         print ('Defining class %s' %cls)
...         print('Bases %s: ' %bases)
...         print('Attributes')
...         for (name, value) in attrs.items():
...             print ('%s :%r' %(name, value))
... 

>>> class NewClass(object, metaclass=MetaClass):
...    get_choch='dairy'
... 
class name: NewClass
Bases <class 'object'>: 
Defining class <class 'NewClass'>
get_choch :'dairy'
__module__ :'builtins'
__qualname__ :'NewClass'

Note:

ขอให้สังเกตว่าชั้นไม่ได้ยกตัวอย่างในเวลาใดก็ได้ metaclassการกระทำที่เรียบง่ายของการสร้างชั้นเรียกการดำเนินการของ


27

คลาส Python เป็นวัตถุของเมตาดาต้า

เมตาคลาสเริ่มต้นซึ่งจะถูกนำไปใช้เมื่อคุณกำหนดคลาสเป็น:

class foo:
    ...

เมตาคลาสใช้เพื่อใช้กฎบางอย่างกับชุดคลาสทั้งหมด ตัวอย่างเช่นสมมติว่าคุณกำลังสร้าง ORM เพื่อเข้าถึงฐานข้อมูลและคุณต้องการให้ระเบียนจากแต่ละตารางเป็นคลาสที่แมปกับตารางนั้น (โดยยึดตามเขตข้อมูลกฎธุรกิจ ฯลฯ ) การใช้ metaclass ที่เป็นไปได้ ตัวอย่างเช่นตรรกะกลุ่มการเชื่อมต่อซึ่งใช้ร่วมกันโดยทุกคลาสของการบันทึกจากตารางทั้งหมด การใช้งานอื่นคือตรรกะเพื่อสนับสนุนคีย์ต่างประเทศซึ่งเกี่ยวข้องกับการบันทึกหลายคลาส

เมื่อคุณกำหนด metaclass คุณเป็นประเภท subclass และสามารถแทนที่วิธีเวทย์มนตร์ต่อไปนี้เพื่อแทรกตรรกะของคุณ

class somemeta(type):
    __new__(mcs, name, bases, clsdict):
      """
  mcs: is the base metaclass, in this case type.
  name: name of the new class, as provided by the user.
  bases: tuple of base classes 
  clsdict: a dictionary containing all methods and attributes defined on class

  you must return a class object by invoking the __new__ constructor on the base metaclass. 
 ie: 
    return type.__call__(mcs, name, bases, clsdict).

  in the following case:

  class foo(baseclass):
        __metaclass__ = somemeta

  an_attr = 12

  def bar(self):
      ...

  @classmethod
  def foo(cls):
      ...

      arguments would be : ( somemeta, "foo", (baseclass, baseofbase,..., object), {"an_attr":12, "bar": <function>, "foo": <bound class method>}

      you can modify any of these values before passing on to type
      """
      return type.__call__(mcs, name, bases, clsdict)


    def __init__(self, name, bases, clsdict):
      """ 
      called after type has been created. unlike in standard classes, __init__ method cannot modify the instance (cls) - and should be used for class validaton.
      """
      pass


    def __prepare__():
        """
        returns a dict or something that can be used as a namespace.
        the type will then attach methods and attributes from class definition to it.

        call order :

        somemeta.__new__ ->  type.__new__ -> type.__init__ -> somemeta.__init__ 
        """
        return dict()

    def mymethod(cls):
        """ works like a classmethod, but for class objects. Also, my method will not be visible to instances of cls.
        """
        pass

อย่างไรก็ตามทั้งสองเป็นตะขอที่ใช้กันมากที่สุด metaclassing นั้นทรงพลังและเหนือสิ่งอื่นใดคือรายการของการใช้งาน metaclassing อย่างละเอียด


21

ฟังก์ชั่น type () สามารถส่งคืนชนิดของวัตถุหรือสร้างชนิดใหม่

ตัวอย่างเช่นเราสามารถสร้างคลาส Hi ด้วยฟังก์ชัน type () และไม่จำเป็นต้องใช้วิธีนี้กับคลาส Hi (วัตถุ):

def func(self, name='mike'):
    print('Hi, %s.' % name)

Hi = type('Hi', (object,), dict(hi=func))
h = Hi()
h.hi()
Hi, mike.

type(Hi)
type

type(h)
__main__.Hi

นอกเหนือจากการใช้ type () เพื่อสร้างคลาสแบบไดนามิกคุณสามารถควบคุมพฤติกรรมการสร้างคลาสและใช้เมตาคลาสได้

ตามโมเดลวัตถุ Python คลาสเป็นวัตถุดังนั้นคลาสจะต้องเป็นอินสแตนซ์ของคลาสอื่นที่แน่นอน โดยค่าเริ่มต้นคลาส Python เป็นอินสแตนซ์ของคลาสชนิด นั่นคือ type คือ metaclass ของคลาสที่มีอยู่แล้วภายในและ metaclass ของคลาสที่ผู้ใช้กำหนดเอง

class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

class CustomList(list, metaclass=ListMetaclass):
    pass

lst = CustomList()
lst.add('custom_list_1')
lst.add('custom_list_2')

lst
['custom_list_1', 'custom_list_2']

Magic จะมีผลเมื่อเราส่งอาร์กิวเมนต์ของคีย์เวิร์ดใน metaclass ซึ่งระบุว่า Python interpreter เพื่อสร้าง CustomList ผ่าน ListMetaclass ใหม่ () ณ จุดนี้เราสามารถแก้ไขคำจำกัดความของคลาสตัวอย่างเช่นและเพิ่มวิธีการใหม่แล้วส่งกลับคำจำกัดความที่แก้ไขแล้ว


11

นอกเหนือจากคำตอบที่เผยแพร่ฉันสามารถพูดได้ว่าการmetaclassกำหนดพฤติกรรมสำหรับชั้นเรียน ดังนั้นคุณสามารถกำหนด metaclass ของคุณได้อย่างชัดเจน เมื่อใดก็ตามที่ได้รับหลามคำหลักแล้วก็เริ่มค้นหาclass metaclassหากไม่พบ - ประเภท metaclass เริ่มต้นจะใช้ในการสร้างวัตถุของชั้นเรียน การใช้__metaclass__คุณสมบัตินี้คุณสามารถกำหนดmetaclassคลาสของคุณได้:

class MyClass:
   __metaclass__ = type
   # write here other method
   # write here one more method

print(MyClass.__metaclass__)

มันจะสร้างผลลัพธ์เช่นนี้:

class 'type'

และแน่นอนคุณสามารถสร้างของคุณเองmetaclassเพื่อกำหนดพฤติกรรมของคลาสใด ๆ ที่สร้างขึ้นโดยใช้คลาสของคุณ

สำหรับการทำเช่นนั้นmetaclassคลาสประเภทเริ่มต้นของคุณจะต้องสืบทอดเนื่องจากนี่คือหลักmetaclass:

class MyMetaClass(type):
   __metaclass__ = type
   # you can write here any behaviour you want

class MyTestClass:
   __metaclass__ = MyMetaClass

Obj = MyTestClass()
print(Obj.__metaclass__)
print(MyMetaClass.__metaclass__)

ผลลัพธ์จะเป็น:

class '__main__.MyMetaClass'
class 'type'

4

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


แทนที่จะให้คำจำกัดความที่เป็นหนอนหนังสือคงจะดีกว่านี้ถ้าคุณเพิ่มตัวอย่าง บรรทัดแรกของคำตอบของคุณดูเหมือนจะถูกคัดลอกมาจากรายการ Wikipedia ของ Metaclasses
ความ

@verisimilitude นอกจากนี้ฉันยังได้เรียนรู้คุณสามารถช่วยฉันปรับปรุงคำตอบนี้โดยการยกตัวอย่างจากการปฏิบัติของคุณ?
Venu Gopal Tewari

2

นี่คืออีกตัวอย่างของสิ่งที่สามารถใช้สำหรับ:

  • คุณสามารถใช้metaclassเพื่อเปลี่ยนการทำงานของอินสแตนซ์ (คลาส)
class MetaMemberControl(type):
    __slots__ = ()

    @classmethod
    def __prepare__(mcs, f_cls_name, f_cls_parents,  # f_cls means: future class
                    meta_args=None, meta_options=None):  # meta_args and meta_options is not necessarily needed, just so you know.
        f_cls_attr = dict()
        if not "do something or if you want to define your cool stuff of dict...":
            return dict(make_your_special_dict=None)
        else:
            return f_cls_attr

    def __new__(mcs, f_cls_name, f_cls_parents, f_cls_attr,
                meta_args=None, meta_options=None):

        original_getattr = f_cls_attr.get('__getattribute__')
        original_setattr = f_cls_attr.get('__setattr__')

        def init_getattr(self, item):
            if not item.startswith('_'):  # you can set break points at here
                alias_name = '_' + item
                if alias_name in f_cls_attr['__slots__']:
                    item = alias_name
            if original_getattr is not None:
                return original_getattr(self, item)
            else:
                return super(eval(f_cls_name), self).__getattribute__(item)

        def init_setattr(self, key, value):
            if not key.startswith('_') and ('_' + key) in f_cls_attr['__slots__']:
                raise AttributeError(f"you can't modify private members:_{key}")
            if original_setattr is not None:
                original_setattr(self, key, value)
            else:
                super(eval(f_cls_name), self).__setattr__(key, value)

        f_cls_attr['__getattribute__'] = init_getattr
        f_cls_attr['__setattr__'] = init_setattr

        cls = super().__new__(mcs, f_cls_name, f_cls_parents, f_cls_attr)
        return cls


class Human(metaclass=MetaMemberControl):
    __slots__ = ('_age', '_name')

    def __init__(self, name, age):
        self._name = name
        self._age = age

    def __getattribute__(self, item):
        """
        is just for IDE recognize.
        """
        return super().__getattribute__(item)

    """ with MetaMemberControl then you don't have to write as following
    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age
    """


def test_demo():
    human = Human('Carson', 27)
    # human.age = 18  # you can't modify private members:_age  <-- this is defined by yourself.
    # human.k = 18  # 'Human' object has no attribute 'k'  <-- system error.
    age1 = human._age  # It's OK, although the IDE will show some warnings. (Access to a protected member _age of a class)

    age2 = human.age  # It's OK! see below:
    """
    if you do not define `__getattribute__` at the class of Human,
    the IDE will show you: Unresolved attribute reference 'age' for class 'Human'
    but it's ok on running since the MetaMemberControl will help you.
    """


if __name__ == '__main__':
    test_demo()

metaclassมีประสิทธิภาพและมีหลายสิ่งหลายอย่าง (เช่นมายากลลิง) คุณสามารถทำอะไรกับมัน แต่ต้องระวังเรื่องนี้อาจจะเป็นที่รู้จักกับคุณเท่านั้น


2

คลาสใน Python เป็นวัตถุและเหมือนกับวัตถุอื่นมันเป็นตัวอย่างของ "บางสิ่ง" "อะไรบางอย่าง" นี่คือสิ่งที่เรียกว่าเป็น Metaclass เมตาคลาสนี้เป็นคลาสพิเศษชนิดที่สร้างวัตถุของคลาสอื่น ดังนั้น metaclass มีหน้าที่สร้างคลาสใหม่ สิ่งนี้ทำให้โปรแกรมเมอร์สามารถกำหนดวิธีการสร้างคลาสได้เอง

หากต้องการสร้าง metaclass การแทนที่เมธอดnew () และinit () มักจะทำ ใหม่ () สามารถถูกแทนที่เพื่อเปลี่ยนวิธีการสร้างวัตถุในขณะที่init () สามารถแทนที่เพื่อเปลี่ยนวิธีการเริ่มต้นวัตถุ Metaclass สามารถสร้างได้หลายวิธี หนึ่งในวิธีคือการใช้ฟังก์ชั่น type () type () ฟังก์ชั่นเมื่อเรียกใช้ด้วยพารามิเตอร์ 3 ตัวจะสร้าง metaclass พารามิเตอร์คือ: -

  1. ชื่อคลาส
  2. Tuple มีคลาสพื้นฐานที่สืบทอดโดยคลาส
  3. พจนานุกรมที่มีเมธอดคลาสและตัวแปรคลาสทั้งหมด

อีกวิธีในการสร้าง metaclass ประกอบด้วยคำหลัก 'metaclass' กำหนด metaclass เป็นคลาสที่ง่าย ในพารามิเตอร์ของคลาสที่สืบทอดผ่าน metaclass = metaclass_name

Metaclass สามารถใช้งานได้เฉพาะในสถานการณ์ต่อไปนี้: -

  1. เมื่อต้องใช้เอฟเฟกต์เฉพาะกับคลาสย่อยทั้งหมด
  2. จำเป็นต้องเปลี่ยนคลาส (เมื่อสร้าง) โดยอัตโนมัติ
  3. โดยนักพัฒนา API

2

โปรดทราบว่าใน python 3.6 มีการใช้วิธีการ dunder ใหม่__init_subclass__(cls, **kwargs)เพื่อแทนที่กรณีการใช้งานทั่วไปจำนวนมากสำหรับ metaclasses ถูกเรียกเมื่อคลาสย่อยของคลาสการกำหนดถูกสร้างขึ้น ดูเอกสารหลาม


-3

Metaclass เป็นประเภทของคลาสที่กำหนดว่าคลาสจะมีลักษณะอย่างไรหรือเราสามารถพูดได้ว่าคลาสนั้นเป็นตัวอย่างของ metaclass

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