Change type of an object after its creation (typecasting in Python)

…衆ロ難τιáo~ 提交于 2021-02-08 02:11:14

问题


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.


回答1:


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)



回答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.



来源:https://stackoverflow.com/questions/42118633/change-type-of-an-object-after-its-creation-typecasting-in-python

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!