Mahi
Mahi

Reputation: 21923

Creating a property-like solution for complicated and repetitive function calls

I am extending an existing PlayerClassFromGameEngine class to allow custom effects to take effect only for a certain duration.

Example: Using the original class, I would freeze a player by saying player.move_type = MoveTypes.Freeze and then unfreeze him by saying player.move_type = MoveTypes.Normal. Now I'd like to extend the class so I can use a function call instead: player.freeze(5), to freeze the player for five seconds.

I obviously need two functions, the effect function and an undo function, f.e. freeze() and unfreeze(). Here's my current class, that works fine:

class MyPlayer(PlayerClassFromGameEngine):
    def __init__(self, index):
        super().__init__(index)  # Let the original game engine handle this
        self.effects = defaultdict(set)

    def freeze(self, duration):
        self.move_type = MoveType.FREEZE  # Move type comes from the super class
        thread = threading.Thread(target=self._unfreeze)
        thread.args = (duration, thread)
        self.effects['freeze'].add(thread)
        thread.start()

    def _unfreeze(self, duration, thread):
        time.sleep(duration)
        self.effects['freeze'].remove(thread)
        if not self.effects['freeze']:  # No more freeze effects
            self.move_type = MoveType.NORMAL

As you see, only one effect takes more than 10 lines of code, having 20 of these would be awful, since they all work the exact same way, just with different key ('freeze', burn, etc.) and some call a function instead of accessing move_type property.

I've got basically zero idea where to start, maybe descriptors and decorators somehow, but can somebody give me some advice, or better yet a working solution?


EDIT: Here's what I came up with after Martijn's suggestion, but it doesn't work since I can't access the player inside the Effect class

from collections import defaultdict
from threading import Thread
from time import sleep

class Effect(object):
    def __init__(self, f, undo_f=None):
        self.f = f
        self.undo_f = undo_f
        self._thread = None

    def __call__(self, duration):
        self._thread = Thread(target=self._execute, args=(duration, ))
        self._thread.start()

    def _execute(self, duration):
        self.f()
        sleep(duration)
        self.undo_f()

    def undo(self, undo_f):
        return type(self)(self.f, undo_f)

class Player:
    def __init__(self, index):
        self.index = index
        self._effects = defaultdict(set)

    @Effect
    def freeze(self):
        print('FROZEN')

    @freeze.undo
    def freeze(self):
        print('UNFROZEN')

p = Player(1)
p.freeze(3)

What I think I need is to somehow access the player inside of the Effect class, since I can't call self.f(player) or self.undo_f(player) in the Effect._execute method, nor can I access player's effects dictionary. I figured I won't be needing the key parameter anywhere, since I can just generate a random number for every effect (an unique one ofc.), since it's not shown to anyone anyways.

Upvotes: 0

Views: 92

Answers (1)

deets
deets

Reputation: 6395

This would be a way to go:

from functools import partial
import time
import threading


class aftereffect(object):
    def __init__(self, func):
        self._func = func
        self._aftereffect = lambda instance: None


    def aftereffect(self, func):
        self._aftereffect = func
        return self


    def __get__(self, instance, cls):
        # this is the descriptor protocol, and
        # instance is the actual object

        def delayed_effect(timeout):
            time.sleep(timeout)
            self._aftereffect(instance)

        def caller(*args, **kwargs):
            timeout = kwargs.pop("_timeout", 1.)
            t = threading.Thread(target=partial(delayed_effect, timeout=timeout))
            t.start()
            return self._func(*args, **kwargs)

        return caller.__get__(instance, cls)


class Thing(object):
    @aftereffect
    def something(self):
        print "something", self

    @something.aftereffect
    def something(self):
        print "after_something", self


t = Thing()
t.something()
t.something(_timeout=5)

Upvotes: 2

Related Questions