Trying To Recreate An SICP Example In Python
This morning I was playing around and decided to try and recreate an example from Structure and Interpretation of Computer Programs (SICP). Along the way I discovered some unexpected gotchas with how Python scopes closures. The example I was trying to reproduce was the following one:
(define (make-account balance) (define (withdraw amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds")) (define (deposit amount) (set! balance (+ balance amount)) balance) (define (dispatch m) (cond ((eq? m 'withdraw) withdraw) ((eq? m 'deposit) deposit) (else (error "Unknown request -- MAKE-ACCOUNT" m)))) dispatch)
Initially, thinking that Python 2.7.x closures were pretty good in my experience, I decided on the following naive implementation:
def make_account(balance): def deposit(amount): balance = balance + amount return balance def withdraw(amount): if balance >= amount: balance = balance - amount return balance return 'Insufficient funds' def dispatch(method_name): if method_name == 'deposit': return deposit elif method_name == 'withdraw': return withdraw elif method_name == 'balance': return balance else: raise ValueError('Unknown request -- {}'.format(method_name)) return dispatch
But when you hop in the Python shell and try to run this you’ll have the following experience:
>>> account = make_account(100) >>> account("deposit")(25) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "closure_objects.py", line 3, in deposit balance = balance + amount UnboundLocalError: local variable 'balance' referenced before assignment
After digging around for a little bit I found this excellent article containing some gotcha’s regarding scoping in Python closures. To quote (with some edits for our example):
…When you say [balance = balance + amount], two things happen at different times:
1. When Python compiles the function, it sees [balance] =, and declares a new variable, scoped within inner, named [balance].
2. When Python executes the function, it needs to compute [balance + amount]. Okay, well, what’s [balance]? It’s a local variable… but, oops, it doesn’t have a value yet! Raise error.
The assignment creates a new inner variable that masks the outer variable
Luckily, Python 3 fixes the issue by introducing the nonlocal keyword, but in Python 2.X the solution is a bit hacky:
def make_account(balance): current_balance = [balance] def deposit(amount): current_balance[0] = current_balance[0] + amount return current_balance[0] def withdraw(amount): if current_balance[0] >= amount: current_balance[0] = current_balance[0] - amount return current_balance[0] return 'Insufficient funds' def dispatch(method_name): if method_name == 'deposit': return deposit elif method_name == 'withdraw': return withdraw elif method_name == 'balance': return current_balance[0] else: raise ValueError('Unknown request -- {}'.format(method_name)) return dispatch
This works because Python leverages 3 different types of assignment, the regular = assignment, __setattr__ assignment, and __setitem__ assignment. The original = assignment attempted to created a new variable, but by triggering __setitem__ assignment instead we get the expected behavior.
Hopefully this will help stop others from getting tripped up on this.
Resources To Better Understand The Issue: