2 decorators in a Python method

Asked

Viewed 119 times

0

Well, I have a very simple class called Task, where I want to store my tasks and then save in a bank.

However, I want to go through the types of my attributes, I would like the task_name attribute only to accept the string type.

For this I have a decorator called validate_type, however I’m not understanding where the error is, but whenever I run the program, it raises the exception.

def validate_type(typee):
    def _validate(func):
        def inner(*args, **kwargs):
            if all(isinstance(val, typee) for val in args):
                return func(*args)
            else:
                raise TypeError("Voce atribuiu o valor errado")
        return inner
    return _validate


class Task(object):
    def __init__(self, task_name):
        self._task_name = task_name

    @property
    def task_name(self):
        return self._task_name

    @task_name.setter
    @validate_type(str)
    def task_name(self, value):
        self._task_name = value

task = Task('minha task')
task.task_name = 4
print(task.task_name)

This decorator works with functions but not methods. I believe it’s because of the class self but I don’t know how to solve

1 answer

1

Precisely. In one method, the self will be inserted as first argument in the call. In your check you check the type of all the past arguments - and the type of the first will always be equal to the class type.

To make it clear - the problem has nothing to do with "2 decorators" - the decorator property.setter always expects a function that will receive a class instance in the first parameter, but it is not a "common" decorator that returns a wrapper of the decorated function. The solution I am proposing here is somewhat different from your question, pointing out an alternative to property, and without using any decorator. The specific problem of your code is addressed just below.

If you want a generic decorator to check all types, which you can use both in functions and methods, I think the most explicit method is to accept a parameter that indicates if the decorated function is a normal function, or a method, and in this case, not check the type of the first argument:

def validate_type(typee, method=False):
    start_arg = 1 if method else 0
    def _validate(func):
        def inner(*args, **kwargs):
            if all(isinstance(val, typee) for val in args[start_arg:]):
                return func(*args)
            else:
                raise TypeError("Voce atribuiu o valor errado")
        return inner
    return _validate
...

class Task(object):
    def __init__(self, task_name):
        self._task_name = task_name

    @property
    def task_name(self):
        return self._task_name

    @task_name.setter
    @validate_type(str, True)
    def task_name(self, value):
        self._task_name = value

(As I suggest another approach below, I have made no further improvements to this code. But note that if your decorator is called with arguments with name, instead of positional, your decorator will not work - they come in the variable kwargs, that that code doesn’t even touch)

If you want to use this decorator in several setters, there are even simpler options: or a simple decorator who checks directly only the type of the second argument (the value).

However, if your intention is to have several properties just for type checking, then the best option is to take a step back and create a Descriptor class - that is, not to use the layer that the property provides that lets you use functions like setters and getters, and yes, create a class that already has Setter logic with type check included.

In other words: we create a class of objects that should be used as class attributes, and automatically checks the instances of the runtime assignments.

With this, you don’t even need to use the attribute name with _ to replicate the attribute in the instance - we can directly use the __dict__ of the instance to store the same name value. (With Property it is also possible, actually).

class TypedAttr:
    def __init__(self, type):
        self.type = type

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError(f"Attribute {self.name!r} must be set to instances of {self.instance!r}")
        instance.__dict__[self.name] = value

And then, you can have your class just like this - notice how I can put multiple attributes with type without needing any code from Setter and getter for each attribute:

class Task(object):

    task_name = TypedAttr(str)
    task_points = TypedAttr(int)

    def __init__(self, task_name, points=1):
        self.task_name = task_name
        self.task_points = 1

And in the interactive terminal:

>>> t = Task("teste")
>>> t.task_name
'teste'
>>> t.task_name = 23
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in __set__
TypeError: Attribute 'task_name' must be set to instances of <class 'str'>
>>> t.task_points = 25.0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in __set__
TypeError: Attribute 'task_points' must be set to instances of <class 'int'>

See how "Descriptors" work in the Python documentation - the Property, as said above, is only a convenience to incorporate specific get and set methods even.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.