Now we all know
exec() is cruise-control for awesome. Despising readability, sanity, and performance, I sprinkle my code liberally with this MSG and joyfully await the ensuing migraine. But sometimes I ask myself, is it enough?
Correct Answers Considered Harmful
A wonderfully dynamic language like python lets us take
exec() where no string literal has gone before by allowing us to pass in global and local environments that are used for variable lookup. The high-priests of python would have us use this functionality to constrain the side-effects of gratuitious
exec()ing, but we can do so much better. A simple subclass of
dict() that overrides
__getitem__ and only returns the item we’re looking for 50% of the time gives us some ammo:
class AreYouFeelingLuckyDict(dict): #this is idealized. see complete source at end of post. def __getitem__(self, item): if random.choice([True, False]): return dict.__getitem__(self, item) else: raise KeyError(item)
With this dictionary implementation, we can move on to more interesting
exec("""a = 1; print a""", AreYouFeelingLuckyDict(), #globals AreYouFeelingLuckyDict()) #locals
Because python is using instances of
AreYouFeelingLuckyDict for global and local variable lookups inside our string of code, half the time the code will work properly. The other half of the time a
NameError will be raised. Introducing another variable brings the odds of success to 1 in 4:
exec("""a = 1; b=2; print(a); print(b)""", AreYouFeelingLuckyDict(), AreYouFeelingLuckyDict())
Of course we can program a bit more defensively and ensure that we’ll get the output we’re looking for. If we encounter a
NameError along the way, we simply try, try, and try again:
exec(""" a=1 b=2 while True: try: print(a) print(b) break except NameError: pass """, AreYouFeelingLuckyDict(), AreYouFeelingLuckyDict())
Keep in mind that “1” will most likely be printed out several times as its lookup occurs first (and most persistently) and the exception handling is coarse-grained. The obvious solution is to pipe the output through
tail -n 2.
But Wait, There’s More
Becaue of a debilitating gambling problem, bogosort is my favorite sorting algorithm. Bogosort is the equivalent of throwing an array of items onto the floor and seeing if they’ve come up in the proper order. Given a list of N unique items, the odds of getting the answer on the first go round of bogosort are 1 in N!. Running bogosort in production on your company’s machines is like playing roullette (with an enormous wheel and your career). Feel that rush.
But bogosort gets even more awesomer with our
myglobals = AreYouFeelingLuckyDict(random=random) mylocals = AreYouFeelingLuckyDict() exec(""" def sorted(is_sorted): prev = is_sorted for x in is_sorted: if prev > x: return False prev = x return True mylist = [4, 1, 9, 5] runs = 1 while True: try: is_sorted =  while True: mutable_mylist = list(mylist) for i in range(len(mutable_mylist)): index = random.randint(0, len(mutable_mylist)-1) is_sorted.append(mutable_mylist.pop(index)) if sorted(is_sorted): break else: runs+=1 is_sorted= break except NameError: runs += 1 pass """, myglobals, mylocals)
Because I’m a generous man, I’ve modified the
AreYouFeelingLuckyDict to always return a result when the item being looked up is “runs”, “NameError”, “True”, “random”, “len”, “list”, “range”, or “sorted.” It’s also worth noting that as soon as we drop into
is_sorted() we’re back to a plain-jane, well-behaved
locals() with boringly reliable dict-like properties.
With this bogosort modification (err, improvement) and considering a list of length 4, I figure the odds of getting an answer the first time out is 1 in 3,145,824. Now we’re talking! Of course, the code above keeps going until it sorts our list. Running it a few times, I’ve seen the answer produced in as little as 700,000 tries. A paragon of efficiency.
I hope you’ve enjoyed this. I’ve been playing around with this post for a while and have had a great time. The source to this party can be found in this gist: https://gist.github.com/945666
Also, please check out Be careful with exec and eval in Python which inspired this post.