Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Workaround for equality of nested functions

I have a nested function that I'm using as a callback in pyglet:

def get_stop_function(stop_key):
    def stop_on_key(symbol, _):
        if symbol == getattr(pyglet.window.key, stop_key):
            pyglet.app.exit()
    return stop_on_key

pyglet.window.set_handler('on_key_press', get_stop_function('ENTER'))

But then I run into problems later when I need to reference the nested function again:

pyglet.window.remove_handler('on_key_press', get_stop_function('ENTER'))

This doesn't work because of the way python treats functions:

my_stop_function = get_stop_function('ENTER')
my_stop_function is get_stop_function('ENTER')  # False
my_stop_function == get_stop_function('ENTER')  # False

Thanks to two similar questions I understand what is going on but I'm not sure what the workaround is for my case. I'm looking through the pyglet source code and it looks like pyglet uses equality to find the handler to remove.

So my final question is: how can I override the inner function's __eq__ method (or some other dunder) so that identical nested functions will be equal?

(Another workaround would be to store a reference to the function myself, but that is duplicating pyglet's job, will get messy with many callbacks, and anyways I'm curious about this question!)

Edit: actually, in the questions I linked above, it's explained that methods have value equality but not reference equality. With nested functions, you don't even get value equality, which is all I need.

Edit2: I will probably accept Bi Rico's answer, but does anyone know why the following doesn't work:

def get_stop_function(stop_key):
    def stop_on_key(symbol, _):
        if symbol == getattr(pyglet.window.key, stop_key):
            pyglet.app.exit()
    stop_on_key.__name__ = '__stop_on_' + stop_key + '__'
    stop_on_key.__eq__ = lambda x: x.__name__ == '__stop_on_' + stop_key + '__'
    return stop_on_key

get_stop_function('ENTER') == get_stop_function('ENTER')  # False
get_stop_function('ENTER').__eq__(get_stop_function('ENTER'))  # True
like image 685
ontologist Avatar asked Jan 17 '14 19:01

ontologist


3 Answers

You could create a class for your stop functions and define your own comparison method.

class StopFunction(object):

    def __init__(self, stop_key):
        self.stop_key = stop_key

    def __call__(self, symbol, _):
        if symbol == getattr(pyglet.window.key, self.stop_key):
            pyglet.app.exit()

    def __eq__(self, other):
        try:
            return self.stop_key == other.stop_key
        except AttributeError:
            return False

StopFunciton('ENTER') == StopFunciton('ENTER')
# True
StopFunciton('ENTER') == StopFunciton('FOO')
# False
like image 146
Bi Rico Avatar answered Sep 18 '22 01:09

Bi Rico


the solution is to keep a dictionary containing the generated functions around, so that when you make the second call, you get the same object as in the first call.

That is, simply build some memoization logic, or use one of the libraries existing with memoizing decorators:

ALL_FUNCTIONS = {}
def get_stop_function(stop_key):
    if not stop_key in ALL_FUNCTIONS:
        def stop_on_key(symbol, _):
            if symbol == getattr(pyglet.window.key, stop_key):
                pyglet.app.exit()
        ALL_FUNCTIONS[stop_key] = stop_on_key
     else:
        stop_on_key = ALL_FUNCTIONS[stop_key]
    return stop_on_key
like image 45
jsbueno Avatar answered Sep 19 '22 01:09

jsbueno


You can generalize Bi Rico's solution to allow wrapping any functions up with some particular equality function pretty easily.

The first problem is defining what the equality function should check. I'm guessing for this case, you want the code to be identical (meaning functions created from the same def statement will be equal, but two functions created from character-for-character copies of the def statement will not), and the closures to be equal (meaning that if you call get_stop_function with two equal but non-identical stop_keys the functions will be equal), and nothing else to be relevant. But that's just a guess, and there are many other possibilities.

Then you just wrap a function the same way you'd wrap any other kind of object; just make sure __call__ is one of the things you delegate:

class EqualFunction(object):
    def __init__(self, f):
        self.f = f
    def __eq__(self, other):
        return (self.__code__ == other.__code__ and 
                all(x.cell_contents == y.cell_contents 
                    for x, y in zip(self.__closure__, other.__closure__)))
    def __getattr__(self, attr):
        return getattr(self.f, attr)
    def __call__(self, *args, **kwargs):
        return self.f(*args, **kwargs)

If you want to support other dunder methods that aren't required to go through getattr (I don't think any of them are critical for functions, but I could be wrong…), either do it explicitly (as with __call__) or loop over them and add a generic wrapper to the type for each one.

To use the wrapper:

def make_f(i):
    def f():
        return i
    return EqualFunction(f)
f1 = f(0)
f2 = f(0.0)
assert f1 == f2

Or, notice that EqualFunction actually works as a decorator, which may be more readable.

So, for your code:

def get_stop_function(stop_key):
    @EqualFunction
    def stop_on_key(symbol, _):
        if symbol == getattr(pyglet.window.key, stop_key):
            pyglet.app.exit()
    return stop_on_key
like image 37
abarnert Avatar answered Sep 22 '22 01:09

abarnert