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.