Added solution code

This commit is contained in:
David Beazley
2020-05-27 17:03:35 -05:00
parent 960d4fa2fa
commit 5b6f15db17
136 changed files with 5828 additions and 350 deletions

View File

@@ -136,10 +136,15 @@ If you want to operate on an instance, you always have to refer too it explicitl
## 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
In section 2 and 3, we worked with data represented as tuples and dictionaries.
For example, a holding of stock could be represented as a tuple like this:
In section 2 and 3, we worked with data represented as tuples and
dictionaries. For example, a holding of stock could be represented as
a tuple like this:
```python
s = ('GOOG',100,490.10)
@@ -161,20 +166,21 @@ def cost(s):
return s['shares'] * s['price']
```
However, as your program gets large, you might want to create a better sense of organization.
Thus, another approach for representing data would be to define a class.
Create a file called `stock.py` and define a class `Stock` that represents a single holding of stock.
Have the instances of `Stock` have `name`, `shares`, and `price` attributes.
However, as your program gets large, you might want to create a better
sense of organization. Thus, another approach for representing data
would be to define a class. Create a file called `stock.py` and
define a class `Stock` that represents a single holding of stock.
Have the instances of `Stock` have `name`, `shares`, and `price`
attributes. For example:
```python
>>> import stock
>>> s = stock.Stock('GOOG',100,490.10)
>>> s.name
>>> a = stock.Stock('GOOG',100,490.10)
>>> a.name
'GOOG'
>>> s.shares
>>> a.shares
100
>>> s.price
>>> a.price
490.1
>>>
```
@@ -182,84 +188,38 @@ Have the instances of `Stock` have `name`, `shares`, and `price` attributes.
Create a few more `Stock` objects and manipulate them. For example:
```python
>>> a = stock.Stock('AAPL',50,122.34)
>>> b = stock.Stock('IBM',75,91.75)
>>> a.shares * a.price
6117.0
>>> b = stock.Stock('AAPL', 50, 122.34)
>>> c = stock.Stock('IBM', 75, 91.75)
>>> b.shares * b.price
6117.0
>>> c.shares * c.price
6881.25
>>> stocks = [a,b,s]
>>> stocks = [a, b, c]
>>> stocks
[<stock.Stock object at 0x37d0b0>, <stock.Stock object at 0x37d110>, <stock.Stock object at 0x37d050>]
>>> for t 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. Heres 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; its 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:
>>> for s in stocks:
print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')
... 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
>>> more100 = [s for s in portfolio if s.shares > 100]
>>> for s in more100:
print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')
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`.
... look at the output ...
>>>
```
Again, notice the similarity between `Stock` objects and dictionaries. Theyre basically the same idea, but the syntax for accessing values differs.
### Exercise 4.3: Adding some Methods
### Exercise 4.2: Adding some Methods
With classes, you can attach functions to your objects. These are
known as methods and are functions that operate on the data stored
inside an object.
Add a `cost()` and `sell()` method to your `Stock` object. They should
work like this:
known as methods and are functions that operate on the data
stored inside an object. Add a `cost()` and `sell()` method to your
`Stock` object. They should work like this:
```python
>>> import stock
@@ -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)

View File

@@ -220,63 +220,53 @@ We're not going to explore multiple inheritance further in this course.
## Exercises
### Exercise 4.4: Print Portfolio
A major use of inheritance is in writing code thats meant to be extended or customized in various ways—especially in libraries or frameworks.
To illustrate, start by adding the following function to your `stock.py` program:
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. To illustrate, consider the `print_report()` function
in your `report.py` program. It should look something like this:
```python
# stock.py
...
def print_portfolio(portfolio):
def print_report(reportdata):
'''
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')
for h in headers:
print(f'{h:>10s}',end=' ')
print()
headers = ('Name','Shares','Price','Change')
print('%10s %10s %10s %10s' % headers)
print(('-'*10 + ' ')*len(headers))
for s in portfolio:
print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')
for row in reportdata:
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)
```
When you run your `stock.py`, you should get this output:
```bash
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
>>> 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
```
### 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,
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
an unmaintainable mess. Instead, this is a perfect opportunity to use
inheritance instead.
To start, focus on the steps that are involved in a creating a
table. At the top of the table is a set of table headers. After that,
rows of table data appear. Lets take those steps and and put them into their own class.
Create a file called `tableformat.py` and define the following class:
To start, focus on the steps that are involved in a creating a table.
At the top of the table is a set of table headers. After that, rows
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
following class:
```python
# tableformat.py
@@ -295,44 +285,68 @@ class TableFormatter(object):
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.
For example, like this:
Modify the `print_report()` function so that it accepts a `TableFormatter` object
as input and invokes methods on it to produce the output. For example, like this:
```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'])
for s in portfolio:
# Form a row of output data (as strings)
rowdata = [s.name, str(s.shares), f'{s.price:0.2f}' ]
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)
```
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
# stock.py
# report.py
import tableformat
...
if __name__ == '__main__':
from tableformat import TableFormatter
portfolio = read_portfolio('Data/portfolio.csv')
formatter = TableFormatter()
print_portfolio(portfolio, formatter)
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
formatter = tableformat.TableFormatter()
print_report(report, formatter)
```
When you run this new code, your program will immediately crash with a `NotImplementedError` exception.
Thats not too exciting, but continue to the next part.
Run this new code:
```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
The `TableFormatter` class you defined in part (a) is meant to be extended via inheritance.
In fact, thats 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
# tableformat.py
@@ -353,33 +367,47 @@ class TextTableFormatter(TableFormatter):
print()
```
Modify your main program in `stock.py` like this and try it:
Modify the `portfolio_report()` function like this and try it:
```python
# stock.py
# report.py
...
if __name__ == '__main__':
from tableformat import TextTableFormatter
portfolio = read_portfolio('Data/portfolio.csv')
formatter = TextTableFormatter()
print_portfolio(portfolio, formatter)
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
formatter = tableformat.TextTableFormatter()
print_report(report, formatter)
```
This should produce the same output as before:
```bash
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
```python
>>> ================================ RESTART ================================
>>> 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
>>>
```
However, lets 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
# tableformat.py
@@ -398,105 +426,170 @@ class CSVTableFormatter(TableFormatter):
Modify your main program as follows:
```python
# stock.py
...
if __name__ == '__main__':
from tableformat import CSVTableFormatter
portfolio = read_portfolio('Data/portfolio.csv')
formatter = CSVTableFormatter()
print_portfolio(portfolio, formatter)
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
formatter = tableformat.CSVTableFormatter()
print_report(report, formatter)
```
You should now see CSV output like this:
```csv
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
```python
>>> ================================ RESTART ================================
>>> 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
```
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> <td>AA</td> <td>100</td> <td>32.20</td> </tr>
<tr> <td>IBM</td> <td>50</td> <td>91.10</td> </tr>
```
<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></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>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
A major feature of object-oriented programming is that you can plug an
object into a program and it will work without having to change any of
the existing code. For example, if you wrote a program that expected
to use a `TableFormatter` object, it would work no matter what kind of
`TableFormatter` you actually gave it.
A major feature of object-oriented programming is that you can
plug an object into a program and it will work without having to
change any of the existing code. For example, if you wrote a program
that expected to use a `TableFormatter` object, it would work no
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 making it easier for the user to pick the formatter that they want.
This can sometimes be fixed by defining a helper function.
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:
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
such as `TextTableFormatter` is often annoying. Thus, you
might consider some simplified approach. Perhaps you embed an `if-`statement
into the code like this:
```python
# stock.py
...
if __name__ == '__main__':
from tableformat import create_formatter
portfolio = read_portfolio('Data/portfolio.csv')
formatter = create_formatter('csv')
print_portfolio(portfolio, formatter)
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
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, youll 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
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
```
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:
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'`.
Modify `portfolio_report()` so that it looks like this:
```python
>>> from tableformat import create_formatter
>>> formatter = create_formatter('xls')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "tableformat.py", line 68, in create_formatter
raise RuntimeError('Unknown table format %s' % name)
RuntimeError: Unknown table format xls
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)
```
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.
For example, a framework might instruct you to define your own object that inherits from a provided base class.
Youre 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.
Modify the main program so that a format can be given on the command line:
That said, understanding what happened in this exercise will take you
pretty far in terms of using most library modules and knowing
what inheritance is good for (extensibility).
```bash
bash $ python3 report.py Data/portfolio.csv Data/prices.csv 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
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)

View File

@@ -202,34 +202,38 @@ x = getattr(obj, 'x', None)
## 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
representation is created by string conversion via `str()` (which is
called by `print`). The string representation is usually a nicely
formatted version of the object meant for humans. The second
representation is a code representation of the object created by
`repr()` (or by viewing a value in the interactive shell). The code
representation typically shows you the code that you have to type to
get the object.
representation is created by string conversion via `str()`
(which is called by `print`). The string representation is
usually a nicely formatted version of the object meant for humans.
The second representation is a code representation of the object
created by `repr()` (or by viewing a value in the
interactive shell). The code representation typically shows you the
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
>>> s = 'Hello\nWorld'
>>> print(str(s)) # Notice nice output (no quotes)
Hello
World
>>> print(repr(s)) # Notice the added quotes and escape codes
'Hello\nWorld'
>>> print(f'{s!r}') # Alternate way to get repr() string
'Hello\nWorld'
>>> from datetime import date
>>> d = date(2017, 4, 9)
>>> print(d) # Nice output
2017-04-09
>>> print(repr(d)) # Representation output
datetime.date(2017, 4, 9)
>>> print(f'{d!r}') # Alternate way to get repr() string
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
>>> 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
>>> portfolio = stock.read_portfolio('Data/portfolio.csv')
```
>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> portfolio
... 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.
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()`.
To begin, try this little example:
`getattr()` is an alternative mechanism for reading attributes. It can be used to
write extremely flexible code. To begin, try this example:
```python
>>> 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
generalized function `print_table()` that prints a table showing
user-specified attributes of a list of arbitrary objects.
As with the earlier `print_portfolio()` function, `print_table()`
should also accept a `TableFormatter` instance to control the output
format. Heres how it should work:
In the file `tableformat.py`, take this idea and expand it into a generalized
function `print_table()` that prints a table showing
user-specified attributes of a list of arbitrary objects. As with the
earlier `print_report()` function, `print_table()` should also accept
a `TableFormatter` instance to control the output format. Here's how
it should work:
```python
>>> import stock
>>> portfolio = stock.read_portfolio('Data/portfolio.csv')
>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> from tableformat import create_formatter, print_table
>>> formatter = create_formatter('txt')
>>> print_table(portfolio, ['name','shares'], formatter)
@@ -306,28 +309,5 @@ format. Heres 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)

View File

@@ -363,16 +363,22 @@ Frameworks / libraries sometimes use it for advanced features involving composit
## Exercises
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
At the interactive shell, inspect the underlying dictionaries of the two instances you created:
```python
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm = Stock('IBM',50, 91.23)
>>> goog.__dict__
... look at the output ...
>>> ibm.__dict__
@@ -393,12 +399,14 @@ Try setting a new attribute on one of the above instances:
>>>
```
In the above output, youll 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 doesnt place any restrictions on attributes.
The attributes of an instance are not limited to those set up in the `__init__()` method.
Instead of setting an attribute, try placing a new value directly into the `__dict__` object:
Instead of setting an attribute, try placing a new value directly into
the `__dict__` object:
```python
>>> 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
top of a dictionary. *Note: it should be emphasized that direct
manipulation of the dictionary is uncommonyou should always write
your code to use the (.) syntax.*
top of a dictionary. Note: it should be emphasized that direct
manipulation of the dictionary is uncommon--you should always write
your code to use the (.) syntax.
### Exercise 5.3: The role of classes
The definitions that make up a class definition are shared by all instances of that class.
Notice, that all instances have a link back to their associated class:
The definitions that make up a class definition are shared by all
instances of that class. Notice, that all instances have a link back
to their associated class:
```python
>>> 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
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:
```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
Python always checks the class dictionary if it cant find something
Python always checks the class dictionary if it can't find something
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:
```python
@@ -495,8 +506,9 @@ class Foo(object):
self.b = b # Instance variable
```
In this class, the variable `a`, assigned in the body of the class itself, is a *class variable*.
It is shared by all of the instances that get created.
In this class, the variable `a`, assigned in the body of the
class itself, is a "class variable." It is shared by all of the
instances that get created. For example:
```python
>>> 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
two steps and something known as a bound method.
two steps and something known as a bound method. For example:
```python
>>> 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.
For instance, they keep a record of the function implementing the method:
Bound methods actually contain all of the pieces needed to call a
method. For instance, they keep a record of the function implementing
the method:
```python
>>> 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` argument.
Bound methods also record the instance, which is the `self`
argument.
```python
>>> s.__self__
@@ -558,8 +571,8 @@ Stock('GOOG',75,490.1)
>>>
```
When you invoke the function using `()` all of the pieces come together.
For example, calling `s(25)` actually does this:
When you invoke the function using `()` all of the pieces come
together. For example, calling `s(25)` actually does this:
```python
>>> 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`.
```python
```
>>> class NewStock(Stock):
def yow(self):
print('Yow!')
@@ -603,7 +616,7 @@ they will be searched for attributes.
>>>
```
Heres 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
>>> for cls in n.__class__.__mro__:

View File

@@ -252,10 +252,10 @@ day-to-day coding.
## Exercises
### Exercise 5.6: Simple properties
### Exercise 5.6: Simple Properties
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
are extracted:
@@ -271,17 +271,22 @@ are extracted:
>>>
```
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.
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.
Take your `Stock` class and modify it so that the cost calculation works like this:
```python
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>
```
Try calling `s.cost()` as a function and observe that it doesnt 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
>>> s.cost()
@@ -289,14 +294,19 @@ Try calling `s.cost()` as a function and observe that it doesnt 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
Modify the `shares` attribute so that the value is stored in a private
attribute and that a pair of property functions are used to ensure
that it is always set to an integer value.
Here is an example of the expected behavior:
Modify the `shares` attribute so that the value is stored in a
private attribute and that a pair of property functions are used to ensure
that it is always set to an integer value. Here is an example of the expected
behavior:
```python
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
@@ -308,10 +318,11 @@ TypeError: expected an integer
### Exercise 5.8: Adding slots
Modify the `Stock` class so that it has a `__slots__` attribute.
Then, verify that new attributes cant be added:
Modify the `Stock` class so that it has a `__slots__` attribute. Then,
verify that new attributes can't be added:
```python
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
@@ -321,8 +332,9 @@ Then, verify that new attributes cant be added:
>>>
```
When you use `__slots__`, Python actually uses a more efficient internal representation of objects.
What happens if you try to inspect the underlying dictionary of `s` above?
When you use `__slots__`, Python actually uses a more efficient
internal representation of objects. What happens if you try to
inspect the underlying dictionary of `s` above?
```python
>>> s.__dict__

View File

@@ -248,7 +248,7 @@ if __name__ == '__main__':
change = float(fields[4])
if name in portfolio:
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
----
```
Note: For this to work, your `Portfolio` class must support the
`in` operator. See the last exercise and make sure you implement the

View File

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

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

View 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

View 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
View 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
View 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')

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

View 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
View 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
View 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
View 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')

View 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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

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

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

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

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

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

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

View 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

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