I really enjoyed seeing all the clever solutions to the python puzzle I posted. You’re all very creative! Here’s a discussion of the solutions I’ve seen, plus some clarifications. All spoilers are below the fold.
First, clarifications. (These weren’t always clear in the problem statement, particularly if you got the problem off of twitter, so award yourself full marks as desired.)
Order doesn’t matter
“Order doesn’t matter” means that the three-line version always returns
False, and the semicolon version always returns
You control only the contents of the lines
1 2 3 4 5 6
I’m being pedantic here, but I rule this cheating, since (a) each line has to be a valid python expression or statement, and a multi-line string literal is only one expression, and (b) the string
"""a; b; c""" is not the same as the string
Solutions appear below the fold.
Jessica suggests the following solution:
1 2 3 4 5 6 7 8 9 10 11 12 13
However, Jessica’s solution
fails the “order doesn’t matter” test, and it is stateful:
1 2 3 4
Edit: As Jessica points out, I’m wrong here: her solution does pass the order test. She also notes that the restriction against state wasn’t present in the blog post (and she didn’t see the original tweet). Full credit to Jessica, then!
Javier Novoa Cataño
Javier suggests a solution for Python 2 that fails the order test:
1 2 3 4 5 6
Again, this depends on order and is stateful.
1 2 3 4
Alex suggests using
sys._getframe(0). I might quibble that
sys._getframe constitutes introspection, but Alex is on to something. He notices that each line of code executed in the interpreter gets its own frame. (foreshadowing)
Alex gets full credit because I just discovered that the behavior underlying the original puzzle works in CPython, but is different in PyPy. Sorry, Alex!
Edit: Never mind – there is a simpler solution in PyPy. See below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
For sheer inventiveness and creativity I have to hand it to Anton, working in the iPython REPL:
1 2 3 4 5 6
He’s exploiting the fact that when executed on a single line, all three lines are considered part of the
for loop. When the lines are broken up, only the
x=n or x-1 part belongs to the loop. (This pretty much works in the standard CPython REPL too, but you have to throw in an extra line break.)
Alexey was the only person outside of Hacker School to hit on my solution.
1 2 3 4 5 6
Nicely done, Alexey!
(For the pedantic among us, like me: this was the version I had in mind when I described my solution as 14 non-whitespace characters. Of course, to make the function version work too, we have to add
So what’s going on here, and why is this interesting?
Analyzing this solution
I described Alex Gaynor’s solution as foreshadowing earlier. Alex appears to have started from a good question: what’s different about executing three lines of code versus executing three statements as one line of code?
One thing to note is that each time you get an interpreter prompt back (
>>>), your python has compiled and executed some code. Every prompt corresponds to at least one code object.
How can we use the difference in code objects to generate this behavior? One way is to use constants: in CPython, two instances of the same number (larger than 256) are different objects. This explains why
a is b returns
False in the REPL (in the extended version, without semicolons). In the semicolon version, all three statements are part of one code object. We’re giving the CPython compiler a chance to optimize. It notices that we have two constants with the same value, and only hangs on to one copy.
Let’s take a look. It’s easy to get a handle on the code object corresponding to a function – use
f.func_code in Python 2, and
f.__code__ in Python 3. Getting a handle on a code object corresponding to a line in the REPL is a little bit trickier, but we can do it if we compile them by hand. In Python 2.x:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
The compiler’s being a little smart here, and we only get one occurence of the constant
257 per code object. When we execute the assignment statements on two different lines, we get two different code objects with two different instances of the integer
257. This explains why
is, which is comparing object identity, sees one as the same and the others as different.
This came up originally at Hacker School when Steve Katz stumbled across that thing with small integers and
== in python. (If you’re not familiar with it, you can read many questions on Stack Overflow about it. As a general rule, don’t use
is for integer comparisons.) Steve went on to notice that the behavior changes when run from a script rather than in the REPL.
I didn’t realize when posing this problem that it was an implementation detail of CPython, and PyPy behaves differently (and arguably correctly). That’s why Alex Gaynor had to hook into the frame object.
Edit: Late submissions
More cleverness in my twitter feed!
Ned Batchelder suggests using
-9 instead of
257 to shave off a few characters.
Zev came up with my favorite so far, in 14 characters including the print.
He exploits the intricacies of floating-point arithmetic
1 2 3 4 5
If you’ve never dug around with floating-point math, do yourself a favor: it’s really interesting stuff. The Julia language suggests some good background reading.
Edit 2: Zev emails that floating-point precision isn’t involved here. Instead, he’s exploiting the fact that floating-point numbers – even those that can be precisely represented – are not interned. We’d get the same results using
1. instead of
.1. This suggests a broader point: many solutions here could have used
a = thing; a is thing, omitting LINE_B.
Nick has a fix to the PyPy integer handling: use a string instead of an int.
1 2 3 4 5 6
This was fun! If I missed your solution and you want it to be included, ping me on twitter.