\[ [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 created 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 "", line 1, in 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/)