1

I would like to allow my class constructor to accept an instance of this class and in that case to return this same instance instead of creating a new object. Like what tuple does:

>>> t = (1, 2, 3)
>>> tuple(t) is t
True

I imagine I need to override the __new__ method for this, and additionally take care of this special case in the __init__ method. Are there any recipes for this?

I would have preferred to completely skip __init__ when the constructor is given a class instance that it is going to return unchanged, but I see no way. I am also a bit suspicious about the triple use of cls in the line with super:

class C:
    @staticmethod
    def __new__(cls, x=None):
        if isinstance(x, cls):
            return x
        else:
            return super(cls, cls).__new__(cls)

    def __init__(self, x=None):
        if x is self: return  # Can I just skip __init__ instead?
        self.x = x

(I know about super() without arguments, but I do not like inconsistent magic.)

After learning more about super and about MRO in Python, i've figure out that this code is not good. For example, subclassing C results in

>>> class D(C): pass
>>> d = D(1)
......
RecursionError: maximum recursion depth exceeded while calling a Python object

I am so terrified by super() without arguments that magically extracts its arguments from the (lexical or dynamic?) context, that rather than using it I've decided to add a "callback" to finalise the class definition:

class C2:
    @classmethod
    def finalise(this_cls_yes_this_one):
        @staticmethod
        def __new__(cls, x=None):
            if isinstance(x, cls):
                return x
            else:
                return super(this_cls_yes_this_one, cls).__new__(cls)

        this_cls_yes_this_one.__new__ = __new__

        del this_cls_yes_this_one.finalise

    def __init__(self, x=None):
        if x is self: return
        self.x = x

C2.finalise()
Alexey
  • 3,843
  • 6
  • 30
  • 44
  • Consider that, for mutable values, this may give surprising results: `a1.val=1; a2=A(a1); a2.val=2; print(a1.val);` The users of your class might expect that the constructor generates a copy for mutable objects. – Robᵩ Feb 23 '18 at 23:53
  • This would actually be the expected result. As i said, i want the constructor to return the same object. Moreover, my class instances are not intended for mutation. – Alexey Feb 23 '18 at 23:56

1 Answers1

2

You can use a metaclass. The __init__ and __new__ methods are called in __call__ method of the metacalss whenever an instance is call. You can handle both by overriding the metaclasses __call__ function.

class MyMetaClass(type):

    def __call__(cls, obj, *args, **kwargs):
        if (isinstance(obj, cls)):
            return obj
        else:
            self = cls.__new__(cls, obj, *args, **kwargs)
            cls.__init__(self, obj, *args, **kwargs)
            return self
            # Note: In order to be sure that you don't miss anything
            # It's better to do super().__call__(obj, *args, **kwargs) here

class A(metaclass=MyMetaClass):
    def __init__(self, obj, *args, **kwargs):
        self.val = obj

Demo:

a = A(10)
b = A(a)
c = A(40)
print(b is a)
print(a.val)
print(b.val)
print(c is a)
print(c.val)

# out:
True
10
10
False
40
Mazdak
  • 105,000
  • 18
  • 159
  • 188
  • Could the `else` clause of `MyMetaClass.__call__` be: `else: return super().__call__(obj, *args, **kwargs)` ? – Robᵩ Feb 23 '18 at 23:45
  • Would it be possible to have a link to Python's documentation to confirm that there is nothing else to take care of in `__cal__` of a class? – Alexey Feb 24 '18 at 08:51
  • @Alexey I'm not sure but for the peace of mind as Rob said you should use `super()`. – Mazdak Feb 24 '18 at 09:01
  • @Kasramvd - sorry, I didn't read the question closely enough. Never mind. – Robᵩ Feb 25 '18 at 04:47