OOP, Inheritance, and Generic Operations (SICP-style Lab)

Predict outputs, trace dispatch (attribute lookup + data-directed tables), then implement and extend a tiny generic-operation system inspired by SICP.

0. Predict: Instance Attribute Shadows Class Attribute (CheckingAccount fee)

**Predict what will be printed** (write your prediction as a comment *before* running).

Focus question: when `withdraw` uses `self.withdrawal_fee`, will it use the class attribute or the instance attribute?

```python
class Account:
    def __init__(self, holder, balance=0):
        self.holder = holder
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            return 'Insufficient funds'
        self.balance -= amount
        return self.balance

class CheckingAccount(Account):
    withdrawal_fee = 1

    def withdraw(self, amount):
        return super().withdraw(amount + self.withdrawal_fee)

ch = CheckingAccount('Eve', 10)
ch.withdrawal_fee = 5
ch.withdraw(3)
print(ch.balance)
```

What does it print?

  

1. Trace: Why adding a 'complex' method works (SICP Ex 2.77 flavor)

This is a tiny data-directed dispatch system.

`z` is tagged as `'complex'`, but inside it stores another tagged value (like `'rect'`).

**Task (trace, by hand):** When evaluating `magnitude(z)`,
1) How many times is `apply_generic` invoked?
2) What type-tag(s) does it see each time?
3) Which function does it dispatch to each time?

Then run to verify it returns `5.0`.

```python
import math

def attach_tag(tag, contents):
    return (tag, contents)

def type_tag(d):
    return d[0]

def contents(d):
    return d[1]

_op = {}

def put(op, tags, fn):
    _op[(op, tuple(tags))] = fn

def get(op, tags):
    return _op.get((op, tuple(tags)))

def apply_generic(op, x):
    fn = get(op, [type_tag(x)])
    return fn(contents(x))

def magnitude(z):
    return apply_generic('magnitude', z)

# rect package
put('magnitude', ['rect'], lambda xy: math.hypot(xy[0], xy[1]))

# complex package (wrapper delegates to inner tagged value)
put('magnitude', ['complex'], lambda inner_tagged: magnitude(inner_tagged))

z = attach_tag('complex', attach_tag('rect', (3.0, 4.0)))
print(magnitude(z))
```

  

2. Implement + Extend: Generic Equality equ? (SICP Ex 2.79 flavor)

You are given a tiny generic-operation table and two complex representations: `'rect'` and `'polar'`.

Your job:

**Part A (implement):** Install generic equality `equ(z1, z2)` for same-type comparisons:
- `('num','num')`
- `('rect','rect')`
- `('polar','polar')`

**Part B (extend):** Without changing `equ` itself, add *cross-type* comparisons so these also work:
- `('rect','polar')` and `('polar','rect')`

Rule: two complex numbers are equal if both their real parts and imaginary parts are close (use `math.isclose` with `abs_tol=1e-9`).

Before running, predict:
- Should `equ(z_rect, z_polar)` be `True` for the two representations of 3+4i below?

Then run the tests.