Python Obfuscation #1
16 Nov 2017As usually I am going here to cheat with Python good practices in order to have fun. As is said at the beginning of a funny piece of code: If you use this for anything important, you’re mad! Still, I will polish everything below and try to provide functions on par with the best written ones in regards to speed and memory usage.
My general rules in such challenges are the following ones:
- avoid statements;
- avoid importing any module;
- obviously avoid
exec
oreval
; - avoid
def
orclass
for defining anything.
Such requirements come from a strong functionnal programming background though I am aware they don’t fit well with Python standards.
The task
I want to write a loop
function; since it has to be a single expression that could easily be embedded in a larger expression, I will rather say I want to write a loop
macro. It should be able to iterate infinitely if required with no stack or heap overflow.
The usage of the macro is intended to be:
result = loop(func, cond, x0)
where func
is a function repeatedly applied to x0
then x1=f(x0)
then x2=f(x1)
, etc., cond
a function applied (before func
) to the current temporary result before iterating and returning True
as long as iteration has to go on, and x0
is the initial value. The macro should return the last computed value, which is the first such that cond(result)
is False
.
Iterating
There are several ways for iterating inside an expression. It is well known that lambda calculus allows iterating with the famous Y combinator, but it will not allow to iterate infinitely because of the maximum recursion depth.
The most common iterating process in an expression (in Python) is performed with the list comprehension syntax. A funny way of using it is to iterate over a list being modified in order to get some control on the number of iterations from the inside of the list comprehension; see the example below for computing consecutive terms in the Collatz sequence:
collatz = lambda n: (lambda c:
[k > 1 and c.append((k%2 and 3*k+1) or k//2) for k in c]
and c)([n])
It is assumed here that the reader is familiar with the exact meaning of and
and or
in Python (what is evaluated or not and what is exactly yielded).
But appending new terms to a list will of course lead soon to a MemoryError when too many iterations are performed.
The same idea can however be tweaked in order to avoid any memory usage; if G
is some iterator running as long as needed, then:
[ None for x in G if <expr> and None ]
will not append any term to the current list; of course, if <expr>
is known to return something like None
, the final part can be removed; if <expr>
is known to return something like True
, another simplification would be:
[ None for x in G if not <expr> ]
and if <expr>
is known to return always the same value (like None
), then a very nice simplification is:
{ <expr> for x in G }
because a set doesn’t keep duplicates!
Creating a class from within an expression
Of course anything like range(2**65536)
does not run infinitely though no actual task should require such a large amount of iterations. On the other hand, I couldn’t figure out any infinite iterator without importing such or such module, which I want to avoid.
Creating a new iterator implies defining a new class, which is achievable in an expression:
type("MyClass", (object,), {})
yielding a kind of empty class. New methods can be added to it with:
Evil = (
lambda o:
setattr(o, '__iter__', lambda x:x)
or setattr(o, '__next__', lambda x:True)
or o
)(type("EvilIterator", (object,), {}))
Of course an instance can be returned by adding ()
at the end of the whole expression or after the or o
part.
It can be checked that this macro looks like what I want by typing:
{ print("Hello world") for _ in Evil() }
The final part
Now comes the final part; the Evil
iterator above has to be tuned according to our needs.
Since the loop has to end when some condition is encountered, the iterator should raise a StopIteration exception, which can be achieved either by evaluating some expression doing the same thing like next(_ for _ in ())
or by using the throw
method on any available generator allowing to throw any kind of exception: (_ for _ in ()).throw(StopIteration)
.
A first attempt is:
loop = lambda f, t, i: (
lambda o, w:
setattr(o, '__iter__', lambda x:x)
or setattr(o, '__next__', lambda x:
(t(w[-1]) and not w.append(f(w.pop())))
or next(_ for _ in ()))
or o
)(type("EvilIterator", (object,), {}), [i])()
tested with:
[x for x in loop(lambda x: x-1, lambda x: x>0, 5)]
which yields the expected [True, True, True, True, True]
. Using a set rather than a list help controlling the size of the set but since we want to return the last computed value, we have to do it inside. As a cosmetic change, the line containing the append
part can also be turned to
(t(w[0]) and not w.__setitem__(0, f(w[0])))
which may be slightly faster.
The final version finally is:
loop = lambda f, t, i: (
lambda o, w:
setattr(o, '__iter__', lambda x:x)
or setattr(o, '__next__', lambda x:
(t(w[0]) and not w.__setitem__(0, f(w[0])))
or next(_ for _ in ()))
or { _ for _ in o() }
and w[0]
)(type("EvilIterator", (object,), {}), [i])
tested with: loop(lambda x: x-1, lambda x:x > 0, 5)
. Of course a list can be used instead of a set as long as a filter helps controlling the size of the list:
or [ x for x in o() if not x ]
or w[0]
but I like rather the version using a set because it does not contain a single if
keyword also!
Now we can try to find some factor of an integer number by using the brute force:
loop(lambda x: x+1, lambda x: 8927%x, 2)