Trying To Recreate An SICP Example In Python
home // page // Trying To Recreate An SICP Example In Python

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: