5.2 KiB
[ 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 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.
(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 -= sharesVerify 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