198 lines
5.2 KiB
Markdown
198 lines
5.2 KiB
Markdown
|
\[ [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
|
||
|
|
||
|
. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
|