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!