I’m looking at timeit and came upon something interesting, leaves a lot of questions.
Why would lambda take more time?
Is it possible that the function has to be defined behind the sceen?
I know this is just one iteration, I study that next(many iterations). As I see it, removing the print function and storing the variable instead got closer to the base time for each.
The reason is because of how timeit executes the provided code. If provided a string, it simply inserts the code into its timing loop, then execs the whole thing. But if you provide a function/callable, it doesn’t have access to the source, so it just inserts a call to the function.
The additional time you’re observing is the time taken to call the function on each iteration of the loop. Actually since the code you’re trying to test (4 + 5) only contains constant values, CPython is smart enough to precalculate this result, so no addition actually takes place. So you’re comparing a local variable assignment to a function call, which is a lot more expensive. To fix that, make one of the two parameters a variable set via the init code/global namespace.
The Python interpreter is smart enough to see that 4 and 5 are small integers that can be pre-computed, so all you are timing is binding the value 9 to the name “output”.
In your other example:
>>> dis.dis('lambda: 4 + 5')
1 0 LOAD_CONST 0 (<code object <lambda> at 0x7f8514392150, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<lambda>')
4 MAKE_FUNCTION 0
6 RETURN_VALUE
Disassembly of <code object <lambda> at 0x7f8514392150, file "<dis>", line 1>:
1 0 LOAD_CONST 1 (9)
2 RETURN_VALUE
you are timing the creation of a function, not calling the function.
The fourth column is a list of numbers (0, 1 , 1, 0,), are they the machine cycles needed to do the command in assembly?
As I see it, cpu work load is the overhead of code. If both have the same human end result, wouldn’t you lean towards using the faster code style in that case. I’m looking well beyond the examples (types of programming, oop, functional, etc), they are wonderful. I can’t get this from any book that I know of, thanks so much.
No, Python isn’t compiled to assembler. This is bytecode executed by the interpreter’s evaluation loop. In dis’s output, the columns are the line number, offset in the bytecode, the operation name, numeric opcode parameter, then a decoded version of the parameter for some opcodes. For instance LOAD_CONST indexes the func.__code__.co_const tuple, so it displays the repr of that item. For a list of bytecode instructions, see the dis docs.
I miss wrote… You are right, it is interpreted language. What I was trying to say is that the bytecode instructions look a lot like assembly language. If this is so, then it helps explain, after it is interpreted, how it is executed. If what comes out of the interpreter is executable code, then why do we have to convert .py to exe? I know the interpreter does all the converting from human to machine usable form, so nothing more would be needed, in my way of seeing it. Just save that as the exec.
I know that .py has to be converted to make sure all the parts of the program have their supporting parts.
Thanks again for helping clear things up. Concepts are so important, we must have them correct as the foundation.
It’s low-level and uses explicit jumps like assembly, but it isn’t implemented directly by any CPU. In CPython there’s a massive switch statement inside a while loop which checks the opcode and then does that operation.
No, those are parameter values. CPython is built with a stack-based bytecode interpreter, and most of the opcodes are either like “RETURN_VALUE” (just does its thing, no extra information needed) or like “LOAD_CONST 1” (take constant number 1 from the list of constants and load it onto the stack). You can see some more examples here:
>>> dis.dis(lambda s, p, a, m: s * p + a * m + s + p * a + m)
1 0 LOAD_FAST 0 (s)
2 LOAD_FAST 1 (p)
4 BINARY_MULTIPLY
6 LOAD_FAST 2 (a)
8 LOAD_FAST 3 (m)
10 BINARY_MULTIPLY
12 BINARY_ADD
14 LOAD_FAST 0 (s)
16 BINARY_ADD
18 LOAD_FAST 1 (p)
20 LOAD_FAST 2 (a)
22 BINARY_MULTIPLY
24 BINARY_ADD
26 LOAD_FAST 3 (m)
28 BINARY_ADD
30 RETURN_VALUE
The variable ‘m’ is always referred to as “fast local #3”, and the disassembler shows that name in parens afterwards. The same goes for constants.
You can see the full tables by delving into the function’s __code__ object:
>>> f = lambda s, p, a, m: s * p + a * m + s + p * a + m
>>> f.__code__.co_varnames
('s', 'p', 'a', 'm')
>>> f.__code__.co_consts
(None,)
(Fun fact: Lambda functions almost always have None as their first constant. Why? I’ll give you a hint: def functions might not have None as a constant, but only if Python isn’t running in -OO mode.)
Thank You. That makes sense now. Its not something we deal with daily but nice to understand how and why for the big picture. Its like looking under the hood of a car… you don’t need to understand the pistons in the engine, but you want to know the type of engine etc Just for knowledge.