\[ [Index](index.md) | [Exercise 6.1](ex6_1.md) | [Exercise 6.3](ex6_3.md) \]

# Exercise 6.2

*Objectives:*

- Learn more about scoping rules
- Learn some scoping tricks

*Files modified:* `structure.py`, `stock.py`

In the last exercise, you create a class `Structure` that made it easy to define
data structures.  For example:

```python
class Stock(Structure):
    _fields = ('name','shares','price')
```

This works fine except that a lot of things are pretty weird about the `__init__()`
function.   For example, if you ask for help using `help(Stock)`, you don't get 
any kind of useful signature.  Also, keyword argument passing doesn't work. For
example:

```python
>>> help(Stock)
... look at output ...

>>> s = Stock(name='GOOG', shares=100, price=490.1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'price'
>>> 
```

In this exercise, we're going to look at a different approach to the problem.

## (a) Show me your locals

First, try an experiment by defining the following class:

```python
>>> class Stock:
        def __init__(self, name, shares, price):
            print(locals())

>>>
```

Now, try running this:

```python
>>> s = Stock('GOOG', 100, 490.1)
{'self': <__main__.Stock object at 0x100699b00>, 'price': 490.1, 'name': 'GOOG', 'shares': 100}
>>>
```

Notice how the locals dictionary contains all of the arguments passed
to `__init__()`. That's interesting.  Now, define the following function
and class definitions:

```python
>>> def _init(locs):
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)

>>> class Stock:
        def __init__(self, name, shares, price):
            _init(locals())
```

In this code, the `_init()` function is used to automatically
initialize an object from a dictionary of passed local variables.
You'll find that `help(Stock)` and keyword arguments work perfectly.

```python
>>> s = Stock(name='GOOG', price=490.1, shares=50)
>>> s.name
'GOOG'
>>> s.shares
50
>>> s.price
490.1
>>>
```

## (b) Frame Hacking

One complaint about the last part is that the `__init__()` function
now looks pretty weird with that call to `locals()` inserted into it.
You can get around that though if you're willing to do a bit of stack
frame hacking.  Try this variant of the `_init()` function:

```python
>>> import sys
>>> def _init():
        locs = sys._getframe(1).f_locals   # Get callers local variables
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)
>>>
```

In this code, the local variables are extracted from the stack frame of the caller.
Here is a modified class definition:

```python
>>> class Stock:
        def __init__(self, name, shares, price):
            _init()

>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>>
```

At this point, you're probably feeling rather disturbed.  Yes, you just wrote a function that reached
into the stack frame of another function and examined its local variables.    

## (c) Putting it Together

Taking the ideas in the first two parts, delete the `__init__()` method that was originally part of the
`Structure` class.  Next, add an `_init()` method like this:

```python
# structure.py
import sys

class Structure:
    ...
    @staticmethod
    def _init():
        locs = sys._getframe(1).f_locals
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)
    ...
```

Note: The reason this is defined as a `@staticmethod` is that the `self` argument
is obtained from the locals--there's no need to additionally have it passed as
an argument to the method itself (admittedly this is a bit subtle).

Now, modify your `Stock` class so that it looks like the following:

```python
# stock.py
from structure import Structure

class Stock(Structure):
    _fields = ('name','shares','price')
    def __init__(self, name, shares, price):
        self._init()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= shares
```

Verify that the class works properly, supports keyword arguments, and has a 
proper help signature.

```python
>>> s = Stock(name='GOOG', price=490.1, shares=50)
>>> s.name
'GOOG'
>>> s.shares
50
>>> s.price
490.1
>>> help(Stock)
... look at the output ...
>>>
```

Run your unit tests in `teststock.py` again.  You should see at least one more test pass. Yay!

At this point, it's going to look like we just took a giant step backwards.  Not
only do the classes need the `__init__()` method, they also need the `_fields`
variable for some of the other methods to work (`__repr__()` and `__setattr__()`).  Plus,
the use of `self._init()` looks pretty hacky.    We'll work on this, but be patient.

\[ [Solution](soln6_2.md) | [Index](index.md) | [Exercise 6.1](ex6_1.md) | [Exercise 6.3](ex6_3.md) \]

----
`>>>` Advanced Python Mastery  
`...` A course by [dabeaz](https://www.dabeaz.com)  
`...` Copyright 2007-2023  

![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
