One thing I missed when switching from Java to Python was multiple constructors. Python does not support them (directly), but there a may other approaches that work very similar (maybe even better).
Problem
Let’s say we are building a client to query remote service (some aggregation service). We want to pass the aggregator.
1 2 3 4 5 |
class Aggregator(object): def __init__(self, value, unit, time_zone=None): self.value = value self.unit = unit self.time_zone = time_zone |
To make code more fluent and giving it more robustness for integrating into other solutions, we have multiple options to create an aggregator.
1 2 3 4 |
query.aggregator(Aggregator(5, 'min', time_zone='Europe/Ljubljana')) query.aggregator({'value': 5, 'unit': 'min', 'time_zone': 'Europe/Ljubljana'}) query.aggregator((5, 'min')) query.aggregator('5min') |
The query.aggregator will create a new instance of Aggregator and pass it to the request.
(Possible) solution
Python has a great feature of passing args and kwargs. We can create a constructor
1 2 |
def __init__(self, value=None, unit=None, time_zone=None, *args, **kwargs): ... |
then in the constructor we check and parse args and kwargs. This solution works, but it has many problems:
-
No indication what is required and what not
This is most important for autocompletion. When I want to create a new instance of the class Aggregator, I want to know what is required. With current constructor, this is really hard. -
Complexity and combinations
There are many combinations how to initialize a new instance by passing different arguments.1Aggregator(5, args=('min', ), kwargs={'time_zone': 'Europe/Ljubljana'})This is absolutely weird and hard to read.
Better solution
Python has an option to decorate a method with @classmethod. We can define custom methods that work as multiple constructors. For example, we can create a method from_arguments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Aggregator(object): def __init__(self, value, unit, time_zone=None): # TODO validate self.value = value self.unit = unit self.time_zone = time_zone @classmethod def from_arguments(cls, args): if isinstance(args, str): # 5min value, unit = parse_time(args) return cls(value, unit) elif isinstance(args, (list, tuple)): # (5, 'min') value, unit = args return cls(value, unit) elif isinstance(args, dict): # {'value': 5, 'unit': 'min'} return cls(**args) elif isinstance(args, Aggregator): return args else: raise ValueError("Invalid args, should be str, list, tuple, dict or instance of Aggregator.") |
We use it as Aggregator.from_arguments(args). The validation of the parameters (if value an int) is done in the constructor.
The from_arguments method just parses the arguments and creates a new instance of the Aggregator. We could add a validation (if list has at least 2 items, if str is in correct format, if dict has all the required elements, …).