2011-01-10 37 views
21

Có thể ghép chuỗi metaclasses không?Metaclass Mixin hoặc Chaining?

Tôi có lớp Model sử dụng __metaclass__=ModelBase để xử lý dict không gian tên của nó. Tôi sẽ kế thừa từ nó và "liên kết" một metaclass khác nên nó sẽ không tô bóng bản gốc.

phương pháp đầu tiên là phải phân lớp class MyModelBase(ModelBase):

MyModel(Model): 
    __metaclass__ = MyModelBase # inherits from `ModelBase` 

Nhưng là nó có thể chỉ để chuỗi họ như mixins, mà không subclassing rõ ràng? Một cái gì đó như

class MyModel(Model): 
    __metaclass__ = (MyMixin, super(Model).__metaclass__) 

... hoặc thậm chí tốt hơn: tạo ra một mixin rằng sẽ sử dụng __metaclass__ từ cha mẹ trực tiếp của các lớp có sử dụng nó:

class MyModel(Model): 
    __metaclass__ = MyMetaMixin, # Automagically uses `Model.__metaclass__` 

Lý do: Đối với linh hoạt hơn trong việc mở rộng các ứng dụng hiện có, tôi muốn tạo cơ chế toàn cầu để gắn vào quá trình Model, Form, ... định nghĩa trong Django để nó có thể được thay đổi khi chạy.

Một cơ chế chung sẽ tốt hơn nhiều so với triển khai nhiều metaclasses với mixback gọi lại.


Với sự giúp đỡ của bạn, cuối cùng tôi đã tìm ra giải pháp: metaclass MetaProxy.

Ý tưởng là: tạo ra một metaclass mà gọi một callback để thay đổi không gian tên của lớp được tạo ra, sau đó, với sự giúp đỡ của __new__, đột biến thành một metaclass của một trong những cha mẹ

#!/usr/bin/env python 
#-*- coding: utf-8 -*- 

# Magical metaclass 
class MetaProxy(type): 
    """ Decorate the class being created & preserve __metaclass__ of the parent 

     It executes two callbacks: before & after creation of a class, 
     that allows you to decorate them. 

     Between two callbacks, it tries to locate any `__metaclass__` 
     in the parents (sorted in MRO). 
     If found — with the help of `__new__` method it 
     mutates to the found base metaclass. 
     If not found — it just instantiates the given class. 
     """ 

    @classmethod 
    def pre_new(cls, name, bases, attrs): 
     """ Decorate a class before creation """ 
     return (name, bases, attrs) 

    @classmethod 
    def post_new(cls, newclass): 
     """ Decorate a class after creation """ 
     return newclass 

    @classmethod 
    def _mrobases(cls, bases): 
     """ Expand tuple of base-classes ``bases`` in MRO """ 
     mrobases = [] 
     for base in bases: 
      if base is not None: # We don't like `None` :) 
       mrobases.extend(base.mro()) 
     return mrobases 

    @classmethod 
    def _find_parent_metaclass(cls, mrobases): 
     """ Find any __metaclass__ callable in ``mrobases`` """ 
     for base in mrobases: 
      if hasattr(base, '__metaclass__'): 
       metacls = base.__metaclass__ 
       if metacls and not issubclass(metacls, cls): # don't call self again 
        return metacls#(name, bases, attrs) 
     # Not found: use `type` 
     return lambda name,bases,attrs: type.__new__(type, name, bases, attrs) 

    def __new__(cls, name, bases, attrs): 
     mrobases = cls._mrobases(bases) 
     name, bases, attrs = cls.pre_new(name, bases, attrs) # Decorate, pre-creation 
     newclass = cls._find_parent_metaclass(mrobases)(name, bases, attrs) 
     return cls.post_new(newclass) # Decorate, post-creation 



# Testing 
if __name__ == '__main__': 
    # Original classes. We won't touch them 
    class ModelMeta(type): 
     def __new__(cls, name, bases, attrs): 
      attrs['parentmeta'] = name 
      return super(ModelMeta, cls).__new__(cls, name, bases, attrs) 

    class Model(object): 
     __metaclass__ = ModelMeta 
     # Try to subclass me but don't forget about `ModelMeta` 

    # Decorator metaclass 
    class MyMeta(MetaProxy): 
     """ Decorate a class 

      Being a subclass of `MetaProxyDecorator`, 
       it will call base metaclasses after decorating 
      """ 
     @classmethod 
     def pre_new(cls, name, bases, attrs): 
      """ Set `washere` to classname """ 
      attrs['washere'] = name 
      return super(MyMeta, cls).pre_new(name, bases, attrs) 

     @classmethod 
     def post_new(cls, newclass): 
      """ Append '!' to `.washere` """ 
      newclass.washere += '!' 
      return super(MyMeta, cls).post_new(newclass) 

    # Here goes the inheritance... 
    class MyModel(Model): 
     __metaclass__ = MyMeta 
     a=1 
    class MyNewModel(MyModel): 
     __metaclass__ = MyMeta # Still have to declare it: __metaclass__ do not inherit 
     a=2 
    class MyNewNewModel(MyNewModel): 
     # Will use the original ModelMeta 
     a=3 

    class A(object): 
     __metaclass__ = MyMeta # No __metaclass__ in parents: just instantiate 
     a=4 
    class B(A): 
     pass # MyMeta is not called until specified explicitly 



    # Make sure we did everything right 
    assert MyModel.a == 1 
    assert MyNewModel.a == 2 
    assert MyNewNewModel.a == 3 
    assert A.a == 4 

    # Make sure callback() worked 
    assert hasattr(MyModel, 'washere') 
    assert hasattr(MyNewModel, 'washere') 
    assert hasattr(MyNewNewModel, 'washere') # inherited 
    assert hasattr(A, 'washere') 

    assert MyModel.washere == 'MyModel!' 
    assert MyNewModel.washere == 'MyNewModel!' 
    assert MyNewNewModel.washere == 'MyNewModel!' # inherited, so unchanged 
    assert A.washere == 'A!' 
+0

Trong Python 3.4, điều này dường như không khẳng định chính xác; dòng 113 thất bại ('MyModel' không có thuộc tính' washere') – Joost

Trả lời

2

tôi đừng nghĩ rằng bạn có thể xích chúng như thế, và tôi cũng không biết nó sẽ hoạt động như thế nào.

Nhưng bạn có thể tạo các metaclasses mới trong thời gian chạy và sử dụng chúng. Nhưng đó là một hack khủng khiếp. :)

zope.interface làm một cái gì đó tương tự, nó có một metaclass cố vấn, mà sẽ chỉ làm một số điều cho lớp sau khi xây dựng. Nếu đã có một metclass rồi, một trong những thứ mà nó sẽ làm là đặt metaclass trước đó thành metaclass sau khi nó kết thúc.

(Tuy nhiên, tránh làm những điều này trừ khi bạn phải, hoặc nghĩ rằng nó là thú vị.)

+0

'zope.interface' đã cho tôi một số ý tưởng, cảm ơn bạn! :) – kolypto

+0

Oooh, bây giờ Django cũng sẽ nhận được những chiếc siêu xe đáng sợ. Django * là * ZOpe mới. ;-) –

10

Một loại chỉ có một metaclass, bởi vì một metaclass chỉ đơn giản là khẳng định những gì tuyên bố lớp nào - có nhiều hơn một sẽ không có ý nghĩa. Đối với cùng một lý do "chuỗi" không có ý nghĩa: metaclass đầu tiên tạo ra loại, vì vậy những gì là 2 phải làm gì?

Bạn sẽ phải hợp nhất hai metaclasses (giống như với bất kỳ lớp nào khác). Nhưng điều đó có thể phức tạp, đặc biệt nếu bạn không thực sự biết họ làm gì.

class MyModelBase(type): 
    def __new__(cls, name, bases, attr): 
     attr['MyModelBase'] = 'was here' 
     return type.__new__(cls,name, bases, attr) 

class MyMixin(type): 
    def __new__(cls, name, bases, attr): 
     attr['MyMixin'] = 'was here' 
     return type.__new__(cls, name, bases, attr) 

class ChainedMeta(MyModelBase, MyMixin): 
    def __init__(cls, name, bases, attr): 
     # call both parents 
     MyModelBase.__init__(cls,name, bases, attr) 
     MyMixin.__init__(cls,name, bases, attr) 

    def __new__(cls, name, bases, attr): 
     # so, how is the new type supposed to look? 
     # maybe create the first 
     t1 = MyModelBase.__new__(cls, name, bases, attr) 
     # and pass it's data on to the next? 
     name = t1.__name__ 
     bases = tuple(t1.mro()) 
     attr = t1.__dict__.copy() 
     t2 = MyMixin.__new__(cls, name, bases, attr) 
     return t2 

class Model(object): 
    __metaclass__ = MyModelBase # inherits from `ModelBase` 

class MyModel(Model): 
    __metaclass__ = ChainedMeta 

print MyModel.MyModelBase 
print MyModel.MyMixin 

Như bạn có thể thấy điều này liên quan đến một số phỏng đoán, vì bạn thực sự không biết các metaclasses khác làm gì.Nếu cả hai metaclasses là thực sự đơn giản này có thể làm việc, nhưng tôi sẽ không có quá nhiều sự tự tin trong một giải pháp như thế này.

Viết một metaclass cho metaclasses mà kết hợp nhiều căn cứ là trái như một bài tập cho người đọc ;-p

+0

Mọi thứ đều tuyệt vời, ngoại trừ sự phụ thuộc rõ ràng vào 'MyModelBase' :) – kolypto

+8

" Vì lý do tương tự "chuỗi" không có ý nghĩa "- Tại sao vậy? Việc ghép nối các metaclasses nên được thực hiện thông qua kế thừa. Trong thực tế, vì metaclass của bạn mở rộng metaclass được xây dựng trong 'loại', bạn đã chaining chúng. Khi bạn gọi cơ sở metaclass của bạn thông qua 'super()', bạn có thể dễ dàng đạt được metaclasses xích bằng nhiều thừa kế. –

+0

"Một loại có thể chỉ có một metaclass, bởi vì một metaclass chỉ đơn giản là tuyên bố những gì tuyên bố lớp học" - Thích hợp hơn tôi sẽ nói: Metaclass là loại của loại. Vì vậy, trong đoạn mã trên, kiểu MyModel là ChainedMeta (thử kiểu (MyModel)) hoặc nói cách khác, MyModel là một cá thể của ChainedMeta. Rõ ràng bất kỳ lớp nào, như bất kỳ đối tượng nào, chỉ có thể có một loại, vì vậy chỉ có thể có một metaclass. Chỉ vì mục đích rõ ràng – nadapez

4

Tôi không biết cách nào để "trộn" metaclasses, nhưng bạn có thể kế thừa và ghi đè lên họ cũng giống như bạn sẽ học các lớp bình thường.

Nói rằng tôi đã có một BaseModel:

class BaseModel(object): 
    __metaclass__ = Blah 

và bạn bây giờ bạn muốn kế thừa này trong một lớp học mới được gọi là MyModel, nhưng bạn muốn chèn một số chức năng bổ sung vào metaclass, nhưng nếu không rời khỏi chức năng ban đầu nguyên vẹn. Để làm điều đó, bạn sẽ làm một việc như:

class MyModelMetaClass(BaseModel.__metaclass__): 
    def __init__(cls, *args, **kwargs): 
     do_custom_stuff() 
     super(MyModelMetaClass, cls).__init__(*args, **kwargs) 
     do_more_custom_stuff() 

class MyModel(BaseModel): 
    __metaclass__ = MyModelMetaClass