简体   繁体   中英

How to change class instance behaviour at runtime in-place?

I am working on a simple simulation where I would like to change the methods of a class instance at runtime. I am quite new to OOP so I am not sure which approach fits my case best.

I have created a couple of samples with the example being a Cat class which can turn into a zombie cat at runtime, changing it's behaviour.

class Cat:
    def __init__(self, foo):
        self.foo = foo
        self.is_zombie = False

    def turn_to_zombie(self, bar):
        self.is_zombie = True
        self.zombie_bar = bar

    def do_things(self):
        if self.is_zombie:
            print('Do zombie cat things')
        else:
            print('Do cat things')

This is the desired behaviour however I would like to separate the Cat and ZombieCat methods and skip the if statement.

class Cat:
    def __init__(self, foo):
        self. foo = foo

    def do_things(self):
        print('Do cat things')

    def turn_to_zombie(self, bar):
        self.bar = bar
        self.__class__ = ZombieCat

class ZombieCat(Cat):
    def __init__(self, foo, bar):
        super().__init__(self, foo)
        self.bar = bar

    def do_things(self):
        print('Do zombie cat things')

This works well but I am not sure if there are any side effects to changing self.__class__ , it seems to be discouraged How dangerous is setting self.__class__ to something else?

class Cat:
    def __init__(self, foo):
        self.foo = foo
        self.strategy = CatStrategy

    def do_things(self):
        self.strategy.do_things(self)

    def turn_to_zombie(self, bar):
        self.bar = bar
        self.strategy = ZombieCatStrategy

class CatStrategy:
    @staticmethod
    def do_things(inst):
        print('Do cat things')

class ZombieCatStrategy(CatStrategy):
    @staticmethod
    def do_things(inst):
        print('Do zombie cat things')

When googling I came across the strategy pattern. This also works but feels a bit messier than creating a child class. For example to override an additional method when the cat is a zombie it requires changes in 3 places instead of 1.

Feel free to suggest other patterns, I'm sure there are things I have not considered yet.


Edit: After a helpful answer from @martineau I'd like to add that it would be useful if any references to a Cat instance are updated when .turn_to_zombie is called, ie

cats_list1 = [cat1, cat2]
cats_list2 = [cat1, cat2]
cats_list1[0].do_things() # -> Do cat things
cats_list1[0].turn_to_zombie('bar')
cats_list2[0].do_things() # -> Do zombie cat things

While something like @Anton Abrosimov's answer using __getattr__() , is probably the canonical way to do it, it does have a negative side-effect of introducing the overhead of an additional function-call to every call to one of the instance's methods.

Well, like the saying goes, there's more than one way to skin a cat , so here's an alternative approach which avoids that overhead by changing what function associated with the method's name of the given instance. (Technically it could also be used to add methods to an instance that didn't already exist.)

import types


class Cat:
    def __init__(self, foo):
        self.foo = foo

    def do_things(self):
        print('Doing Cat things')

    def _change_method(self, method_name, method, **kwattrs):
        bound_method = types.MethodType(method, self)
        setattr(self, method_name, bound_method)
        self.__dict__.update(kwattrs)


class ZombieCat(Cat):
    def __init__(self, foo, bar):
        super().__init__(foo)
        self.bar = bar

    @classmethod
    def turn_into_zombie(cls, cat, bar):
        cat._change_method('do_things', cls.do_things, bar=bar)

    def do_things(self):
        print(f'Doing ZombieCat things (bar={bar!r})')


if __name__ == '__main__':

    foo, bar = 'foo bar'.split()

    cat = Cat(foo)
    cat.do_things()  # -> Doing Cat things
    ZombieCat.turn_into_zombie(cat, bar)
    cat.do_things()  # -> Doing ZombieCat things (bar='bar')

Something like that, I think:

class Cat:
    def __init__(self, foo):
        self.foo = foo

    def do_things(self):
        print('Do cat things')

    def turn_to_zombie(self, bar):
        self.bar = bar
        self.__class__ = ZombieCat

class ZombieCat(Cat):
    def __init__(self, foo, bar):
        super().__init__(foo)
        self.bar = bar

    def do_things(self):
        print('Do zombie cat things')

class SchroedingerCat:
    _cat = Cat
    _zombie = ZombieCat
    _schroedinger = None

    def __init__(self, foo, bar=None):
        if bar is not None:
            self._schroedinger = self._zombie(foo, bar)
        else:
            self._schroedinger = self._cat(foo)

    def turn_to_zombie(self, bar):
        self._schroedinger = self._zombie(self._schroedinger.foo, bar)
        return self

    def __getattr__(self, name):
        return getattr(self._schroedinger, name)

SchroedingerCat('aaa').do_things()
SchroedingerCat('aaa').turn_to_zombie('bbb').do_things()

Self merged classes is too complex and not intuitive, I think.

Dark magic attention Do not use it, but it's worked:

from functools import partial

class DarkMagicCat:
    _cat = Cat
    _zombie = ZombieCat
    _schroedinger = None

    def __init__(self, foo, bar):
        self.foo = foo
        self.bar = bar
        self._schroedinger = self._cat

    def turn_to_zombie(self, bar):
        self._schroedinger = self._zombie
        return self

    def __getattr__(self, name):
        return partial(getattr(self._schroedinger, name), self=self)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM