In general, this is not a good idea for the reasons that @yak mentioned in his comment. You are basically preventing the user from supplying valid arguments that have the correct attributes/behavior but are not in the inheritance tree you hard-coded in.
Disclaimer aside, there are a few of options available for what you are trying to do. The main issue is that there are no private attributes in Python. So if you just have a plain old object reference, say self._a
, you can not guarantee that the user won't set it directly even though you have provided a setter that does type checking for it. The options below demonstrate how to really enforce the type checking.
Override __setattr__
This method will only be convenient for a (very) small number of attributes that you do this to. The __setattr__
method is what gets called when you use dot notation to assign a regular attribute. For example,
class A:
def __init__(self, a0):
self.a = a0
If we now do A().a = 32
, it would call A().__setattr__('a', 32)
under the hood. In fact, self.a = a0
in __init__
uses self.__setattr__
as well. You can use this to enforce the type check:
class A:
def __init__(self, a0):
self.a = a0
def __setattr__(self, name, value):
if name == 'a' and not isinstance(value, int):
raise TypeError('A.a must be an int')
super().__setattr__(name, value)
The disadvantage of this method is that you have to have a separate if name == ...
for each type you want to check (or if name in ...
to check multiple names for a given type). The advantage is that it is the most straightforward way to make it nearly impossible for the user to circumvent the type check.
Make a property
Properties are objects that replace your normal attribute with a descriptor object (usually by using a decorator). Descriptors can have __get__
and __set__
methods that customize how the underlying attribute is accessed. This is sort of like taking the corresponding if
branch in __setattr__
and putting it into a method that will run just for that attribute. Here is an example:
class A:
def __init__(self, a0):
self.a = a0
@property
def a(self):
return self._a
@a.setter
def a(self, value):
if not isinstance(value, int):
raise TypeError('A.a must be an int')
self._a = value
A slightly different way of doing the same thing can be found in @jsbueno's answer.
While using a property this way is nifty and mostly solves the problem, it does present a couple of issues. The first is that you have a "private" _a
attribute that the user can modify directly, bypassing your type check. This is almost the same problem as using a plain getter and setter, except that now a
is accessible as the "correct" attribute that redirects to the setter behind the scenes, making it less likely that the user will mess with _a
. The second issue is that you have a superfluous getter to make the property work as read-write. These issues are the subject of this question.
Create a True Setter-Only Descriptor
This solution is probably the most robust overall. It is suggested in the accepted answer to the question mentioned above. Basically, instead of using a property, which has a bunch of frills and conveniences that you can not get rid of, create your own descriptor (and decorator) and use that for any attributes that require type checking:
class SetterProperty:
def __init__(self, func, doc=None):
self.func = func
self.__doc__ = doc if doc is not None else func.__doc__
def __set__(self, obj, value):
return self.func(obj, value)
class A:
def __init__(self, a0):
self.a = a0
@SetterProperty
def a(self, value):
if not isinstance(value, int):
raise TypeError('A.a must be an int')
self.__dict__['a'] = value
The setter stashes the actual value directly into the __dict__
of the instance to avoid recursing into itself indefinitely. This makes it possible to get the attribute's value without supplying an explicit getter. Since the descriptor a
does not have the __get__
method, the search will continue until it finds the attribute in __dict__
. This ensures that all sets go through the descriptor/setter while gets allow direct access to the attribute value.
If you have a large number of attributes that require a check like this, you can move the line self.__dict__['a'] = value
into the descriptor's __set__
method:
class ValidatedSetterProperty:
def __init__(self, func, name=None, doc=None):
self.func = func
self.__name__ = name if name is not None else func.__name__
self.__doc__ = doc if doc is not None else func.__doc__
def __set__(self, obj, value):
ret = self.func(obj, value)
obj.__dict__[self.__name__] = value
class A:
def __init__(self, a0):
self.a = a0
@ValidatedSetterProperty
def a(self, value):
if not isinstance(value, int):
raise TypeError('A.a must be an int')
Update
Python3.6 does this for you almost out-of the box: https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements
TL;DR
For a very small number of attributes that need type-checking, override __setattr__
directly. For a larger number of attributes, use the setter-only descriptor as shown above. Using properties directly for this sort of application introduces more problems than it solves.
isinstance
checks but otherwise is fine (duck typing). – Classy