Surprises
My colleague Dave told me about an interesting bit of Ruby the other day. If you make a new Class
object, it initially has no name. If you then assign a name to it, with your new class on the right-hand side of the assignment, the name is attached to the Class object.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
I found this shockingly non-intuitive. Really, an assignment statement permanently modified the object on the right-hand side?
In a word, yes.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
To recap:
Step 1: c
is some object
Step 2: set Foo = c
Step 3: c
is permanently altered.
Wow.
Luckily for our intuition, this is a pretty special case, and not a way that Ruby generally behaves. But it got me wondering if there’s something similar in python.
What about python?
The simplest answer is “no”. In python, assignment is a simple statement, not an operator, so you can’t do things like operator overloading. This also means we can’t somehow add a hook to assignment.
Ok, fine. But wait – we’re programmers! We don’t give up that easily.
What about hooking on __getattribute__
? (Defining your own __getattribute__
is “the bad way” to do it. You almost always want to define your own __getattr__
, unless you’re doing something silly and/or malicious, like we are. Note that we have to use the parent class (object
)’s __getattribute__
to extract and save the attribute we want. If we didn’t do that, we’d trigger an infinitely recursive lookup.)
1 2 3 4 5 6 7 8 |
|
This looks like it works!
1 2 3 4 5 6 7 8 |
|
Unfortunately, this behavior has nothing to do with assignment. The object was mutated when __getattribute__
was called, which happened to be in an assignment statement in this code above. We’d get exactly the same behavior without the assignment:
1 2 3 4 5 6 7 |
|
Darn.
But we’re programmers, right? We don’t give up that easily.
Trace functions to the rescue
There’s a settrace
function in python’s sys
module that we can use to examine stack frames, events, and lots of other code data while our program is running. sys.settrace(fn)
takes a trace function as an argument, and that trace function must take frame, event, args
as arguments. It will then get called every time an event happens. A line of code, a function call, a function return, and an exception are all “events”. We can use this to inspect each line of code before it runs and check manually if it’s an assignment statement.
(I know this is silly, but isn’t it fun?)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
(The trace function returns a reference to itself to indicate that tracing should continue.)
Now let’s write our code to mess up assignment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Here our “box” object is pretty silly. It takes the original object and wraps it in a box. (You could implement __getattr__
and __setattr__
so that the box behaves basically like the original object, which I haven’t done.) The box then explodes when you try to print it. We’re also tracking which names we’ve seen with the aptly-named JANKY_NAMESPACE_MANAGER
(which has all kinds of scope issues, but whatever).
Meanwhile, in the mess_up_on_assignment
code, we check to see if any of the whitespace-separated words on the right-hand side of the equation are names we recognize from frame.f_locals
or frame.f_globals
. If so, and if we haven’t seen them before, throw them in a self-destructing box!
Add a simple helper to get things set up.
1 2 3 |
|
We can now sabotage simple-looking programs!
This one runs fine:
1 2 3 4 5 6 7 8 9 10 11 |
|
And this one blows up:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
And there you have it – a (incredibly goofy) side-effecting assignment that mutates the object on the right-hand side of the assignment statement.