I just made an Advogato entry, but it can't handle Python code, so for now I'm going to stick the code here:

class Prototype (type):
    def __init__ (metacls, name, bases, attributes):
        super(Prototype, metacls).__init__(name, bases, attributes)

    def __new__ (metacls, name, bases, attributes):
        klass = super(Prototype, metacls).__new__(
            metacls, name,
            tuple(map(type, bases)),
            attributes)
        inst = super(type(klass), klass).__call__()
        return inst

    def __call__(self, *args):
        return type(self)(*args)

__metaclass__ = Prototype

class Room:
    d = 3

    def describe (self):
        print 'I am a room.  There are %i kittens here.' % self.d

class SpecialRoom(Room):
    d = 4

class SpecialerRoom(SpecialRoom):
    d = 6

print Room, type(Room), Room.d
Room.describe()
print SpecialRoom, type(SpecialRoom), SpecialRoom.d
SpecialRoom.describe()
SpecialerRoom.describe()

Does your brain hurt yet?


Anonymous@65.93.149.228 (2004/03/22): Why is that supposed to hurt my brain? It works as expected.

  • jnc (2004/03/22): As an introduction to metaclasses, it's probably not ideal, though...


jnc (2004/03/22): Belatedly, it occurs to me that I linked to the original Google post I took this from on advogato, so there was no reason to repeat it here.


jnc (2004/03/22): Ok, I've traced through the steps in object creation, and I've finally figured out exactly when everything is called. Prototype._init_ never is: instead, Room._init_ is called twice, with different parameters. This is pretty ugly.

The reason is that the init sequence is: call Metaclass._new_, which returns a class object C, and then calls C._init_ (which, for a class, is implicitly Metaclass._init_ if I'm not mistaken). But Prototype._new_ returns an instance c, not class C. So c._init_ gets called once when it's instantiated, as expected, and then again when it's returned. Oops. Not sure how to get around this, or even if it's necessary.

  • jnc (2004/04/03): Very interesting. Room._init_ is called twice in version 2.2.1, but not version 2.2.3 or later (haven't tried 2.2.2). I find that odd because the behaviour I described was perfectly comprehensible, so I wouldn't have called it a bug. Ah, well, I'm not complaining.


jnc (2004/04/04): This implements IoStyleInheritance?, and therefore CloningByDelegation?. It turns the "class" keyword into an ObjectDefinitionStatement?, which I think is a pretty neat trick, although it would be nice to be able to make an entirely new statement - any ideas? This is missing anonymous objects, but they're easy to add:

  def clone(self):
    class anon(self): pass
    return self


jnc (2004/04/04): Note also that this could be done without metaclasses with the following syntax:

  class SpecialRoom(Room):
    d = 4
  SpecialRoom = SpecialRoom()

PEP 318's decorator syntax could turn this into

  class SpecialRoom(Room) [prototype]:
    d = 4


jnc (2004/07/09): The super() call is usable directly, because you can still get at an object's class with type(obj) - it's just hidden. So SpecialRoom? could chain on to Room.describe() by adding super(type(SpecialRoom?), self).describe(), or simply type(SpecialRoom?).describe() for simple single inheritance. It would be even better to wrap super so that it doesn't even require the type keyword.


jnc (2004/07/16): Slight problem here:

class Room:
    class exits: pass    # a structure to inherit

class TrollRoom(Room):
    exits.west = Maze

The definition of TrollRoom? will fail, because "exits" does not yet exist - it doesn't get access to the parent's fields until the metaclass _new_ runs, which isn't until after the class definition is complete. I can't find anywhere to intervene before that. (I can override exits by assigning a new value, but not modify the existing one.)

However, you don't want people to be able to modify the existing one. It's shared - exits.west = Maze would add a west attribute to Room.exits. To get a private copy, I want to do this:

class TrollRoom(Room):
    def __init__(self):
        exits = Room.exits.clone()    # assume a clone() statement for making anonymous objects
        exits.west = Maze

Being unable to alter exits without doing that is a safety feature.


Anonymous@4.60.130.252 (2004/09/10): Here's my version:
import copy
class ClassObj(type):

	def __new__(meta, name, bases, attrs):
		for a in attrs:
			obj = attrs[a]
			if issubclass(type(obj), ClassObj):
                                attrs[a] = obj._new()
			else:
				try:
					newval = copy.deepcopy(obj)
				except TypeError:
					newval = obj
				attrs[a] = newval
				
		def newClassObj(self):
			return self.__class__.__class__(self._className, self._classBases, self._classAttrs)
		attrs['_new'] = newClassObj
		
		return super(ClassObj, meta).__new__(meta, name, bases, attrs)

	def __init__(self, name, bases, attrs):
		inst = self()
		self.inst = inst
		self._className = name
		self._classBases = bases
		self._classAttrs = attrs
		for a in attrs:
			if issubclass(type(attrs[a]), type(object)):
				getattr(self, a).lexicalParent = self
		
	def __getattribute__(self, attr):
		import types
		try:
			val = super(ClassObj, self).__getattribute__(attr)
		except:
			raise
		else:
			if type(val) == types.MethodType:
				return getattr(super(ClassObj, self).__getattribute__('inst'), attr)
			else:
				return val
The approach here is a little different -- the class doesn't replace itself with an instance. Instead, at init, it creates an instance and stores it as a class attribute. It uses this instance for just one thing: method calling. All other attributes are actually stored on the class object. The advantage is that the object really IS a class, so you can use super() and issubclass() normally.

The _new method is my clone thing; I add it as a method to the class. I've also thrown some deepcopy stuff in there just to make sure the clone actually copies the class, instead of referencing the same attributes.

-- BrenBarn