Skip to content

Latest commit

 

History

History
197 lines (152 loc) · 5.17 KB

ex6_2.md

File metadata and controls

197 lines (152 loc) · 5.17 KB

[ Index | Exercise 6.1 | Exercise 6.3 ]

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:

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:

>>> 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:

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

>>>

Now, try running this:

>>> 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:

>>> 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.

>>> 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:

>>> 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:

>>> 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:

# 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:

# 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.

>>> 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 | Index | Exercise 6.1 | Exercise 6.3 ]


>>> Advanced Python Mastery
... A course by dabeaz
... Copyright 2007-2023

. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License