Search code examples
pythonpython-3.xdecoratorpython-decorators

Function decorator raising positional argument error?


I'm trying to write a function decorator that tests for bounds of x, y

#this is my bound test function
def boundtest(func):
    def onDecorator(self, x, y, *args, **kwargs):
        print(x, y, *args, **kwargs)
        assert x in range(self.width) and y in range(self.height)
        return func(x, y, *args, **kwargs)

    return onDecorator

class Game:
    #these are the functions that need bound checking

    @boundtest
    def at(self, x: int, y: int) -> int:
        return self.map[x, y]

    @boundtest
    def set(self, x: int, y: int, data):
        self.map[x, y] = data.value

When I execute game.set(1, 1, Color.RED) I get:

Traceback (most recent call last):
  File "C:\Users\Ben\Desktop\Projects\bubble-breaker-bot\game.py", line 61, in <module>
    game.set(1, 1, Color.RED)
  File "C:\Users\Ben\Desktop\Projects\bubble-breaker-bot\game.py", line 21, in onDecorator
    return func(x, y, *args, **kwargs)
TypeError: set() missing 1 required positional argument: 'data'

I need the boundtest function to check if x and y are in range of self.width, and self.height respectively while being able to pass an arbitrary amount of parameters to the function it is decorating.

Why does this happen?


Solution

  • Decorators are applied to function objects, not to bound methods. This means you need to pass on the self argument manually:

    def boundtest(func):
        def onDecorator(self, x, y, *args, **kwargs):
            print(x, y, *args, **kwargs)
            assert x in range(self.width) and y in range(self.height)
            return func(self, x, y, *args, **kwargs)
    
        return onDecorator
    

    Python uses a process called binding to turn a function into a bound method, and calling a bound method automatically passes in whatever it is bound to as the first argument; this is how self is passed into a method when you call a fuction on an instance. See the Descriptor HowTo for details. Instead of manually passing on self, you could invoke descriptor binding manually, by calling func.__get__() to produce a bound method:

    def boundtest(func):
        def onDecorator(self, x, y, *args, **kwargs):
            print(x, y, *args, **kwargs)
            assert x in range(self.width) and y in range(self.height)
            bound_method = func.__get__(self, type(self))
            return bound_method(x, y, *args, **kwargs)
    
        return onDecorator
    

    That binding behaviour was applied to the onDecorator function object your decorator returned when game.set was being resolved, but not to the wrapped func object.