Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the gd-system-plugin domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /var/www/wp-includes/functions.php on line 6114
Trying To Recreate An SICP Example In Python – Eric Scrivner
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: