Magellan88
Magellan88

Reputation: 2573

Using classmethods to implement alternative constructors, how can I add functions to those alternative constructors?

I have an class that can be constructed via alternative constructors using class methods.

class A:

    def __init__(self, a, b):
            self.a = a
            self.b = b

    @classmethod
    def empty(cls, b):
        return cls( 0 , b)

So let's say instead of constructing A like A() I can now also do A.empty().

For user convenience, I would like to extend this empty method even further, so that I can initialize A via A.empty() as well as the more specialized but closely-related A.empty.typeI() and A.empty.typeII().

My naive approach did not quite do what I wanted:

class A:

    def __init__(self, a, b):
            self.a = a
            self.b = b

    @classmethod
    def empty(cls, b):

        def TypeI(b):
            return cls( 0 , b-1)

        def TypeII(b):
            return cls( 0 , b-2)

        return cls( 0 , b)

Can anyone tell me how that could be done (or at least convince me why that would be terrible idea). I want to stress that for usage I imagine such an approach to be very convenient and clear for the users as the functions are grouped intuitively.

Upvotes: 1

Views: 272

Answers (4)

martineau
martineau

Reputation: 123473

Here's an enhanced implementation of my other answer that makes it — as one commenter put it — "play well with inheritance". You may not need this, but others doing something similar might.

It accomplishes this by using a metaclass to dynamically create and add an nested Empty class similar to that shown in the other answer. The main difference is that the default Empty class in derived classes will now return Derived instances instead of instances of A, the base class.

Derived classes can override this default behavior by defining their own nested Empty class (it can even be derived from the one in the one in the base class). Also note that for Python 3, metaclasses are specified using different syntax:

class A(object, metaclass=MyMetaClass):

Here's the revised implementation using Python 2 metaclass syntax:

class MyMetaClass(type):
    def __new__(metaclass, name, bases, classdict):
        # create the class normally
        MyClass = super(MyMetaClass, metaclass).__new__(metaclass, name, bases,
                                                        classdict)
        # add a default nested Empty class if one wasn't defined
        if 'Empty' not in classdict:
            class Empty(object):
                def __new__(cls, b):
                    return MyClass(0, b)

                @staticmethod
                def TypeI(b):
                    return MyClass(0, b-1)

                @staticmethod
                def TypeII(b):
                    return MyClass(0, b-2)
            setattr(MyClass, 'Empty', Empty)
        return MyClass

class A(object):
    __metaclass__ = MyMetaClass
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __repr__(self):
        return '{}({}, {})'.format(self.__class__.__name__, self.a, self.b)

a = A(1, 1)
print('a: {}'.format(a))      # --> a: A(1, 1)
b = A.Empty(2)
print('b: {}'.format(b))      # --> b: A(0, 2)
bi = A.Empty.TypeI(4)
print('bi: {}'.format(bi))    # --> bi: A(0, 3)
bii = A.Empty.TypeII(6)
print('bii: {}'.format(bii))  # --> bii: A(0, 4)

With the above, you can now do something like this:

class Derived(A):
    pass  # inherits everything, except it will get a custom Empty

d = Derived(1, 2)
print('d: {}'.format(d))      # --> d: Derived(1, 2)
e = Derived.Empty(3)
print('e: {}'.format(e))      # --> e: Derived(0, 3)
ei = Derived.Empty.TypeI(5)
print('ei: {}'.format(ei))    # --> ei: Derived(0, 4)
eii = Derived.Empty.TypeII(7)
print('eii: {}'.format(eii))  # --> eii: Derived(0, 5)

Upvotes: 1

martineau
martineau

Reputation: 123473

You can implement what you want by making Empty a nested class of A rather than a class method. More than anything else this provides a convenient namespace — instances of it are never created — in which to place various alternative constructors and can easily be extended.

class A(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __repr__(self):
        return 'A({}, {})'.format(self.a, self.b)

    class Empty(object):  # nested class
        def __new__(cls, b):
            return A(0, b)  # ignore cls & return instance of enclosing class

        @staticmethod
        def TypeI(b):
            return A(0, b-1)

        @staticmethod
        def TypeII(b):
            return A(0, b-2)

a = A(1, 1)
print('a: {}'.format(a))      # --> a: A(1, 1)
b = A.Empty(2)
print('b: {}'.format(b))      # --> b: A(0, 2)
bi = A.Empty.TypeI(4)
print('bi: {}'.format(bi))    # --> bi: A(0, 3)
bii = A.Empty.TypeII(6)
print('bii: {}'.format(bii))  # --> bii: A(0, 4)

Upvotes: 2

poke
poke

Reputation: 387707

You can’t really do that because A.empty.something would require the underlying method object to be bound to the type, so you can actually call it. And Python simply won’t do that because the type’s member is empty, not TypeI.

So what you would need to do is to have some object empty (for example a SimpleNamespace) in your type which returns bound classmethods. The problem is that we cannot yet access the type as we define it with the class structure. So we cannot access its members to set up such an object. Instead, we would have to do it afterwards:

class A:
    def __init__ (self, a, b):
        self.a = a
        self.b = b

    @classmethod
    def _empty_a (cls, b):
        return cls(1, b)

    @classmethod
    def _empty_b (cls, b):
        return cls(2, b)

A.empty = SimpleNamespace(a = A._empty_a, b = A._empty_b)

Now, you can access that member’s items and get bound methods:

>>> A.empty.a
<bound method type._empty_a of <class '__main__.A'>>
>>> A.empty.a('foo').a
1

Of course, that isn’t really that pretty. Ideally, we want to set this up when we define the type. We could use meta classes for this but we can actually solve this easily using a class decorator. For example this one:

def delegateMember (name, members):
    def classDecorator (cls):
        mapping = { m: getattr(cls, '_' + m) for m in members }
        setattr(cls, name, SimpleNamespace(**mapping))
        return cls
    return classDecorator

@delegateMember('empty', ['empty_a', 'empty_b'])
class A:
    def __init__ (self, a, b):
        self.a = a
        self.b = b

    @classmethod
    def _empty_a (cls, b):
        return cls(1, b)

    @classmethod
    def _empty_b (cls, b):
        return cls(2, b)

And magically, it works:

>>> A.empty.empty_a
<bound method type._empty_a of <class '__main__.A'>>

Now that we got it working somehow, of course we should discuss whether this is actually something you want to do. My opinion is that you shouldn’t. You can already see from the effort it took that this isn’t something that’s usually done in Python. And that’s already a good sign that you shouldn’t do it. Explicit is better than implicit, so it’s probably a better idea to just expect your users to type the full name of the class method. My example above was of course structured in a way that A.empty.empty_a would have been longer than just a A.empty_a. But even with your name, there isn’t a reason why it couldn’t be just an underscore instead of a dot.

And also, you can simply add multiple default paths inside a single method. Provide default argument values, or use sensible fallbacks, and you probably don’t need many class methods to create alternative versions of your type.

Upvotes: 2

shx2
shx2

Reputation: 64318

It is generally better to have uniform class interfaces, meaning the different usages should be consistent with each other. I consider A.empty() and A.empty.type1() to be inconsistent with each other, because the prefix A.empty ituitively means different things in each of them.

A better interface would be:

class A:
    @classmethod
    def empty_default(cls, ...): ...
    @classmethod
    def empty_type1(cls, ...): ...
    @classmethod
    def empty_type2(cls, ...): ...

Or:

class A:
    @classmethod
    def empty(cls, empty_type, ...): ...

Upvotes: 1

Related Questions