Every now and then Liraz and I find ourselves chatting about how much we love Python, but more so the lessons we have learned from coding, and how to apply them to create beautiful Python code. I've tried to refine some of the "lessons" into practical guidelines that I apply religiously to all new code I write, and the refactoring of old code I written.
When reading other peoples code it sometimes ties my mind into knots, and on occasions I want to pull my hair out in frustration and disgust. That's not to say I'm perfect, but hopefully these guidelines will benefit others (and indirectly help reduce my hair loss).
I couldn't possibly include everything I wanted to in one post, so this will be the first, and more will follow...
#1 - OO structure == Well defined mental concepts
Object Orientated structure should always map to well defined mental concepts in the problem domain. If you don't have a well defined mental model of the problem domain, start with that. Class Responsibility Collaboration (CRC)
cards are really useful in this.
Basically you need a sketch of the architecture. What parts does your system have, what are their names, what does each part do? What parts is that part made out of? How do the parts interact with each other?
You can save quite a bit of effort if you come up with a good architecture up-front, but sometimes it may be easier to start without and figure it out a bit later after you have a bit more knowledge about the problem you are solving.
The rule is that the sooner you refine your architecture, the better. It is easy to dig yourself further and further into a complexity hole that makes restructuring very difficult later on. So do that as early as possible.
Refining the architecture is part of the "refactor mercilessly" rule.
#2 - Leverage built-in Python types
It is often a good idea to build on top of built-in Python types or at least emulate them.
The big advantage in building on top of Python's conventions is that you get to re-use Python's abstractions instead of reinventing your own. Python is famous for its minimal elegance, and the language designers take great care making small beautiful data structures. Making your code structures more Pythonic is a pretty good way to leverage the built-in elegance of the language while saving yourself quite a bit of work (inventing good abstractions is hard!).
If you've never inherited from a built-in data type, experiment with a small test case in a throw away script. This way you don't mangle your current project and you can be as experimental as you like.
Tip: Construction can be a bit trickier than a normal object if your initialization interface is different from the built-in type. In that case you'll need to override the __new__ magic method as well to call the __new__ constructor of the base class with different arguments than your __new__ is called.
def __new__(cls, val, name):
return int.__new__(cls, val)
def __init__(self, val, name):
self.name = name
self.val = val
As for emulating built in types, Python provides magical method names for emulating any built-in behavior. See special method names
#3 - Use the class namespace, Luke!
Use the class namespace when defining class-level attributes and the instance namespace (which inherits from the class namespace on initialization) when defining instance level attributes.
For example, constants are always class level attributes because they are shared by all instances. On a practical level there are two reasons to do this:
Enable inheritance - you can't override instance level attributes, only class level attributes.
Readability - code is communication. Setting an attribute at the class level or instance level is making a statement about the nature of that attribute, which makes the code easier to read and understand.
#4 - staticmethod vs. classmethod vs. regular method
Static methods are simpler and more readable than class methods because they don't have access to the class attribute, so it's easier to determine its input (i.e., arguments), and output (i.e., the return value).
Class methods are simpler than regular methods because the convention is that you don't usually manipulate attributes in the class namespace the way you would manipulate attributes in the instance namespace. So when you call a class method you know its not going to be sneaking around behind you back reading or writing to any instance level attributes set by other methods. The input for a class method is therefore the arguments it receives + class level constants. Its output is the return value.
A regular method is the most complex and easy to abuse type of method. Its harder for the programmer to see what the inputs for the method are, and what its output is.
Lets consider the following case: imagine a class in which all private methods are called without arguments and do not return output values. Instead, the private methods freely read and write to any attributes in self.
The problem with such a pattern, is that all methods have now become entangled in a spaghetti like dependency structure
. The instance namespace in effect behaves like a global namespace and it becomes very difficult to understand methods in terms of input and output. Furthermore, the boundaries between methods can easily become fuzzy and unclear.
You must never ever do this (I have in the past, and I'm ashamed). If your code exhibits this pattern, go fix it, now!
Private methods have input arguments and return values for a reason. The instance namespace must only be used for instance level attributes which need to persist after a public method has been called.
Until it becomes second nature, these rules should help:
If a method doesn't need access to instance level attributes then it should be a class method, not a regular method.
If a method doesn't need access to class level attributes then it should be a static method, not a class method.
The Python Paradox
Paul Graham wrote an interesting essay entitled The Python Paradox
. It's a quick read - take a look.