Evaluation vs Execution in Python
PythonMost people, when they’re introduced to Python, are told it’s run top to bottom.
That is to say, if you write
def f():
print("hello")
print("world")
f()
it’ll execute the top statement first, and the second statement second.
hello
world
This helps us as teachers to explain to new programmers how to read code, and it’s a contrast to languages like Javascript.
How Javascript Does It
Javascript is asynchronous by default and so you can’t depend on something that is invoked above happening before something that is invoked below.
const first = () => {
console.log("first!");
}
const second = () => {
console.log("second!");
}
const third = () => {
console.log("third!");
}
first();
setTimeout(second);
third();
Because we technically schedule the execution of second
with setTimeout
, even though we tell it to happen as soon as it can, we get the following
first!
third!
second!
node has moved on.
For more on this behavior, I recommend this video on Javascript’s execution model and the event loop.
The “equivalent” python code would look something like this
import time
def first():
print("first!")
def second():
print("second!")
def first():
print("third!")
first()
time.sleep(0)
second()
third()
but this isn’t really the same as saying “enqueue an invocation of second
.” What it’s really saying is “pause momentarily before executing second
and then proceed, which it does
first!
second!
third!
without you even knowing the sleep
call was there.
Without involving an async framework like asyncio we can’t really replicate the same behavior in Python, which only reinforces what we were told in the beginning: Python executes top to bottom.
You Were (sort of) Lied To
Recently, though, I was introduced to a youtube channel mCoding and he put up a video Variable Lookup Weirdness in Python that broke my mental model of Python’s execution.
This gets back to my import vs runtime post, and only reinforces my point that there is a difference, even if it’s not as clearly cut as in a compiled language.
In his video, mCoding distinguishes between the following two functions
def f():
print(x)
def g():
print(x)
x = 1
My inclination when watching this was that both functions would have the same result, but they don’t.
>>> f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in f
NameError: name 'x' is not defined
>>> g()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in g
UnboundLocalError: local variable 'x' referenced before assignment
This is only possible because Python doesn’t just evaluate code top to bottom on the fly. Code is executed top to bottom (as long as it’s spaghetti code), but it’s interpreted first and that interpretation allows for what looks, to us at runtime, like look-ahead.
This is to say nothing of a language like Rust which can tell that this type of thing will happen at compile time, instead of letting us define the function and only complaining when it’s executed, but that’s a story for another time.