Added solution code
This commit is contained in:
@@ -136,10 +136,15 @@ If you want to operate on an instance, you always have to refer too it explicitl
|
|||||||
|
|
||||||
## Exercises
|
## Exercises
|
||||||
|
|
||||||
|
Note: For this exercise you want to have fully working code from earlier
|
||||||
|
exercises. If things are broken look at the solution code for Exercise 3.18.
|
||||||
|
You can find this code in the `Solutions/3_18` directory.
|
||||||
|
|
||||||
### Exercise 4.1: Objects as Data Structures
|
### Exercise 4.1: Objects as Data Structures
|
||||||
|
|
||||||
In section 2 and 3, we worked with data represented as tuples and dictionaries.
|
In section 2 and 3, we worked with data represented as tuples and
|
||||||
For example, a holding of stock could be represented as a tuple like this:
|
dictionaries. For example, a holding of stock could be represented as
|
||||||
|
a tuple like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
s = ('GOOG',100,490.10)
|
s = ('GOOG',100,490.10)
|
||||||
@@ -161,20 +166,21 @@ def cost(s):
|
|||||||
return s['shares'] * s['price']
|
return s['shares'] * s['price']
|
||||||
```
|
```
|
||||||
|
|
||||||
However, as your program gets large, you might want to create a better sense of organization.
|
However, as your program gets large, you might want to create a better
|
||||||
Thus, another approach for representing data would be to define a class.
|
sense of organization. Thus, another approach for representing data
|
||||||
|
would be to define a class. Create a file called `stock.py` and
|
||||||
Create a file called `stock.py` and define a class `Stock` that represents a single holding of stock.
|
define a class `Stock` that represents a single holding of stock.
|
||||||
Have the instances of `Stock` have `name`, `shares`, and `price` attributes.
|
Have the instances of `Stock` have `name`, `shares`, and `price`
|
||||||
|
attributes. For example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> import stock
|
>>> import stock
|
||||||
>>> s = stock.Stock('GOOG',100,490.10)
|
>>> a = stock.Stock('GOOG',100,490.10)
|
||||||
>>> s.name
|
>>> a.name
|
||||||
'GOOG'
|
'GOOG'
|
||||||
>>> s.shares
|
>>> a.shares
|
||||||
100
|
100
|
||||||
>>> s.price
|
>>> a.price
|
||||||
490.1
|
490.1
|
||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
@@ -182,88 +188,42 @@ Have the instances of `Stock` have `name`, `shares`, and `price` attributes.
|
|||||||
Create a few more `Stock` objects and manipulate them. For example:
|
Create a few more `Stock` objects and manipulate them. For example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> a = stock.Stock('AAPL',50,122.34)
|
>>> b = stock.Stock('AAPL', 50, 122.34)
|
||||||
>>> b = stock.Stock('IBM',75,91.75)
|
>>> c = stock.Stock('IBM', 75, 91.75)
|
||||||
>>> a.shares * a.price
|
|
||||||
6117.0
|
|
||||||
>>> b.shares * b.price
|
>>> b.shares * b.price
|
||||||
|
6117.0
|
||||||
|
>>> c.shares * c.price
|
||||||
6881.25
|
6881.25
|
||||||
>>> stocks = [a,b,s]
|
>>> stocks = [a, b, c]
|
||||||
>>> stocks
|
>>> stocks
|
||||||
[<stock.Stock object at 0x37d0b0>, <stock.Stock object at 0x37d110>, <stock.Stock object at 0x37d050>]
|
[<stock.Stock object at 0x37d0b0>, <stock.Stock object at 0x37d110>, <stock.Stock object at 0x37d050>]
|
||||||
>>> for t in stocks:
|
>>> for s in stocks:
|
||||||
print(f'{t.name:>10s} {t.shares:>10d} {t.price:>10.2f}')
|
|
||||||
|
|
||||||
... look at the output ...
|
|
||||||
>>>
|
|
||||||
```
|
|
||||||
|
|
||||||
One thing to emphasize here is that the class `Stock` acts like a factory for creating instances of objects.
|
|
||||||
Basically, you just call it as a function and it creates a new object for you.
|
|
||||||
|
|
||||||
Also, it needs to be emphasized that each object is distinct---they
|
|
||||||
each have their own data that is separate from other objects that have
|
|
||||||
been created. An object defined by a class is somewhat similar to a
|
|
||||||
dictionary, just with somewhat different syntax.
|
|
||||||
For example, instead of writing `s['name']` or `s['price']`, you now
|
|
||||||
write `s.name` and `s.price`.
|
|
||||||
|
|
||||||
### Exercise 4.2: Reading Data into a List of Objects
|
|
||||||
|
|
||||||
In your `stock.py` program, write a function
|
|
||||||
`read_portfolio(filename)` that reads portfolio data from a file into
|
|
||||||
a list of `Stock` objects. This function is going to mimic the
|
|
||||||
behavior of earlier code you have written. Here’s how your function
|
|
||||||
will behave:
|
|
||||||
|
|
||||||
```python
|
|
||||||
>>> import stock
|
|
||||||
>>> portfolio = stock.read_portfolio('Data/portfolio.csv')
|
|
||||||
>>> portfolio
|
|
||||||
[<stock.Stock object at 0x81d70>, <stock.Stock object at 0x81cf0>, <stock.Stock object at 0x81db0>,
|
|
||||||
<stock.Stock object at 0x81df0>, <stock.Stock object at 0x81e30>, <stock.Stock object at 0x81e70>,
|
|
||||||
<stock.Stock object at 0x81eb0>]
|
|
||||||
>>>
|
|
||||||
```
|
|
||||||
|
|
||||||
It is important to emphasize that `read_portfolio()` is a top-level function, not a method of the `Stock` class.
|
|
||||||
This function is merely creating a list of `Stock` objects; it’s not an operation on an individual `Stock` instance.
|
|
||||||
|
|
||||||
Try performing some calculations with the above data. First, try printing a formatted table:
|
|
||||||
|
|
||||||
```python
|
|
||||||
>>> for s in portfolio:
|
|
||||||
print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')
|
print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')
|
||||||
|
|
||||||
... look at the output ...
|
... look at the output ...
|
||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Try a list comprehension:
|
One thing to emphasize here is that the class `Stock` acts like a
|
||||||
|
factory for creating instances of objects. Basically, you call
|
||||||
|
it as a function and it creates a new object for you. Also, it needs
|
||||||
|
to be emphasized that each object is distinct---they each have their
|
||||||
|
own data that is separate from other objects that have been created.
|
||||||
|
|
||||||
```python
|
An object defined by a class is somewhat similar to a dictionary--just
|
||||||
>>> more100 = [s for s in portfolio if s.shares > 100]
|
with somewhat different syntax. For example, instead of writing
|
||||||
>>> for s in more100:
|
`s['name']` or `s['price']`, you now write `s.name` and `s.price`.
|
||||||
print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')
|
|
||||||
|
|
||||||
... look at the output ...
|
### Exercise 4.2: Adding some Methods
|
||||||
>>>
|
|
||||||
```
|
|
||||||
|
|
||||||
Again, notice the similarity between `Stock` objects and dictionaries. They’re basically the same idea, but the syntax for accessing values differs.
|
|
||||||
|
|
||||||
### Exercise 4.3: Adding some Methods
|
|
||||||
|
|
||||||
With classes, you can attach functions to your objects. These are
|
With classes, you can attach functions to your objects. These are
|
||||||
known as methods and are functions that operate on the data stored
|
known as methods and are functions that operate on the data
|
||||||
inside an object.
|
stored inside an object. Add a `cost()` and `sell()` method to your
|
||||||
|
`Stock` object. They should work like this:
|
||||||
Add a `cost()` and `sell()` method to your `Stock` object. They should
|
|
||||||
work like this:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> import stock
|
>>> import stock
|
||||||
>>> s = stock.Stock('GOOG',100,490.10)
|
>>> s = stock.Stock('GOOG', 100, 490.10)
|
||||||
>>> s.cost()
|
>>> s.cost()
|
||||||
49010.0
|
49010.0
|
||||||
>>> s.shares
|
>>> s.shares
|
||||||
@@ -276,4 +236,54 @@ work like this:
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Exercise 4.3: Creating a list of instances
|
||||||
|
|
||||||
|
Try these steps to make a list of Stock instances and compute the total
|
||||||
|
cost:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> import fileparse
|
||||||
|
>>> with open('Data/portfolio.csv') as lines:
|
||||||
|
... portdicts = fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float])
|
||||||
|
...
|
||||||
|
>>> portfolio = [ stock.Stock(d['name'], d['shares'], d['price']) for d in portdicts]
|
||||||
|
>>> portfolio
|
||||||
|
[<stock.Stock object at 0x10c9e2128>, <stock.Stock object at 0x10c9e2048>, <stock.Stock object at 0x10c9e2080>,
|
||||||
|
<stock.Stock object at 0x10c9e25f8>, <stock.Stock object at 0x10c9e2630>, <stock.Stock object at 0x10ca6f748>,
|
||||||
|
<stock.Stock object at 0x10ca6f7b8>]
|
||||||
|
>>> sum([s.cost() for s in portfolio])
|
||||||
|
44671.15
|
||||||
|
>>>
|
||||||
|
----
|
||||||
|
|
||||||
|
### Exercise 4.4: Using your class
|
||||||
|
|
||||||
|
Modify the `read_portfolio()` function in the `report.py` program so that it
|
||||||
|
reads a portfolio into a list of `Stock` instances. Once you have done that,
|
||||||
|
fix all of the code in `report.py` and `pcost.py` so that it works with
|
||||||
|
`Stock` instances instead of dictionaries.
|
||||||
|
|
||||||
|
Hint: You should not have to make major changes to the code. You will mainly
|
||||||
|
be changing dictionary access such as `s['shares']` into `s.shares`.
|
||||||
|
|
||||||
|
You should be able to run your functions the same as before:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> import pcost
|
||||||
|
>>> pcost.portfolio_cost('Data/portfolio.csv')
|
||||||
|
44671.15
|
||||||
|
>>> import report
|
||||||
|
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
|
||||||
|
Name Shares Price Change
|
||||||
|
---------- ---------- ---------- ----------
|
||||||
|
AA 100 9.22 -22.98
|
||||||
|
IBM 50 106.28 15.18
|
||||||
|
CAT 150 35.46 -47.98
|
||||||
|
MSFT 200 20.89 -30.34
|
||||||
|
GE 95 13.48 -26.89
|
||||||
|
MSFT 50 20.89 -44.21
|
||||||
|
IBM 100 106.28 35.84
|
||||||
|
>>>
|
||||||
|
```
|
||||||
|
|
||||||
[Contents](../Contents) \| [Previous (3.6 Design discussion)](../03_Program_organization/06_Design_discussion) \| [Next (4.2 Inheritance)](02_Inheritance)
|
[Contents](../Contents) \| [Previous (3.6 Design discussion)](../03_Program_organization/06_Design_discussion) \| [Next (4.2 Inheritance)](02_Inheritance)
|
||||||
|
|||||||
@@ -220,63 +220,53 @@ We're not going to explore multiple inheritance further in this course.
|
|||||||
|
|
||||||
## Exercises
|
## Exercises
|
||||||
|
|
||||||
### Exercise 4.4: Print Portfolio
|
A major use of inheritance is in writing code that's meant to be
|
||||||
|
extended or customized in various ways--especially in libraries or
|
||||||
A major use of inheritance is in writing code that’s meant to be extended or customized in various ways—especially in libraries or frameworks.
|
frameworks. To illustrate, consider the `print_report()` function
|
||||||
To illustrate, start by adding the following function to your `stock.py` program:
|
in your `report.py` program. It should look something like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# stock.py
|
def print_report(reportdata):
|
||||||
...
|
|
||||||
def print_portfolio(portfolio):
|
|
||||||
'''
|
'''
|
||||||
Make a nicely formatted table showing portfolio contents.
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
'''
|
'''
|
||||||
headers = ('Name','Shares','Price')
|
headers = ('Name','Shares','Price','Change')
|
||||||
for h in headers:
|
print('%10s %10s %10s %10s' % headers)
|
||||||
print(f'{h:>10s}',end=' ')
|
|
||||||
print()
|
|
||||||
print(('-'*10 + ' ')*len(headers))
|
print(('-'*10 + ' ')*len(headers))
|
||||||
for s in portfolio:
|
for row in reportdata:
|
||||||
print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
```
|
```
|
||||||
|
|
||||||
Add a little testing section to the bottom of your `stock.py` file that runs the above function:
|
When you run your report program, you should be getting output like this:
|
||||||
|
|
||||||
```python
|
|
||||||
if __name__ == '__main__':
|
|
||||||
portfolio = read_portfolio('Data/portfolio.csv')
|
|
||||||
print_portfolio(portfolio)
|
|
||||||
```
|
```
|
||||||
|
>>> import report
|
||||||
When you run your `stock.py`, you should get this output:
|
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
|
||||||
|
Name Shares Price Change
|
||||||
```bash
|
---------- ---------- ---------- ----------
|
||||||
Name Shares Price
|
AA 100 9.22 -22.98
|
||||||
---------- ---------- ----------
|
IBM 50 106.28 15.18
|
||||||
AA 100 32.20
|
CAT 150 35.46 -47.98
|
||||||
IBM 50 91.10
|
MSFT 200 20.89 -30.34
|
||||||
CAT 150 83.44
|
GE 95 13.48 -26.89
|
||||||
MSFT 200 51.23
|
MSFT 50 20.89 -44.21
|
||||||
GE 95 40.37
|
IBM 100 106.28 35.84
|
||||||
MSFT 50 65.10
|
|
||||||
IBM 100 70.44
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exercise 4.5: An Extensibility Problem
|
### Exercise 4.5: An Extensibility Problem
|
||||||
|
|
||||||
Suppose that you wanted to modify the `print_portfolio()` function to
|
Suppose that you wanted to modify the `print_report()` function to
|
||||||
support a variety of different output formats such as plain-text,
|
support a variety of different output formats such as plain-text,
|
||||||
HTML, CSV, or XML. To do this, you could try to write one gigantic
|
HTML, CSV, or XML. To do this, you could try to write one gigantic
|
||||||
function that did everything. However, doing so would likely lead to
|
function that did everything. However, doing so would likely lead to
|
||||||
an unmaintainable mess. Instead, this is a perfect opportunity to use
|
an unmaintainable mess. Instead, this is a perfect opportunity to use
|
||||||
inheritance instead.
|
inheritance instead.
|
||||||
|
|
||||||
To start, focus on the steps that are involved in a creating a
|
To start, focus on the steps that are involved in a creating a table.
|
||||||
table. At the top of the table is a set of table headers. After that,
|
At the top of the table is a set of table headers. After that, rows
|
||||||
rows of table data appear. Let’s take those steps and and put them into their own class.
|
of table data appear. Let's take those steps and and put them into
|
||||||
|
their own class. Create a file called `tableformat.py` and define the
|
||||||
Create a file called `tableformat.py` and define the following class:
|
following class:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# tableformat.py
|
# tableformat.py
|
||||||
@@ -295,44 +285,68 @@ class TableFormatter(object):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
```
|
```
|
||||||
|
|
||||||
This class does nothing, but it serves as a kind of design specification for additional classes that will be defined shortly.
|
This class does nothing, but it serves as a kind of design specification for
|
||||||
|
additional classes that will be defined shortly.
|
||||||
|
|
||||||
Modify the `print_portfolio()` function so that it accepts a `TableFormatter` object as input and invokes methods on it to produce the output.
|
Modify the `print_report()` function so that it accepts a `TableFormatter` object
|
||||||
For example, like this:
|
as input and invokes methods on it to produce the output. For example, like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# stock.py
|
# report.py
|
||||||
...
|
...
|
||||||
def print_portfolio(portfolio, formatter):
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
'''
|
'''
|
||||||
Make a nicely formatted table showing portfolio contents.
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
'''
|
'''
|
||||||
formatter.headings(['Name', 'Shares', 'Price'])
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
for s in portfolio:
|
for name, shares, price, change in reportdata:
|
||||||
# Form a row of output data (as strings)
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
rowdata = [s.name, str(s.shares), f'{s.price:0.2f}' ]
|
|
||||||
formatter.row(rowdata)
|
formatter.row(rowdata)
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, try your new class by modifying the main program like this:
|
Since you added an argument to print_report(), you're going to need to modify the
|
||||||
|
`portfolio_report()` function as well. Change it so that it creates a `TableFormatter`
|
||||||
|
like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# stock.py
|
# report.py
|
||||||
|
|
||||||
|
import tableformat
|
||||||
|
|
||||||
...
|
...
|
||||||
if __name__ == '__main__':
|
def portfolio_report(portfoliofile, pricefile):
|
||||||
from tableformat import TableFormatter
|
'''
|
||||||
portfolio = read_portfolio('Data/portfolio.csv')
|
Make a stock report given portfolio and price data files.
|
||||||
formatter = TableFormatter()
|
'''
|
||||||
print_portfolio(portfolio, formatter)
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.TableFormatter()
|
||||||
|
print_report(report, formatter)
|
||||||
```
|
```
|
||||||
|
|
||||||
When you run this new code, your program will immediately crash with a `NotImplementedError` exception.
|
Run this new code:
|
||||||
That’s not too exciting, but continue to the next part.
|
|
||||||
|
```python
|
||||||
|
>>> ================================ RESTART ================================
|
||||||
|
>>> import report
|
||||||
|
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
|
||||||
|
... crashes ...
|
||||||
|
```
|
||||||
|
|
||||||
|
It should immediately crash with a `NotImplementedError` exception. That's not
|
||||||
|
too exciting, but continue to the next part.
|
||||||
|
|
||||||
### Exercise 4.6: Using Inheritance to Produce Different Output
|
### Exercise 4.6: Using Inheritance to Produce Different Output
|
||||||
|
|
||||||
The `TableFormatter` class you defined in part (a) is meant to be extended via inheritance.
|
The `TableFormatter` class you defined in part (a) is meant to be extended via inheritance.
|
||||||
In fact, that’s the whole idea. To illustrate, define a class `TextTableFormatter` like this:
|
In fact, that's the whole idea. To illustrate, define a class `TextTableFormatter` like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# tableformat.py
|
# tableformat.py
|
||||||
@@ -353,33 +367,47 @@ class TextTableFormatter(TableFormatter):
|
|||||||
print()
|
print()
|
||||||
```
|
```
|
||||||
|
|
||||||
Modify your main program in `stock.py` like this and try it:
|
Modify the `portfolio_report()` function like this and try it:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# stock.py
|
# report.py
|
||||||
...
|
...
|
||||||
if __name__ == '__main__':
|
def portfolio_report(portfoliofile, pricefile):
|
||||||
from tableformat import TextTableFormatter
|
'''
|
||||||
portfolio = read_portfolio('Data/portfolio.csv')
|
Make a stock report given portfolio and price data files.
|
||||||
formatter = TextTableFormatter()
|
'''
|
||||||
print_portfolio(portfolio, formatter)
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.TextTableFormatter()
|
||||||
|
print_report(report, formatter)
|
||||||
```
|
```
|
||||||
|
|
||||||
This should produce the same output as before:
|
This should produce the same output as before:
|
||||||
|
|
||||||
```bash
|
```python
|
||||||
Name Shares Price
|
>>> ================================ RESTART ================================
|
||||||
---------- ---------- ----------
|
>>> import report
|
||||||
AA 100 32.20
|
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
|
||||||
IBM 50 91.10
|
Name Shares Price Change
|
||||||
CAT 150 83.44
|
---------- ---------- ---------- ----------
|
||||||
MSFT 200 51.23
|
AA 100 9.22 -22.98
|
||||||
GE 95 40.37
|
IBM 50 106.28 15.18
|
||||||
MSFT 50 65.10
|
CAT 150 35.46 -47.98
|
||||||
IBM 100 70.44
|
MSFT 200 20.89 -30.34
|
||||||
|
GE 95 13.48 -26.89
|
||||||
|
MSFT 50 20.89 -44.21
|
||||||
|
IBM 100 106.28 35.84
|
||||||
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
However, let’s change the output to something else. Define a new class `CSVTableFormatter` that produces output in CSV format:
|
However, let's change the output to something else. Define a new
|
||||||
|
class `CSVTableFormatter` that produces output in CSV format:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# tableformat.py
|
# tableformat.py
|
||||||
@@ -398,105 +426,170 @@ class CSVTableFormatter(TableFormatter):
|
|||||||
Modify your main program as follows:
|
Modify your main program as follows:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# stock.py
|
def portfolio_report(portfoliofile, pricefile):
|
||||||
...
|
'''
|
||||||
if __name__ == '__main__':
|
Make a stock report given portfolio and price data files.
|
||||||
from tableformat import CSVTableFormatter
|
'''
|
||||||
portfolio = read_portfolio('Data/portfolio.csv')
|
# Read data files
|
||||||
formatter = CSVTableFormatter()
|
portfolio = read_portfolio(portfoliofile)
|
||||||
print_portfolio(portfolio, formatter)
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.CSVTableFormatter()
|
||||||
|
print_report(report, formatter)
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now see CSV output like this:
|
You should now see CSV output like this:
|
||||||
|
|
||||||
```csv
|
```python
|
||||||
Name,Shares,Price
|
>>> ================================ RESTART ================================
|
||||||
AA,100,32.20
|
>>> import report
|
||||||
IBM,50,91.10
|
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
|
||||||
CAT,150,83.44
|
Name,Shares,Price,Change
|
||||||
MSFT,200,51.23
|
AA,100,9.22,-22.98
|
||||||
GE,95,40.37
|
IBM,50,106.28,15.18
|
||||||
MSFT,50,65.10
|
CAT,150,35.46,-47.98
|
||||||
IBM,100,70.44
|
MSFT,200,20.89,-30.34
|
||||||
|
GE,95,13.48,-26.89
|
||||||
|
MSFT,50,20.89,-44.21
|
||||||
|
IBM,100,106.28,35.84
|
||||||
```
|
```
|
||||||
|
|
||||||
Using a similar idea, define a class `HTMLTableFormatter` that produces a table with the following output:
|
Using a similar idea, define a class `HTMLTableFormatter`
|
||||||
|
that produces a table with the following output:
|
||||||
|
|
||||||
```html
|
```
|
||||||
<tr> <th>Name</th> <th>Shares</th> <th>Price</th> </tr>
|
<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
|
||||||
<tr> <td>AA</td> <td>100</td> <td>32.20</td> </tr>
|
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
|
||||||
<tr> <td>IBM</td> <td>50</td> <td>91.10</td> </tr>
|
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
|
||||||
|
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
|
||||||
|
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
|
||||||
|
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
|
||||||
|
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
|
||||||
|
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>
|
||||||
```
|
```
|
||||||
|
|
||||||
Test your code by modifying the main program to create a `HTMLTableFormatter` object instead of a `CSVTableFormatter` object.
|
Test your code by modifying the main program to create a
|
||||||
|
`HTMLTableFormatter` object instead of a
|
||||||
|
`CSVTableFormatter` object.
|
||||||
|
|
||||||
### Exercise 4.7: Polymorphism in Action
|
### Exercise 4.7: Polymorphism in Action
|
||||||
|
|
||||||
A major feature of object-oriented programming is that you can plug an
|
A major feature of object-oriented programming is that you can
|
||||||
object into a program and it will work without having to change any of
|
plug an object into a program and it will work without having to
|
||||||
the existing code. For example, if you wrote a program that expected
|
change any of the existing code. For example, if you wrote a program
|
||||||
to use a `TableFormatter` object, it would work no matter what kind of
|
that expected to use a `TableFormatter` object, it would work no
|
||||||
`TableFormatter` you actually gave it.
|
matter what kind of `TableFormatter` you actually gave it. This
|
||||||
|
behavior is sometimes referred to as "polymorphism."
|
||||||
|
|
||||||
This behavior is sometimes referred to as *polymorphism*.
|
One potential problem is figuring out how to allow a user to pick
|
||||||
|
out the formatter that they want. Direct use of the class names
|
||||||
One potential problem is making it easier for the user to pick the formatter that they want.
|
such as `TextTableFormatter` is often annoying. Thus, you
|
||||||
This can sometimes be fixed by defining a helper function.
|
might consider some simplified approach. Perhaps you embed an `if-`statement
|
||||||
|
into the code like this:
|
||||||
In the `tableformat.py` file, add a function `create_formatter(name)`
|
|
||||||
that allows a user to create a formatter given an output name such as
|
|
||||||
`'txt'`, `'csv'`, or `'html'`.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# stock.py
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
...
|
'''
|
||||||
if __name__ == '__main__':
|
Make a stock report given portfolio and price data files.
|
||||||
from tableformat import create_formatter
|
'''
|
||||||
portfolio = read_portfolio('Data/portfolio.csv')
|
# Read data files
|
||||||
formatter = create_formatter('csv')
|
portfolio = read_portfolio(portfoliofile)
|
||||||
print_portfolio(portfolio, formatter)
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
if fmt == 'txt':
|
||||||
|
formatter = tableformat.TextTableFormatter()
|
||||||
|
elif fmt == 'csv':
|
||||||
|
formatter = tableformat.CSVTableFormatter()
|
||||||
|
elif fmt == 'html':
|
||||||
|
formatter = tableformat.HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f'Unknown format {fmt}')
|
||||||
|
print_report(report, formatter)
|
||||||
```
|
```
|
||||||
|
|
||||||
When you run this program, you’ll see output such as this:
|
In this code, the user specifies a simplified name such as `'txt'` or
|
||||||
|
`'csv'` to pick a format. However, is putting a big `if`-statement in
|
||||||
|
the `portfolio_report()` function like that the best idea? It might
|
||||||
|
be better to move that code to a general purpose function somewhere
|
||||||
|
else.
|
||||||
|
|
||||||
```csv
|
In the `tableformat.py` file, add a
|
||||||
Name,Shares,Price
|
function `create_formatter(name)` that allows a user to create a
|
||||||
AA,100,32.20
|
formatter given an output name such as `'txt'`, `'csv'`, or `'html'`.
|
||||||
IBM,50,91.10
|
Modify `portfolio_report()` so that it looks like this:
|
||||||
CAT,150,83.44
|
|
||||||
MSFT,200,51.23
|
|
||||||
GE,95,40.37
|
|
||||||
MSFT,50,65.10
|
|
||||||
IBM,100,70.44
|
|
||||||
```
|
|
||||||
|
|
||||||
Try changing the format to `'txt'` and `'html'` just to make sure your
|
|
||||||
code is working correctly. If the user provides a bad output format
|
|
||||||
to the `create_formatter()` function, have it raise a `RuntimeError`
|
|
||||||
exception. For example:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> from tableformat import create_formatter
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
>>> formatter = create_formatter('xls')
|
'''
|
||||||
Traceback (most recent call last):
|
Make a stock report given portfolio and price data files.
|
||||||
File "<stdin>", line 1, in <module>
|
'''
|
||||||
File "tableformat.py", line 68, in create_formatter
|
# Read data files
|
||||||
raise RuntimeError('Unknown table format %s' % name)
|
portfolio = read_portfolio(portfoliofile)
|
||||||
RuntimeError: Unknown table format xls
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
```
|
||||||
|
|
||||||
|
Try calling the function with different formats to make sure it's working.
|
||||||
|
|
||||||
|
### Exercise 4.8: Putting it all together
|
||||||
|
|
||||||
|
Modify the `report.py` program so that the `portfolio_report()` function takes
|
||||||
|
an optional argument specifying the output format. For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv', 'txt')
|
||||||
|
Name Shares Price Change
|
||||||
|
---------- ---------- ---------- ----------
|
||||||
|
AA 100 9.22 -22.98
|
||||||
|
IBM 50 106.28 15.18
|
||||||
|
CAT 150 35.46 -47.98
|
||||||
|
MSFT 200 20.89 -30.34
|
||||||
|
GE 95 13.48 -26.89
|
||||||
|
MSFT 50 20.89 -44.21
|
||||||
|
IBM 100 106.28 35.84
|
||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Writing extensible code is one of the most common uses of inheritance in libraries and frameworks.
|
Modify the main program so that a format can be given on the command line:
|
||||||
For example, a framework might instruct you to define your own object that inherits from a provided base class.
|
|
||||||
You’re then told to fill in various methods that implement various bits of functionality.
|
|
||||||
That said, designing object oriented programs can be extremely
|
|
||||||
difficult. For more information, you should probably look for books on
|
|
||||||
the topic of design patterns.
|
|
||||||
|
|
||||||
That said, understanding what happened in this exercise will take you
|
```bash
|
||||||
pretty far in terms of using most library modules and knowing
|
bash $ python3 report.py Data/portfolio.csv Data/prices.csv csv
|
||||||
what inheritance is good for (extensibility).
|
Name,Shares,Price,Change
|
||||||
|
AA,100,9.22,-22.98
|
||||||
|
IBM,50,106.28,15.18
|
||||||
|
CAT,150,35.46,-47.98
|
||||||
|
MSFT,200,20.89,-30.34
|
||||||
|
GE,95,13.48,-26.89
|
||||||
|
MSFT,50,20.89,-44.21
|
||||||
|
IBM,100,106.28,35.84
|
||||||
|
bash $
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discussion
|
||||||
|
|
||||||
|
Writing extensible code is one of the most common uses of inheritance
|
||||||
|
in libraries and frameworks. For example, a framework might instruct
|
||||||
|
you to define your own object that inherits from a provided base
|
||||||
|
class. You're then told to fill in various methods that implement
|
||||||
|
various bits of functionality.
|
||||||
|
|
||||||
|
That said, designing object oriented programs can be extremely difficult.
|
||||||
|
For more information, you should probably look for books on the topic of
|
||||||
|
design patterns (although understanding what happened in this exercise
|
||||||
|
will take you pretty far in terms of using most library modules).
|
||||||
|
|
||||||
[Contents](../Contents) \| [Previous (4.1 Classes)](01_Class) \| [Next (4.3 Special methods)](03_Special_methods)
|
[Contents](../Contents) \| [Previous (4.1 Classes)](01_Class) \| [Next (4.3 Special methods)](03_Special_methods)
|
||||||
|
|||||||
@@ -202,34 +202,38 @@ x = getattr(obj, 'x', None)
|
|||||||
|
|
||||||
## Exercises
|
## Exercises
|
||||||
|
|
||||||
### Exercise 4.8: Better output for printing objects
|
### Exercise 4.9: Better output for printing objects
|
||||||
|
|
||||||
All Python objects have two string representations. The first
|
All Python objects have two string representations. The first
|
||||||
representation is created by string conversion via `str()` (which is
|
representation is created by string conversion via `str()`
|
||||||
called by `print`). The string representation is usually a nicely
|
(which is called by `print`). The string representation is
|
||||||
formatted version of the object meant for humans. The second
|
usually a nicely formatted version of the object meant for humans.
|
||||||
representation is a code representation of the object created by
|
The second representation is a code representation of the object
|
||||||
`repr()` (or by viewing a value in the interactive shell). The code
|
created by `repr()` (or by viewing a value in the
|
||||||
representation typically shows you the code that you have to type to
|
interactive shell). The code representation typically shows you the
|
||||||
get the object.
|
code that you have to type to get the object.
|
||||||
|
|
||||||
The two representations of an object are often different. For example, you can see the difference by trying the following:
|
The two representations of an object are often different. For example,
|
||||||
|
you can see the difference by trying the following:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> s = 'Hello\nWorld'
|
>>> from datetime import date
|
||||||
>>> print(str(s)) # Notice nice output (no quotes)
|
>>> d = date(2017, 4, 9)
|
||||||
Hello
|
>>> print(d) # Nice output
|
||||||
World
|
2017-04-09
|
||||||
>>> print(repr(s)) # Notice the added quotes and escape codes
|
>>> print(repr(d)) # Representation output
|
||||||
'Hello\nWorld'
|
datetime.date(2017, 4, 9)
|
||||||
>>> print(f'{s!r}') # Alternate way to get repr() string
|
>>> print(f'{d!r}') # Alternate way to get repr() string
|
||||||
'Hello\nWorld'
|
datetime.date(2017, 4, 9)
|
||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Both kinds of string conversions can be redefined in a class if it defines the `__str__()` and `__repr__()` methods.
|
Both kinds of string conversions can be redefined in a class if it
|
||||||
|
defines the `__str__()` and `__repr__()` methods.
|
||||||
|
|
||||||
Modify the `Stock` object that you defined in Exercise 4.1 so that the `__repr__()` method produces more useful output.
|
Modify the `Stock` object that you defined in `stock.py`
|
||||||
|
so that the `__repr__()` method produces more useful output. For
|
||||||
|
example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> goog = Stock('GOOG', 100, 490.1)
|
>>> goog = Stock('GOOG', 100, 490.1)
|
||||||
@@ -238,22 +242,21 @@ Stock('GOOG', 100, 490.1)
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
See what happens when you read a portfolio of stocks and view the resulting list after you have made these changes.
|
See what happens when you read a portfolio of stocks and view the
|
||||||
|
resulting list after you have made these changes. For example:
|
||||||
|
|
||||||
```python
|
```
|
||||||
>>> import stock
|
>>> import report
|
||||||
>>> portfolio = stock.read_portfolio('Data/portfolio.csv')
|
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
|
||||||
>>> portfolio
|
>>> portfolio
|
||||||
... see what the output is ...
|
... see what the output is ...
|
||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exercise 4.9: An example of using `getattr()`
|
### Exercise 4.10: An example of using getattr()
|
||||||
|
|
||||||
In Exercise 4.2 you worked with a function `print_portfolio()` that made a table for a stock portfolio.
|
`getattr()` is an alternative mechanism for reading attributes. It can be used to
|
||||||
That function was hard-coded to only work with stock data—-how limiting! You can do so much more if you use functions such as `getattr()`.
|
write extremely flexible code. To begin, try this example:
|
||||||
|
|
||||||
To begin, try this little example:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> import stock
|
>>> import stock
|
||||||
@@ -267,19 +270,19 @@ shares = 100
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Carefully observe that the output data is determined entirely by the attribute names listed in the `columns` variable.
|
Carefully observe that the output data is determined entirely by the attribute
|
||||||
|
names listed in the `columns` variable.
|
||||||
|
|
||||||
In the file `tableformat.py`, take this idea and expand it into a
|
In the file `tableformat.py`, take this idea and expand it into a generalized
|
||||||
generalized function `print_table()` that prints a table showing
|
function `print_table()` that prints a table showing
|
||||||
user-specified attributes of a list of arbitrary objects.
|
user-specified attributes of a list of arbitrary objects. As with the
|
||||||
|
earlier `print_report()` function, `print_table()` should also accept
|
||||||
As with the earlier `print_portfolio()` function, `print_table()`
|
a `TableFormatter` instance to control the output format. Here's how
|
||||||
should also accept a `TableFormatter` instance to control the output
|
it should work:
|
||||||
format. Here’s how it should work:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> import stock
|
>>> import report
|
||||||
>>> portfolio = stock.read_portfolio('Data/portfolio.csv')
|
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
|
||||||
>>> from tableformat import create_formatter, print_table
|
>>> from tableformat import create_formatter, print_table
|
||||||
>>> formatter = create_formatter('txt')
|
>>> formatter = create_formatter('txt')
|
||||||
>>> print_table(portfolio, ['name','shares'], formatter)
|
>>> print_table(portfolio, ['name','shares'], formatter)
|
||||||
@@ -306,28 +309,5 @@ format. Here’s how it should work:
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exercise 4.10: Exercise Bonus: Column Formatting
|
|
||||||
|
|
||||||
Modify the `print_table()` function in part (B) so that it also
|
|
||||||
accepts a list of format specifiers for formatting the contents of
|
|
||||||
each column.
|
|
||||||
|
|
||||||
```python
|
|
||||||
>>> print_table(portfolio,
|
|
||||||
['name','shares','price'],
|
|
||||||
['s','d','0.2f'],
|
|
||||||
formatter)
|
|
||||||
name shares price
|
|
||||||
---------- ---------- ----------
|
|
||||||
AA 100 32.20
|
|
||||||
IBM 50 91.10
|
|
||||||
CAT 150 83.44
|
|
||||||
MSFT 200 51.23
|
|
||||||
GE 95 40.37
|
|
||||||
MSFT 50 65.10
|
|
||||||
IBM 100 70.44
|
|
||||||
>>>
|
|
||||||
```
|
|
||||||
|
|
||||||
[Contents](../Contents) \| [Previous (4.2 Inheritance)](02_Inheritance) \| [Next (4.4 Exceptions)](04_Defining_exceptions)
|
[Contents](../Contents) \| [Previous (4.2 Inheritance)](02_Inheritance) \| [Next (4.4 Exceptions)](04_Defining_exceptions)
|
||||||
|
|
||||||
|
|||||||
@@ -363,16 +363,22 @@ Frameworks / libraries sometimes use it for advanced features involving composit
|
|||||||
## Exercises
|
## Exercises
|
||||||
|
|
||||||
In Section 4, you defined a class `Stock` that represented a holding of stock.
|
In Section 4, you defined a class `Stock` that represented a holding of stock.
|
||||||
In this exercise, we will use that class.
|
In this exercise, we will use that class. Restart the interpreter and make a
|
||||||
|
few instances:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> ================================ RESTART ================================
|
||||||
|
>>> from stock import Stock
|
||||||
|
>>> goog = Stock('GOOG',100,490.10)
|
||||||
|
>>> ibm = Stock('IBM',50, 91.23)
|
||||||
|
>>>
|
||||||
|
```
|
||||||
|
|
||||||
### Exercise 5.1: Representation of Instances
|
### Exercise 5.1: Representation of Instances
|
||||||
|
|
||||||
At the interactive shell, inspect the underlying dictionaries of the two instances you created:
|
At the interactive shell, inspect the underlying dictionaries of the two instances you created:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> from stock import Stock
|
|
||||||
>>> goog = Stock('GOOG',100,490.10)
|
|
||||||
>>> ibm = Stock('IBM',50, 91.23)
|
|
||||||
>>> goog.__dict__
|
>>> goog.__dict__
|
||||||
... look at the output ...
|
... look at the output ...
|
||||||
>>> ibm.__dict__
|
>>> ibm.__dict__
|
||||||
@@ -393,12 +399,14 @@ Try setting a new attribute on one of the above instances:
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
In the above output, you’ll notice that the `goog` instance has a attribute `date` whereas the `ibm` instance does not.
|
In the above output, you'll notice that the `goog` instance has a
|
||||||
|
attribute `date` whereas the `ibm` instance does not. It is important
|
||||||
|
to note that Python really doesn't place any restrictions on
|
||||||
|
attributes. For example, the attributes of an instance are not
|
||||||
|
limited to those set up in the `__init__()` method.
|
||||||
|
|
||||||
It is important to note that Python really doesn’t place any restrictions on attributes.
|
Instead of setting an attribute, try placing a new value directly into
|
||||||
The attributes of an instance are not limited to those set up in the `__init__()` method.
|
the `__dict__` object:
|
||||||
|
|
||||||
Instead of setting an attribute, try placing a new value directly into the `__dict__` object:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> goog.__dict__['time'] = '9:45am'
|
>>> goog.__dict__['time'] = '9:45am'
|
||||||
@@ -408,14 +416,15 @@ Instead of setting an attribute, try placing a new value directly into the `__di
|
|||||||
```
|
```
|
||||||
|
|
||||||
Here, you really notice the fact that an instance is just a layer on
|
Here, you really notice the fact that an instance is just a layer on
|
||||||
top of a dictionary. *Note: it should be emphasized that direct
|
top of a dictionary. Note: it should be emphasized that direct
|
||||||
manipulation of the dictionary is uncommon—you should always write
|
manipulation of the dictionary is uncommon--you should always write
|
||||||
your code to use the (.) syntax.*
|
your code to use the (.) syntax.
|
||||||
|
|
||||||
### Exercise 5.3: The role of classes
|
### Exercise 5.3: The role of classes
|
||||||
|
|
||||||
The definitions that make up a class definition are shared by all instances of that class.
|
The definitions that make up a class definition are shared by all
|
||||||
Notice, that all instances have a link back to their associated class:
|
instances of that class. Notice, that all instances have a link back
|
||||||
|
to their associated class:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> goog.__class__
|
>>> goog.__class__
|
||||||
@@ -435,7 +444,7 @@ Try calling a method on the instances:
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice that the name *cost* is not defined in either `goog.__dict__`
|
Notice that the name 'cost' is not defined in either `goog.__dict__`
|
||||||
or `ibm.__dict__`. Instead, it is being supplied by the class
|
or `ibm.__dict__`. Instead, it is being supplied by the class
|
||||||
dictionary. Try this:
|
dictionary. Try this:
|
||||||
|
|
||||||
@@ -455,7 +464,9 @@ Try calling the `cost()` method directly through the dictionary:
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how you are calling the function defined in the class definition and how the `self` argument gets the instance.
|
Notice how you are calling the function defined in the class
|
||||||
|
definition and how the `self` argument gets the instance.
|
||||||
|
|
||||||
Try adding a new attribute to the `Stock` class:
|
Try adding a new attribute to the `Stock` class:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -482,10 +493,10 @@ However, notice that it is not part of the instance dictionary:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The reason you can access the `foo` attribute on instances is that
|
The reason you can access the `foo` attribute on instances is that
|
||||||
Python always checks the class dictionary if it can’t find something
|
Python always checks the class dictionary if it can't find something
|
||||||
on the instance itself.
|
on the instance itself.
|
||||||
|
|
||||||
This part of the exercise illustrates something known as a class
|
Note: This part of the exercise illustrates something known as a class
|
||||||
variable. Suppose, for instance, you have a class like this:
|
variable. Suppose, for instance, you have a class like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -495,8 +506,9 @@ class Foo(object):
|
|||||||
self.b = b # Instance variable
|
self.b = b # Instance variable
|
||||||
```
|
```
|
||||||
|
|
||||||
In this class, the variable `a`, assigned in the body of the class itself, is a *class variable*.
|
In this class, the variable `a`, assigned in the body of the
|
||||||
It is shared by all of the instances that get created.
|
class itself, is a "class variable." It is shared by all of the
|
||||||
|
instances that get created. For example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> f = Foo(10)
|
>>> f = Foo(10)
|
||||||
@@ -517,10 +529,10 @@ It is shared by all of the instances that get created.
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exercise 5.4: Bound Methods
|
### Exercise 5.4: Bound methods
|
||||||
|
|
||||||
A subtle feature of Python is that invoking a method actually involves
|
A subtle feature of Python is that invoking a method actually involves
|
||||||
two steps and something known as a bound method.
|
two steps and something known as a bound method. For example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> s = goog.sell
|
>>> s = goog.sell
|
||||||
@@ -532,8 +544,9 @@ two steps and something known as a bound method.
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Bound methods actually contain all of the pieces needed to call a method.
|
Bound methods actually contain all of the pieces needed to call a
|
||||||
For instance, they keep a record of the function implementing the method:
|
method. For instance, they keep a record of the function implementing
|
||||||
|
the method:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> s.__func__
|
>>> s.__func__
|
||||||
@@ -549,8 +562,8 @@ This is the same value as found in the `Stock` dictionary.
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Take a close look at both references do `0x10049af50`. They are both the same in `s` and `Stock.__dict__['sell']`.
|
Bound methods also record the instance, which is the `self`
|
||||||
Bound methods also record the instance, which is the `self` argument.
|
argument.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> s.__self__
|
>>> s.__self__
|
||||||
@@ -558,8 +571,8 @@ Stock('GOOG',75,490.1)
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
When you invoke the function using `()` all of the pieces come together.
|
When you invoke the function using `()` all of the pieces come
|
||||||
For example, calling `s(25)` actually does this:
|
together. For example, calling `s(25)` actually does this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> s.__func__(s.__self__, 25) # Same as s(25)
|
>>> s.__func__(s.__self__, 25) # Same as s(25)
|
||||||
@@ -572,7 +585,7 @@ For example, calling `s(25)` actually does this:
|
|||||||
|
|
||||||
Make a new class that inherits from `Stock`.
|
Make a new class that inherits from `Stock`.
|
||||||
|
|
||||||
```python
|
```
|
||||||
>>> class NewStock(Stock):
|
>>> class NewStock(Stock):
|
||||||
def yow(self):
|
def yow(self):
|
||||||
print('Yow!')
|
print('Yow!')
|
||||||
@@ -603,7 +616,7 @@ they will be searched for attributes.
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Here’s how the `cost()` method of instance `n` above would be found:
|
Here's how the `cost()` method of instance `n` above would be found:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> for cls in n.__class__.__mro__:
|
>>> for cls in n.__class__.__mro__:
|
||||||
|
|||||||
@@ -252,10 +252,10 @@ day-to-day coding.
|
|||||||
|
|
||||||
## Exercises
|
## Exercises
|
||||||
|
|
||||||
### Exercise 5.6: Simple properties
|
### Exercise 5.6: Simple Properties
|
||||||
|
|
||||||
Properties are a useful way to add "computed attributes" to an object.
|
Properties are a useful way to add "computed attributes" to an object.
|
||||||
In Exercise 4.1, you created an object `Stock`. Notice that on your
|
In `stock.py`, you created an object `Stock`. Notice that on your
|
||||||
object there is a slight inconsistency in how different kinds of data
|
object there is a slight inconsistency in how different kinds of data
|
||||||
are extracted:
|
are extracted:
|
||||||
|
|
||||||
@@ -271,17 +271,22 @@ are extracted:
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Specifically, notice how you have to add the extra `()` to `cost` because it is a method.
|
Specifically, notice how you have to add the extra () to `cost` because it is a method.
|
||||||
You can get rid of the extra `()` on `cost()` if you turn it into a property.
|
|
||||||
|
You can get rid of the extra () on `cost()` if you turn it into a property.
|
||||||
Take your `Stock` class and modify it so that the cost calculation works like this:
|
Take your `Stock` class and modify it so that the cost calculation works like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
>>> ================================ RESTART ================================
|
||||||
|
>>> from stock import Stock
|
||||||
|
>>> s = Stock('GOOG', 100, 490.1)
|
||||||
>>> s.cost
|
>>> s.cost
|
||||||
49010.0
|
49010.0
|
||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
Try calling `s.cost()` as a function and observe that it doesn’t work now that `cost` has been defined as a property.
|
Try calling `s.cost()` as a function and observe that it
|
||||||
|
doesn't work now that `cost` has been defined as a property.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> s.cost()
|
>>> s.cost()
|
||||||
@@ -289,14 +294,19 @@ Try calling `s.cost()` as a function and observe that it doesn’t work now that
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Making this change will likely break your earlier `pcost.py` program.
|
||||||
|
You might need to go back and get rid of the `()` on the `cost()` method.
|
||||||
|
|
||||||
### Exercise 5.7: Properties and Setters
|
### Exercise 5.7: Properties and Setters
|
||||||
|
|
||||||
Modify the `shares` attribute so that the value is stored in a private
|
Modify the `shares` attribute so that the value is stored in a
|
||||||
attribute and that a pair of property functions are used to ensure
|
private attribute and that a pair of property functions are used to ensure
|
||||||
that it is always set to an integer value.
|
that it is always set to an integer value. Here is an example of the expected
|
||||||
Here is an example of the expected behavior:
|
behavior:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
>>> ================================ RESTART ================================
|
||||||
|
>>> from stock import Stock
|
||||||
>>> s = Stock('GOOG',100,490.10)
|
>>> s = Stock('GOOG',100,490.10)
|
||||||
>>> s.shares = 50
|
>>> s.shares = 50
|
||||||
>>> s.shares = 'a lot'
|
>>> s.shares = 'a lot'
|
||||||
@@ -308,10 +318,11 @@ TypeError: expected an integer
|
|||||||
|
|
||||||
### Exercise 5.8: Adding slots
|
### Exercise 5.8: Adding slots
|
||||||
|
|
||||||
Modify the `Stock` class so that it has a `__slots__` attribute.
|
Modify the `Stock` class so that it has a `__slots__` attribute. Then,
|
||||||
Then, verify that new attributes can’t be added:
|
verify that new attributes can't be added:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
>>> ================================ RESTART ================================
|
||||||
>>> from stock import Stock
|
>>> from stock import Stock
|
||||||
>>> s = Stock('GOOG', 100, 490.10)
|
>>> s = Stock('GOOG', 100, 490.10)
|
||||||
>>> s.name
|
>>> s.name
|
||||||
@@ -321,8 +332,9 @@ Then, verify that new attributes can’t be added:
|
|||||||
>>>
|
>>>
|
||||||
```
|
```
|
||||||
|
|
||||||
When you use `__slots__`, Python actually uses a more efficient internal representation of objects.
|
When you use `__slots__`, Python actually uses a more efficient
|
||||||
What happens if you try to inspect the underlying dictionary of `s` above?
|
internal representation of objects. What happens if you try to
|
||||||
|
inspect the underlying dictionary of `s` above?
|
||||||
|
|
||||||
```python
|
```python
|
||||||
>>> s.__dict__
|
>>> s.__dict__
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ if __name__ == '__main__':
|
|||||||
change = float(fields[4])
|
change = float(fields[4])
|
||||||
if name in portfolio:
|
if name in portfolio:
|
||||||
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
|
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
|
||||||
----
|
```
|
||||||
|
|
||||||
Note: For this to work, your `Portfolio` class must support the
|
Note: For this to work, your `Portfolio` class must support the
|
||||||
`in` operator. See the last exercise and make sure you implement the
|
`in` operator. See the last exercise and make sure you implement the
|
||||||
|
|||||||
@@ -158,4 +158,4 @@ Note: `lambda` is a useful shortcut because it allows you to
|
|||||||
define a special processing function directly in the call to `sort()` as
|
define a special processing function directly in the call to `sort()` as
|
||||||
opposed to having to define a separate function first (as in part a).
|
opposed to having to define a separate function first (as in part a).
|
||||||
|
|
||||||
[Contents](../Contents) \| [Previous (7.1 Variable Arguments)](01_Variable_arguments) \| [Next (7.3 Returning Functions)](03_Returning_function)
|
[Contents](../Contents) \| [Previous (7.1 Variable Arguments)](01_Variable_arguments) \| [Next (7.3 Returning Functions)](03_Returning_functions)
|
||||||
|
|||||||
28
Solutions/1_10/mortgage.py
Normal file
28
Solutions/1_10/mortgage.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# mortgage.py
|
||||||
|
|
||||||
|
principal = 500000.0
|
||||||
|
rate = 0.05
|
||||||
|
payment = 2684.11
|
||||||
|
total_paid = 0.0
|
||||||
|
month = 0
|
||||||
|
|
||||||
|
extra_payment = 1000.0
|
||||||
|
extra_payment_start_month = 60
|
||||||
|
extra_payment_end_month = 108
|
||||||
|
|
||||||
|
while principal > 0:
|
||||||
|
month = month + 1
|
||||||
|
principal = principal * (1+rate/12) - payment
|
||||||
|
total_paid = total_paid + payment
|
||||||
|
|
||||||
|
if month >= extra_payment_start_month and month <= extra_payment_end_month:
|
||||||
|
principal = principal - extra_payment
|
||||||
|
total_paid = total_paid + extra_payment
|
||||||
|
|
||||||
|
print(month, round(total_paid,2), round(principal, 2))
|
||||||
|
|
||||||
|
print('Total paid', round(total_paid, 2))
|
||||||
|
print('Months', month)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
13
Solutions/1_27/pcost.py
Normal file
13
Solutions/1_27/pcost.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
total_cost = 0.0
|
||||||
|
|
||||||
|
with open('../../Work/Data/portfolio.csv', 'rt') as f:
|
||||||
|
headers = next(f)
|
||||||
|
for line in f:
|
||||||
|
row = line.split(',')
|
||||||
|
nshares = int(row[1])
|
||||||
|
price = float(row[2])
|
||||||
|
total_cost += nshares * price
|
||||||
|
|
||||||
|
print('Total cost', total_cost)
|
||||||
31
Solutions/1_32/pcost.py
Normal file
31
Solutions/1_32/pcost.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
total_cost = 0.0
|
||||||
|
|
||||||
|
with open(filename, 'rt') as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
headers = next(rows)
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
nshares = int(row[1])
|
||||||
|
price = float(row[2])
|
||||||
|
total_cost += nshares * price
|
||||||
|
# This catches errors in int() and float() conversions above
|
||||||
|
except ValueError:
|
||||||
|
print('Bad row:', row)
|
||||||
|
|
||||||
|
return total_cost
|
||||||
|
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
filename = sys.argv[1]
|
||||||
|
else:
|
||||||
|
filename = input('Enter a filename:')
|
||||||
|
|
||||||
|
cost = portfolio_cost(filename)
|
||||||
|
print('Total cost:', cost)
|
||||||
8
Solutions/1_5/bounce.py
Normal file
8
Solutions/1_5/bounce.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# bounce.py
|
||||||
|
|
||||||
|
height = 100
|
||||||
|
bounce = 1
|
||||||
|
while bounce <= 10:
|
||||||
|
height = height * (3/5)
|
||||||
|
print(bounce, round(height, 4))
|
||||||
|
bounce += 1
|
||||||
66
Solutions/2_11/report.py
Normal file
66
Solutions/2_11/report.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# report.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
portfolio = []
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
headers = next(rows)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
stock = {
|
||||||
|
'name' : row[0],
|
||||||
|
'shares' : int(row[1]),
|
||||||
|
'price' : float(row[2])
|
||||||
|
}
|
||||||
|
portfolio.append(stock)
|
||||||
|
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
prices = {}
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
prices[row[0]] = float(row[1])
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return prices
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for stock in portfolio:
|
||||||
|
current_price = prices[stock['name']]
|
||||||
|
change = current_price - stock['price']
|
||||||
|
summary = (stock['name'], stock['shares'], current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
# Read data files and create the report data
|
||||||
|
|
||||||
|
portfolio = read_portfolio('../../Work/Data/portfolio.csv')
|
||||||
|
prices = read_prices('../../Work/Data/prices.csv')
|
||||||
|
|
||||||
|
# Generate the report data
|
||||||
|
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Output the report
|
||||||
|
headers = ('Name', 'Shares', 'Price', 'Change')
|
||||||
|
print('%10s %10s %10s %10s' % headers)
|
||||||
|
print(('-' * 10 + ' ') * len(headers))
|
||||||
|
for row in report:
|
||||||
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
32
Solutions/2_16/pcost.py
Normal file
32
Solutions/2_16/pcost.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
total_cost = 0.0
|
||||||
|
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
headers = next(rows)
|
||||||
|
for rowno, row in enumerate(rows, start=1):
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
try:
|
||||||
|
nshares = int(record['shares'])
|
||||||
|
price = float(record['price'])
|
||||||
|
total_cost += nshares * price
|
||||||
|
# This catches errors in int() and float() conversions above
|
||||||
|
except ValueError:
|
||||||
|
print(f'Row {rowno}: Bad row: {row}')
|
||||||
|
|
||||||
|
return total_cost
|
||||||
|
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
filename = sys.argv[1]
|
||||||
|
else:
|
||||||
|
filename = input('Enter a filename:')
|
||||||
|
|
||||||
|
cost = portfolio_cost(filename)
|
||||||
|
print('Total cost:', cost)
|
||||||
67
Solutions/2_16/report.py
Normal file
67
Solutions/2_16/report.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# report.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
portfolio = []
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
headers = next(rows)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
stock = {
|
||||||
|
'name' : record['name'],
|
||||||
|
'shares' : int(record['shares']),
|
||||||
|
'price' : float(record['price'])
|
||||||
|
}
|
||||||
|
portfolio.append(stock)
|
||||||
|
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
prices = {}
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
prices[row[0]] = float(row[1])
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return prices
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for stock in portfolio:
|
||||||
|
current_price = prices[stock['name']]
|
||||||
|
change = current_price - stock['price']
|
||||||
|
summary = (stock['name'], stock['shares'], current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
# Read data files and create the report data
|
||||||
|
|
||||||
|
portfolio = read_portfolio('../../Work/Data/portfolio.csv')
|
||||||
|
prices = read_prices('../../Work/Data/prices.csv')
|
||||||
|
|
||||||
|
# Generate the report data
|
||||||
|
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Output the report
|
||||||
|
headers = ('Name', 'Shares', 'Price', 'Change')
|
||||||
|
print('%10s %10s %10s %10s' % headers)
|
||||||
|
print(('-' * 10 + ' ') * len(headers))
|
||||||
|
for row in report:
|
||||||
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
55
Solutions/2_7/report.py
Normal file
55
Solutions/2_7/report.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# report.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
portfolio = []
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
headers = next(rows)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
stock = {
|
||||||
|
'name' : row[0],
|
||||||
|
'shares' : int(row[1]),
|
||||||
|
'price' : float(row[2])
|
||||||
|
}
|
||||||
|
portfolio.append(stock)
|
||||||
|
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
prices = {}
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
prices[row[0]] = float(row[1])
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return prices
|
||||||
|
|
||||||
|
portfolio = read_portfolio('../../Work/Data/portfolio.csv')
|
||||||
|
prices = read_prices('../../Work/Data/prices.csv')
|
||||||
|
|
||||||
|
# Calculate the total cost of the portfolio
|
||||||
|
total_cost = 0.0
|
||||||
|
for s in portfolio:
|
||||||
|
total_cost += s['shares']*s['price']
|
||||||
|
|
||||||
|
print('Total cost', total_cost)
|
||||||
|
|
||||||
|
# Compute the current value of the portfolio
|
||||||
|
total_value = 0.0
|
||||||
|
for s in portfolio:
|
||||||
|
total_value += s['shares']*prices[s['name']]
|
||||||
|
|
||||||
|
print('Current value', total_value)
|
||||||
|
print('Gain', total_value - total_cost)
|
||||||
48
Solutions/3_10/fileparse.py
Normal file
48
Solutions/3_10/fileparse.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
48
Solutions/3_14/fileparse.py
Normal file
48
Solutions/3_14/fileparse.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
19
Solutions/3_14/pcost.py
Normal file
19
Solutions/3_14/pcost.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return sum([s['shares']*s['price'] for s in portfolio])
|
||||||
|
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
filename = sys.argv[1]
|
||||||
|
else:
|
||||||
|
filename = input('Enter a filename:')
|
||||||
|
|
||||||
|
cost = portfolio_cost(filename)
|
||||||
|
print('Total cost:', cost)
|
||||||
56
Solutions/3_14/report.py
Normal file
56
Solutions/3_14/report.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float])
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
return dict(fileparse.parse_csv(filename,types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio,prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for stock in portfolio:
|
||||||
|
current_price = prices[stock['name']]
|
||||||
|
change = current_price - stock['price']
|
||||||
|
summary = (stock['name'], stock['shares'], current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
headers = ('Name','Shares','Price','Change')
|
||||||
|
print('%10s %10s %10s %10s' % headers)
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
for row in reportdata:
|
||||||
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile,pricefile):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
print_report(report)
|
||||||
|
|
||||||
|
portfolio_report('../../Work/Data/portfolio.csv',
|
||||||
|
'../../Work/Data/prices.csv')
|
||||||
48
Solutions/3_16/fileparse.py
Normal file
48
Solutions/3_16/fileparse.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
20
Solutions/3_16/pcost.py
Normal file
20
Solutions/3_16/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return sum([s['shares'] * s['price'] for s in portfolio])
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
62
Solutions/3_16/report.py
Normal file
62
Solutions/3_16/report.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float])
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
return dict(fileparse.parse_csv(filename,types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio,prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for stock in portfolio:
|
||||||
|
current_price = prices[stock['name']]
|
||||||
|
change = current_price - stock['price']
|
||||||
|
summary = (stock['name'], stock['shares'], current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
headers = ('Name','Shares','Price','Change')
|
||||||
|
print('%10s %10s %10s %10s' % headers)
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
for row in reportdata:
|
||||||
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
print_report(report)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 3:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile' % args[0])
|
||||||
|
portfolio_report(args[1], args[2])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
47
Solutions/3_18/fileparse.py
Normal file
47
Solutions/3_18/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
20
Solutions/3_18/pcost.py
Normal file
20
Solutions/3_18/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return sum([s['shares'] * s['price'] for s in portfolio])
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
64
Solutions/3_18/report.py
Normal file
64
Solutions/3_18/report.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float])
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio,prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for stock in portfolio:
|
||||||
|
current_price = prices[stock['name']]
|
||||||
|
change = current_price - stock['price']
|
||||||
|
summary = (stock['name'], stock['shares'], current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
headers = ('Name','Shares','Price','Change')
|
||||||
|
print('%10s %10s %10s %10s' % headers)
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
for row in reportdata:
|
||||||
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
print_report(report)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 3:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile' % args[0])
|
||||||
|
portfolio_report(args[1], args[2])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
78
Solutions/3_2/report.py
Normal file
78
Solutions/3_2/report.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# report.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
portfolio = []
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
headers = next(rows)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
stock = {
|
||||||
|
'name' : record['name'],
|
||||||
|
'shares' : int(record['shares']),
|
||||||
|
'price' : float(record['price'])
|
||||||
|
}
|
||||||
|
portfolio.append(stock)
|
||||||
|
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
prices = {}
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f)
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
prices[row[0]] = float(row[1])
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return prices
|
||||||
|
|
||||||
|
def make_report_data(portfolio,prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for stock in portfolio:
|
||||||
|
current_price = prices[stock['name']]
|
||||||
|
change = current_price - stock['price']
|
||||||
|
summary = (stock['name'], stock['shares'], current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
headers = ('Name','Shares','Price','Change')
|
||||||
|
print('%10s %10s %10s %10s' % headers)
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
for row in reportdata:
|
||||||
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile,pricefile):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio,prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
print_report(report)
|
||||||
|
|
||||||
|
portfolio_report('../../Work/Data/portfolio.csv',
|
||||||
|
'../../Work/Data/prices.csv')
|
||||||
39
Solutions/3_7/fileparse.py
Normal file
39
Solutions/3_7/fileparse.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=','):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
with open(filename) as f:
|
||||||
|
rows = csv.reader(f, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for row in rows:
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
47
Solutions/4_10/fileparse.py
Normal file
47
Solutions/4_10/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
20
Solutions/4_10/pcost.py
Normal file
20
Solutions/4_10/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return sum([s.cost() for s in portfolio])
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
71
Solutions/4_10/report.py
Normal file
71
Solutions/4_10/report.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float])
|
||||||
|
|
||||||
|
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
26
Solutions/4_10/stock.py
Normal file
26
Solutions/4_10/stock.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
def __init__(self, name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
|
|
||||||
81
Solutions/4_10/tableformat.py
Normal file
81
Solutions/4_10/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
47
Solutions/4_4/fileparse.py
Normal file
47
Solutions/4_4/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
20
Solutions/4_4/pcost.py
Normal file
20
Solutions/4_4/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return sum([s.cost() for s in portfolio])
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
70
Solutions/4_4/report.py
Normal file
70
Solutions/4_4/report.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float])
|
||||||
|
|
||||||
|
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
headers = ('Name','Shares','Price','Change')
|
||||||
|
print('%10s %10s %10s %10s' % headers)
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
for row in reportdata:
|
||||||
|
print('%10s %10d %10.2f %10.2f' % row)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
print_report(report)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 3:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile' % args[0])
|
||||||
|
portfolio_report(args[1], args[2])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
23
Solutions/4_4/stock.py
Normal file
23
Solutions/4_4/stock.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
def __init__(self, name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
|
|
||||||
47
Solutions/5_8/fileparse.py
Normal file
47
Solutions/5_8/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
20
Solutions/5_8/pcost.py
Normal file
20
Solutions/5_8/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return sum([s.cost() for s in portfolio])
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
71
Solutions/5_8/report.py
Normal file
71
Solutions/5_8/report.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float])
|
||||||
|
|
||||||
|
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
37
Solutions/5_8/stock.py
Normal file
37
Solutions/5_8/stock.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
__slots__ = ('name','_shares','price')
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shares(self):
|
||||||
|
return self._shares
|
||||||
|
|
||||||
|
@shares.setter
|
||||||
|
def shares(self, value):
|
||||||
|
if not isinstance(value,int):
|
||||||
|
raise TypeError("Must be integer")
|
||||||
|
self._shares = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/5_8/tableformat.py
Normal file
81
Solutions/5_8/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
47
Solutions/6_12/fileparse.py
Normal file
47
Solutions/6_12/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
30
Solutions/6_12/follow.py
Normal file
30
Solutions/6_12/follow.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# follow.py
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
def follow(filename):
|
||||||
|
'''
|
||||||
|
Generator that produces a sequence of lines being written at the end of a file.
|
||||||
|
'''
|
||||||
|
f = open(filename, 'r')
|
||||||
|
f.seek(0,os.SEEK_END)
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line == '':
|
||||||
|
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||||
|
continue
|
||||||
|
yield line
|
||||||
|
|
||||||
|
# Example use
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import report
|
||||||
|
|
||||||
|
portfolio = report.read_portfolio('../../Data/portfolio.csv')
|
||||||
|
|
||||||
|
for line in follow('../../Data/stocklog.csv'):
|
||||||
|
row = line.split(',')
|
||||||
|
name = row[0].strip('"')
|
||||||
|
price = float(row[1])
|
||||||
|
change = float(row[4])
|
||||||
|
if name in portfolio:
|
||||||
|
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
|
||||||
20
Solutions/6_12/pcost.py
Normal file
20
Solutions/6_12/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return portfolio.total_cost
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
31
Solutions/6_12/portfolio.py
Normal file
31
Solutions/6_12/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# portfolio.py
|
||||||
|
|
||||||
|
class Portfolio(object):
|
||||||
|
def __init__(self, holdings):
|
||||||
|
self._holdings = holdings
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._holdings.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._holdings)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._holdings[index]
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return any([s.name == name for s in self._holdings])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return sum([s.shares * s.price for s in self._holdings])
|
||||||
|
|
||||||
|
def tabulate_shares(self):
|
||||||
|
from collections import Counter
|
||||||
|
total_shares = Counter()
|
||||||
|
for s in self._holdings:
|
||||||
|
total_shares[s.name] += s.shares
|
||||||
|
return total_shares
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
72
Solutions/6_12/report.py
Normal file
72
Solutions/6_12/report.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
from portfolio import Portfolio
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float])
|
||||||
|
|
||||||
|
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||||
|
return Portfolio(portfolio)
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
37
Solutions/6_12/stock.py
Normal file
37
Solutions/6_12/stock.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
__slots__ = ('name','_shares','price')
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shares(self):
|
||||||
|
return self._shares
|
||||||
|
|
||||||
|
@shares.setter
|
||||||
|
def shares(self, value):
|
||||||
|
if not isinstance(value,int):
|
||||||
|
raise TypeError("Must be integer")
|
||||||
|
self._shares = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/6_12/tableformat.py
Normal file
81
Solutions/6_12/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
50
Solutions/6_12/ticker.py
Normal file
50
Solutions/6_12/ticker.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# ticker.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import report
|
||||||
|
import tableformat
|
||||||
|
from follow import follow
|
||||||
|
import time
|
||||||
|
|
||||||
|
def select_columns(rows, indices):
|
||||||
|
for row in rows:
|
||||||
|
yield [row[index] for index in indices]
|
||||||
|
|
||||||
|
def convert_types(rows, types):
|
||||||
|
for row in rows:
|
||||||
|
yield [func(val) for func, val in zip(types, row)]
|
||||||
|
|
||||||
|
def make_dicts(rows, headers):
|
||||||
|
for row in rows:
|
||||||
|
yield dict(zip(headers, row))
|
||||||
|
|
||||||
|
def parse_stock_data(lines):
|
||||||
|
rows = csv.reader(lines)
|
||||||
|
rows = select_columns(rows, [0, 1, 4])
|
||||||
|
rows = convert_types(rows, [str,float,float])
|
||||||
|
rows = make_dicts(rows, ['name','price','change'])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def filter_symbols(rows, names):
|
||||||
|
for row in rows:
|
||||||
|
if row['name'] in names:
|
||||||
|
yield row
|
||||||
|
|
||||||
|
def ticker(portfile, logfile, fmt):
|
||||||
|
portfolio = report.read_portfolio(portfile)
|
||||||
|
lines = follow(logfile)
|
||||||
|
rows = parse_stock_data(lines)
|
||||||
|
rows = filter_symbols(rows, portfolio)
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
formatter.headings(['Name','Price','Change'])
|
||||||
|
for row in rows:
|
||||||
|
formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] )
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0])
|
||||||
|
ticker(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
47
Solutions/6_15/fileparse.py
Normal file
47
Solutions/6_15/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
17
Solutions/6_15/follow.py
Normal file
17
Solutions/6_15/follow.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# follow.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
def follow(filename):
|
||||||
|
'''
|
||||||
|
Generator that produces a sequence of lines being written at the end of a file.
|
||||||
|
'''
|
||||||
|
with open(filename,'r') as f:
|
||||||
|
f.seek(0,os.SEEK_END)
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line != '':
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||||
20
Solutions/6_15/pcost.py
Normal file
20
Solutions/6_15/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return portfolio.total_cost
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
31
Solutions/6_15/portfolio.py
Normal file
31
Solutions/6_15/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# portfolio.py
|
||||||
|
|
||||||
|
class Portfolio(object):
|
||||||
|
def __init__(self, holdings):
|
||||||
|
self._holdings = holdings
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._holdings.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._holdings)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._holdings[index]
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return any(s.name == name for s in self._holdings)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return sum(s.shares * s.price for s in self._holdings)
|
||||||
|
|
||||||
|
def tabulate_shares(self):
|
||||||
|
from collections import Counter
|
||||||
|
total_shares = Counter()
|
||||||
|
for s in self._holdings:
|
||||||
|
total_shares[s.name] += s.shares
|
||||||
|
return total_shares
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
72
Solutions/6_15/report.py
Normal file
72
Solutions/6_15/report.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
from portfolio import Portfolio
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float])
|
||||||
|
|
||||||
|
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||||
|
return Portfolio(portfolio)
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
37
Solutions/6_15/stock.py
Normal file
37
Solutions/6_15/stock.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
__slots__ = ('name','_shares','price')
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shares(self):
|
||||||
|
return self._shares
|
||||||
|
|
||||||
|
@shares.setter
|
||||||
|
def shares(self, value):
|
||||||
|
if not isinstance(value,int):
|
||||||
|
raise TypeError("Must be integer")
|
||||||
|
self._shares = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/6_15/tableformat.py
Normal file
81
Solutions/6_15/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
44
Solutions/6_15/ticker.py
Normal file
44
Solutions/6_15/ticker.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# ticker.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import report
|
||||||
|
import tableformat
|
||||||
|
from follow import follow
|
||||||
|
import time
|
||||||
|
|
||||||
|
def select_columns(rows, indices):
|
||||||
|
for row in rows:
|
||||||
|
yield [row[index] for index in indices]
|
||||||
|
|
||||||
|
def convert_types(rows, types):
|
||||||
|
for row in rows:
|
||||||
|
yield [func(val) for func, val in zip(types, row)]
|
||||||
|
|
||||||
|
def make_dicts(rows, headers):
|
||||||
|
return (dict(zip(headers,row)) for row in rows)
|
||||||
|
|
||||||
|
def parse_stock_data(lines):
|
||||||
|
rows = csv.reader(lines)
|
||||||
|
rows = select_columns(rows, [0, 1, 4])
|
||||||
|
rows = convert_types(rows, [str,float,float])
|
||||||
|
rows = make_dicts(rows, ['name','price','change'])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def ticker(portfile, logfile, fmt):
|
||||||
|
portfolio = report.read_portfolio(portfile)
|
||||||
|
lines = follow(logfile)
|
||||||
|
rows = parse_stock_data(lines)
|
||||||
|
rows = (row for row in rows if row['name'] in portfolio)
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
formatter.headings(['Name','Price','Change'])
|
||||||
|
for row in rows:
|
||||||
|
formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] )
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0])
|
||||||
|
ticker(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
47
Solutions/6_3/fileparse.py
Normal file
47
Solutions/6_3/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
20
Solutions/6_3/pcost.py
Normal file
20
Solutions/6_3/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return portfolio.total_cost
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
31
Solutions/6_3/portfolio.py
Normal file
31
Solutions/6_3/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# portfolio.py
|
||||||
|
|
||||||
|
class Portfolio(object):
|
||||||
|
def __init__(self, holdings):
|
||||||
|
self._holdings = holdings
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._holdings.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._holdings)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._holdings[index]
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return any([s.name == name for s in self._holdings])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return sum([s.shares * s.price for s in self._holdings])
|
||||||
|
|
||||||
|
def tabulate_shares(self):
|
||||||
|
from collections import Counter
|
||||||
|
total_shares = Counter()
|
||||||
|
for s in self._holdings:
|
||||||
|
total_shares[s.name] += s.shares
|
||||||
|
return total_shares
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
72
Solutions/6_3/report.py
Normal file
72
Solutions/6_3/report.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
from portfolio import Portfolio
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float])
|
||||||
|
|
||||||
|
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||||
|
return Portfolio(portfolio)
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
37
Solutions/6_3/stock.py
Normal file
37
Solutions/6_3/stock.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
__slots__ = ('name','_shares','price')
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shares(self):
|
||||||
|
return self._shares
|
||||||
|
|
||||||
|
@shares.setter
|
||||||
|
def shares(self, value):
|
||||||
|
if not isinstance(value,int):
|
||||||
|
raise TypeError("Must be integer")
|
||||||
|
self._shares = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/6_3/tableformat.py
Normal file
81
Solutions/6_3/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
47
Solutions/6_7/fileparse.py
Normal file
47
Solutions/6_7/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
30
Solutions/6_7/follow.py
Normal file
30
Solutions/6_7/follow.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# follow.py
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
def follow(filename):
|
||||||
|
'''
|
||||||
|
Generator that produces a sequence of lines being written at the end of a file.
|
||||||
|
'''
|
||||||
|
f = open(filename, 'r')
|
||||||
|
f.seek(0,os.SEEK_END)
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line == '':
|
||||||
|
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||||
|
continue
|
||||||
|
yield line
|
||||||
|
|
||||||
|
# Example use
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import report
|
||||||
|
|
||||||
|
portfolio = report.read_portfolio('../../Data/portfolio.csv')
|
||||||
|
|
||||||
|
for line in follow('../../Data/stocklog.csv'):
|
||||||
|
row = line.split(',')
|
||||||
|
name = row[0].strip('"')
|
||||||
|
price = float(row[1])
|
||||||
|
change = float(row[4])
|
||||||
|
if name in portfolio:
|
||||||
|
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
|
||||||
20
Solutions/6_7/pcost.py
Normal file
20
Solutions/6_7/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return portfolio.total_cost
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
31
Solutions/6_7/portfolio.py
Normal file
31
Solutions/6_7/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# portfolio.py
|
||||||
|
|
||||||
|
class Portfolio(object):
|
||||||
|
def __init__(self, holdings):
|
||||||
|
self._holdings = holdings
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._holdings.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._holdings)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._holdings[index]
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return any([s.name == name for s in self._holdings])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return sum([s.shares * s.price for s in self._holdings])
|
||||||
|
|
||||||
|
def tabulate_shares(self):
|
||||||
|
from collections import Counter
|
||||||
|
total_shares = Counter()
|
||||||
|
for s in self._holdings:
|
||||||
|
total_shares[s.name] += s.shares
|
||||||
|
return total_shares
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
72
Solutions/6_7/report.py
Normal file
72
Solutions/6_7/report.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
from portfolio import Portfolio
|
||||||
|
|
||||||
|
def read_portfolio(filename):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float])
|
||||||
|
|
||||||
|
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||||
|
return Portfolio(portfolio)
|
||||||
|
|
||||||
|
def read_prices(filename):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
37
Solutions/6_7/stock.py
Normal file
37
Solutions/6_7/stock.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
__slots__ = ('name','_shares','price')
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shares(self):
|
||||||
|
return self._shares
|
||||||
|
|
||||||
|
@shares.setter
|
||||||
|
def shares(self, value):
|
||||||
|
if not isinstance(value,int):
|
||||||
|
raise TypeError("Must be integer")
|
||||||
|
self._shares = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/6_7/tableformat.py
Normal file
81
Solutions/6_7/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
21
Solutions/7_10/timethis.py
Normal file
21
Solutions/7_10/timethis.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# timethis.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
def timethis(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
return func(*args,**kwargs)
|
||||||
|
finally:
|
||||||
|
end = time.time()
|
||||||
|
print("%s.%s : %f" % (func.__module__,func.__name__,end-start))
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
@timethis
|
||||||
|
def countdown(n):
|
||||||
|
while n > 0:
|
||||||
|
n-= 1
|
||||||
|
|
||||||
|
countdown(1000000)
|
||||||
47
Solutions/7_12/fileparse.py
Normal file
47
Solutions/7_12/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
17
Solutions/7_12/follow.py
Normal file
17
Solutions/7_12/follow.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# follow.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
def follow(filename):
|
||||||
|
'''
|
||||||
|
Generator that produces a sequence of lines being written at the end of a file.
|
||||||
|
'''
|
||||||
|
with open(filename,'r') as f:
|
||||||
|
f.seek(0,os.SEEK_END)
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line != '':
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||||
20
Solutions/7_12/pcost.py
Normal file
20
Solutions/7_12/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return portfolio.total_cost
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
51
Solutions/7_12/portfolio.py
Normal file
51
Solutions/7_12/portfolio.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# portfolio.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
import stock
|
||||||
|
|
||||||
|
class Portfolio(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._holdings = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_csv(cls, lines, **opts):
|
||||||
|
self = cls()
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float],
|
||||||
|
**opts)
|
||||||
|
|
||||||
|
for d in portdicts:
|
||||||
|
self.append(stock.Stock(**d))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def append(self, holding):
|
||||||
|
self._holdings.append(holding)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._holdings.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._holdings)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._holdings[index]
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return any(s.name == name for s in self._holdings)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return sum(s.shares * s.price for s in self._holdings)
|
||||||
|
|
||||||
|
def tabulate_shares(self):
|
||||||
|
from collections import Counter
|
||||||
|
total_shares = Counter()
|
||||||
|
for s in self._holdings:
|
||||||
|
total_shares[s.name] += s.shares
|
||||||
|
return total_shares
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
67
Solutions/7_12/report.py
Normal file
67
Solutions/7_12/report.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
from portfolio import Portfolio
|
||||||
|
import tableformat
|
||||||
|
|
||||||
|
def read_portfolio(filename, **opts):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return Portfolio.from_csv(lines, **opts)
|
||||||
|
|
||||||
|
def read_prices(filename, **opts):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts))
|
||||||
|
|
||||||
|
def make_report(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
32
Solutions/7_12/stock.py
Normal file
32
Solutions/7_12/stock.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
from typedproperty import String, Integer, Float
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
name = String('name')
|
||||||
|
shares = Integer('shares')
|
||||||
|
price = Float('price')
|
||||||
|
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/7_12/tableformat.py
Normal file
81
Solutions/7_12/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
43
Solutions/7_12/ticker.py
Normal file
43
Solutions/7_12/ticker.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# ticker.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import report
|
||||||
|
import tableformat
|
||||||
|
from follow import follow
|
||||||
|
|
||||||
|
def select_columns(rows, indices):
|
||||||
|
for row in rows:
|
||||||
|
yield [row[index] for index in indices]
|
||||||
|
|
||||||
|
def convert_types(rows, types):
|
||||||
|
for row in rows:
|
||||||
|
yield [func(val) for func, val in zip(types, row)]
|
||||||
|
|
||||||
|
def make_dicts(rows, headers):
|
||||||
|
return (dict(zip(headers,row)) for row in rows)
|
||||||
|
|
||||||
|
def parse_stock_data(lines):
|
||||||
|
rows = csv.reader(lines)
|
||||||
|
rows = select_columns(rows, [0, 1, 4])
|
||||||
|
rows = convert_types(rows, [str,float,float])
|
||||||
|
rows = make_dicts(rows, ['name','price','change'])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def ticker(portfile, logfile, fmt):
|
||||||
|
portfolio = report.read_portfolio(portfile)
|
||||||
|
lines = follow(logfile)
|
||||||
|
rows = parse_stock_data(lines)
|
||||||
|
rows = (row for row in rows if row['name'] in portfolio)
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
formatter.headings(['Name','Price','Change'])
|
||||||
|
for row in rows:
|
||||||
|
formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] )
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0])
|
||||||
|
ticker(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
21
Solutions/7_12/timethis.py
Normal file
21
Solutions/7_12/timethis.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# timethis.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
def timethis(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
return func(*args,**kwargs)
|
||||||
|
finally:
|
||||||
|
end = time.time()
|
||||||
|
print("%s.%s : %f" % (func.__module__,func.__name__,end-start))
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
@timethis
|
||||||
|
def countdown(n):
|
||||||
|
while n > 0:
|
||||||
|
n-= 1
|
||||||
|
|
||||||
|
countdown(1000000)
|
||||||
34
Solutions/7_12/typedproperty.py
Normal file
34
Solutions/7_12/typedproperty.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# typedproperty.py
|
||||||
|
|
||||||
|
def typedproperty(name, expected_type):
|
||||||
|
private_name = '_' + name
|
||||||
|
@property
|
||||||
|
def prop(self):
|
||||||
|
return getattr(self, private_name)
|
||||||
|
|
||||||
|
@prop.setter
|
||||||
|
def prop(self, value):
|
||||||
|
if not isinstance(value, expected_type):
|
||||||
|
raise TypeError(f'Expected {expected_type}')
|
||||||
|
setattr(self, private_name, value)
|
||||||
|
|
||||||
|
return prop
|
||||||
|
|
||||||
|
String = lambda name: typedproperty(name, str)
|
||||||
|
Integer = lambda name: typedproperty(name, int)
|
||||||
|
Float = lambda name: typedproperty(name, float)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
if __name__ == '__main__':
|
||||||
|
class Stock(object):
|
||||||
|
name = typedproperty('name', str)
|
||||||
|
shares = typedproperty('shares', int)
|
||||||
|
price = typedproperty('price', float)
|
||||||
|
|
||||||
|
def __init__(self, name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
47
Solutions/7_4/fileparse.py
Normal file
47
Solutions/7_4/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
17
Solutions/7_4/follow.py
Normal file
17
Solutions/7_4/follow.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# follow.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
def follow(filename):
|
||||||
|
'''
|
||||||
|
Generator that produces a sequence of lines being written at the end of a file.
|
||||||
|
'''
|
||||||
|
with open(filename,'r') as f:
|
||||||
|
f.seek(0,os.SEEK_END)
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line != '':
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||||
20
Solutions/7_4/pcost.py
Normal file
20
Solutions/7_4/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return portfolio.total_cost
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
31
Solutions/7_4/portfolio.py
Normal file
31
Solutions/7_4/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# portfolio.py
|
||||||
|
|
||||||
|
class Portfolio(object):
|
||||||
|
def __init__(self, holdings):
|
||||||
|
self._holdings = holdings
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._holdings.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._holdings)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._holdings[index]
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return any(s.name == name for s in self._holdings)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return sum(s.shares * s.price for s in self._holdings)
|
||||||
|
|
||||||
|
def tabulate_shares(self):
|
||||||
|
from collections import Counter
|
||||||
|
total_shares = Counter()
|
||||||
|
for s in self._holdings:
|
||||||
|
total_shares[s.name] += s.shares
|
||||||
|
return total_shares
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
73
Solutions/7_4/report.py
Normal file
73
Solutions/7_4/report.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
from portfolio import Portfolio
|
||||||
|
|
||||||
|
def read_portfolio(filename, **opts):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float],
|
||||||
|
**opts)
|
||||||
|
|
||||||
|
portfolio = [ Stock(**d) for d in portdicts ]
|
||||||
|
return Portfolio(portfolio)
|
||||||
|
|
||||||
|
def read_prices(filename, **opts):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines,types=[str,float], has_headers=False, **opts))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
37
Solutions/7_4/stock.py
Normal file
37
Solutions/7_4/stock.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
__slots__ = ('name','_shares','price')
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shares(self):
|
||||||
|
return self._shares
|
||||||
|
|
||||||
|
@shares.setter
|
||||||
|
def shares(self, value):
|
||||||
|
if not isinstance(value,int):
|
||||||
|
raise TypeError("Must be integer")
|
||||||
|
self._shares = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/7_4/tableformat.py
Normal file
81
Solutions/7_4/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
44
Solutions/7_4/ticker.py
Normal file
44
Solutions/7_4/ticker.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# ticker.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import report
|
||||||
|
import tableformat
|
||||||
|
from follow import follow
|
||||||
|
import time
|
||||||
|
|
||||||
|
def select_columns(rows, indices):
|
||||||
|
for row in rows:
|
||||||
|
yield [row[index] for index in indices]
|
||||||
|
|
||||||
|
def convert_types(rows, types):
|
||||||
|
for row in rows:
|
||||||
|
yield [func(val) for func, val in zip(types, row)]
|
||||||
|
|
||||||
|
def make_dicts(rows, headers):
|
||||||
|
return (dict(zip(headers,row)) for row in rows)
|
||||||
|
|
||||||
|
def parse_stock_data(lines):
|
||||||
|
rows = csv.reader(lines)
|
||||||
|
rows = select_columns(rows, [0, 1, 4])
|
||||||
|
rows = convert_types(rows, [str,float,float])
|
||||||
|
rows = make_dicts(rows, ['name','price','change'])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def ticker(portfile, logfile, fmt):
|
||||||
|
portfolio = report.read_portfolio(portfile)
|
||||||
|
lines = follow(logfile)
|
||||||
|
rows = parse_stock_data(lines)
|
||||||
|
rows = (row for row in rows if row['name'] in portfolio)
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
formatter.headings(['Name','Price','Change'])
|
||||||
|
for row in rows:
|
||||||
|
formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] )
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0])
|
||||||
|
ticker(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
47
Solutions/7_9/fileparse.py
Normal file
47
Solutions/7_9/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
17
Solutions/7_9/follow.py
Normal file
17
Solutions/7_9/follow.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# follow.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
def follow(filename):
|
||||||
|
'''
|
||||||
|
Generator that produces a sequence of lines being written at the end of a file.
|
||||||
|
'''
|
||||||
|
with open(filename,'r') as f:
|
||||||
|
f.seek(0,os.SEEK_END)
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line != '':
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||||
20
Solutions/7_9/pcost.py
Normal file
20
Solutions/7_9/pcost.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pcost.py
|
||||||
|
|
||||||
|
import report
|
||||||
|
|
||||||
|
def portfolio_cost(filename):
|
||||||
|
'''
|
||||||
|
Computes the total cost (shares*price) of a portfolio file
|
||||||
|
'''
|
||||||
|
portfolio = report.read_portfolio(filename)
|
||||||
|
return portfolio.total_cost
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 2:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile' % args[0])
|
||||||
|
filename = args[1]
|
||||||
|
print('Total cost:', portfolio_cost(filename))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
31
Solutions/7_9/portfolio.py
Normal file
31
Solutions/7_9/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# portfolio.py
|
||||||
|
|
||||||
|
class Portfolio(object):
|
||||||
|
def __init__(self, holdings):
|
||||||
|
self._holdings = holdings
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._holdings.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._holdings)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._holdings[index]
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return any(s.name == name for s in self._holdings)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return sum(s.shares * s.price for s in self._holdings)
|
||||||
|
|
||||||
|
def tabulate_shares(self):
|
||||||
|
from collections import Counter
|
||||||
|
total_shares = Counter()
|
||||||
|
for s in self._holdings:
|
||||||
|
total_shares[s.name] += s.shares
|
||||||
|
return total_shares
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
73
Solutions/7_9/report.py
Normal file
73
Solutions/7_9/report.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# report.py
|
||||||
|
|
||||||
|
import fileparse
|
||||||
|
from stock import Stock
|
||||||
|
import tableformat
|
||||||
|
from portfolio import Portfolio
|
||||||
|
|
||||||
|
def read_portfolio(filename, **opts):
|
||||||
|
'''
|
||||||
|
Read a stock portfolio file into a list of dictionaries with keys
|
||||||
|
name, shares, and price.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
portdicts = fileparse.parse_csv(lines,
|
||||||
|
select=['name','shares','price'],
|
||||||
|
types=[str,int,float],
|
||||||
|
**opts)
|
||||||
|
|
||||||
|
portfolio = [ Stock(**d) for d in portdicts ]
|
||||||
|
return Portfolio(portfolio)
|
||||||
|
|
||||||
|
def read_prices(filename, **opts):
|
||||||
|
'''
|
||||||
|
Read a CSV file of price data into a dict mapping names to prices.
|
||||||
|
'''
|
||||||
|
with open(filename) as lines:
|
||||||
|
return dict(fileparse.parse_csv(lines,types=[str,float], has_headers=False, **opts))
|
||||||
|
|
||||||
|
def make_report_data(portfolio, prices):
|
||||||
|
'''
|
||||||
|
Make a list of (name, shares, price, change) tuples given a portfolio list
|
||||||
|
and prices dictionary.
|
||||||
|
'''
|
||||||
|
rows = []
|
||||||
|
for s in portfolio:
|
||||||
|
current_price = prices[s.name]
|
||||||
|
change = current_price - s.price
|
||||||
|
summary = (s.name, s.shares, current_price, change)
|
||||||
|
rows.append(summary)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_report(reportdata, formatter):
|
||||||
|
'''
|
||||||
|
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||||
|
'''
|
||||||
|
formatter.headings(['Name','Shares','Price','Change'])
|
||||||
|
for name, shares, price, change in reportdata:
|
||||||
|
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
|
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
|
||||||
|
'''
|
||||||
|
Make a stock report given portfolio and price data files.
|
||||||
|
'''
|
||||||
|
# Read data files
|
||||||
|
portfolio = read_portfolio(portfoliofile)
|
||||||
|
prices = read_prices(pricefile)
|
||||||
|
|
||||||
|
# Create the report data
|
||||||
|
report = make_report_data(portfolio, prices)
|
||||||
|
|
||||||
|
# Print it out
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
print_report(report, formatter)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfile pricefile format' % args[0])
|
||||||
|
portfolio_report(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
32
Solutions/7_9/stock.py
Normal file
32
Solutions/7_9/stock.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# stock.py
|
||||||
|
|
||||||
|
from typedproperty import String, Integer, Float
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
'''
|
||||||
|
An instance of a stock holding consisting of name, shares, and price.
|
||||||
|
'''
|
||||||
|
name = String('name')
|
||||||
|
shares = Integer('shares')
|
||||||
|
price = Float('price')
|
||||||
|
|
||||||
|
def __init__(self,name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cost(self):
|
||||||
|
'''
|
||||||
|
Return the cost as shares*price
|
||||||
|
'''
|
||||||
|
return self.shares * self.price
|
||||||
|
|
||||||
|
def sell(self, nshares):
|
||||||
|
'''
|
||||||
|
Sell a number of shares and return the remaining number.
|
||||||
|
'''
|
||||||
|
self.shares -= nshares
|
||||||
81
Solutions/7_9/tableformat.py
Normal file
81
Solutions/7_9/tableformat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# tableformat.py
|
||||||
|
|
||||||
|
class TableFormatter(object):
|
||||||
|
def headings(self, headers):
|
||||||
|
'''
|
||||||
|
Emit the table headers
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
'''
|
||||||
|
Emit a single row of table data
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class TextTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in plain-text format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
for h in headers:
|
||||||
|
print(f'{h:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
print(('-'*10 + ' ')*len(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'{d:>10s}', end=' ')
|
||||||
|
print()
|
||||||
|
|
||||||
|
class CSVTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in CSV format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print(','.join(headers))
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print(','.join(rowdata))
|
||||||
|
|
||||||
|
class HTMLTableFormatter(TableFormatter):
|
||||||
|
'''
|
||||||
|
Output data in HTML format.
|
||||||
|
'''
|
||||||
|
def headings(self, headers):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for h in headers:
|
||||||
|
print(f'<th>{h}</th>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
def row(self, rowdata):
|
||||||
|
print('<tr>', end='')
|
||||||
|
for d in rowdata:
|
||||||
|
print(f'<td>{d}</td>', end='')
|
||||||
|
print('</tr>')
|
||||||
|
|
||||||
|
class FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_formatter(name):
|
||||||
|
'''
|
||||||
|
Create an appropriate formatter given an output format name
|
||||||
|
'''
|
||||||
|
if name == 'txt':
|
||||||
|
return TextTableFormatter()
|
||||||
|
elif name == 'csv':
|
||||||
|
return CSVTableFormatter()
|
||||||
|
elif name == 'html':
|
||||||
|
return HTMLTableFormatter()
|
||||||
|
else:
|
||||||
|
raise FormatError(f'Unknown table format {name}')
|
||||||
|
|
||||||
|
def print_table(objects, columns, formatter):
|
||||||
|
'''
|
||||||
|
Make a nicely formatted table from a list of objects and attribute names.
|
||||||
|
'''
|
||||||
|
formatter.headings(columns)
|
||||||
|
for obj in objects:
|
||||||
|
rowdata = [ str(getattr(obj, name)) for name in columns ]
|
||||||
|
formatter.row(rowdata)
|
||||||
|
|
||||||
44
Solutions/7_9/ticker.py
Normal file
44
Solutions/7_9/ticker.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# ticker.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import report
|
||||||
|
import tableformat
|
||||||
|
from follow import follow
|
||||||
|
import time
|
||||||
|
|
||||||
|
def select_columns(rows, indices):
|
||||||
|
for row in rows:
|
||||||
|
yield [row[index] for index in indices]
|
||||||
|
|
||||||
|
def convert_types(rows, types):
|
||||||
|
for row in rows:
|
||||||
|
yield [func(val) for func, val in zip(types, row)]
|
||||||
|
|
||||||
|
def make_dicts(rows, headers):
|
||||||
|
return (dict(zip(headers,row)) for row in rows)
|
||||||
|
|
||||||
|
def parse_stock_data(lines):
|
||||||
|
rows = csv.reader(lines)
|
||||||
|
rows = select_columns(rows, [0, 1, 4])
|
||||||
|
rows = convert_types(rows, [str,float,float])
|
||||||
|
rows = make_dicts(rows, ['name','price','change'])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def ticker(portfile, logfile, fmt):
|
||||||
|
portfolio = report.read_portfolio(portfile)
|
||||||
|
lines = follow(logfile)
|
||||||
|
rows = parse_stock_data(lines)
|
||||||
|
rows = (row for row in rows if row['name'] in portfolio)
|
||||||
|
formatter = tableformat.create_formatter(fmt)
|
||||||
|
formatter.headings(['Name','Price','Change'])
|
||||||
|
for row in rows:
|
||||||
|
formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] )
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
if len(args) != 4:
|
||||||
|
raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0])
|
||||||
|
ticker(args[1], args[2], args[3])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
34
Solutions/7_9/typedproperty.py
Normal file
34
Solutions/7_9/typedproperty.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# typedproperty.py
|
||||||
|
|
||||||
|
def typedproperty(name, expected_type):
|
||||||
|
private_name = '_' + name
|
||||||
|
@property
|
||||||
|
def prop(self):
|
||||||
|
return getattr(self, private_name)
|
||||||
|
|
||||||
|
@prop.setter
|
||||||
|
def prop(self, value):
|
||||||
|
if not isinstance(value, expected_type):
|
||||||
|
raise TypeError(f'Expected {expected_type}')
|
||||||
|
setattr(self, private_name, value)
|
||||||
|
|
||||||
|
return prop
|
||||||
|
|
||||||
|
String = lambda name: typedproperty(name, str)
|
||||||
|
Integer = lambda name: typedproperty(name, int)
|
||||||
|
Float = lambda name: typedproperty(name, float)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
if __name__ == '__main__':
|
||||||
|
class Stock(object):
|
||||||
|
name = typedproperty('name', str)
|
||||||
|
shares = typedproperty('shares', int)
|
||||||
|
price = typedproperty('price', float)
|
||||||
|
|
||||||
|
def __init__(self, name, shares, price):
|
||||||
|
self.name = name
|
||||||
|
self.shares = shares
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
47
Solutions/8_1/fileparse.py
Normal file
47
Solutions/8_1/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# fileparse.py
|
||||||
|
import csv
|
||||||
|
|
||||||
|
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||||
|
'''
|
||||||
|
Parse a CSV file into a list of records with type conversion.
|
||||||
|
'''
|
||||||
|
if select and not has_headers:
|
||||||
|
raise RuntimeError('select requires column headers')
|
||||||
|
|
||||||
|
rows = csv.reader(lines, delimiter=delimiter)
|
||||||
|
|
||||||
|
# Read the file headers (if any)
|
||||||
|
headers = next(rows) if has_headers else []
|
||||||
|
|
||||||
|
# If specific columns have been selected, make indices for filtering and set output columns
|
||||||
|
if select:
|
||||||
|
indices = [ headers.index(colname) for colname in select ]
|
||||||
|
headers = select
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rowno, row in enumerate(rows, 1):
|
||||||
|
if not row: # Skip rows with no data
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If specific column indices are selected, pick them out
|
||||||
|
if select:
|
||||||
|
row = [ row[index] for index in indices]
|
||||||
|
|
||||||
|
# Apply type conversion to the row
|
||||||
|
if types:
|
||||||
|
try:
|
||||||
|
row = [func(val) for func, val in zip(types, row)]
|
||||||
|
except ValueError as e:
|
||||||
|
if not silence_errors:
|
||||||
|
print(f"Row {rowno}: Couldn't convert {row}")
|
||||||
|
print(f"Row {rowno}: Reason {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make a dictionary or a tuple
|
||||||
|
if headers:
|
||||||
|
record = dict(zip(headers, row))
|
||||||
|
else:
|
||||||
|
record = tuple(row)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
17
Solutions/8_1/follow.py
Normal file
17
Solutions/8_1/follow.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# follow.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
def follow(filename):
|
||||||
|
'''
|
||||||
|
Generator that produces a sequence of lines being written at the end of a file.
|
||||||
|
'''
|
||||||
|
with open(filename,'r') as f:
|
||||||
|
f.seek(0,os.SEEK_END)
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line != '':
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user