Как изменить поведение экземпляра класса во время выполнения на месте? - PullRequest
1 голос
/ 07 июля 2019

Я работаю над простой симуляцией, где я хотел бы изменить методы экземпляра класса во время выполнения. Я совершенно новичок в ООП, поэтому я не уверен, какой подход лучше всего подходит для моего случая.

Я создал пару примеров, примером которых является класс Cat, который может превращаться в зомби-кошку во время выполнения, изменяя его поведение.

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')

Это желаемое поведение, однако я бы хотел разделить методы Cat и ZombieCat и пропустить оператор if.

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')

Это хорошо работает, но я не уверен, есть ли какие-либо побочные эффекты при изменении self.__class__, это, похоже, не рекомендуется Насколько опасно устанавливать self .__ class__ на что-то еще?

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')

При поиске в Google я наткнулся на шаблон стратегии. Это также работает, но выглядит немного сложнее, чем создание дочернего класса. Например, чтобы переопределить дополнительный метод, когда кошка - зомби, она требует изменений в 3 местах вместо 1.

Не стесняйтесь предлагать другие шаблоны, я уверен, что есть вещи, которые я еще не рассмотрел.


Edit: После полезного ответа от @martineau я хотел бы добавить, что было бы полезно, если бы какие-либо ссылки на экземпляр Cat обновлялись при вызове .turn_to_zombie, т.е.

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

Ответы [ 2 ]

0 голосов
/ 08 июля 2019

Что-то в этом роде, я думаю:

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()

Само слитые классы слишком сложны и не интуитивны, я думаю.

Темная магия внимания Не использоватьэто, но это сработало:

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)
0 голосов
/ 08 июля 2019

Хотя что-то вроде ответа @Anton Abrosimov, использующего __getattr__(), вероятно, является каноническим способом сделать это, у него есть отрицательный побочный эффект, связанный с добавлением дополнительной функции-вызова к каждому вызову одного из экземпляров.методы.

Что ж, как говорится, есть более чем один способ скинуть кошку , так что вот альтернативный подход, который позволяет избежать этих накладных расходов путем изменения функции, связанной с именем метода:данный экземпляр.(Технически это также может быть использовано для добавления методов в экземпляр, который еще не существует.)

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')
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...