Reputation: 2196
Please ignore my calculation just want to wonder if there is any performance preference between using class attribute or method parameter since both of them pretty much works the same way just that class attribute can be called anywhere inside the class instead method parameter only stays in its own scope
class Circle():
def __init__(self, radius=1):
self.pi = 3.14
self.radius = 1
# use class attribute pi
def get_circum_self(self):
return self.pi * self.radius * 2
# use param for pi
def get_circum_pi(self, pi, radius):
return pi * radius * 2
nc = Circle()
print(nc.pi)
print(nc.radius)
print(nc.get_circum_self()) # use class attribute pi
print(nc.get_circum_pi(111, 1)) # use param for pi
Thanks in advance for any explanations
Upvotes: 4
Views: 2391
Reputation: 365767
It's very rare that a performance difference will matter here.
But these are very different interfaces, that do different things, and that almost certainly will matter.
So, that's how you should decide which one to write: Do you want to ask a circle for its circumference, or do you want to ask a circle to compute the circumference of a completely different circle?
But if you do care about performance, the only way to get an answer is to test it. Python comes with a timeit
module specifically designed for benchmarking snippets of code like this. If you use IPython/Jupyter, it has an even nicer wrapper around this, called %timeit
.
Here's what %timeit
says on my machine, running 64-bit python.org CPython 3.7, with your sample data:
In [417]: %timeit nc.get_circum_self()
323 ns ± 10.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [418]: %timeit nc.get_circum_pi(111, 1)
258 ns ± 6.55 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
This makes sense. Just passing integers around isn't quite free (they have to get pushed and popped from the stack, and, in CPython, their refcounts have to be twiddled), but it's very fast. Looking up attributes in an object by name is a bit more work on top of that. Apparently, it's about 70 nanoseconds of extra work.
But consider how you'd use this in a more realistic way. If you only want to calculate one circumference with hardcoded values in your source code, that's obviously only ever going to happen once, so who cares whether it's 323ns or 258ns? If you wanted to calculate zillions of them, the values would presumably be coming from some variable, right? So, let's compare that:
In [419]: pi, rad = 111, 1
In [420]: %timeit nc.get_circum_pi(pi, rad)
319 ns ± 15.19 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
It looks like looking up a pair of global variables is just as expensive as looking up a pair of attributes. Which, again, is not too surprising—either way, we're looking up a name (a string whose hash value has already been pre-computed before we get here) in a namespace (which is just a plain old dict both for globals and for a normal class like the one you wrote), so it's about the same amount of work.
It's also worth noting that get_circum_pi
doesn't do anything with self
, and has no reason to be a method at all. So, if you're really trying to squeeze out the last few nanoseconds, why force yourself to look the method up as an attribute? Why not just make it a function?
In [423]: def get_circum_pi(pi, radius):
...: return pi * radius * 2
In [424]: %timeit get_circum_pi(111, 1)
180 ns ± 4.54 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
That saved us even more time. Which, again, makes sense, but only if you know a bit more about how methods work. Looking up a method requires looking up the function, not finding it in the object's own dictionary, falling back to the class's dictionary, and then calling a descriptor __get__
on the function to bind it as a method. That's a whole lot of work.
Well, it's 78 nanoseconds worth of work, which still isn't very much.
It's worth having a sense of what all these things do, and how long they take, and what the alternatives are. For example, if you're computing a zillion circumferences, you can store the bound method in a variable instead of looking it up over and over again. You can move the whole loop inside a function, so the bound method and the global variables all become local variables (which are a bit faster). And so on.
It's rarely worth doing any of these things—but "rarely" isn't "never". For a real-life example, see the unique_everseen
function in the recipes in the itertools
docs—that seen_add = seen.add
is there because it turns out that it really does make a difference in some real-life programs using this recipe.
Upvotes: 8