Is there a way to prevent some but not all arguments from being sent up to a superclass?
I have a base class that verifies user input:
class Base(object):
def __init__(self, **kwargs):
self.kwargs = kwargs
super(Base, self).__init__()
@staticmethod
def check_integrity(allowed, given):
"""
Verify user input.
:param allowed: list. Keys allowed in class
:param given: list. Keys given by user
:return:
"""
for key in given:
if key not in allowed:
raise Exception('{} not in {}'.format(key, allowed))
>>> base = Base()
>>> print base.__dict__
output[0]: {'kwargs': {}}
A
inherits from Base
and uses the method to check its keywords
class A(Base):
def __init__(self, **kwargs):
super(A, self).__init__(**kwargs)
self.default_properties = {'a': 1,
'b': 2}
self.check_integrity(self.default_properties.keys(), kwargs.keys())
>>> a = A(a=4)
>>> print a.__dict__
output[1]: {'default_properties': {'a': 1, 'b': 2}, 'kwargs': {'a': 4}}
I should also mention that I've pulled out another method I used in this class for updating class properties, since its a complication not relevant to the question (hence why a
is not updated to 4
in the above example)
The problem comes when trying to inherit from A
and adding additional kwargs
to the subclass:
class B(A):
def __init__(self, **kwargs):
super(B, self).__init__(**kwargs)
self.default_properties = {'a': 2,
'c': 3,
'd': 4}
self.check_integrity(self.default_properties.keys(), kwargs.keys())
>>> b = B(d=5)
Traceback (most recent call last):
File "/home/b3053674/Documents/PyCoTools/PyCoTools/Tests/scrap_paper.py", line 112, in <module>
b = B(d=5)
File "/home/b3053674/Documents/PyCoTools/PyCoTools/Tests/scrap_paper.py", line 96, in __init__
super(B, self).__init__(**kwargs)
File "/home/b3053674/Documents/PyCoTools/PyCoTools/Tests/scrap_paper.py", line 92, in __init__
self.check_integrity(self.default_properties.keys(), kwargs.keys())
File "/home/b3053674/Documents/PyCoTools/PyCoTools/Tests/scrap_paper.py", line 84, in check_integrity
raise Exception('{} not in {}'.format(key, allowed))
Exception: d not in ['a', 'b']
Here d
is being passed up to the superclass, even though its only required in the subclass. However, a
and b
arguments are used in A
and should be passed from B
to A
.
Is there a way to prevent some but not all arguments from being sent up to a superclass?
Well yes quite simply: don't pass them. You're supposed to know which arguments your class takes and which arguments it's superclass(es) take too, so only pass what the superclass expects:
class Base(object):
def __init__(self, arg1, arg2):
self.arg1 = arg1
self.arg2 = arg2
class Child(object):
def __init__(self, arg1, arg2, arg3):
super(Child, self).__init__(arg1, arg2)
self.arg3 = arg3
The above is plain simple readable and maintainable AND garanteed to work. And if you want default values it's not a problem either:
class Base(object):
def __init__(self, arg1=1, arg2=2):
self.arg1 = arg1
self.arg2 = arg2
class Child(object):
def __init__(self, arg1=1, arg2=2, arg3=3):
super(Child, self).__init__(arg1, arg2)
self.arg3 = arg3
Now if your class's responsability is to validate arbitrary user inputs against a given "schema" (default_properties
in your snippet), there are indeed a couple logical errors in your code - mainly, that you 1. validate your inputs in the initializer and 2. call the parent's initializer BEFORE overriding your object's default_properties
, so when the superclass initializer is called it doesn't validate against the correct schema. Also you're defining default_properties
as an instance attribute in your initializer so if you just swap the instructions to first define default_propertie
and only then call the parent's initializer this one will redefine default_properties
A simple fix would be to just make default_properties
a class attribute:
class Base(object):
# empty by default
default_properties = {}
def __init__(self, **kwargs):
self.kwargs = kwargs
# object.__init__ is a noop so no need for a super call here
self.check_integrity()
def check_integrity(self):
"""
Verify user input.
"""
for key in self.kwargs:
if key not in self.default_properties:
raise ValueError('{} not allowed in {}'.format(key, self.default_properties))
and then you don't have to override the initializer at all:
class A(Base):
default_properties = {'a': 1,
'b': 2}
class B(A):
default_properties = {'a': 1,
'b': 2,
'd': 3}
and you're done - check_integrity
will use the current instance's class default_properties
and you don't have to care about "being selective about which kwargs you pass to the superclass".
Now this is still way to simplistic to effectively work as an input validation framewoek, specially if you want inheritance... If B
is a proper subclass of A
, it should be able to add to default_properties
WITHOUT having to redefine it completely (which is an obvious DRY violation). And user input validation is usually much more involved than just checking arguments names... You may want to study how other libs / frameworks solved the problem (Django's forms come to mind here).