James
James

Reputation: 3231

Ordered classes in Python 3

I am trying to use an 'ordered class' as described in PEP 3115 (that is, a class whose members can be accessed in the order they were declared). The implementation given there is

# The custom dictionary
class member_table(dict):
    def __init__(self):
        self.member_names = []

    def __setitem__(self, key, value):
        # if the key is not already defined, add to the
        # list of keys.
        if key not in self:
            self.member_names.append(key)

        # Call superclass
        dict.__setitem__(self, key, value)

# The metaclass
class OrderedClass(type):

    # The prepare function
    @classmethod
    def __prepare__(metacls, name, bases): # No keywords in this case
        return member_table()

    # The metaclass invocation
    def __new__(cls, name, bases, classdict):
        # Note that we replace the classdict with a regular
        # dict before passing it to the superclass, so that we
        # don't continue to record member names after the class
        # has been created.
        result = type.__new__(cls, name, bases, dict(classdict))
        result.member_names = classdict.member_names
        return result

class MyClass(metaclass=OrderedClass):
    # method1 goes in array element 0
    def method1(self):
        pass

    # method2 goes in array element 1
    def method2(self):
        pass

There are a few things I am confused about. Firstly, is there a reason why __prepare__ is a classmethod? The definition doesn't use metacls - is this just a convention?

Secondly, when I try this code, '__module__' ends up in MyClass.member_names before 'method1' and 'method2', apparently contradicting the comments which claim 'method1' is the first element. Why does this special attribute end up in the list while no others do? Are there any others that might surprise me (apart from __doc__ if the class has a docstring, and any I define explicitly)?

Finally, this implementation does not retrieve the member_names from base classes. If I want to achieve that, is there anything wrong with the following change to __prepare__ (apart from the fact that it doesn't check for duplicates)?

@classmethod
def __prepare__(metacls, name, bases):
    prep_dict = member_table()
    for base in bases:
        try:
            prep_dict.member_names.extend(base.member_names)
        except AttributeError:
            pass
    return prep_dict

Upvotes: 4

Views: 1959

Answers (1)

jsbueno
jsbueno

Reputation: 110271

Firstly, is there a reason why __prepare__ is a classmethod? The definition doesn't use metacls - is this just a convention?

As pointed out in the comments, when __prepare__ is called the class itself is not yet instantiated. That means, if it where an ordinary method (with self as the first parameter) - there would be no value to become the self in this call.

And it makes sense, because what __prepare__ does is to return a dict like object to be used instead of an ordinary dict in the parsing of the class body - so it does not depend on the class being created at all.

If, as mentioned in the comments, it were a staticmethod (meaning it would not get the metacls first parameter) then __prepare__ would be unable to access any other methods of the metaclass -- this is unnecessarily limiting.

And yes, just like self, metacls in this case is just a convention - an ordinary Python identifier. (Note that when classmethods are used in ordinary classes, the first parameter is usually denoted cls instead of metacls.)

Secondly, when I try this code, __module__ ends up in MyClass.member_names before method1 and method2, apparently contradicting the comments which claim method1 is the first element. Why does this special attribute end up in the list while no others do? Are there any others that might surprise me (apart from __doc__ if the class has a docstring, and any I define explicitly)?

Probably because the __module__ special member for classes was thought off after they thought about the __prepare__ method for metaclasses. (I was unable to verify by googling for the PEP defining __module__ but I'd bet this is the case.)

And no, there are no guarantees that future versions of Python won't add some more magic attributes to classes that could come before your explicit class members in the dictionary.

But with metaclasses, you are fully in control - you can just tweak your dict like object (member_table in your code) to not count any attributes starting with "__" - for example. Or even not to add then to the final class instance at all (at risk of your classes defined this way not working with certain Python features).

Finally, this implementation does not retrieve the member_names from base classes. If I want to achieve that, is there anything wrong with the following change to __prepare__?

By reading it, I see nothing wrong with your proposed implementation, but you'd have to test it, of course.

Update (2013-06-30): this subject is being actively discussed in the Python developer list, and itlooks like from Python 3.4 all classes will be ordered by default, with no need to use a metaclass or __prepare__ only for ordering

Upvotes: 5

Related Questions