2

In my project, I generate an object obj of type CubicObject. At runtime, a GUI setting should be allowed to change the type of obj to Tofu or Box (and back), depending on what the user wants to do and what (s)he thinks the object is best represented by. Then the user should benefit from specific algorithms implemented in the corresponding classes. I am looking for a nice implementation of this behaviour. I have played with the code below, which changes the __class__ attribute, but I am sure that this is bad style.

class CubicObject(object):
    name = 'Baseclass'

    def __init__(self, sidelength):
        self.sidelength = sidelength


class Tofu(CubicObject):
    name = 'Class A'

    def eat(self):
        print("I've eaten a volume of %s. " % (self.sidelength**3))


class Box(CubicObject):
    name = 'Class B'

    def paint(self):
        print("I painted a surface of %s. " % (self.sidelength**2 * 6))

# user only knows the object is vaguely cubic
obj = CubicObject(sidelength=1.0)
# user thinks the object is a Box
obj.__class__ = Box
obj.paint()
# user changes mind and thinks its a piece of Tofu
obj.__class__ = Tofu
obj.eat()
obj.paint()  # generates an error as it should, since we cannot paint Tofu

My two questions are:

  • What kind properties of class A are transferred to the object 'obj' when I change its __class__ attribute? What functions are called
    and what attributes are updated, or how else does it happen that obj changes its name to the one of A?
  • What other, cleaner ways exist to implement the behaviour I want? If necessary, I could destroy the object obj and recreate another one, but in this case I would like to do so in a generic manner (like obj = RoundObject(subclasstype='Tofu') because of other parts of the code).

The underlying problem is that I allow the user to implement own functions in subclasses of CubicObject and that one should be able to switch between these subclasses while the program is running.

Leonhard Neuhaus
  • 228
  • 3
  • 11
  • 6
    This looks like a massive XY problem. What exactly are you trying to do that requires this functionality? – Mad Physicist Feb 08 '17 at 16:35
  • 1
    Because I am pretty sure I can come up with a dozen examples that show why this is impossible. – Mad Physicist Feb 08 '17 at 16:35
  • I think that this is something you'd do in Java or C++, where there is type casting. – synchronizer Feb 08 '17 at 16:39
  • 1
    Type casting doesnt' change the type of an object. It just views it as a different type. – Jacques de Hooge Feb 08 '17 at 16:40
  • Maybe what he really wants is a wrapper class that could receive different inner delegate objects for the duration of run-time. That would make more sense. – synchronizer Feb 08 '17 at 16:41
  • Whenever I hear typecasting I think: polymorphism, virtual function... – Jacques de Hooge Feb 08 '17 at 16:43
  • @JacquesdeHooge That's what I was thinking. – synchronizer Feb 08 '17 at 16:46
  • So indeed, Leonhard, I think that the hint about the inner delegate object that @synchronizer commented, helps you in the right direction. – Jacques de Hooge Feb 08 '17 at 16:48
  • Right, disregard the typecasting comment. It was more about the fact that Python has less of a notion about types (duck typing) when it comes to enforcing type rules, etc. In Java you can do what is asked with fields though, – synchronizer Feb 08 '17 at 16:54
  • I have tried to make the example more explicit. My current implementation already uses a delegate object, but it is impractical because it hides all the interesting functions in the API that I want to provide in that delegate object (I usually duplicate all functions of the delegate object, but that understandably confuses people). Would you say my way of dealing with the problem in the example is acceptable? – Leonhard Neuhaus Feb 08 '17 at 20:23
  • It seems to me that this proposal contravenes every principle of [SOLID design](https://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29). – PM 2Ring Feb 09 '17 at 07:34

2 Answers2

3

What kind properties of class A are transferred to the object 'obj' when I change its class attribute? What functions are called and what attributes are updated, or how else does it happen that obj changes its name to the one of A?

All instance assigned attributes are kept - that is, Python objects normally have a __dict__ attribute where all instance attributes are recorded - that is kept. And the object's class effectively changes to the one assigned. (Python runtime forbids __class__ assignment for objects which have a different memory layout). That is: all methods and class attributes on the new class are available for the instance, and none of the methods or class attributes of the previous class are there, as if the object had been created in this new class. No side effects are triggered by the assignment (as in: no special method is called) So - for what you are making, it "works".

What other, cleaner ways exist to implement the behaviour I want? If necessary, I could destroy the object obj and recreate another one, but in this case I would like to do so in a generic manner (like obj = RoundObject(subclasstype='Tofu') because of other parts of the code).

Yes, as you've noted, this not is the best way of doing things. What you could have is a class hierarchy with the distinct methods you need, that get your object as an attribute - and depending on what you are doing, you create a new object os this outer hierarchy - and keep your core object with its attributes unchanged. This is known as the Adapter Pattern.

class CubicObject(object):
    name = 'Baseclass'

    def __init__(self, sidelength):
        self.sidelength = sidelength


class BaseMethods(object):
    def __init__(self, related):
         self.related = related

class Tofu(BaseMethods):
    name = 'Class A'

    def eat(self):
        print("I've eaten a volume of %s. " % (self.related.sidelength**3))


class Box(BaseMethods):
    name = 'Class B'

    def paint(self):
        print("I painted a surface of %s. " % (self.related.sidelength**2 * 6))

# user only knows the object is vaguely cubic
obj = CubicObject(sidelength=1.0)
# user thinks the object is a Box
box  = Box(obj)
box.paint()

# user changes mind and thinks its a piece of Tofu
tofu = Tofu(obj)

tofu.eat()
# or simply:
Tofu(obj).eat()

You can roll it on your own, manual classes, or use a well known and tested library that implements features to automate parts of the process. One such library is zope.interface, that allows you to write huge and complex systems using the adapter pattern. So you can have hundreds of different types of objects - you can mark them as having the interface "Cubic" as long as they have a side_length attribute. And then you cna have tens of classes that do "Cube" things with the side_length attribute - the zope.interface facilities will allow you to use any of these tens of classes with any of the objects that have the Cubic interface by simply calling the interface for the desired method passing the original object as a parameter.

But zope.interfaces may be a little hard to grasp, due to poor documentation done as needed over nearly two decades of use (and at some point, people resorted to use XML files to declare interfaces and adapters - just skip any docs that deal with XML), so for smaller projects, you can just roll it manually as above.

My current implementation already uses a delegate object, but it is impractical because it hides all the interesting functions in the API that I want to provide in that delegate object (I usually duplicate all functions of the delegate object, but that understandably confuses people).

Since your real-use example is big, that s a case to indeed learn and use zope.interface - but another workaround to a fully interface/registry/adapter system, if you want to permit access of several Cube methods on Tofu and others is to implement on the BaseMethods class I have above the magic __getattr__ Python method that would allow you to retrieve methods and attributes on the referred object in a transparent way, with no re-write needed:

class BaseMethods(object):
    def __init__(self, related):
         self.related = related

    def __getattr__(self, attr):
        return getattr(self.related, attr)
jsbueno
  • 99,910
  • 10
  • 151
  • 209
2

A variation of the borg pattern could be of help here:

class CubicObject(object):
    name = 'Baseclass'

    def __init__(self, __shared_state, sidelength):
        self.__dict__ = __shared_state
        self.sidelength = sidelength


class Tofu(CubicObject):
    name = 'Class A'

    def eat(self):
        print("I've eaten a volume of %s. " % (self.sidelength**3))


class Box(CubicObject):
    name = 'Class B'

    def paint(self):
        print("I painted a surface of %s. " % (self.sidelength**2 * 6))

Now, make multiple instances that share the same state:

def make_objs(classes, *args, **kwargs):
    __shared_state = {}
    return tuple(cls(__shared_state, *args, **kwargs) for cls in classes)


box, tofu = make_objs(sidelength=1.0, classes=(Box, Tofu))

Switch back and force between them keeping the same state:

obj = box
obj.paint()
obj = tofu
obj.eat()
obj.paint() 

The sidelength will be shared by both.

Mike Müller
  • 82,630
  • 20
  • 166
  • 161
  • That could only remotely be of some use if none of the objects is to change state, in none of its facets. – jsbueno Feb 10 '17 at 12:19
  • BTW, your example will raise an error due to double declaration of argument `classes` – jsbueno Feb 10 '17 at 12:20
  • Works if you use only keyword arguments `box, tofu = make_objs(sidelength=1.0, classes=(Box, Tofu))` or put the classes first `box, tofu = make_objs((Box, Tofu), 1.0)`. – Mike Müller Feb 10 '17 at 15:59