14 May 2015 0 comments Python
It's the old problem of "Do I seek permission or ask for forgiveness?". It's rarely easy to know which one to use in Python because working with exceptions in Python is so damn easy.
Generally I prefer neither. I.e. just do. Don't write defensive code if you don't have to. Only seek permission or ask for forgiveness if you expect it to happen and that that's normal.
Consider the following three functions:
def f0(x): return PI / x def f1(x): if x != 0: return PI / x else: return -1 def f2(x): try: return PI / x except ZeroDivisionError: return -1
Which one do you think is the fastest? If I run this 1,000,000 times and never pass in a value for
x=0 will it make any difference?
Before you look at it, what do you think the result will be?
The answer is below.
Scroll down for the results.
Have you made a guess yet?
What do you think it's going to be?
Scroll some more.
Ok, the results are as follows when running each of the above mentioned functions ~33,000,000 times on my MacBook:
f0 4.16087803245 f1 4.84187698364 f2 4.73760977387 (smaller is better)
Conclusion, the difference is miniscule. The fastest is to not do any exception handling or condition checking but it's generally no big difference.
This test was done with Python 2.7.9. You can try the code for yourself.
Just one more thought
As I wrote this post I started thinking more and more about the "code style aspect" rather than the performance.
Basically, I think it boils down to the following rules:
If you're working with external I/O (e.g. network or a database) use the "ask for forgiveness" approach (aka. exception wrapping). I.e. don't do
if requests.head(url).status_code == 200: stuff = requests.get(url)
If you want to make a really user-friendly Python API, use the "seek permission" approach (aka. if-statement first). E.g.
def calculate(guests): if isinstance(guests, basestring): guests = [guests]
All else just do. That makes the code more Pythonic. If you have a sub-routine that sends in variable of the totally crazy-wrong type to your function, don't change the function, change the sub-routine.
Here are the numbers for PyPy:
f0 0.369750552707 f1 0.321069081624 f2 0.411438703537 (smaller is better)
That's after averaging 15 runs of the script.
Note that the function with the extra if statement is faster.
And here are the numbers of Python 3.4.2:
f0 4.99579153742 f1 5.77459328515 f2 5.38382162367 (smaller is better)
That's averaging 10 rounds.
One almost interesting thing about these numbers is that the sum of them are different and tells us a tiny story about performance for the language:
Python 2.7.9 13.74036478996 PyPy 2.4.0 1.102258337868 Python 3.4.2 16.15420644624 (smaller is better)
Here's the node equivalent version and its times:
f0 0.215509441 f1 0.228280196357 f2 0.316222934714 (smaller is better)
That means that my Node v0.10.35 is 45% faster than PyPy. But please, don't take that seriously.