What are some (concrete) use-cases for metaclasses?

前端 未结 19 941
悲&欢浪女
悲&欢浪女 2020-12-07 06:51

I have a friend who likes to use metaclasses, and regularly offers them as a solution.

I am of the mind that you almost never need to use metaclasses. Why? because I

相关标签:
19条回答
  • 2020-12-07 07:44

    The way I used metaclasses was to provide some attributes to classes. Take for example:

    class NameClass(type):
        def __init__(cls, *args, **kwargs):
           type.__init__(cls, *args, **kwargs)
           cls.name = cls.__name__
    

    will put the name attribute on every class that will have the metaclass set to point to NameClass.

    0 讨论(0)
  • 2020-12-07 07:45

    Some GUI libraries have trouble when multiple threads try to interact with them. tkinter is one such example; and while one can explicitly handle the problem with events and queues, it can be far simpler to use the library in a manner that ignores the problem altogether. Behold -- the magic of metaclasses.

    Being able to dynamically rewrite an entire library seamlessly so that it works properly as expected in a multithreaded application can be extremely helpful in some circumstances. The safetkinter module does that with the help of a metaclass provided by the threadbox module -- events and queues not needed.

    One neat aspect of threadbox is that it does not care what class it clones. It provides an example of how all base classes can be touched by a metaclass if needed. A further benefit that comes with metaclasses is that they run on inheriting classes as well. Programs that write themselves -- why not?

    0 讨论(0)
  • 2020-12-07 07:52

    You never absolutely need to use a metaclass, since you can always construct a class that does what you want using inheritance or aggregation of the class you want to modify.

    That said, it can be very handy in Smalltalk and Ruby to be able to modify an existing class, but Python doesn't like to do that directly.

    There's an excellent DeveloperWorks article on metaclassing in Python that might help. The Wikipedia article is also pretty good.

    0 讨论(0)
  • 2020-12-07 07:53

    I was asked the same question recently, and came up with several answers. I hope it's OK to revive this thread, as I wanted to elaborate on a few of the use cases mentioned, and add a few new ones.

    Most metaclasses I've seen do one of two things:

    1. Registration (adding a class to a data structure):

      models = {}
      
      class ModelMetaclass(type):
          def __new__(meta, name, bases, attrs):
              models[name] = cls = type.__new__(meta, name, bases, attrs)
              return cls
      
      class Model(object):
          __metaclass__ = ModelMetaclass
      

      Whenever you subclass Model, your class is registered in the models dictionary:

      >>> class A(Model):
      ...     pass
      ...
      >>> class B(A):
      ...     pass
      ...
      >>> models
      {'A': <__main__.A class at 0x...>,
       'B': <__main__.B class at 0x...>}
      

      This can also be done with class decorators:

      models = {}
      
      def model(cls):
          models[cls.__name__] = cls
          return cls
      
      @model
      class A(object):
          pass
      

      Or with an explicit registration function:

      models = {}
      
      def register_model(cls):
          models[cls.__name__] = cls
      
      class A(object):
          pass
      
      register_model(A)
      

      Actually, this is pretty much the same: you mention class decorators unfavorably, but it's really nothing more than syntactic sugar for a function invocation on a class, so there's no magic about it.

      Anyway, the advantage of metaclasses in this case is inheritance, as they work for any subclasses, whereas the other solutions only work for subclasses explicitly decorated or registered.

      >>> class B(A):
      ...     pass
      ...
      >>> models
      {'A': <__main__.A class at 0x...> # No B :(
      
    2. Refactoring (modifying class attributes or adding new ones):

      class ModelMetaclass(type):
          def __new__(meta, name, bases, attrs):
              fields = {}
              for key, value in attrs.items():
                  if isinstance(value, Field):
                      value.name = '%s.%s' % (name, key)
                      fields[key] = value
              for base in bases:
                  if hasattr(base, '_fields'):
                      fields.update(base._fields)
              attrs['_fields'] = fields
              return type.__new__(meta, name, bases, attrs)
      
      class Model(object):
          __metaclass__ = ModelMetaclass
      

      Whenever you subclass Model and define some Field attributes, they are injected with their names (for more informative error messages, for example), and grouped into a _fields dictionary (for easy iteration, without having to look through all the class attributes and all its base classes' attributes every time):

      >>> class A(Model):
      ...     foo = Integer()
      ...
      >>> class B(A):
      ...     bar = String()
      ...
      >>> B._fields
      {'foo': Integer('A.foo'), 'bar': String('B.bar')}
      

      Again, this can be done (without inheritance) with a class decorator:

      def model(cls):
          fields = {}
          for key, value in vars(cls).items():
              if isinstance(value, Field):
                  value.name = '%s.%s' % (cls.__name__, key)
                  fields[key] = value
          for base in cls.__bases__:
              if hasattr(base, '_fields'):
                  fields.update(base._fields)
          cls._fields = fields
          return cls
      
      @model
      class A(object):
          foo = Integer()
      
      class B(A):
          bar = String()
      
      # B.bar has no name :(
      # B._fields is {'foo': Integer('A.foo')} :(
      

      Or explicitly:

      class A(object):
          foo = Integer('A.foo')
          _fields = {'foo': foo} # Don't forget all the base classes' fields, too!
      

      Although, on the contrary to your advocacy for readable and maintainable non-meta programming, this is much more cumbersome, redundant and error prone:

      class B(A):
          bar = String()
      
      # vs.
      
      class B(A):
          bar = String('bar')
          _fields = {'B.bar': bar, 'A.foo': A.foo}
      

    Having considered the most common and concrete use cases, the only cases where you absolutely HAVE to use metaclasses are when you want to modify the class name or list of base classes, because once defined, these parameters are baked into the class, and no decorator or function can unbake them.

    class Metaclass(type):
        def __new__(meta, name, bases, attrs):
            return type.__new__(meta, 'foo', (int,), attrs)
    
    class Baseclass(object):
        __metaclass__ = Metaclass
    
    class A(Baseclass):
        pass
    
    class B(A):
        pass
    
    print A.__name__ # foo
    print B.__name__ # foo
    print issubclass(B, A)   # False
    print issubclass(B, int) # True
    

    This may be useful in frameworks for issuing warnings whenever classes with similar names or incomplete inheritance trees are defined, but I can't think of a reason beside trolling to actually change these values. Maybe David Beazley can.

    Anyway, in Python 3, metaclasses also have the __prepare__ method, which lets you evaluate the class body into a mapping other than a dict, thus supporting ordered attributes, overloaded attributes, and other wicked cool stuff:

    import collections
    
    class Metaclass(type):
    
        @classmethod
        def __prepare__(meta, name, bases, **kwds):
            return collections.OrderedDict()
    
        def __new__(meta, name, bases, attrs, **kwds):
            print(list(attrs))
            # Do more stuff...
    
    class A(metaclass=Metaclass):
        x = 1
        y = 2
    
    # prints ['x', 'y'] rather than ['y', 'x']
    

     

    class ListDict(dict):
        def __setitem__(self, key, value):
            self.setdefault(key, []).append(value)
    
    class Metaclass(type):
    
        @classmethod
        def __prepare__(meta, name, bases, **kwds):
            return ListDict()
    
        def __new__(meta, name, bases, attrs, **kwds):
            print(attrs['foo'])
            # Do more stuff...
    
    class A(metaclass=Metaclass):
    
        def foo(self):
            pass
    
        def foo(self, x):
            pass
    
    # prints [<function foo at 0x...>, <function foo at 0x...>] rather than <function foo at 0x...>
    

    You might argue ordered attributes can be achieved with creation counters, and overloading can be simulated with default arguments:

    import itertools
    
    class Attribute(object):
        _counter = itertools.count()
        def __init__(self):
            self._count = Attribute._counter.next()
    
    class A(object):
        x = Attribute()
        y = Attribute()
    
    A._order = sorted([(k, v) for k, v in vars(A).items() if isinstance(v, Attribute)],
                      key = lambda (k, v): v._count)
    

     

    class A(object):
    
        def _foo0(self):
            pass
    
        def _foo1(self, x):
            pass
    
        def foo(self, x=None):
            if x is None:
                return self._foo0()
            else:
                return self._foo1(x)
    

    Besides being much more ugly, it's also less flexible: what if you want ordered literal attributes, like integers and strings? What if None is a valid value for x?

    Here's a creative way to solve the first problem:

    import sys
    
    class Builder(object):
        def __call__(self, cls):
            cls._order = self.frame.f_code.co_names
            return cls
    
    def ordered():
        builder = Builder()
        def trace(frame, event, arg):
            builder.frame = frame
            sys.settrace(None)
        sys.settrace(trace)
        return builder
    
    @ordered()
    class A(object):
        x = 1
        y = 'foo'
    
    print A._order # ['x', 'y']
    

    And here's a creative way to solve the second one:

    _undefined = object()
    
    class A(object):
    
        def _foo0(self):
            pass
    
        def _foo1(self, x):
            pass
    
        def foo(self, x=_undefined):
            if x is _undefined:
                return self._foo0()
            else:
                return self._foo1(x)
    

    But this is much, MUCH voodoo-er than a simple metaclass (especially the first one, which really melts your brain). My point is, you look at metaclasses as unfamiliar and counter-intuitive, but you can also look at them as the next step of evolution in programming languages: you just have to adjust your mindset. After all, you could probably do everything in C, including defining a struct with function pointers and passing it as the first argument to its functions. A person seeing C++ for the first time might say, "what is this magic? Why is the compiler implicitly passing this to methods, but not to regular and static functions? It's better to be explicit and verbose about your arguments". But then, object-oriented programming is much more powerful once you get it; and so is this, uh... quasi-aspect-oriented programming, I guess. And once you understand metaclasses, they're actually very simple, so why not use them when convenient?

    And finally, metaclasses are rad, and programming should be fun. Using standard programming constructs and design patterns all the time is boring and uninspiring, and hinders your imagination. Live a little! Here's a metametaclass, just for you.

    class MetaMetaclass(type):
        def __new__(meta, name, bases, attrs):
            def __new__(meta, name, bases, attrs):
                cls = type.__new__(meta, name, bases, attrs)
                cls._label = 'Made in %s' % meta.__name__
                return cls 
            attrs['__new__'] = __new__
            return type.__new__(meta, name, bases, attrs)
    
    class China(type):
        __metaclass__ = MetaMetaclass
    
    class Taiwan(type):
        __metaclass__ = MetaMetaclass
    
    class A(object):
        __metaclass__ = China
    
    class B(object):
        __metaclass__ = Taiwan
    
    print A._label # Made in China
    print B._label # Made in Taiwan
    

    Edit

    This is a pretty old question, but it's still getting upvotes, so I thought I'd add a link to a more comprehensive answer. If you'd like to read more about metaclasses and their uses, I've just published an article about it here.

    0 讨论(0)
  • 2020-12-07 07:53

    There seems to be a legitimate use described here - Rewriting Python Docstrings with a Metaclass.

    0 讨论(0)
  • 2020-12-07 07:54

    Another use case is when you want to be able to modify class-level attributes and be sure that it only affects the object at hand. In practice, this implies "merging" the phases of metaclasses and classes instantiations, thus leading you to deal only with class instances of their own (unique) kind.

    I also had to do that when (for concerns of readibility and polymorphism) we wanted to dynamically define propertys which returned values (may) result from calculations based on (often changing) instance-level attributes, which can only be done at the class level, i.e. after the metaclass instantiation and before the class instantiation.

    0 讨论(0)
提交回复
热议问题