views:

389

answers:

5

Hello,

In Python, I would like to construct an instance of the Child's class directly from an instance of the Parent class. For example:

A = Parent(x, y, z)
B = Child(A)

This is a hack that I thought might work:

class Parent(object):

    def __init__(self, x, y, z):
        print "INITILIZING PARENT"
        self.x = x
        self.y = y
        self.z = z

class Child(Parent):

    def __new__(cls, *args, **kwds):
        print "NEW'ING CHILD"
        if len(args) == 1 and str(type(args[0])) == "<class '__main__.Parent'>":
            new_args = []
            new_args.extend([args[0].x, args[0].y, args[0].z])
            print "HIJACKING"
            return Child(*new_args)
        print "RETURNING FROM NEW IN CHILD"
        return object.__new__(cls, *args, **kwds)

But when I run

B = Child(A)

I get:

NEW'ING CHILD  
HIJACKING  
NEW'ING CHILD  
RETURNING FROM NEW IN CHILD  
INITILIZING PARENT  
Traceback (most recent call last):  
  File "classes.py", line 52, in <module>  
    B = Child(A)  
TypeError: __init__() takes exactly 4 arguments (2 given)

It seems the hack works just as I expected but the compiler throws a TypeError at the end. I was wondering if I could overload TypeError to make it ignore the B = Child(A) idiom but I wasn't sure how to do that. In any case, would you please give me your solutions for inheriting from instances?

Thanks!

+3  A: 

Once __new__ in class Child returns an instance of Child, Child.__init__ will be called (with the same arguments __new__ was given) on that instance -- and apparently it just inherits Parent.__init__, which does not take well to being called with just one arg (the other Parent, A).

If there is no other way a Child can be made, you can define a Child.__init__ that accepts either one arg (which it ignores) or three (in which case it calls Parent.__init__). But it's simpler to forego __new__ and have all the logic in Child.__init__, just calling the Parent.__init__ appropriately!

To make this concrete with a code example:

class Parent(object):

    def __init__(self, x, y, z):
        print "INITIALIZING PARENT"
        self.x = x
        self.y = y
        self.z = z

    def __str__(self):
        return "%s(%r, %r, %r)" % (self.__class__.__name__,
            self.x, self.y, self.z)


class Child(Parent):

    _sentinel = object()

    def __init__(self, x, y=_sentinel, z=_sentinel):
        print "INITIALIZING CHILD"
        if y is self._sentinel and z is self._sentinel:
            print "HIJACKING"
            z = x.z; y = x.y; x = x.x
        Parent.__init__(self, x, y, z)
        print "CHILD IS DONE!"

p0 = Parent(1, 2, 3)
print p0
c1 = Child(p0)
print c1
c2 = Child(4, 5, 6)
print c2
Alex Martelli
+1  A: 

Here is a rough of what you could do

class Parent(object):
    def __init__( self, x, y, z ):
        print "INITIALIZING PARENT"
        self.x = x
        self.y = y
        self.z = z

class Child(Parent):
    def __init__( self, *args ):
        if len( args ) == 1 and isinstance( args[0], Parent ):
            print "CHILD FROM PARENT"
            parent = args[0]
            args = [ parent.x, parent.y, parent.z ]
        super( Child, self ).__init__( *args )

A = Parent( 1,2,3 )
B = Child(A)

Or consider using the copy module in Child's __init__

abhinavg
+1  A: 

You don't define a constructor (init) for Child, so the Parent constructor is called, expecting 4 arguments while only 2 are passed in (from new). Here's one way to accomplish what you want:

class Child(Parent):
    def __init__(self, *args, **kwargs):
        if len(args) == 1 and isinstance(args[0], Parent):
            Parent.__init__(self, args[0].x, args[0].y, args[0].z)

        else:
            # do something else
ars
+1: Simply create a Child by copying attributes of the parent.
S.Lott
A: 

Thanks, guys, that was quick! I first read Alex's comment and I rewrote the Child's __init__ as

def __init__(self, *args, **kwds):
    if len(args) == 1 and str(type(args[0])) == "<class '__main__.Parent'>":
        new_args = [args[0].x, args[0].y, args[0].z]
        super(Child, self).__init__(*new_args, **kwds)
    else:
        super(Child, self).__init__(*args, **kwds)

which is very similar to what abhinavg suggested (as I just found out). And it works. Only his and ars' line

if len(args) == 1 and isinstance(args[0], Parent):

is cleaner than mine.

Thanks again!!

A: 

OK, so I didn't realize that you where happy with a static copy of the arguments until I was already halfway done with my solution. But I decided not to waste it, so here it is anyway. The difference from the other solutions is that it will actually get the attributes from the parent even if they updated.

_marker = object()

class Parent(object):

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

class Child(Parent):

    _inherited = ['x', 'y', 'z']

    def __init__(self, parent):
        self._parent = parent
        self.a = "not got from dad"

    def __getattr__(self, name, default=_marker):
        if name in self._inherited:
            # Get it from papa:
            try:
                return getattr(self._parent, name)
            except AttributeError:
                if default is _marker:
                    raise
                return default

        if name not in self.__dict__:
            raise AttributeError(name)
        return self.__dict__[name]

Now if we do this:

>>> A = Parent('gotten', 'from', 'dad')
>>> B = Child(A)
>>> print "a, b and c is", B.x, B.y, B.z
a, b and c is gotten from dad

>>> print "But x is", B.a
But x is not got from dad

>>> A.x = "updated!"
>>> print "And the child also gets", B.x
And the child also gets updated!

>>> print B.doesnotexist
Traceback (most recent call last):
  File "acq.py", line 44, in <module>
    print B.doesnotexist
  File "acq.py", line 32, in __getattr__
    raise AttributeError(name)
AttributeError: doesnotexist

For a more generic version of this, look at the http://pypi.python.org/pypi/Acquisition package. It is in fact, in some cases a bloody need solution.

Lennart Regebro