Python factory class inherits random parent element

I have a 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 can I implement RandomPerson with the following requirements:

  • calling person = RandomPerson() should return a RandomPerson object.
  • RandomPerson must be subclassed as John or Kyle at random.
+5
source share
3 answers

In my original answer (which I deleted because it was simply wrong), I said that I would consider this as follows:

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

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

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

You can disable the object class as follows:

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

However, doing this means that calling RandomPerson will not return an instance of RandomPerson for your requirement, but Kyle or John . So this is not an option.

Here's a way to get a RandomPerson object that acts like Kyle or John , but not:

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

This one is very similar to the Borg idiom, except that it does it with classes instead of instance objects, and we only copy the current version of the selected dict class - it's really pretty evil: we lobotomized the RandomPerson class and (accidentally) stuck the brains of the Kyle class or John And, unfortunately, there is no indication that this has happened:

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

Thus, we still do not have a subclass of Kyle or John . In addition, it is really really evil. Therefore, please do not do this unless you have a really good reason.

Now, assuming that you really have a good reason, the solution above should be good enough if all you need to do is make sure that you can use any class state information (class methods and attributes) from Kyle or John with using RandomPerson . However, as shown above, RandomPerson is still not a true subclass.

Close, since I can say that there is no way to actually randomly subclass an object's class when creating an AND instance to maintain the state of the class in multiple instance instances. You have to fake it.

One way to fake this is to let RandomPerson be considered a subclass of John and Kyle using an abstract base class and __subclasshook__ and adding this to your Person class. This seems to be a good solution, as the Person class is an interface and will not be used directly anyway.

Here's how to do it:

 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 - although it is not technically a subclass - is considered a subclass of Kyle or John , and also shares the state of Kyle or John . In fact, it will switch between them arbitrarily, every time a new instance is created (or when RandomPerson.identity changes). Another effect of this: if you have multiple instances of RandomPerson , they all share the state of any RandomPerson at the moment , i.e. rperson1 can start Kyle , and then when rperson2 is created, both rperson2 AND rperson1 can be John (or both of them can be Kyle , and then switch to John when rperson3 is created)),

Needless to say, this is a rather strange behavior. In fact, it is so strange, my suspicion is that your design needs a major overhaul. I really don't think there is a very good reason EVER to do this (except maybe a bad joke on someone).

If you do not want to mix this behavior in your Person class, you can also do this 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 
+1
source

You can simply implement the RandomPerson class to have a member named _my_driver or whatever you need. You would just call their drive method from the RandomPerson.drive method. It might 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 rigorous in making sure that the class has the same methods as Kyle or John , you can set the method in the constructor as follows:

 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 
+1
source

In your last comment on my other answer, you said:

I'm going to go with changing the class of the object, as you indicated: rand_person = random.choice((John, Kyle)) and self.__class__ = rand_person . I moved the RandomPerson methods back to Person , and RandomPerson now works as a factory -ish generation class.

If I can say so, this is a strange choice. First of all, replacing the class after creating the object does not seem very pythonic (this is too complicated). It would be better to create an instance of the object randomly instead of assigning a class after the fact:

 class RandomPerson(): # Doing it this way, you also don't need to inherit from Person def __new__(self): return random.choice((Kyle,John))() 

Secondly, if the code was reorganized so that you no longer need the RandomPerson object, why use it at all? Just use the factory function:

 def RandomPerson(): return random.choice((Kyle,John))() 
0
source

All Articles