Python class design: explicit keyword arguments vs. **kwargs vs. @property
I'm sure there are many different schools of thought on this, by here's how I've usually thought about it:
Explicit Keyword Arguments
Pros
- Simple, less code
- Very explicit, clear what attributes you can pass to the class
Cons
- Can get very unwieldy as you mention when you have LOTs of things to pass in
Prognosis
This should usually be your method of first attack. If you find however that your list of things you are passing in is getting too long, it is likely pointing to more of a structural problem with the code. Do some of these things you are passing in share any common ground? Could you encapsulate that in a separate object? Sometimes I've used config objects for this and then you go from passing in a gazillion args to passing in 1 or 2
Using **kwargs
Pros
- Seamlessly modify or transform arguments before passing it to a wrapped system
- Great when you want to make a variable number of arguments look like part of the api, e.g. if you have a list or dictionary
- Avoid endlessly long and hard to maintain passthrough definitions to a lower level system,
e.g.
def do_it(a, b, thing=None, zip=2, zap=100, zimmer='okay', zammer=True):
# do some stuff with a and b
# ...
get_er_done(abcombo, thing=thing, zip=zip, zap=zap, zimmer=zimmer, zammer=zammer)
Instead becomes:
def do_it(a, b, **kwargs):
# do some stuff with a and b
# ...
get_er_done(abcombo, **kwargs)
Much cleaner in cases like this, and can see get_er_done
for the full signature, although good docstrings can also just list all the arguments as if they were real arguments accepted by do_it
Cons
- Makes it less readable and explicit what the arguments are in cases where it is not a more or less simple passthrough
- Can really easily hide bugs and obfuscate things for maintainers if you are not careful
Prognosis
The *args and **kwargs syntax is super useful, but also can be super dangerous and hard to maintain as you lose the explicit nature of what arguments you can pass in. I usually like to use these in situations when I have a method that basically is just a wrapper around another method or system and you want to just pass things through without defining everything again, or in interesting cases where the arguments need to be pre-filtered or made more dynamic, etc. If you are just using it to hide the fact that you have tons and tons of arguments and keyword arguments, **kwargs will probably just exacerbate the problem by making your code even more unwieldy and arcane.
Using Properties
Pros
- Very explicit
- Provides a great way of creating objects when they are somehow still "valid" when not all parameters are you known and passing around half-formed objects through a pipeline to slowly populate args. Also for attributes that don't need to be set, but could be, it sometimes provides a clean way of pairing down your
__init__
's - Are great when you want to present a simple interface of attributes, e.g. for an api, but under the hood are doing more complicated cooler things like maintaining caches, or other fun stuff
Cons
- A lot more verbose, more code to maintain
- Counterpoint to above, can introduce danger in allowing invalid objects with some properties not yet fully initialized to be generated when they should never be allowed to exist
Prognosis
I actually really like taking advantage of the getter and setter properties, especially when I am doing tricky stuff with private versions of those attributes that I don't want to expose. It can also be good for config objects and other things and is nice and explicit, which I like. However, if I am initializing an object where I don't want to allow half-formed ones to be walking around and they are serving no purpose, it's still better to just go with explicit argument and keyword arguments.
TL;DR
**kwargs and properties have nice specific use cases, but just stick to explicit keyword arguments whenever practical/possible. If there are too many instance variables, consider breaking up your class into hierarchical container objects.
Without really knowing the particulars of your situation, the classic answer is this: if your class initializer requires a whole bunch of arguments, then it is probably doing too much, and it should be factored into several classes.
Take a Car
class defined as such:
class Car:
def __init__(self, tire_size, tire_tread, tire_age, paint_color,
paint_condition, engine_size, engine_horsepower):
self.tire_size = tire_size
self.tire_tread = tire_tread
# ...
self.engine_horsepower = engine_horsepower
Clearly a better approach would be to define Engine
, Tire
, and Paint
classes (or namedtuple
s) and pass instances of these into Car()
:
class Car:
def __init__(self, tire, paint, engine):
self.tire = tire
self.paint = paint
self.engine = engine
If something is required to make an instance of a class, for example, radius
in your Circle
class, it should be a required argument to __init__
(or factored into a smaller class which is passed into __init__
, or set by an alternative constructor). The reason is this: IDEs, automatic documentation generators, code autocompleters, linters, and the like can read a method's argument list. If it's just **kwargs
, there's no information there. But if it has the names of the arguments you expect, then these tools can do their work.
Now, properties are pretty cool, but I'd hesitate to use them until necessary (and you'll know when they are necessary). Leave your attributes as they are and allow people to access them directly. If they shouldn't be set or changed, document it.
Lastly, if you really must have a whole bunch of arguments, but don't want to write a bunch of assignments in your __init__
, you might be interested in Alex Martelli's answer to a related question.
Passing arguments to the __init__
is usually the best practice like in any Object Oriented programming language. In your example, setters/getters would allow the object to be in this weird state where it doesn't have any attribute yet.
Specifying the arguments, or using **kwargs
depends on the situation. Here's a good rule of thumb:
- If you have many arguments,
**kwargs
is a good solution, since it avoids code like this:
def __init__(first, second, third, fourth, fifth, sixth, seventh,
ninth, tenth, eleventh, twelfth, thirteenth, fourteenth,
...
)
- If you're heavily using inheritance.
**kwargs
is the best solution:
class Parent:
def __init__(self, many, arguments, here):
self.many = many
self.arguments = arguments
self.here = here
class Child(Parent):
def __init__(self, **kwargs):
self.extra = kwargs.pop('extra')
super().__init__(**kwargs)
avoids writing:
class Child:
def __init__(self, many, arguments, here, extra):
self.extra = extra
super().__init__(many, arguments, here)
For all other cases, specifying the arguments is better since it allows developers to use both positional and named arguments, like this:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
Can be instantiated by Point(1, 2)
or Point(x=1, y=2)
.
For general knowledge, you can see how namedtuple
does it and use it.