6
\$\begingroup\$

I wrote this prototype after reading the Wikipedia article on hooking. I didn't bother to read any of the code examples listed there because, well, I just didn't. I don't have a good excuse.

The documentation in this file should tell you enough about its purpose and intended usage. I'm looking for a confirmation of whether I even understand what hooks are and how they should be used. I'm also curious about any sort of preferred method of implementation or any way that I could improve these two objects (Hook and OnHook).

"""Implement hooks with a simple interface using decorators.

The Hook class creates a function that can be placed in code as an
anchoring point for as yet undetermined functionality, while the
OnHook function creates the interface to that functionality.

To create a hook, use the @Hook decorator on an empty function of
your chosen name.  The function's __call__ method will be generated
by the decorator.  Then, create a function that accepts a function
as an argument and adds it to the targets callback by using @OnHook.
OnHook expects the target hook as an argument.

Example:

    @Hook                           # decorator, no args
    def my_hook(): pass             # empty func, no params
    @OnHook(my_hook)                # decorator, hook as arg
    def on_my_hook(func): pass      # empty func, func as param

To add a callback to the new hook, use:

    on_my_hook(my_callback)

When the hook is executed, your callback will be executed along with
any other callbacks that have been registered.

Written 2014-02-02 by Jack Stout.

"""

class Hook:
    """Decorator to create a hook."""
    def __init__(self, func):
        self.callbacks = []

    def __call__(self):
        for callback in self.callbacks:
            callback()


def OnHook(target):
    """Decorator to create an interface to a hook.

    Requires a target hook as only argument.

    """
    def decorator(func):
        def add_callback(func):
            target.callbacks.append(func)
        return add_callback
    return decorator



# Here I've created two hooks with interfaces which would be used
# immediately before and after a hypothetical initiative() function.
@Hook
def pre_initiative(): pass
@OnHook(pre_initiative)
def on_pre_initiative(func): pass


@Hook
def post_initiative(): pass
@OnHook(post_initiative)
def on_post_initiative(func): pass



# Two dummy functions are created and are added to the hooks' callback lists.
def dummy_func_1():
    print("Inside pre_initiative!")
on_pre_initiative(dummy_func_1)


def dummy_func_2():
    print("Inside post_initiative!")
on_post_initiative(dummy_func_2)


# These print functions reveal what has been registered in the hook.
print(pre_initiative.callbacks)
# This function call is what we would see in production code.
pre_initiative()

print(post_initiative.callbacks)
post_initiative()

Less important but related:

This is a prototype but I'm working on a game project that could benefit from this method of quickly attaching function pointers to upcoming state changes. While working on this file, I was imagining the first phase in a tabletop combat encounter where the players and game master roll initiatives and determine the combat order. If a character were to gain a power that allowed them to adjust their initiative roll after everyone has rolled, it would require only a call to on_post_initiative(power_name). When initiative() completes, post_initiative() would fire and execute power_name().

Before getting any feedback, if I were to revisit this code I would add the ability to include arguments to be used with the callback, i.e. on_post_initiative(power_name, character_name).

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

This appears to be an implementation of something akin to the observer pattern. Your implementation is straightforward enough, but there are two things that jump out at me.

First, I'm surprised that this involves two top-level decorators? I think I would rather see a pattern similar to what property provides. Namely, rather than exposing both @Hook and @OnHook, make @Hook add an attribute to the function that can itself, as a decorator, hook other functions to the first (implementation left as an exercise):

@Hook
def pre_initiative(): pass

@pre_initiative.when_called   # or preferred name
def dummy_func_1(): print("pre-initiative!")

Second, I'm a little confused about your larger use case. Do hooks have a chance to change the inputs or outputs of the hooked function? Are you working on this project with other people who may be more used to some particular approach? (If so, it's easier for them to work with if it matches the approach they expect.) Are these phases easier to implement as functions called on an instance or a subclass?

class Player:
    def pre_initiative(self): pass
    def post_initiative(self): pass

def encounter(players):
    for player in players:
        player.pre_initiative()
    for player in players:
        player.post_initiative()
\$\endgroup\$
2
  • \$\begingroup\$ You've left me plenty to study. After some reading, it looks like you're exactly right in nesting OnHook within the object returned by Hook. \$\endgroup\$
    – user19988
    Feb 8, 2014 at 16:28
  • \$\begingroup\$ As for using this in my current project: It seems like a reasonable method of modifying game state without modifying any code within the game. I'll be able to drop resources into a folder and link them into whichever code I need to. In this case, I'll only add hooks as the need arises, but can do so without putting the code at risk. \$\endgroup\$
    – user19988
    Feb 8, 2014 at 16:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.