1

My goal is to construct an argument validator in the form of a class decorator.

Consider the following example:

class MyDecorator:
    """
    Decorator called to check the args types of a given function.
    """
    def __init__(self, *args):
        self.types = args
        self.func = None

    def __call__(self, func):
        self.func = func
        return self.wraper_func

    def wraper_func(self, *args, **kwargs):
        i = 0
        while i < len(args):
            arg = args[i]
            types = self.types[i]

            if not isinstance(arg, types):
                print(f"Attention ! Argument {i} should be {types} but is {arg}")

            i += 1

        return self.func(*args, **kwargs)

This code works well with a function :

@MyDecorator(int, float)
def addition(a, b):
    return a + b

The code addition(4, 5) wille raise the message printed in MyDecorator as 5 is not a float.

BUT, if I apply this Decorator to an instance method :

class Car:
    @MyDecorator(str, float)
    def __init__(self, color, price):
        self.color = color
        self.price = price

my_car = Car('Green', 120) raise TypeError: __init__() missing 1 required positional argument: 'price'

I tried to add the get method to transform MyDecorator class as a Descriptor class but it doesn't work :

    def __get__(self, instance, owner):
        from functools import partial
        return partial(self.__call__, instance)

Is there a standard pattern to manage a class decorator with arguments in the case of instance methods?

Amine
  • 11
  • 2
  • Do the answers to this [question](https://stackoverflow.com/questions/1367514/how-to-decorate-a-method-inside-a-class) help at all? – quamrana Jun 10 '23 at 08:08
  • @quamrana Not yet ! – Amine Jun 10 '23 at 10:50
  • Are you asking for a decorator which will decorate both functions *and* methods, or just methods? – quamrana Jun 10 '23 at 11:12
  • @quamrana I'm asking for a decorator (built with a class) which will decorate instance methods. – Amine Jun 10 '23 at 11:22
  • Do you *need* a class, or would the more standard pattern of nested functions and closures be sufficient? – chepner Jun 10 '23 at 12:12
  • @chepner Yes, I want my decorator to be a class. I also find the class formulation of decorators much more readable than that of nested functions. – Amine Jun 10 '23 at 13:31
  • Your decorator assumes a one-to-one correspondence between the decorator's arguments (`str` and `float`) and the parameters of the function it decorates (`self`, `color`, and `price`). Note that the assumption fails: you need to pass something to line up with `self`. Try `MyDecorator(object, str, float)` instead (which is not idea, since `self` is trivially an instance of `object`, which means you aren't really effectively asserting that its type is correct, but you can't use `Car` in the definition of `Car`.) – chepner Jun 10 '23 at 15:59
  • @chepner Yes, you're right, but that doesn't entirely solve the problem. The 'wraper_func' function returns self.func(*args, **kwargs). So there will always be an argument missing in the instantiation of the object (because of the self). – Amine Jun 11 '23 at 06:18

3 Answers3

1

I don't know the actual reason why, (someone please chime in) but having wraper_func() as a method of MyDecorator disturbs the binding of methods and you lose access to the self of your target class.

That is, at the line: my_car = Car('Green', 120), python creates an instance and tries to call __init__(), but actually calls wraper_func() due to the decoration. I can't work out where the self argument has gone.

A fix is to make wraper_func() an inner function:

class MyDecorator:
    """
    Decorator called to check the args types of a given function.
    """

    def __init__(self, *args):
        self.types = args

    def __call__(self, func):
        def wraper_func(*selfargs, **kwargs):
            i = 0
            itself, *args = selfargs
            while i < len(args):
                arg = args[i]
                types = self.types[i]

                if not isinstance(arg, types):
                    print(f"Attention ! Argument {i} should be {types} but is {arg}")

                i += 1

            return func(*selfargs, **kwargs)
        return wraper_func
quamrana
  • 37,849
  • 12
  • 53
  • 71
0

The problem is that the types you declared were wrong. It should really be:

@MyDecorator(Car, str, float)
def __init__(self, color, price):
    self.color = color
    self.price = price

... but that's not valid Python, because Car has not yet been created when the decorator is called.

A possible solution would be to make a second wrapper:

@MyMethodDecorator(str, float)
def __init__(self, color, price):
    self.color = color
    self.price = price

or add a feature that skips an argument, for example:

@MyDecorator(..., str, float)
def __init__(self, color, price):
    self.color = color
    self.price = price

which could be accomplished as follows:

class MyDecorator:
    """
    Decorator called to check the args types of a given function.
    """
    def __init__(self, *args):
        self.types = args
        self.func = None

    def __call__(self, func):
        self.func = func
        def wrapper_func(*args, **kwargs):
            for i, (arg, type_) in enumerate(zip(args, self.types)):
                if type_ is not ... and not isinstance(arg, type_):
                    print(f"Attention ! Argument {i} should be {type_} but is {arg}")
            return self.func(*args, **kwargs)

        return wrapper_func


@MyDecorator(int, float)
def addition(a, b):
    return a + b

addition(4, 5) # prints Attention ! Argument 1 should be <class 'float'> but is 5

class Car:
    @MyDecorator(..., str, float)
    def __init__(self, color, price):
        self.color = color
        self.price = price

my_car = Car('Green', 120) # prints Attention ! Argument 2 should be <class 'float'> but is 120

Note that you can bypass typechecking by passing in keyword arguments. I recommend looking into PEP 484 type hints and the inspect.signature function, which may be useful to make this work more generally.

Jasmijn
  • 9,370
  • 2
  • 29
  • 43
-1

An abstraction of the problem is presented and a possible solution with a class decorator decorating a class.

Implementation details of wraper_func are skipped, considered just a minimal working example.


The error message is quite clear: you provide two parameters but the compiler complains that one (the last) is missing. What happened? The method is waiting also for a self argument pointing to of the decorated class Car.

What makes everything even more confusing is that you testing the decorator against the class "constructor"... check this example where a non-__init__ method is successfully decorated

class Car:

    def __init__(self, color, price):
        self.color = color
        self.price = price

    @MyDecorator(str, float)
    def m(self, a, b):
        print('>>>', a, b)


my_car = Car('Green', 120)
my_car.m(my_car, "1", 3)
# >>> 1 3

and, as you can see, the method's call takes a further argument representing the instance of the class, my_car.m(my_car, "1", 3). Without it will raise the same error as before.

Try to respect "class decorator in the form" you gave is probably impossible because there is no way to get a reference of either the class or instance of the decorated method.

The possible ways to decorate a method are huge: function, __new__, ... You should try to figure out what best fit your needs.

Danger: remember that by passing the extra instance then len(args) = len(self.types)+1 so either slice args[1:] or args.pop(0)


Implementation of a class decorator decorating a class. Notice that the method name is also required: {'method_name': tuple(of arguments)}.

For sake of simplicity only positional arguments are considered.

class MyDecorator:

    def __init__(self, **types):
        # types: dict of the form method's name - tuple of its args parameter
        self.types = types

    def __call__(self, cls):
        self.cls = cls # <- reference of the class
        self.type_changer() # <- your action
        return cls

    def type_changer(self):
        print('type_changer')

        for method_name, args in self.types.items():
            # do whatever you want here

            func = getattr(self.cls, method_name)
            setattr(self.cls, method_name, self.wrapper(func, args))

    @staticmethod
    def wrapper(func, args):
        # support function: needed to avoid for-loop scope problems
        return lambda subself: func(subself, *args) # <- the extra parameter!
        

@MyDecorator(**{'m1': (1, 2), 'm3': ("X", "Y"), '__init__': ("yes __init__!",)})
class A: 
    def __init__(self, c) -> None:
        print('A.__init__', c)

    def m1(self, *args):
        print('A.m1', args)

    def m2(self, *args):
        print('A.m2', args)
    
    def m3(self, *args):
        print('A.m3', args)


a = A()
a.m1()
a.m2() # not decorated
a.m3()
#
#A.__init__ yes __init__!
#A.m1 (1, 2)
#A.m2 ()
#A.m3 ('X', 'Y')

A class decorator decorating a class has the following advantages:

  • a single instance is needed to change the behaviour of many methods
  • is "portable", it can be used without syntactic sugar decoration @ in a more friendly way
my_filter_1 = {...}
dec_my_filter_1 = MyDecorator(**my_filter_1)
# assuming A and B to be classes
A_filtered = dec_my_filter_1(A) # decorated class
B_filtered = dec_my_filter_1(B) # decorated class
cards
  • 3,936
  • 1
  • 7
  • 25