Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python class factory inherit random parent

I have some code like this:

class Person(object):
    def drive(self, f, t):
        raise NotImplementedError

class John(Person):
    def drive(self, f, t):
        print "John drove from %s to %s" % (f,t)

class Kyle(Person):
    def drive(self, f, t):
        print "Kyle drove from %s to %s" % (f,t)

class RandomPerson(Person):
    # instansiate either John or Kyle, and inherit it.
    pass

class Vehicle(object):
    pass

class Driver(Person, Vehicle):
    def __init__(self):
        # instantiate and inherit a RandomPerson somehow
        pass

d1 = Driver()
d1.drive('New York', 'Boston')
>>> "John drove from New York to Boston"

d2 = Driver()
d2.drive('New Jersey', 'Boston')
>>> "Kyle drove from New Jersey to Boston"

How could i implement RandomPerson, with the following requirements:

  • calling person = RandomPerson() must return a RandomPerson object.
  • RandomPerson should subclass either John or Kyle randomly.
like image 257
user37203 Avatar asked Oct 19 '22 09:10

user37203


2 Answers

In my original answer (which I deleted because it was just plain wrong) I said I would consider doing it like this:

class RandomPerson(Person):
    def __init__(self):
        rand_person = random.choice((John, Kyle))()
        self.__dict__ = rand_person.__dict__

This way is an adaptation of the Python Borg idiom; the idea was that everything that matters about an object is contained in its __dict__.

However, this only works when overwriting objects of the same class (which is what you are doing in the Borg idiom); the object __dict__ only contains state information pertaining to object instance, not the object class.

It is possible to switch out the class of an object like so:

class RandomPerson(Person):
    def __init__(self):
        rand_person = random.choice((John, Kyle))
        self.__class__ = rand_person

However, doing it this way would mean that the call to RandomPerson would then not return an instance of RandomPerson per your requirement, but of Kyle or of John. So this is a no go.

Here is a way to get a RandomPerson object that acts like Kyle or John, but isn't:

class RandomPerson(Person): 
    def __new__(cls):
        new = super().__new__(cls)
        new.__dict__.update(random.choice((Kyle,John)).__dict__)
        return new

This one - very similar to the Borg idiom, except doing it with classes instead of instance objects and we're only copying the current version of the chosen class dict - is really pretty evil: we have lobotomized the RandomPerson class and (randomly) stuck the brains of a Kyle or John class in place. And there is no indication, unfortunately, that this happened:

>>> rperson = RandomPerson()
>>> assert isinstance(rperson,Kyle) or isinstance(rperson,John)
AssertionError

So we still haven't really subclassed Kyle or John. Also, this is really really evil. So please don't do it unless you have a really good reason.

Now, assuming you do in fact have a good reason, the above solution should be good enough if all you are after is making sure you can use any class state information (methods and class attributes) from Kyle or John with RandomPerson. However, as illustrated prior, RandomPerson still isn't a true subclass of either.

Near as I can tell there is no way to actually randomly subclass an object's class at instance creation AND to have the class maintain state across multiple instance creations. You're going to have to fake it.

One way to fake it is to allow RandomPerson to be considered a subclass of John and Kyle using the abstract baseclass module and __subclasshook__, and adding that to your Person class. This looks like it will be a good solution since the Person class is an interface and isn't going to be directly used, anyway.

Here's a way to do that:

class Person(object):
    __metaclass__ = abc.ABCMeta
    def drive(self, f, t):
        raise NotImplementedError
    @classmethod
    def __subclasshook__(cls, C):
        if C.identity is cls:
            return True
        return NotImplemented

class John(Person):
    def drive(self, f, t):
        print "John drove from %s to %s" % (f,t)

class Kyle(Person):
    def drive(self, f, t):
        print "Kyle drove from %s to %s" % (f,t)

class RandomPerson(Person): 
    identity = None
    def __new__(cls):
        cls.identity = random.choice((John,Kyle))
        new = super().__new__(cls)
        new.__dict__.update(cls.identity.__dict__)
        return new

>>> type(RandomPerson())
class RandomPerson
>>> rperson = RandomPerson()
>>> isinstance(rperson,John) or isinstance(rperson,Kyle)
True

Now RandomPerson - though it technically is not a subclass - is considered to be a subclass of Kyle or John, and it also shares the state of Kyle or John. In fact, it will switch back and forth between the two, randomly, every time a new instance is created (or when RandomPerson.identity is changed). Another effect of doing things this way: if you have multiple RandomPerson instances, they all share the state of whatever RandomPerson happens to be in that moment -- i.e., rperson1 might start out being Kyle, and then when rperson2 is instantiated, both rperson2 AND rperson1 could be John (or they could both be Kyle and then switch to John when rperson3 is created).

Needless to say, this is pretty weird behavior. In fact it is so weird, my suspicion is that your design needs a complete overhaul. I really don't think there is a very good reason to EVER do this (other than maybe playing a bad joke on someone).

If you don't want to mix this behavior into your Person class, you could also do it separately:

class Person(object):
    def drive(self, f, t):
        raise NotImplementedError

class RandomPersonABC():
    __metaclass__ = abc.ABCMeta
    @classmethod
    def __subclasshook__(cls, C):
        if C.identity is cls:
            return True
        return NotImplemented

class John(Person, RandomPersonABC):
    def drive(self, f, t):
        print "John drove from %s to %s" % (f,t)

class Kyle(Person, RandomPersonABC):
    def drive(self, f, t):
        print "Kyle drove from %s to %s" % (f,t)

class RandomPerson(Person): 
    identity = None
    def __new__(cls):
        cls.identity = random.choice((John,Kyle))
        new = super().__new__(cls)
        new.__dict__.update(cls.identity.__dict__)
        return new
like image 59
Rick supports Monica Avatar answered Nov 03 '22 07:11

Rick supports Monica


You could just implement the RandomPerson class to have a member called _my_driver or whatever else you wanted. You would just call their drive method from the RandomPerson.drive method. It could look something like this:

    class RandomPerson(Person):
    # instantiate either John or Kyle, and inherit it.
    def __init__(self):
        self._my_person = John() if random.random() > 0.50 else Kyle()
    def drive(self, f, t):
        self._my_person.drive(f,t)

Alternatively, if you want to be more strict about making sure that the class has exactly the same methods as Kyle or John, you could set the method in the constructor like the following:

class RandomPerson(Person):
    # instantiate either John or Kyle, and inherit it.
    def __init__(self):
        self._my_person = John() if random.random() > 0.50 else Kyle()
        self.drive = self._my_person.drive
like image 22
Brien Avatar answered Nov 03 '22 06:11

Brien