Surprisingly, I find startswith
is slower than in
:
In [10]: s="ABCD"*10
In [11]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 307 ns per loop
In [12]: %timeit "XYZ" in s
10000000 loops, best of 3: 81.7 ns per loop
As we all know, the in
operation needs to search the whole string and startswith
just needs to check the first few characters, so startswith
should be more efficient.
When s
is big enough, startswith
is faster:
In [13]: s="ABCD"*200
In [14]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 306 ns per loop
In [15]: %timeit "XYZ" in s
1000000 loops, best of 3: 666 ns per loop
So it seems that calling startswith
has some overhead which makes it slower when the string is small.
And than I tried to figure out what's the overhead of the startswith
call.
First, I used an f
variable to reduce the cost of the dot operation - as mentioned in this answer - here we can see startswith
is still slower:
In [16]: f=s.startswith
In [17]: %timeit f("XYZ")
1000000 loops, best of 3: 270 ns per loop
Further, I tested the cost of an empty function call:
In [18]: def func(a): pass
In [19]: %timeit func("XYZ")
10000000 loops, best of 3: 106 ns per loop
Regardless of the cost of the dot operation and function call, the time of startswith
is about (270-106)=164ns, but the in
operation takes only 81.7ns. It seems there are still some overheads for startswith
, what's that?
Add the test result between startswith
and __contains__
as suggested by poke and lvc:
In [28]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 314 ns per loop
In [29]: %timeit s.__contains__("XYZ")
1000000 loops, best of 3: 192 ns per loop
This is likely because
str.startswith()
does more thanstr.__contains__()
, and also because I believestr.__contains__
operates fully in C, whereasstr.startswith()
has to interact with Python types. Its signature isstr.startswith(prefix[, start[, end]])
, where prefix can be a tuple of strings to try.As already mentioned in the comments, if you use
s.__contains__("XYZ")
you get a result that is more similar tos.startswith("XYZ")
because it needs to take the same route: Member lookup on the string object, followed by a function call. This is usually somewhat expensive (not enough that you should worry about of course). On the other hand, when you do"XYZ" in s
, the parser interprets the operator and can short-cut the member access to the__contains__
(or rather the implementation behind it, because__contains__
itself is just one way to access the implementation).You can get an idea about this by looking at the bytecode:
So comparing
s.__contains__("XYZ")
withs.startswith("XYZ")
will produce a more similar result, however for your example strings
, thestartswith
will still be slower.To get to that, you could check the implementation of both. Interesting to see for the contains implementation is that it is statically typed, and just assumes that the argument is a unicode object itself. So this is quite efficient.
The
startswith
implementation however is a “dynamic” Python method which requires the implementation to actually parse the arguments.startswith
also supports a tuple as an argument, which makes the whole start-up of the method a bit slower: (shortened by me, with my comments):This is likely a big reason why
startswith
is slower for strings for which acontains
is fast because of its simplicity.