Here's my experiment with cProfile to see if I can confirm @yuvi's measurements above.
CODE: par_profile.py
import cProfile as profile
import functools
path = u'/a/b/c'
lam = lambda f: f.lower().startswith(u'distantlod')
par = functools.partial(lam, path)
startsWith = path.lower().startswith
par2 = lambda: startsWith(u'distantlod')
if __name__ == '__main__':
profile.run("for _ in range(1000000): par()")
profile.run("for _ in range(1000000): par2()")
OUT
$ python par_profile.py
3000003 function calls in 0.536 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.148 0.148 0.536 0.536 <string>:1(<module>)
1000000 0.242 0.000 0.388 0.000 par_profile.py:7(<lambda>)
1 0.000 0.000 0.536 0.536 {built-in method exec}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1000000 0.054 0.000 0.054 0.000 {method 'lower' of 'str' objects}
1000000 0.092 0.000 0.092 0.000 {method 'startswith' of 'str' objects}
2000003 function calls in 0.347 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.130 0.130 0.347 0.347 <string>:1(<module>)
1000000 0.126 0.000 0.218 0.000 par_profile.py:11(<lambda>)
1 0.000 0.000 0.347 0.347 {built-in method exec}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1000000 0.092 0.000 0.092 0.000 {method 'startswith' of 'str' objects}
Firstly it looks like these measurements on my machine tally with @yuvi's numbers:
par
is about 540 nanoseconds
par2
is about 350 nanoseconds
So I agree par2
looks faster by about 200 ns.
It looks like if you're trying to compare lambda
and partial
it's not a fair test - par2
has one less call since it doesn't call lower
, whereas par
does.
To illustrate why, startsWith
could be rewritten as:
lower_path = path.lower()
startsWith = lower_path.startswith
... so par2
is just a lambda
that contains a single startswith
whereas par
is a partial that contains both lower
and startswith
calls.
Therefore they are not equivalent and par
is slower as a result.
WHY?
The question is "why f.lower().startswith
is not inlined - what prohibits the language from inlining it?"
Firstly, this Python itself does not prohibit this kind of inlining - it's the different Python implementations that makes the decisions, in the case of my tests above it's cpython 3.
Secondly, partial
's job is not to inline functions, it just...
“freezes” some portion of a function’s arguments and/or keywords resulting in a new object with a simplified signature (doc)
Instead if you're looking at something that will do inlining in Python, then I'd check out something like Numba's jit or run some experiments with PyPy (please post back here if you find some interesting improvements).
If you're not able to find anything that'll do the inlining you're looking for, then maybe it's a good case for a new Python module!