If one asks about performance, but didn’t mention profiling results, the question is wrong. This is where this post should end.
If we ignore above, though we should not and mind it remains relevant, steandard rules of optimization apply. That means the kind of optimization you asked about is among the last things to care about. It’s also least effective, often counterproductive, and detrimental to code quality. It’s however very attractive, because it gives the feeling of control and comprehension, while requiring little actual mental effort, understanding, or experience. The priority goes as follow: system model, program model, algorithms and data structures, code-level changes, employing techniques below language level. Did you already handle the first three?
Going deeper, again with the note that the previous paragraphs remain more important, note Python is a language. There are multiple implementations, each with different performance characteristics. I hope you see, how that complicates your question. What worked in one environment may have a very different effect in another. This is a common issue in JIT environments and heavily-optimizing static compilers: people spend a lot of time micro-optimizing their lines, just to learn that in the next release of the tool the modification has an opposite effect. Or even while run on another CPU (for native programs).
As noted above, local variables are generally the fastest in Python. This comes directly from the language model and how the name look-up has to be done. Python shares performance penalty with JavaScript: the dynamic nature of everything requires each access to behave as if a complete, multi-level search for a name was performed. So caching a reference in a local variable may be preferred, where it’s found the access impacts performance. This may be particularly important for properties of some objects, where they invoke
__getattr__ and/or go through
property objects. Using
__slots__ may (not must) help, if it doesn’t affect semantics. But remember it’s not a silver bullet. Being smart about repeated
dict accesses helps. Python made a bad API decision with
dict, compared to e.g. Java’s
Map. Insertion methods can’t in one go determine, if the insertion actually happened or avoid overwriting. That often forces the programmer to do unneeded double search.
dict.setdefault provides some relief, but only in some situations.
But, circling back to the begining: profile, profile, profile. And profile. There is no optimization without profiling and understanding profiling data. CPython has a built-in
cProfile module for that. It can be invoked from command line:
python -m cProfile script.pyMay be used for a complete program, an isolated fragment of a program, and for micro-benchmarks. While doing the last group, be sure to not fall to common mistakes. The costs are so tiny, that they can be easily dwarfed by data generation, memory access patterns, or cold start. That can be reproducible, leading to wrong conclusions. They may also have huge variance. A parameter notoriously ignored, but in this case very relevant: any apparent difference between alternative solutions, even if consistent, may actually be still within noise level.