How does one create a metaclass?
This question already has an answer here:
There are (at this point) two key methods in a metaclass:
__prepare__
, and __new__
__prepare__
lets you supply a custom mapping (such as an OrderedDict
) to be used as the namespace while the class is being created. You must return an instance of whatever namespace you choose. If you don't implement __prepare__
a normal dict
is used.
__new__
is responsible for the actual creation/modification of the final class.
A bare-bones, do-nothing-extra metaclass would look like:
class Meta(type):
def __prepare__(metaclass, cls, bases):
return dict()
def __new__(metacls, cls, bases, clsdict):
return super().__new__(metacls, cls, bases, clsdict)
A simple example:
Say you want some simple validation code to run on your attributes -- like it must always be an int
or a str
. Without a metaclass, your class would look something like:
class Person:
weight = ValidateType('weight', int)
age = ValidateType('age', int)
name = ValidateType('name', str)
As you can see, you have to repeat the name of the attribute twice. This makes typos possible along with irritating bugs.
A simple metaclass can address that problem:
class Person(metaclass=Validator):
weight = ValidateType(int)
age = ValidateType(int)
name = ValidateType(str)
This is what the metaclass would look like (not using __prepare__
since it is not needed):
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)
A sample run of:
p = Person()
p.weight = 9
print(p.weight)
p.weight = '9'
produces:
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')
Notes
This example is simple enough it could have also been accomplished with a class decorator, but presumably an actual metaclass would be doing much more.
In Python 2.x, the __prepare__
method doesn't exist, and the class speficies its metaclass by including a class variable __metaclass__ = ...
, like this:
class Person(object):
__metaclass__ = ValidateType
The 'ValidateType' class for reference:
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
I've just written a fully commented example of a metaclass. It's in Python 2.7. I'm sharing it here and hope that it can help you understand more about the __new__
, __init__
, __call__
, __dict__
methods and the concept of bounded/unbounded in Python, as well as the use of metaclasses.
The problem with a metaclass, I feel, is that it has too many places where you can do the same things , or similar yet with some slight differences . So my comments and test cases mainly emphasizes where to write what , what goes to where at certain points, and what are accessible to a certain object.
The example tries to build a class factory while maintaining well-formed class definitions.
from pprint import pprint
from types import DictType
class FactoryMeta(type):
""" Factory Metaclass """
# @ Anything "static" (bounded to the classes rather than the instances)
# goes in here. Or use "@classmethod" decorator to bound it to meta.
# @ Note that these members won't be visible to instances, you have to
# manually add them to the instances in metaclass' __call__ if you wish
# to access them through a instance directly (see below).
extra = "default extra"
count = 0
def clsVar(cls):
print "Class member 'var': " + str(cls.var)
@classmethod
def metaVar(meta):
print "Metaclass member 'var': " + str(meta.var)
def __new__(meta, name, bases, dict):
# @ Metaclass' __new__ serves as a bi-functional slot capable for
# initiating the classes as well as alternating the meta.
# @ Suggestion is putting majority of the class initialization code
# in __init__, as you can directly reference to cls there; saving
# here for anything you want to dynamically added to the meta (such
# as shared variables or lazily GC'd temps).
# @ Any changes here to dict will be visible to the new class and their
# future instances, but won't affect the metaclass. While changes
# directly through meta will be visible to all (unless you override
# it later).
dict['new_elem'] = "effective"
meta.var = "Change made to %s by metaclass' __new__" % str(meta)
meta.count += 1
print "================================================================"
print " Metaclass's __new__ (creates class objects)"
print "----------------------------------------------------------------"
print "Bounded to object: " + str(meta)
print "Bounded object's __dict__: "
pprint(DictType(meta.__dict__), depth = 1)
print "----------------------------------------------------------------"
print "Parameter 'name': " + str(name)
print "Parameter 'bases': " + str(bases)
print "Parameter 'dict': "
pprint(dict, depth = 1)
print "n"
return super(FactoryMeta, meta).__new__(meta, name, bases, dict)
def __init__(cls, name, bases, dict):
# @ Metaclass' __init__ is the standard slot for class initialization.
# Classes' common variables should mainly goes in here.
# @ Any changes here to dict won't actually affect anything. While
# changes directly through cls will be visible to the created class
# and its future instances. Metaclass remains untouched.
dict['init_elem'] = "defective"
cls.var = "Change made to %s by metaclass' __init__" % str(cls)
print "================================================================"
print " Metaclass's __init__ (initiates class objects)"
print "----------------------------------------------------------------"
print "Bounded to object: " + str(cls)
print "Bounded object's __dict__: "
pprint(DictType(cls.__dict__), depth = 1)
print "----------------------------------------------------------------"
print "Parameter 'name': " + str(name)
print "Parameter 'bases': " + str(bases)
print "Parameter 'dict': "
pprint(dict, depth = 1)
print "n"
return super(FactoryMeta, cls).__init__(name, bases, dict)
def __call__(cls, *args):
# @ Metaclass' __call__ gets called when a class name is used as a
# callable function to create an instance. It is called before the
# class' __new__.
# @ Instance's initialization code can be put in here, although it
# is bounded to "cls" rather than instance's "self". This provides
# a slot similar to the class' __new__, where cls' members can be
# altered and get copied to the instances.
# @ Any changes here through cls will be visible to the class and its
# instances. Metaclass remains unchanged.
cls.var = "Change made to %s by metaclass' __call__" % str(cls)
# @ "Static" methods defined in the meta which cannot be seen through
# instances by default can be manually assigned with an access point
# here. This is a way to create shared methods between different
# instances of the same metaclass.
cls.metaVar = FactoryMeta.metaVar
print "================================================================"
print " Metaclass's __call__ (initiates instance objects)"
print "----------------------------------------------------------------"
print "Bounded to object: " + str(cls)
print "Bounded object's __dict__: "
pprint(DictType(cls.__dict__), depth = 1)
print "n"
return super(FactoryMeta, cls).__call__(*args)
class Factory(object):
""" Factory Class """
# @ Anything declared here goes into the "dict" argument in the metaclass'
# __new__ and __init__ methods. This provides a chance to pre-set the
# member variables desired by the two methods, before they get run.
# @ This also overrides the default values declared in the meta.
__metaclass__ = FactoryMeta
extra = "overridng extra"
def selfVar(self):
print "Instance member 'var': " + str(self.var)
@classmethod
def classFactory(cls, name, bases, dict):
# @ With a factory method embedded, the Factory class can act like a
# "class incubator" for generating other new classes.
# @ The dict parameter here will later be passed to the metaclass'
# __new__ and __init__, so it is the right place for setting up
# member variables desired by these two methods.
dict['class_id'] = cls.__metaclass__.count # An ID starts from 0.
# @ Note that this dict is for the *factory product classes*. Using
# metaclass as callable is another way of writing class definition,
# with the flexibility of employing dynamically generated members
# in this dict.
# @ Class' member methods can be added dynamically by using the exec
# keyword on dict.
exec(cls.extra, dict)
exec(dict['another_func'], dict)
return cls.__metaclass__(name + ("_%02d" % dict['class_id']), bases, dict)
def __new__(cls, function):
# @ Class' __new__ "creates" the instances.
# @ This won't affect the metaclass. But it does alter the class' member
# as it is bounded to cls.
cls.extra = function
print "================================================================"
print " Class' __new__ ("creates" instance objects)"
print "----------------------------------------------------------------"
print "Bounded to object: " + str(cls)
print "Bounded object's __dict__: "
pprint(DictType(cls.__dict__), depth = 1)
print "----------------------------------------------------------------"
print "Parameter 'function': n" + str(function)
print "n"
return super(Factory, cls).__new__(cls)
def __init__(self, function, *args, **kwargs):
# @ Class' __init__ initializes the instances.
# @ Changes through self here (normally) won't affect the class or the
# metaclass; they are only visible locally to the instances.
# @ However, here you have another chance to make "static" things
# visible to the instances, "locally".
self.classFactory = self.__class__.classFactory
print "================================================================"
print " Class' __init__ (initiates instance objects)"
print "----------------------------------------------------------------"
print "Bounded to object: " + str(self)
print "Bounded object's __dict__: "
pprint(DictType(self.__dict__), depth = 1)
print "----------------------------------------------------------------"
print "Parameter 'function': n" + str(function)
print "n"
return super(Factory, self).__init__(*args, **kwargs)
# @ The metaclass' __new__ and __init__ will be run at this point, where the
# (manual) class definition hitting its end.
# @ Note that if you have already defined everything well in a metaclass, the
# class definition can go dummy with simply a class name and a "pass".
# @ Moreover, if you use class factories extensively, your only use of a
# manually defined class would be to define the incubator class.
The output looks like this (adapted for better demonstration):
================================================================
Metaclass's __new__ (creates class objects)
----------------------------------------------------------------
Bounded to object: <class '__main__.FactoryMeta'>
Bounded object's __dict__:
{ ...,
'clsVar': <function clsVar at 0x00000000029BC828>,
'count': 1,
'extra': 'default extra',
'metaVar': <classmethod object at 0x00000000029B4B28>,
'var': "Change made to <class '__main__.FactoryMeta'> by metaclass' __new__"}
----------------------------------------------------------------
Parameter 'name': Factory
Parameter 'bases': (<type 'object'>,)
Parameter 'dict':
{ ...,
'classFactory': <classmethod object at 0x00000000029B4DC8>,
'extra': 'overridng extra',
'new_elem': 'effective',
'selfVar': <function selfVar at 0x00000000029BC6D8>}
================================================================
Metaclass's __init__ (initiates class objects)
----------------------------------------------------------------
Bounded to object: <class '__main__.Factory'>
Bounded object's __dict__:
{ ...,
'classFactory': <classmethod object at 0x00000000029B4DC8>,
'extra': 'overridng extra',
'new_elem': 'effective',
'selfVar': <function selfVar at 0x00000000029BC6D8>,
'var': "Change made to <class '__main__.Factory'> by metaclass' __init__"}
----------------------------------------------------------------
Parameter 'name': Factory
Parameter 'bases': (<type 'object'>,)
Parameter 'dict':
{ ...,
'classFactory': <classmethod object at 0x00000000029B4DC8>,
'extra': 'overridng extra',
'init_elem': 'defective',
'new_elem': 'effective',
'selfVar': <function selfVar at 0x00000000029BC6D8>}
The calling sequence is metaclass' __new__
then its __init__
. __call__
won't be called at this time.
And if we create an instance,
func1 = (
"def printElems(self):n"
" print "Member new_elem: " + self.new_elemn"
" print "Member init_elem: " + self.init_elemn"
)
factory = Factory(func1)
The output is:
================================================================
Metaclass's __call__ (initiates instance objects)
----------------------------------------------------------------
Bounded to object: <class '__main__.Factory'>
Bounded object's __dict__:
{ ...,
'classFactory': <classmethod object at 0x00000000029B4DC8>,
'extra': 'overridng extra',
'metaVar': <bound method type.metaVar of <class '__main__.FactoryMeta'>>,
'new_elem': 'effective',
'selfVar': <function selfVar at 0x00000000029BC6D8>,
'var': "Change made to <class '__main__.Factory'> by metaclass' __call__"}
================================================================
Class' __new__ ("creates" instance objects)
----------------------------------------------------------------
Bounded to object: <class '__main__.Factory'>
Bounded object's __dict__:
{ ...,
'classFactory': <classmethod object at 0x00000000029B4DC8>,
'extra': 'def printElems(self):n print "Member new_elem: " + self.new_elemn print "Member init_elem: " + self.init_elemn',
'metaVar': <bound method type.metaVar of <class '__main__.FactoryMeta'>>,
'new_elem': 'effective',
'selfVar': <function selfVar at 0x00000000029BC6D8>,
'var': "Change made to <class '__main__.Factory'> by metaclass' __call__"}
----------------------------------------------------------------
Parameter 'function':
def printElems(self):
print "Member new_elem: " + self.new_elem
print "Member init_elem: " + self.init_elem
================================================================
Class' __init__ (initiates instance objects)
----------------------------------------------------------------
Bounded to object: <__main__.Factory object at 0x00000000029BB7B8>
Bounded object's __dict__:
{'classFactory': <bound method FactoryMeta.classFactory of <class '__main__.Factory'>>}
----------------------------------------------------------------
Parameter 'function':
def printElems(self):
print "Member new_elem: " + self.new_elem
print "Member init_elem: " + self.init_elem
The metaclass' __call__
gets called first, then class' __new__
and __init__
.
Comparing the printed members of each object, you can discover when and where they're added or changed, just as I commented in the code.
I also run the following test cases:
factory.clsVar() # Will raise exception
Factory.clsVar()
factory.metaVar()
factory.selfVar()
func2 = (
"@classmethodn"
"def printClassID(cls):n"
" print "Class ID: %02d" % cls.class_idn"
)
ProductClass1 = factory.classFactory("ProductClass", (object, ), { 'another_func': func2 })
product = ProductClass1()
product.printClassID()
product.printElems() # Will raise exception
ProductClass2 = Factory.classFactory("ProductClass", (Factory, ), { 'another_func': "pass" })
ProductClass2.printClassID() # Will raise exception
ProductClass3 = ProductClass2.classFactory("ProductClass", (object, ), { 'another_func': func2 })
Which you can run by yourself to see how it works.
Note that I intentionally left the dynamically generated classes' names different from the variable names they assigned to. This is to display which names are actually in effect.
Another note is that I put "static" in quotes, which I refer to the concept like in C++ rather than the Python decorator. Traditionally I'm a C++ programmer, so I still like to think in its way.
链接地址: http://www.djcxy.com/p/6986.html上一篇: 我们可以重载类对象的行为吗?
下一篇: 如何创建一个元类?