问题
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 (likeobj = 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