235 lines
6.5 KiB
Markdown
235 lines
6.5 KiB
Markdown
\[ [Index](index.md) | [Exercise 3.2](ex3_2.md) | [Exercise 3.4](ex3_4.md) \]
|
|
|
|
# Exercise 3.3
|
|
|
|
*Objectives:*
|
|
|
|
- Learn about class variables and class methods
|
|
|
|
*Files Modified:* `stock.py`, `reader.py`
|
|
|
|
Instances of the `Stock` class defined in the previous exercise are
|
|
normally created as follows:
|
|
|
|
```python
|
|
>>> s = Stock('GOOG', 100, 490.1)
|
|
>>>
|
|
```
|
|
|
|
However, the `read_portfolio()` function also creates instances from rows
|
|
of data read from files. For example, code such as the following is used:
|
|
|
|
```python
|
|
>>> import csv
|
|
>>> f = open('Data/portfolio.csv')
|
|
>>> rows = csv.reader(f)
|
|
>>> headers = next(rows)
|
|
>>> row = next(rows)
|
|
>>> row
|
|
['AA', '100', '32.20']
|
|
>>> s = Stock(row[0], int(row[1]), float(row[2]))
|
|
>>>
|
|
```
|
|
|
|
## (a) Alternate constructors
|
|
|
|
Perhaps the creation of a `Stock` from a row of raw data is better handled
|
|
by an alternate constructor. Modify the `Stock` class so that it has
|
|
a `types` class variable and `from_row()` class method like this:
|
|
|
|
```python
|
|
class Stock:
|
|
types = (str, int, float)
|
|
def __init__(self, name, shares, price):
|
|
self.name = name
|
|
self.shares = shares
|
|
self.price = price
|
|
|
|
@classmethod
|
|
def from_row(cls, row):
|
|
values = [func(val) for func, val in zip(cls.types, row)]
|
|
return cls(*values)
|
|
...
|
|
```
|
|
|
|
Here's how to test it:
|
|
|
|
```python
|
|
>>> row = ['AA', '100', '32.20']
|
|
>>> s = Stock.from_row(row)
|
|
>>> s.name
|
|
'AA'
|
|
>>> s.shares
|
|
100
|
|
>>> s.price
|
|
32.2
|
|
>>> s.cost()
|
|
3220.0000000000005
|
|
>>>
|
|
```
|
|
|
|
Carefully observe how the string values in the row have been converted to a proper type.
|
|
|
|
## (b) Class variables and inheritance
|
|
|
|
Class variables such as `types` are sometimes used as a customization mechanism
|
|
when inheritance is used. For example, in the `Stock` class, the types can be
|
|
easily changed in a subclass. Try this example which changes the `price` attribute
|
|
to a `Decimal` instance (which is often better suited to financial calculations):
|
|
|
|
```python
|
|
>>> from decimal import Decimal
|
|
>>> class DStock(Stock):
|
|
types = (str, int, Decimal)
|
|
|
|
>>> row = ['AA', '100', '32.20']
|
|
>>> s = DStock.from_row(row)
|
|
>>> s.price
|
|
Decimal('32.20')
|
|
>>> s.cost()
|
|
Decimal('3220.0')
|
|
>>>
|
|
```
|
|
|
|
**Design Discussion**
|
|
|
|
The problem being addressed in this exercise concerns the conversion of data read
|
|
from a file. Would it make sense to perform these conversions in the `__init__()`
|
|
method of the `Stock` class instead? For example:
|
|
|
|
```python
|
|
class Stock:
|
|
def __init__(self, name, shares, price):
|
|
self.name = str(name)
|
|
self.shares = int(shares)
|
|
self.price = float(price)
|
|
```
|
|
|
|
By doing this, you would convert a row of data as follows:
|
|
|
|
```python
|
|
>>> row = ['AA', '100', '32.20']
|
|
>>> s = Stock(*row)
|
|
>>> s.name
|
|
'AA'
|
|
>>> s.shares
|
|
100
|
|
>>> s.price
|
|
32.2
|
|
>>>
|
|
```
|
|
|
|
Is this good or bad? What are your thoughts? On the whole, I think
|
|
it's a questionable design since it mixes two different things
|
|
together--the creation of an instance and the conversion of data.
|
|
Plus, the implicit conversions in `__init__()` limit flexibility and
|
|
might introduce weird bugs if a user isn't paying careful attention.
|
|
|
|
## (c) Reading Objects
|
|
|
|
Change the `read_portfolio()` function to use the new `Stock.from_row()`
|
|
method that you wrote.
|
|
|
|
Point of thought: Which version of code do you like better? The
|
|
version of `read_portfolio()` that performed the type conversions or
|
|
the one that performs conversions in the `Stock.from_row()` method?
|
|
Think about data abstraction.
|
|
|
|
Can you modify `read_portfolio()` to create objects using a class other
|
|
than `Stock`? For example, can the user select the class that they want?
|
|
|
|
## (d) Generalizing
|
|
|
|
A useful feature of class-methods is that you can use them to put a
|
|
highly uniform instance creation interface on a wide variety of
|
|
classes and write general purpose utility functions that use them.
|
|
|
|
Earlier, you created a file `reader.py` that had some functions for
|
|
reading CSV data. Add the following `read_csv_as_instances()`
|
|
function to the file which accepts a class as input and uses the class
|
|
`from_row()` method to create a list of instances:
|
|
|
|
```python
|
|
# reader.py
|
|
...
|
|
|
|
def read_csv_as_instances(filename, cls):
|
|
'''
|
|
Read a CSV file into a list of instances
|
|
'''
|
|
records = []
|
|
with open(filename) as f:
|
|
rows = csv.reader(f)
|
|
headers = next(rows)
|
|
for row in rows:
|
|
records.append(cls.from_row(row))
|
|
return records
|
|
```
|
|
|
|
Get rid of the `read_portfolio()` function--you don't need that anymore. If you want to
|
|
read a list of `Stock` objects, do this:
|
|
|
|
```python
|
|
>>> # Read a portfolio of Stock instances
|
|
>>> from reader import read_csv_as_instances
|
|
>>> from stock import Stock
|
|
>>> portfolio = read_csv_as_instances('Data/portfolio.csv', Stock)
|
|
>>> portfolio
|
|
[<__main__.Stock object at 0x100674748>,
|
|
<__main__.Stock object at 0x1006746d8>,
|
|
<__main__.Stock object at 0x1006747b8>,
|
|
<__main__.Stock object at 0x100674828>,
|
|
<__main__.Stock object at 0x100674898>,
|
|
<__main__.Stock object at 0x100674908>,
|
|
<__main__.Stock object at 0x100674978>]
|
|
>>>
|
|
```
|
|
|
|
Here is another example of how you might use `read_csv_as_instances()` with a completely different class:
|
|
|
|
```python
|
|
>>> class Row:
|
|
def __init__(self, route, date, daytype, numrides):
|
|
self.route = route
|
|
self.date = date
|
|
self.daytype = daytype
|
|
self.numrides = numrides
|
|
@classmethod
|
|
def from_row(cls, row):
|
|
return cls(row[0], row[1], row[2], int(row[3]))
|
|
|
|
>>> rides = read_csv_as_instances('Data/ctabus.csv', Row)
|
|
>>> len(rides)
|
|
577563
|
|
>>>
|
|
```
|
|
|
|
**Discussion**
|
|
|
|
This exercise illustrates the two most common uses of class variables
|
|
and class methods. Class variables are often used to hold a global
|
|
parameter (e.g., a configuration setting) that is shared across all
|
|
instances. Sometimes subclasses will inherit from the base class and
|
|
override the setting to change behavior.
|
|
|
|
Class methods are most commonly used to implement alternate
|
|
constructors as shown. A common way to spot such class methods is to
|
|
look for the word "from" in the name. For example, here is an example
|
|
on built-in dictionaries:
|
|
|
|
```python
|
|
>>> d = dict.fromkeys(['a','b','c'], 0) # class method
|
|
>>> d
|
|
{'a': 0, 'c': 0, 'b': 0}
|
|
>>>
|
|
```
|
|
|
|
\[ [Solution](soln3_3.md) | [Index](index.md) | [Exercise 3.2](ex3_2.md) | [Exercise 3.4](ex3_4.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/)
|