half object/half dictionary

Carl Banks imbosol at vt.edu
Mon Dec 9 16:53:35 EST 2002


Manuel M. Garcia wrote:


> The code for this "metaclass" is at the end of this post.
> 
> Anyway, is this really a "metaclass"?  I guess I should be using
> "__metaclass__", but I really don't understand the difference between
> setting "__metaclass__" and regular subclassing.  I don't understand
> the difference between "__init__" and "__new__".

It isn't a metaclass.

Simply put, a metaclass (or metatype, they are interchangable) is a
type who's instances are types.

(And, to be specific, a type is any Python object that is capable of
having instances, whereas "type" is a certain Python object that all
Python built-in types are instances of.  "type" is a metatype.)

To understand, on the surface, what __metaclass__ does, you have to
think about what the class statement does, ignoring old-style classes
for the moment.  What the class statements really does is ask the
"type" object to create an instance of itself.  It does this by
calling the object with three arguments: the class's name, a tuple of
it's bases, and a dict of the class attributes.  Essentially, the
following class definition:

    class spam(object):
        a = 1
        def b: pass

is equivalent to the following assignment:

    spam = type("spam", (object,), {'a': 1, 'b': <function b at 0x80d3a14>})

What "type" does, when called this way, is to create an instance of
itself (initializing the name, bases, and dict using the arguments).
Instances of "type" are, of course, types, and that's how the class
statement creates types.

When you use __metaclass__ = some_other_object, you are telling Python
that you want the some_other_object (instead of "type") to handle the
creation of this object.  Therefore, the following class definition:

    class spam(object):
        __metaclass__ = some_other_object
        a = 1

is equivalent to this assignment:

    spam = some_other_object("spam", (object,),
                             {'a': 1, '__metaclass__': some_other_object)

If some_other_object is a metaclass (it doesn't have to be), it
typically returns an instance of itself, as "type" does.  However, it
is free to create an object with radically different behavior than an
instance of "type".  And instances of that object (which is a type,
since it is an instance of a metaclass) will have radically different
behavior also.


Confuesed yet?

However, this is only the beginning.  Understanding what use you can
put that power to is quite hard; I am only beginning to get a feel for
what I can do with metaclasses.


> Does it make sense for a class to have more than one "__metaclass__"
> set?
> 
> Metaclasses typically subclass 'type', right?

Yes, because instances of metaclasses are types.  In C, you could
write a class that does not subclass type but is a metaclass; but in
Python, a metaclass has to be a subclass of another metaclass.

The only metaclass built into Python (for now), is, of course, type,
so that's what Python metaclasses typically subclass.


> I haven't found any documentation on Python metaclasses that I
> understand.  What is everyone's favorite reference on Python
> metaclasses?

Don't feel too bad.  I've had a hard time getting my head around this
metaclasses stuff, and I usually tackle new concepts easily.  This is
hard stuff to get a feel for.

I basically learned metaclasses from the PEPs, so I can't help you
much.  There is a page on the Python site that shows how to use
old-style classes as metaclasses.

http://www.python.org/doc/essays/metaclasses/

It wasn't much help in learning the details of how to use metaclasses
with new-style clasess, but it was helpful in getting me a feel for
what metaclasses could do.

If you're a visual learner, I suggest drawing a graph showing type,
instance, and subclass relationships.  Use boxes to represent
_objects_, and arrows to represent type-of, instance-of, and
subclass-of.


> (code follows)
> 
> class HalfObjectHalfDict(object):
>    """Subclass to make object you can operate on like a dictionary.
>    Assumes you are setting __slots__
> 
>    """
>    #
>    __slots__ = ()
>    #
>    def __init__(self, *args0, **args1):
>        self.Update(*args0, **args1)
>    #
>    def __len__(self):
>        return len(self.SlotsDict())
>    #
>    def __contains__(self, item):
>        return item in self.SlotsDict()
>    #
>    def __iter__(self):
>        return iter(self.SlotsDict())
>    #
>    def __getitem__(self, key):
>        if self.IsInSlots(key):
>            try:
>                value = self.__getattribute__(key)
>            except AttributeError:
>                raise KeyError(key)
>            else:
>                return value
>        else:
>            raise KeyError(key)
>    #
>    def __setitem__(self, key, value):
>        if not self.IsInSlots(key):
>            raise AttributeError
>        self.__setattr__(key, value)
>    #
>    def Slots(self):
>        return self.__class__.__slots__
>    #
>    def IsInSlots(self, name):
>        return name in self.__class__.__slots__
>    #
>    def SetOnlySlot(self, name, value):
>        if not self.IsInSlots(name): return
>        self.__setattr__(name, value)
>    #
>    def UpdateFromDict(self, d):
>        for key,value in d.items():
>            self.SetOnlySlot(key, value)
>    #
>    def Update(self, *args0, **args1):
>        for a in args0: self.UpdateFromDict(a)
>        self.UpdateFromDict(args1)
>    #
>    def SlotsDict(self):
>        d = {}
>        for name in self.Slots():
>            try:
>                value = self.__getattribute__(name)
>            except AttributeError:
>                pass
>            else:
>                d[name] = value
>        return d
>    #
>    def has_key(self, k):
>        return self.SlotsDict().has_key(k)
>    #
>    def keys(self):
>        return self.SlotsDict().keys()
>    #
>    def values(self):
>        return self.SlotsDict().values()
>    #
>    def items(self):
>        return self.SlotsDict().items()
>    #
>    def copy(self):
>        return copy.copy(self)
>    #
>    def update(self, d):
>        self.UpdateFromDict(d)
>    #
>    def get(self, key, default=None):
>        return self.SlotsDict().get(key, default)


I guess it'll work, but it seems highly inefficient to me.  You are
creating a new dict every call to SlotsDict, which seems to happen
every call.

But you don't really have to create the dict; you can use getattr and
setattr and __slots__.  I'll give you an example:


class HalfObjectHalfDict(object):
    __slots__ = ()

    def keys(self):
        return self.__slots__

    def values(self):
        return [ getattr(self,x) for x in self.__slots__ ]

    def items(self):
        return [ (x,getattr(self,x)) for x in self.__slots__ ]

    def __getitem__(self,attr):
        return getattr(self,attr)

    def __setitem__(self,attr,value):
        return setitme(self,attr,value)

    def update(self,other):
        for attr in other.__slots__:
            setattr(self,attr,getattr(other,attr))

    ...

-- 
CARL BANKS



More information about the Python-list mailing list