Reputation: 361
Let's say I have a simple function which calculates the cube root of a number and returns it as a string:
def cuberoot4u(number):
return str(pow(number, 1/3))
I could rewrite this as:
def cuberoot4u(number):
cube_root = pow(number, 1/3)
string_cube_root = str(cube_root)
return string_cube_root
The latter version has the extra steps of declaring extra variables which show the value through each respective operation; multiplying by 1/3 and the conversion to a string - the code looks a little easier to follow and understand.
Now for such a menial task of finding the cuberoot, both functions would appear pretty self-explanatory to the layman. However, if the function did something far more complicated which involved tens or hundreds of algebraic manipulations or some other kinds of operations, at what point should one simply write all of these in the return
section of the function or instead detail all, if not most, steps in the main body like the 2nd example above?
To my understanding, the first version of the function seems less legible but more efficient. How and when do I balance legibility against efficiency in code such as in this example?
Upvotes: 4
Views: 809
Reputation: 77454
I hate developers who believe they make code more readable by adding even more methods.
a = cuberoot4u(x)
is code that requires me to look up the function to understand what is happening.
a = str(pow(x, 1./3.))
without an unnecessary function cuberoot4u
is very readable.
Do not measure readability by size or verbosity. What you need is code that
so that it can be verified easily, debugged easily, and is not misused. Above function pretends it does something complex, but it isn't - that is bad. It hides a type conversion, this is bad, too. The "inline" version is very clearn one math operarion, then convert to string.
Upvotes: 3
Reputation: 55479
Definitely write for legibility. The overhead of binding an object to a name is quite minimal, and even if the object is large, eg a big list or dictionary, it won't make a difference since the only thing that's being copied is a reference to the object, the bytes of the object itself aren't copied when you make a simple assignment.
However, your first example is fairly easy to read since the nesting is linear. If it was something non-linear, like
return f(g(i(1, 2), j(3, 4)), h(k(5, 6), l(7, 8)))
I'd definitely recommend breaking it up. But for simple linear-nested expressions, a good rule-of-thumb is: if it's too long to fit on one PEP-008 line you should probably break it up.
Here's some code to illustrate that there's virtually no difference in the speed of your two functions. I've also added a version that is more efficient: it uses the **
operator to perform the exponentiation, which saves the overhead of a function call.
This code first prints the Python bytecode for each function. It then runs the functions with a small set of data to verify that they actually do what they're supposed to do. And finally it performs the timing tests. Note that sometimes your three-line version is sometimes faster than your one-line version, depending on system load.
from __future__ import print_function, division
from timeit import Timer
import dis
def cuberoot_1line(number):
return str(pow(number, 1/3))
def cuberoot_1lineA(number):
return str(number ** (1/3))
def cuberoot_3line(number):
cube_root = pow(number, 1/3)
string_cube_root = str(cube_root)
return string_cube_root
#All the functions
funcs = (
cuberoot_1line,
cuberoot_3line,
cuberoot_1lineA,
)
def show_dis():
''' Show the disassembly for each function '''
print('Disassembly')
for func in funcs:
fname = func.func_name
print('\n%s' % fname)
dis.dis(func)
print()
#Some numbers to test the functions with
nums = (1, 2, 8, 27, 64)
def verify():
''' Verify that the functions actually perform as intended '''
print('Verifying...')
for func in funcs:
fname = func.func_name
print('\n%s' % fname)
for n in nums:
print(n, func(n))
print()
def time_test(loops, reps):
''' Print timing stats for all the functions '''
print('Timing tests\nLoops = %d, Repetitions = %d' % (loops, reps))
for func in funcs:
fname = func.func_name
print('\n%s' % fname)
setup = 'from __main__ import nums, %s' % fname
t = Timer('[%s(n) for n in nums]' % fname, setup)
r = t.repeat(reps, loops)
r.sort()
print(r)
show_dis()
verify()
time_test(loops=10000, reps=5)
typical output
Disassembly
cuberoot_1line
27 0 LOAD_GLOBAL 0 (str)
3 LOAD_GLOBAL 1 (pow)
6 LOAD_FAST 0 (number)
9 LOAD_CONST 3 (0.33333333333333331)
12 CALL_FUNCTION 2
15 CALL_FUNCTION 1
18 RETURN_VALUE
cuberoot_3line
33 0 LOAD_GLOBAL 0 (pow)
3 LOAD_FAST 0 (number)
6 LOAD_CONST 3 (0.33333333333333331)
9 CALL_FUNCTION 2
12 STORE_FAST 1 (cube_root)
34 15 LOAD_GLOBAL 1 (str)
18 LOAD_FAST 1 (cube_root)
21 CALL_FUNCTION 1
24 STORE_FAST 2 (string_cube_root)
35 27 LOAD_FAST 2 (string_cube_root)
30 RETURN_VALUE
cuberoot_1lineA
30 0 LOAD_GLOBAL 0 (str)
3 LOAD_FAST 0 (number)
6 LOAD_CONST 3 (0.33333333333333331)
9 BINARY_POWER
10 CALL_FUNCTION 1
13 RETURN_VALUE
Verifying...
cuberoot_1line
1 1.0
2 1.25992104989
8 2.0
27 3.0
64 4.0
cuberoot_3line
1 1.0
2 1.25992104989
8 2.0
27 3.0
64 4.0
cuberoot_1lineA
1 1.0
2 1.25992104989
8 2.0
27 3.0
64 4.0
Timing tests
Loops = 10000, Repetitions = 5
cuberoot_1line
[0.29448986053466797, 0.29581117630004883, 0.29786992073059082, 0.30267000198364258, 0.36836600303649902]
cuberoot_3line
[0.29777216911315918, 0.29979610443115234, 0.30110907554626465, 0.30503296852111816, 0.3104550838470459]
cuberoot_1lineA
[0.2623140811920166, 0.26727819442749023, 0.26873588562011719, 0.26911497116088867, 0.2725379467010498]
Tested on a 2GHz machine running Python 2.6.6 on Linux.
Upvotes: 0
Reputation: 12022
Always prioritize readability first.
Always prioritize readability first.
Premature optimization is evil. So always prioritize readability first.
Once you're readable code is working, if performance is an issue, or if it becomes an issue, profile your code before optimizing anything. You don't want to waste time and reduce readability optimizing something that won't give you much benefit.
First optimize things that will still be pretty much as readable after they're optimized; for example, binding methods to local variables.
These will tend to not improve performance too much (though binding methods to local variables can make quite a difference in some code), but sometimes you can see something more efficient and just as readable that you missed before.
Whether X reduction in readability is worth Y increase in performance is subjective and depends on the importance of each in the particular situation.
Then, if you still need to optimize, start factoring out the parts of the code you're going to optimize into functions; that way, even after they've been optimized and made less readable, the main code is still readable because it's just calling a function do_something()
without worrying about the ugly, optimized blob of code in that function.
If every tiny bit of performance helps, then you might want to inline the functions back into the main code. This depends on which implementation of Python you're using, e.g. CPython versus PyPy.
If one big mass of optimized-to-hell Python code isn't fast enough.. rewrite it in C.
Upvotes: 3
Reputation: 18567
I don't believe you're talking about (runtime) efficiency of the code, i.e. does it run fast or with minimal memory/CPU consumption; rather you're talking about the efficiency/compactness of how the code is expressed.
I also don't think you're talking about the legibility of the code, i.e. do the look and formatting make the code easy to parse; rather you're talking about the comprehensibility, i.e. how easy is it to read it and understand the potentially complex logic involved.
Compactness is an objective thing, you can measure in terms of lines or characters. Comprehensibility is a subjective thing, but there are general trends in what most people find comprehensible vs. not. In your case, the first example is clearly more compact, and in my opinion, it's also easier to comprehend.
You almost always want to prioritize comprehensibility over compactness, although in some cases (e.g. this one) the two may go hand in hand. However, when the two goals are pulling your code in opposite directions, comprehensibility should always win. Comprehensibility has obvious benefits, it makes the code easier to fix, change, etc. in the future, and in particular it makes it easier for someone else to fix, change, etc. It makes it easier for you to come back to it later and verify its correctness, in case some doubt arises that it might be the source of some bug.
Compactness buys you very little (unless you're playing code golf). The only minor benefit I can see for compactness is to help avoid having overly large files of code, because it can make it difficult if you ever need to scan the large file and get a quick overview of what the code does. But that's a pretty minor benefit, and there are often better ways of keeping your code files at a reasonable size (refactoring, reorganizing).
Upvotes: 2
Reputation: 1
I'd like to have a legible code ion general but it really depends on how important is the performance of your program, if it is not an application with serious performance requirements I go always for making the code readable.
if the function did something far more complicated which involved tens or hundreds of algebraic manipulations or some other kinds of operations
With more complicated functions the readability achieved with breaking the code in several small functions bring a very important benefit: make your code easy to test.
Upvotes: 0
Reputation: 20254
In general you should prioritise legibility over efficiency in your code, however if you have proved that your codes performance is causing an issue then (and only then) should you start to optimise.
If you do need to make your code less legible in order to speed it up you can always use a comment to explain what it is doing (perhaps even including the more readable version of the code in the comment to allow people to follow what it is doing).
Beware however, one of the problems with explaining your code via a comment rather than by just writing legible code is that comments can become out of date. If you change the code but don't update the comment, then your comment goes from being a helpful commentary to being a weasel-faced liar who ruins everyone's day - try to avoid that if possible.
Upvotes: 9