Scoping Rules and Tricks

PythonPythonBeginner
Practice Now

This tutorial is from open-source community. Access the source code

Introduction

Objectives:

  • Learn more about scoping rules
  • Learn some scoping tricks

Files Modified: structure.py, stock.py

Preparation

In the last exercise, you create 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.

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

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.

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.

âœĻ Check Solution and Practice

Summary

Congratulations! You have completed the Scoping Rules and Tricks lab. You can practice more labs in LabEx to improve your skills.

Other Python Tutorials you may like