Present a talk in 5 days with no experience


Posted on | 1129 words | ~6 minute read

INFO1110: Introduction to Programming

In the presentation I presented with SYNCS I mentioned that I learned Python, moved on, and never looked back. Well, at the computer science fellowship at the University of Sydney, I take INFO1110. It assumes zero prior knowledge, and aims to teach Python and core programming concepts to new developers.

I won’t sugarcoat it, its an easy course with hard assesments. Thats how it usually is.

Task Description

I was excited to get this assesment, I’ll show below.

Your task is to write a program in Python that removes all of the keywords found in the text file from the Python file into a new Python script while maintaining its functionality.

So far so good.

In other words, your program will produce a script that, when given an identical input to the original program, will produce an identical output to what the original program would produce.

A Python to Python compiler? I’m in, I know this stuff.

Removal of and or not - 2 marks

Removal of else - 2 marks

Removal of elif - 2 marks

Removal of assert - 2 marks

Removal of return - 3 marks

Removal of break - 3 marks

Removal of for (counter) - 3 marks

Removal of in - 4 marks

Removal of for (iterator) - 4 marks

All I need to do is remove all of these keywords, simple enough?

I’ll explain everything in two parts, first how to replace these keywords, then the architecture of my logical transformer.

Transformations

These aren’t all hard at all, but some are more difficult than others. I’ll go through them in order of difficulty. Original code on the left, transformed code on the right.

assert

Starting with the easiest, replacing assert.

To replace assert, just extract the condition and message into two parts, negate the condition wrapped in an if statement, and raise AssertionError with the message.

assert expr(), 'test'
if not expr():
	raise AssertionError('test')

else and elif

Use an intermediate temporary variable to store the status that a branch was taken. If the branch was taken, set the variable to False and skip the rest of the branches.

Ensure to place the condition to the right of the and to short-circuit the condition and ensure it’s only evaluated if the branch was not taken. Also, put parentheses around the condition just to make sure further transformations on the expression level don’t mess with the operator precedence.

if cond():
	print('test0')
elif cond():
	print('test1')
elif cond():
	print('test2')
else:
	print('test3')
_if0 = True
if cond():
	_if0 = False
	print('test0')
if _if0 and (cond()):
	_if0 = False
	print('test1')
if _if0 and (cond()):
	_if0 = False
	print('test2')
if _if0:
	print('test3')

break

Replacing break is a similar to the above, a temporary variable stores the loops “continuation” status, and it’s set to false when break is encountered.

while cond():
	if expr():
		break
	print('cond')
_while0 = True
while _while0 and (cond()):
	if expr():
		_while0 = False
		continue
	print('cond')

for (counter) + for (iterator)

I assume a for (counter) just means a range() for loop. Anyway, iterator or range, it doesn’t matter.

The transformation is the same.

for k, v in value:
#   ^^^^    ^^^^^
# /-- transform
_iter0 = iter(value)
k, v = next(_iter0)

Take the header of the for loop, and split on “in” to grab both sides.

It’s possible for a for loop to unpack into multiple variables as well, but this transformation handles that.

Catching a StopIteration exception is the proper way to handle next() on all iterators. Instead of testing for another value, such as None on a next(..., None), this allows me to preserve keep the variable declaration list and unpack all on one line.

Then, create a while loop with a temporary variable to store the loop continuation status. The temporary variable is set to false when the loop is done.

for v in range(0, 15):
	print(v)
_iter0 = iter(range(0, 15))
_for0 = True
while _for0:
	try:
		v = next(_iter0)
	except StopIteration:
		_for0 = False
		continue
	print(v)

return

So we can’t use the return keyword?

No return means no functions, but we still have lambdas.

def func(test):
	return 10 + test

func(20)
# exactly like a function, because it is.
func = lambda test: 10 + test

func(20)

They’re just functions, except with an expression oriented syntax. A lambda evaluates to an expression itself, and it’s body can only be a single expression, so we need to get creative here.

I want a replacement that doesn’t require me to hijack all function calls, something that just works.

The solution is simple, the return keyword gives us two things.

  1. Return a value from a function
  2. Break out of a function

Returning a value is easy, just keep track of some global variable set by the function and read by the lambda. Breaking out of a function is the hard part, there aren’t many keywords that do the same thing as a return.

What you can do though, is convert the entire function into a generator and use yield to relinquish control back to the caller.

So, the function body is replaced with a generator that yields the return value, and the function itself is replaced with a lambda that calls the generator and returns the value.

We’re just redefining the entire function with the new lambda.

def func(test):
	if test:
		return 10

a = func(False)
def _func0(test):
	global _ret_func0
	_ret_func0 = None
	if test:
		_ret_func0 = 10
		yield
	yield

func = lambda test : (next(_func0(test)), _ret_func0)[1]

a = func(False)

(Why not treat it like a single use generator, you wouldn’t need next() in that case? I know, this was just my implementation)

Okay, that’s all the statements. What comes next is much harder.

Transforming Expressions?

Think for a second, how you could replace the and expression with something similar?

  1. expr and expr
    Base case, we need to replace this.
  2. expr & expr
    Bitwise and, close, but not the same thing.
    Will fail on numerical operands, or just about any operands other than bool.
  3. expr * expr
    Will not work, same reasons above.
  4. bool(expr) & bool(expr)
    This is the same as the first case, but with a type cast, and doesn’t work like this!

So how does and work?

in and not in

This keyword tests if the left-hand-side is in the right-hand-side, whatever that is.

What I need to do with any new transformations, is to avoid introducing any banned keywords.

I assume that the assesment is only testing the built in classes, so arrays, dictionaries, and strings.

If I can create a construct that works similarly to the right function, I can use it to replace in.

def key_in(needle, haystack):
	if isinstance(haystack, str):
		return haystack.find(needle) != -1
	
	for v in haystack:
		if v == needle:
			return True
	return False

Architecture