├── .gitignore
├── Data
├── ctabus.csv
├── dowstocks.csv
├── missing.csv
├── portfolio.csv
├── portfolio.csv.gz
├── portfolio.dat
├── portfolio1.dat
├── portfolio2.csv
├── portfolio2.dat
├── portfolio3.csv
├── portfolio3.dat
├── portfolio_noheader.csv
├── prices.csv
├── stocksim.py
└── words.txt
├── Exercises
├── README.md
├── ex1_1.md
├── ex1_2.md
├── ex1_3.md
├── ex1_4.md
├── ex1_5.md
├── ex1_6.md
├── ex2_1.md
├── ex2_2.md
├── ex2_3.md
├── ex2_4.md
├── ex2_5.md
├── ex2_6.md
├── ex3_1.md
├── ex3_2.md
├── ex3_3.md
├── ex3_4.md
├── ex3_5.md
├── ex3_6.md
├── ex3_7.md
├── ex3_8.md
├── ex4_1.md
├── ex4_2.md
├── ex4_3.md
├── ex4_4.md
├── ex5_1.md
├── ex5_2.md
├── ex5_3.md
├── ex5_4.md
├── ex5_5.md
├── ex5_6.md
├── ex6_1.md
├── ex6_2.md
├── ex6_3.md
├── ex6_4.md
├── ex6_5.md
├── ex7_1.md
├── ex7_2.md
├── ex7_3.md
├── ex7_4.md
├── ex7_5.md
├── ex7_6.md
├── ex8_1.md
├── ex8_2.md
├── ex8_3.md
├── ex8_4.md
├── ex8_5.md
├── ex8_6.md
├── ex9_1.md
├── ex9_2.md
├── ex9_3.md
├── ex9_4.md
├── index.md
├── soln1_1.md
├── soln1_2.md
├── soln1_3.md
├── soln1_4.md
├── soln1_5.md
├── soln1_6.md
├── soln2_1.md
├── soln2_2.md
├── soln2_3.md
├── soln2_4.md
├── soln2_5.md
├── soln2_6.md
├── soln3_1.md
├── soln3_2.md
├── soln3_3.md
├── soln3_4.md
├── soln3_5.md
├── soln3_6.md
├── soln3_7.md
├── soln3_8.md
├── soln4_1.md
├── soln4_2.md
├── soln4_3.md
├── soln4_4.md
├── soln5_1.md
├── soln5_2.md
├── soln5_3.md
├── soln5_4.md
├── soln5_5.md
├── soln5_6.md
├── soln6_1.md
├── soln6_2.md
├── soln6_3.md
├── soln6_4.md
├── soln6_5.md
├── soln7_1.md
├── soln7_2.md
├── soln7_3.md
├── soln7_4.md
├── soln7_5.md
├── soln7_6.md
├── soln8_1.md
├── soln8_2.md
├── soln8_3.md
├── soln8_4.md
├── soln8_5.md
├── soln8_6.md
├── soln9_1.md
├── soln9_2.md
├── soln9_3.md
└── soln9_4.md
├── LICENSE.md
├── PythonMastery.pdf
├── README.md
└── Solutions
├── 1_1
└── art.py
├── 1_3
└── pcost.py
├── 1_4
└── pcost.py
├── 1_5
└── stock.py
├── 1_6
└── pcost.py
├── 2_1
└── readrides.py
├── 2_2
├── cta.py
├── readport.py
└── readrides.py
├── 2_4
└── mutint.py
├── 2_5
├── cta.py
└── readrides.py
├── 2_6
├── colreader.py
├── cta.py
└── reader.py
├── 3_1
└── stock.py
├── 3_2
├── stock.py
└── tableformat.py
├── 3_3
├── reader.py
├── stock.py
└── tableformat.py
├── 3_4
└── stock.py
├── 3_5
├── reader.py
├── stock.py
└── tableformat.py
├── 3_6
├── reader.py
├── stock.py
└── tableformat.py
├── 3_7
├── reader.py
├── stock.py
└── tableformat.py
├── 3_8
├── reader.py
├── stock.py
└── tableformat.py
├── 4_2
└── validate.py
├── 4_3
└── validate.py
├── 5_2
├── reader.py
└── stock.py
├── 5_3
├── reader.py
└── stock.py
├── 5_4
└── typedproperty.py
├── 5_5
└── reader.py
├── 5_6
├── stock.py
└── teststock.py
├── 6_1
├── stock.py
├── structure.py
└── teststock.py
├── 6_2
├── stock.py
├── structure.py
└── teststock.py
├── 6_3
├── stock.py
├── structure.py
└── teststock.py
├── 6_4
├── stock.py
├── structure.py
└── teststock.py
├── 6_5
└── validate.py
├── 7_1
├── logcall.py
├── sample.py
└── validate.py
├── 7_2
├── logcall.py
├── sample.py
├── spam.py
└── validate.py
├── 7_3
├── reader.py
├── stock.py
├── structure.py
├── teststock.py
└── validate.py
├── 7_4
├── stock.py
├── structure.py
├── teststock.py
└── validate.py
├── 7_5
└── mymeta.py
├── 7_6
├── reader.py
├── stock.py
├── structure.py
├── tableformat.py
├── teststock.py
└── validate.py
├── 8_1
├── follow.py
├── reader.py
├── stock.py
├── structure.py
├── teststock.py
└── validate.py
├── 8_2
├── follow.py
├── structure.py
├── tableformat.py
├── ticker.py
└── validate.py
├── 8_3
├── cofollow.py
├── coticker.py
├── structure.py
├── tableformat.py
└── validate.py
├── 8_4
├── cofollow.py
└── follow.py
├── 8_5
├── multitask.py
└── server.py
├── 8_6
├── asyncserver.py
├── cofollow.py
├── coticker.py
├── server.py
├── structure.py
├── tableformat.py
└── validate.py
├── 9_1
└── simplemod.py
├── 9_2
├── stock.py
└── structly
│ ├── __init__.py
│ ├── reader.py
│ ├── structure.py
│ ├── tableformat.py
│ └── validate.py
├── 9_3
├── stock.py
└── structly
│ ├── __init__.py
│ ├── reader.py
│ ├── structure.py
│ ├── tableformat
│ ├── __init__.py
│ ├── formats
│ │ ├── __init__.py
│ │ ├── csv.py
│ │ ├── html.py
│ │ └── text.py
│ └── formatter.py
│ └── validate.py
├── 9_4
├── stock.py
└── structly
│ ├── __init__.py
│ ├── reader.py
│ ├── structure.py
│ ├── tableformat
│ ├── __init__.py
│ ├── formats
│ │ ├── __init__.py
│ │ ├── csv.py
│ │ ├── html.py
│ │ ├── text.py
│ │ └── tsv.py
│ └── formatter.py
│ └── validate.py
└── README.md
/Data/missing.csv:
--------------------------------------------------------------------------------
1 | name,shares,price
2 | "AA",15,39.48
3 | "AXP",10,62.58
4 | "BA",5,98.31
5 | "C",,53.08
6 | "CAT",15,78.29
7 | "DD",10,50.75
8 | "DIS",50,N/A
9 | "GE",,37.23
10 | "GM",15,31.44
11 | "HD",20,37.67
12 | "HPQ",5,45.81
13 | "IBM",10,102.86
14 | "INTC",,21.84
15 | "JNJ",20,62.25
16 | "JPM",10,50.35
17 | "KO",5,51.65
18 | "MCD",,51.11
19 | "MMM",10,85.60
20 | "MO",,70.09
21 | "MRK",5,50.21
22 | "MSFT",20,30.08
23 | "PFE",,26.40
24 | "PG",5,62.79
25 | "T",10,40.03
26 | "UTX",8,69.81
27 | "VZ",,42.92
28 | "WMT",10,49.78
29 | "XOM",15,82.50
30 |
--------------------------------------------------------------------------------
/Data/portfolio.csv:
--------------------------------------------------------------------------------
1 | name,shares,price
2 | "AA",100,32.20
3 | "IBM",50,91.10
4 | "CAT",150,83.44
5 | "MSFT",200,51.23
6 | "GE",95,40.37
7 | "MSFT",50,65.10
8 | "IBM",100,70.44
9 |
--------------------------------------------------------------------------------
/Data/portfolio.csv.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dabeaz-course/python-mastery/dcf5e16d1fedfc9a40620141d18c3c7a75059425/Data/portfolio.csv.gz
--------------------------------------------------------------------------------
/Data/portfolio.dat:
--------------------------------------------------------------------------------
1 | AA 100 32.20
2 | IBM 50 91.10
3 | CAT 150 83.44
4 | MSFT 200 51.23
5 | GE 95 40.37
6 | MSFT 50 65.10
7 | IBM 100 70.44
8 |
--------------------------------------------------------------------------------
/Data/portfolio1.dat:
--------------------------------------------------------------------------------
1 | AA 100 32.20
2 | IBM 50 91.10
3 | CAT 150 83.44
4 | MSFT 200 51.23
5 | GE 95 40.37
6 | MSFT 50 65.10
7 | IBM 100 70.44
8 |
--------------------------------------------------------------------------------
/Data/portfolio2.csv:
--------------------------------------------------------------------------------
1 | name,shares,price
2 | "AA",50,27.10
3 | "HPQ",250,43.15
4 | "MSFT",25,50.15
5 | "GE",125,52.10
6 |
--------------------------------------------------------------------------------
/Data/portfolio2.dat:
--------------------------------------------------------------------------------
1 | AA 50 27.10
2 | HPQ 250 43.15
3 | MSFT 25 50.15
4 | GE 125 52.10
5 |
--------------------------------------------------------------------------------
/Data/portfolio3.csv:
--------------------------------------------------------------------------------
1 | "AA",15,39.48
2 | "AXP",10,62.58
3 | "BA",5,98.31
4 | "C",-,53.08
5 | "CAT",15,78.29
6 | "DD",10,50.75
7 | "DIS",-,N/A
8 | "GE",-,37.23
9 | "GM",15,31.44
10 | "HD",20,37.67
11 | "HPQ",5,45.81
12 | "IBM",10,102.86
13 | "INTC",-,21.84
14 | "JNJ",20,62.25
15 | "JPM",10,50.35
16 | "KO",5,51.65
17 | "MCD",-,51.11
18 | "MMM",10,85.60
19 | "MO",-,70.09
20 | "MRK",5,50.21
21 | "MSFT",20,30.08
22 | "PFE",-,26.40
23 | "PG",5,62.79
24 | "T",10,40.03
25 | "UTX",8,69.81
26 | "VZ",-,42.92
27 | "WMT",10,49.78
28 | "XOM",15,82.50
29 |
--------------------------------------------------------------------------------
/Data/portfolio3.dat:
--------------------------------------------------------------------------------
1 | AA 15 39.48
2 | AXP 10 62.58
3 | BA 5 98.31
4 | C - 53.08
5 | CAT 15 78.29
6 | DD 10 50.75
7 | DIS - N/A
8 | GE - 37.23
9 | GM 15 31.44
10 | HD 20 37.67
11 | HPQ 5 45.81
12 | IBM 10 102.86
13 | INTC - 21.84
14 | JNJ 20 62.25
15 | JPM 10 50.35
16 | KO 5 51.65
17 | MCD - 51.11
18 | MMM 10 85.60
19 | MO - 70.09
20 | MRK 5 50.21
21 | MSFT 20 30.08
22 | PFE - 26.40
23 | PG 5 62.79
24 | T 10 40.03
25 | UTX 8 69.81
26 | VZ - 42.92
27 | WMT 10 49.78
28 | XOM 15 82.50
29 |
--------------------------------------------------------------------------------
/Data/portfolio_noheader.csv:
--------------------------------------------------------------------------------
1 | "AA",100,32.20
2 | "IBM",50,91.10
3 | "CAT",150,83.44
4 | "MSFT",200,51.23
5 | "GE",95,40.37
6 | "MSFT",50,65.10
7 | "IBM",100,70.44
8 |
--------------------------------------------------------------------------------
/Data/prices.csv:
--------------------------------------------------------------------------------
1 | "AA",9.22
2 | "AXP",24.85
3 | "BA",44.85
4 | "BAC",11.27
5 | "C",3.72
6 | "CAT",35.46
7 | "CVX",66.67
8 | "DD",28.47
9 | "DIS",24.22
10 | "GE",13.48
11 | "GM",0.75
12 | "HD",23.16
13 | "HPQ",34.35
14 | "IBM",106.28
15 | "INTC",15.72
16 | "JNJ",55.16
17 | "JPM",36.90
18 | "KFT",26.11
19 | "KO",49.16
20 | "MCD",58.99
21 | "MMM",57.10
22 | "MRK",27.58
23 | "MSFT",20.89
24 | "PFE",15.19
25 | "PG",51.94
26 | "T",24.79
27 | "UTX",52.61
28 | "VZ",29.26
29 | "WMT",49.74
30 | "XOM",69.35
31 |
--------------------------------------------------------------------------------
/Data/words.txt:
--------------------------------------------------------------------------------
1 | look into my eyes
2 | look into my eyes
3 | the eyes the eyes the eyes
4 | not around the eyes
5 | don't look around the eyes
6 | look into my eyes you're under
7 |
--------------------------------------------------------------------------------
/Exercises/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Python Mastery
2 |
3 | Copyright (C) 2007-2023
4 | David Beazley (dave@dabeaz.com)
5 | http://www.dabeaz.com
6 |
7 | Welcome to the Python Mastery course. This
8 | directory, `pythonmaster` is where you find support files
9 | related to the class exercises. It is also where you will be doing
10 | your work.
11 |
12 | This course requires the use of Python 3.6 or newer. If you are
13 | using Python 2, most of the material still applies, but you will
14 | have to make minor code modifications here and there.
15 |
16 | - [`PythonMastery.pdf`](../PythonMastery.pdf) is a PDF that contains
17 | all of the presentation slides.
18 |
19 | - The [`Exercises/`](index.md) folder is where you
20 | find all the class exercises.
21 |
22 | - The [`Data/`](../Data/) folder is where you find data files, scripts, and
23 | other files used by the exercises.
24 |
25 | - The [`Solutions/`](../Solutions/) folder contains complete solution code for
26 | various exercises. Each problem has its own directory. For example,
27 | the solution to exercise 3.2 can be found in the [`Solutions/3_2/`](../Solutions/3_2/) directory.
28 |
29 | Every attempt has been made to make sure exercises work. However, it's
30 | possible that you will find typos or minor mistakes. If you find any
31 | errors, please let me know so that I can fix them for future editions
32 | of the course.
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Exercises/ex1_1.md:
--------------------------------------------------------------------------------
1 | \[ [Index](index.md) | []() | [Exercise 1.2](ex1_2.md) \]
2 |
3 | # Exercise 1.1
4 |
5 | *Objectives:*
6 |
7 | - Make sure Python is installed correctly on your machine
8 | - Start the interactive interpreter
9 | - Edit and run a small program
10 |
11 | *Files Created:* `art.py`
12 |
13 | ## (a) Launch Python
14 |
15 | Start Python3 on your machine. Make sure you can type simple
16 | statements such as the "hello world" program:
17 |
18 | ```python
19 | >>> print('Hello World')
20 | Hello World
21 | >>>
22 | ```
23 |
24 | In much of this course, you'll want to make sure you can work from
25 | the interactive REPL like this. If you're working from a different
26 | environment such as IPython or Jupyter Notebooks, that's fine.
27 |
28 | ## (b) Some Generative Art
29 |
30 | Create the following program and put it in a file called `art.py`:
31 |
32 | ```python
33 | # art.py
34 |
35 | import sys
36 | import random
37 |
38 | chars = '\|/'
39 |
40 | def draw(rows, columns):
41 | for r in rows:
42 | print(''.join(random.choice(chars) for _ in range(columns)))
43 |
44 | if __name__ == '__main__':
45 | if len(sys.argv) != 3:
46 | raise SystemExit("Usage: art.py rows columns")
47 | draw(int(sys.argv[1]), int(sys.argv[2]))
48 | ```
49 |
50 | Make sure you can run this program from the command line or a terminal.
51 |
52 | ```
53 | bash % python3 art.py 10 20
54 | ```
55 |
56 | If you run the above command, you'll get a crash and traceback message.
57 | Go fix the problem and run the program again. You should get output like
58 | this:
59 |
60 | ```
61 | bash % python3 art.py 10 20
62 | ||||/\||//\//\|||\|\
63 | ///||\/||\//|\\|\\/\
64 | |\////|//|||\//|/\||
65 | |//\||\/|\///|\|\|/|
66 | |/|//|/|/|\\/\/\||//
67 | |\/\|\//\\//\|\||\\/
68 | |||\\\\/\\\|/||||\/|
69 | \\||\\\|\||||////\\|
70 | //\//|/|\\|\//\|||\/
71 | \\\|/\\|/|\\\|/|/\/|
72 | bash %
73 | ```
74 |
75 | ### Important Note
76 |
77 | It is absolutely essential that you are able to edit, run, and debug
78 | ordinary Python programs for the rest of this course. The choice
79 | of editor, IDE, or operating system doesn't matter as long as you
80 | are able to experiment interactively and create normal Python source
81 | files that can execute from the command line.
82 |
83 |
84 | \[ [Solution](soln1_1.md) | [Index](index.md) | [Exercise 1.2](ex1_2.md) \]
85 |
86 |
87 |
88 | ----
89 | `>>>` Advanced Python Mastery
90 | `...` A course by [dabeaz](https://www.dabeaz.com)
91 | `...` Copyright 2007-2023
92 |
93 | . This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
94 |
--------------------------------------------------------------------------------
/Exercises/ex1_3.md:
--------------------------------------------------------------------------------
1 | \[ [Index](index.md) | [Exercise 1.2](ex1_2.md) | [Exercise 1.4](ex1_4.md) \]
2 |
3 | # Exercise 1.3
4 |
5 | *Objectives:*
6 |
7 | - Review basic file I/O
8 |
9 | *Files Created:* `pcost.py`
10 |
11 | ## (a) Working with files
12 |
13 | The file `Data/portfolio.dat` contains a list of lines with information
14 | on a portfolio of stocks. The file looks like this:
15 |
16 | ```
17 | AA 100 32.20
18 | IBM 50 91.10
19 | CAT 150 83.44
20 | MSFT 200 51.23
21 | GE 95 40.37
22 | MSFT 50 65.10
23 | IBM 100 70.44
24 | ```
25 |
26 | The first column is the stock name, the second column is the number of
27 | shares, and the third column is the purchase price of a single share.
28 |
29 | Write a program called `pcost.py` that opens this file, reads
30 | all lines, and calculates how much it cost to purchase all of the shares
31 | in the portfolio. To do this, compute the sum of the second column
32 | multiplied by the third column.
33 |
34 | \[ [Solution](soln1_3.md) | [Index](index.md) | [Exercise 1.2](ex1_2.md) | [Exercise 1.4](ex1_4.md) \]
35 |
36 | ----
37 | `>>>` Advanced Python Mastery
38 | `...` A course by [dabeaz](https://www.dabeaz.com)
39 | `...` Copyright 2007-2023
40 |
41 | . This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
42 |
--------------------------------------------------------------------------------
/Exercises/ex1_5.md:
--------------------------------------------------------------------------------
1 | \[ [Index](index.md) | [Exercise 1.4](ex1_4.md) | [Exercise 1.6](ex1_6.md) \]
2 |
3 | # Exercise 1.5
4 |
5 | *Objectives:*
6 |
7 | - Review of how to define a simple object
8 |
9 | *Files Created:* `stock.py`
10 |
11 | ## (a) Defining a simple object
12 |
13 | Create a file `stock.py` and define the following class:
14 |
15 | ```python
16 | class Stock:
17 | def __init__(self, name, shares, price):
18 | self.name = name
19 | self.shares = shares
20 | self.price = price
21 | def cost(self):
22 | return self.shares * self.price
23 | ```
24 |
25 | Once you have done this, run your program and experiment with your new
26 | `Stock` object:
27 |
28 | ```python
29 | >>> s = Stock('GOOG',100,490.10)
30 | >>> s.name
31 | 'GOOG'
32 | >>> s.shares
33 | 100
34 | >>> s.price
35 | 490.1
36 | >>> s.cost()
37 | 49010.0
38 | >>> print('%10s %10d %10.2f' % (s.name, s.shares, s.price))
39 | GOOG 100 490.10
40 | >>> t = Stock('IBM', 50, 91.5)
41 | >>> t.cost()
42 | 4575.0
43 | >>>
44 | ```
45 |
46 | \[ [Solution](soln1_5.md) | [Index](index.md) | [Exercise 1.4](ex1_4.md) | [Exercise 1.6](ex1_6.md) \]
47 |
48 | ----
49 | `>>>` Advanced Python Mastery
50 | `...` A course by [dabeaz](https://www.dabeaz.com)
51 | `...` Copyright 2007-2023
52 |
53 | . This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
54 |
--------------------------------------------------------------------------------
/Exercises/ex1_6.md:
--------------------------------------------------------------------------------
1 | \[ [Index](index.md) | [Exercise 1.5](ex1_5.md) | [Exercise 2.1](ex2_1.md) \]
2 |
3 | # Exercise 1.6
4 |
5 | *Objectives:*
6 |
7 | - Defining modules
8 | - Using the import statement
9 |
10 | *Files Created:* None
11 |
12 | **Note:**
13 | For this exercise involving modules, it is
14 | critically important to make sure you are running Python in a proper
15 | environment. You may need to check the value of `sys.path` if you
16 | can't get import statements to work. Ask for assistance if everything
17 | seems broken.
18 |
19 | Before starting this exercise, first restart your Python interpreter session. If using IDLE, click on
20 | the shell window and look for a menu option "Shell > Restart Shell". You should get a message like this:
21 |
22 | ```python
23 | >>> ##################== RESTART ##################==
24 | >>>
25 | ```
26 |
27 | If you are using Unix, simply exit Python and restart the interpreter.
28 |
29 | ## (a) Using the import statement
30 |
31 | In previous exercises, you wrote two programs `pcost.py` and
32 | `stock.py`. Use the `import` statement to load these
33 | programs and use their functionality:
34 |
35 | ```python
36 | >>> import pcost
37 | 44671.15
38 | >>> pcost.portfolio_cost('Data/portfolio2.dat')
39 | 19908.75
40 | >>> from stock import Stock
41 | >>> s = Stock('GOOG', 100, 490.10)
42 | >>> s.name
43 | 'GOOG'
44 | >>> s.cost()
45 | 49010.0
46 | >>>
47 | ```
48 |
49 | If you can't get the above statements to work, you might have placed
50 | your programs in a funny directory. Make sure you are running Python
51 | in the same directory as your files or that the directory is included
52 | on `sys.path`.
53 |
54 |
55 | ## (b) Main Module
56 |
57 | In your `pcost.py` program, the last statement called a
58 | function and printed out the result. Modify the program so that this
59 | step only occurs if the program is run as the main program. Now,
60 | try running the program two ways:
61 |
62 | First, run the program as main:
63 |
64 | ```
65 | bash % python3 pcost.py
66 | 44671.25
67 | bash %
68 | ```
69 |
70 | Next, run the program as a library import. You should not see any
71 | output.
72 |
73 | ```python
74 | >>> import pcost
75 | >>>
76 | ```
77 |
78 | \[ [Solution](soln1_6.md) | [Index](index.md) | [Exercise 1.5](ex1_5.md) | [Exercise 2.1](ex2_1.md) \]
79 |
80 | ----
81 | `>>>` Advanced Python Mastery
82 | `...` A course by [dabeaz](https://www.dabeaz.com)
83 | `...` Copyright 2007-2023
84 |
85 | . This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
86 |
--------------------------------------------------------------------------------
/Exercises/ex9_2.md:
--------------------------------------------------------------------------------
1 | \[ [Index](index.md) | [Exercise 9.1](ex9_1.md) | [Exercise 9.3](ex9_3.md) \]
2 |
3 | # Exercise 9.2
4 |
5 | *Objectives:*
6 |
7 | - Learn how to create a Python package
8 |
9 | **Note**
10 |
11 | This exercise mostly just involves copying files on the file system.
12 | There shouldn't be a lot of coding.
13 |
14 | ## (a) Making a Package
15 |
16 | In previous exercises, you created the following files that were related to
17 | type-checked structures, reading data, and making tables:
18 |
19 | - `structure.py`
20 | - `validate.py`
21 | - `reader.py`
22 | - `tableformat.py`
23 |
24 | Your task is to take all of these files and move them into a package called `structly`.
25 | To do that, follow these steps:
26 |
27 | - Make a directory called `structly`
28 | - Make an empty file `__init__.py` and put it in the `structly` directory
29 | - Move the files `structure.py`, `validate.py`, `reader.py`, and `tableformat.py` into the `structly` directory.
30 | - Fix any import statements between modules (specifically, the `structure` module depends on `validate`).
31 |
32 | Once you've done that, modify the `stock.py` program so that it looks exactly like this
33 | and that it works:
34 |
35 | ```python
36 | # stock.py
37 |
38 | from structly.structure import Structure
39 |
40 | class Stock(Structure):
41 | name = String()
42 | shares = PositiveInteger()
43 | price = PositiveFloat()
44 |
45 | @property
46 | def cost(self):
47 | return self.shares * self.price
48 |
49 | def sell(self, nshares: PositiveInteger):
50 | self.shares -= nshares
51 |
52 | if __name__ == '__main__':
53 | from structly.reader import read_csv_as_instances
54 | from structly.tableformat import create_formatter, print_table
55 | portfolio = read_csv_as_instances('Data/portfolio.csv', Stock)
56 | formatter = create_formatter('text')
57 | print_table(portfolio, ['name','shares','price'], formatter)
58 | ```
59 |
60 | \[ [Solution](soln9_2.md) | [Index](index.md) | [Exercise 9.1](ex9_1.md) | [Exercise 9.3](ex9_3.md) \]
61 |
62 | ----
63 | `>>>` Advanced Python Mastery
64 | `...` A course by [dabeaz](https://www.dabeaz.com)
65 | `...` Copyright 2007-2023
66 |
67 | . This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
68 |
--------------------------------------------------------------------------------
/Exercises/soln1_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 1.1 - Solution
2 |
3 | Nothing here. Just follow along with the exercise.
4 |
5 |
6 | [Back](ex1_1.md)
7 |
--------------------------------------------------------------------------------
/Exercises/soln1_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 1.2 - Solution
2 |
3 | Solution code is shown in the exercise.
4 |
5 |
6 | [Back](ex1_2.md)
7 |
--------------------------------------------------------------------------------
/Exercises/soln1_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 1.3 - Solution
2 |
3 | ```python
4 | # pcost.py
5 |
6 | total_cost = 0.0
7 |
8 | with open('Data/portfolio.dat', 'r') as f:
9 | for line in f:
10 | fields = line.split()
11 | nshares = int(fields[1])
12 | price = float(fields[2])
13 | total_cost = total_cost + nshares * price
14 |
15 | print(total_cost)
16 | ```
17 |
18 |
19 | [Back](ex1_3.md)
20 |
--------------------------------------------------------------------------------
/Exercises/soln1_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 1.4 - Solution
2 |
3 | ## (a) Defining a function
4 |
5 | ```python
6 | # pcost.py
7 |
8 | def portfolio_cost(filename):
9 | total_cost = 0.0
10 | with open(filename) as f:
11 | for line in f:
12 | fields = line.split()
13 | nshares = int(fields[1])
14 | price = float(fields[2])
15 | total_cost = total_cost + nshares * price
16 | return total_cost
17 |
18 | print(portfolio_cost('Data/portfolio.dat'))
19 | ```
20 |
21 | ## (b) Adding some error handling
22 |
23 | ```python
24 | # pcost.py
25 |
26 | def portfolio_cost(filename):
27 | total_cost = 0.0
28 | with open(filename) as f:
29 | for line in f:
30 | fields = line.split()
31 | try:
32 | nshares = int(fields[1])
33 | price = float(fields[2])
34 | total_cost = total_cost + nshares*price
35 |
36 | # This catches errors in int() and float() conversions above
37 | except ValueError as e:
38 | print("Couldn't parse:", repr(line))
39 | print("Reason:", e)
40 | return total_cost
41 |
42 | print(portfolio_cost('Data/portfolio3.dat'))
43 | ```
44 |
45 |
46 | [Back](ex1_4.md)
47 |
--------------------------------------------------------------------------------
/Exercises/soln1_5.md:
--------------------------------------------------------------------------------
1 | # Exercise 1.5 - Solution
2 |
3 | The solution is shown in the exercise description.
4 |
5 |
6 | [Back](ex1_5.md)
7 |
--------------------------------------------------------------------------------
/Exercises/soln1_6.md:
--------------------------------------------------------------------------------
1 | # Exercise 1.6 - Solution
2 |
3 | ## (b) Main Module
4 |
5 | ```python
6 | # pcost.py
7 |
8 | def portfolio_cost(filename):
9 | total_cost = 0.0
10 | with open(filename) as f:
11 | for line in f:
12 | fields = line.split()
13 | try:
14 | nshares = int(fields[1])
15 | price = float(fields[2])
16 | total_cost = total_cost + nshares * price
17 |
18 | # This catches errors in int() and float() conversions above
19 | except ValueError as e:
20 | print("Couldn't parse:", line)
21 | print("Reason:", e)
22 |
23 | return total_cost
24 |
25 | if __name__ == '__main__':
26 | print(portfolio_cost('Data/portfolio.dat'))
27 | ```
28 |
29 |
30 | [Back](ex1_6.md)
31 |
--------------------------------------------------------------------------------
/Exercises/soln2_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 2.1 - Solution
2 |
3 | ```python
4 | # readrides.py
5 |
6 | import csv
7 |
8 | def read_rides_as_tuples(filename):
9 | '''
10 | Read the bus ride data as a list of tuples
11 | '''
12 | records = []
13 | with open(filename) as f:
14 | rows = csv.reader(f)
15 | headings = next(rows) # Skip headers
16 | for row in rows:
17 | route = row[0]
18 | date = row[1]
19 | daytype = row[2]
20 | rides = int(row[3])
21 | record = (route, date, daytype, rides)
22 | records.append(record)
23 | return records
24 |
25 | def read_rides_as_dicts(filename):
26 | '''
27 | Read the bus ride data as a list of dicts
28 | '''
29 | records = []
30 | with open(filename) as f:
31 | rows = csv.reader(f)
32 | headings = next(rows) # Skip headers
33 | for row in rows:
34 | route = row[0]
35 | date = row[1]
36 | daytype = row[2]
37 | rides = int(row[3])
38 | record = {
39 | 'route': route,
40 | 'date': date,
41 | 'daytype': daytype,
42 | 'rides' : rides
43 | }
44 | records.append(record)
45 | return records
46 |
47 | class Row:
48 | # Uncomment to see effect of slots
49 | # __slots__ = ('route', 'date', 'daytype', 'rides')
50 | def __init__(self, route, date, daytype, rides):
51 | self.route = route
52 | self.date = date
53 | self.daytype = daytype
54 | self.rides = rides
55 |
56 | # Uncomment to use a namedtuple instead
57 | #from collections import namedtuple
58 | #Row = namedtuple('Row',('route','date','daytype','rides'))
59 |
60 | def read_rides_as_instances(filename):
61 | '''
62 | Read the bus ride data as a list of instances
63 | '''
64 | records = []
65 | with open(filename) as f:
66 | rows = csv.reader(f)
67 | headings = next(rows) # Skip headers
68 | for row in rows:
69 | route = row[0]
70 | date = row[1]
71 | daytype = row[2]
72 | rides = int(row[3])
73 | record = Row(route, date, daytype, rides)
74 | records.append(record)
75 | return records
76 |
77 | if __name__ == '__main__':
78 | import tracemalloc
79 | tracemalloc.start()
80 | read_rides = read_rides_as_tuples # Change to as_dicts, as_instances, etc.
81 | rides = read_rides("Data/ctabus.csv")
82 |
83 | print('Memory Use: Current %d, Peak %d' % tracemalloc.get_traced_memory())
84 | ```
85 |
86 |
87 |
88 | [Back](ex2_1.md)
89 |
--------------------------------------------------------------------------------
/Exercises/soln2_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 2.2 - Solution
2 |
3 | There is no official solution for this exercise--you need rely on your
4 | current Python knowledge. However, there are a few tips that can
5 | help.
6 |
7 | - For problems where you need to determine uniqueness, use a set. Sets aren't allowed to
8 | have duplicates.
9 |
10 | - If you need to tabulate data, consider using a dictionary that maps keys to a numeric
11 | count. For example, to count rides on each bus route, you could make a dictionary that
12 | maps the route name to the total ride count for that route. A `Counter` object from
13 | `collections` is perfect for this task.
14 |
15 | - If you need to keep data in order or sort data, store it in a list.
16 |
17 | - You can make Python data structures out of almost anything. For
18 | example, dictionaries of sets, nested dictionaries, etc. You might
19 | need to do this to answer questions 3 and 4.
20 |
21 | Even though no code is provided, here are some answers to the questions
22 | so that you can check your work:
23 |
24 | 1. How many bus routes exist in Chicago? (Answer: 181)
25 |
26 | 2. How many people rode the number 22 bus on February 2, 2011? (Answer: 5055)
27 |
28 | 3. What is the total number of rides taken on each bus route? (Answer: for the top three routes, [('79', 133796763), ('9', 117923787), ('49', 95915008)])
29 |
30 | 4. What five bus routes had the greatest ten-year increase in ridership from 2001 to 2011? (Answer: [('15', 2732209), ('147', 2107910), ('66', 1612958), ('12', 1612067), ('14', 1351308)])
31 |
32 | [Back](ex2_2.md)
33 |
--------------------------------------------------------------------------------
/Exercises/soln2_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 2.3 - Solution
2 |
3 | Work through the exercise. Code is given in the description.
4 |
5 |
6 |
7 | [Back](ex2_3.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln2_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 2.4 - Solution
2 |
3 | ```python
4 | # mutint.py
5 |
6 | from functools import total_ordering
7 |
8 | @total_ordering
9 | class MutInt:
10 | __slots__ = ['value']
11 |
12 | def __init__(self, value):
13 | self.value = value
14 |
15 | def __str__(self):
16 | return str(self.value)
17 |
18 | def __repr__(self):
19 | return f'MutInt({self.value!r})'
20 |
21 | def __format__(self, fmt):
22 | return format(self.value, fmt)
23 |
24 | # Implement the "+" operator. Forward operands (MutInt + other)
25 | def __add__(self, other):
26 | if isinstance(other, MutInt):
27 | return MutInt(self.value + other.value)
28 | elif isinstance(other, int):
29 | return MutInt(self.value + other)
30 | else:
31 | return NotImplemented
32 |
33 | # Support for reversed operands (other + MutInt)
34 | __radd__ = __add__
35 |
36 | # Support for in-place update (MutInt += other)
37 | def __iadd__(self, other):
38 | if isinstance(other, MutInt):
39 | self.value += other.value
40 | return self
41 | elif isinstance(other, int):
42 | self.value += other
43 | return self
44 | else:
45 | return NotImplemented
46 |
47 | # Support for equality testing
48 | def __eq__(self, other):
49 | if isinstance(other, MutInt):
50 | return self.value == other.value
51 | elif isinstance(other, int):
52 | return self.value == other
53 | else:
54 | return NotImplemented
55 |
56 | # One relation is needed for @total_ordering decorator. It fills in others
57 | def __lt__(self, other):
58 | if isinstance(other, MutInt):
59 | return self.value < other.value
60 | elif isinstance(other, int):
61 | return self.value < other
62 | else:
63 | return NotImplemented
64 |
65 | # Conversions to int() and float()
66 | def __int__(self):
67 | return int(self.value)
68 |
69 | def __float__(self):
70 | return float(self.value)
71 |
72 | # Support for indexing s[MutInt]
73 | __index__ = __int__
74 | ```
75 |
76 |
77 |
78 | [Back](ex2_4.md)
79 |
--------------------------------------------------------------------------------
/Exercises/soln2_6.md:
--------------------------------------------------------------------------------
1 | # Exercise 2.6 - Solution
2 |
3 | ```python
4 | # reader.py
5 |
6 | import csv
7 | from collections import defaultdict
8 |
9 | def read_csv_as_dicts(filename, types):
10 | '''
11 | Read a CSV file with column type conversion
12 | '''
13 | records = []
14 | with open(filename) as f:
15 | rows = csv.reader(f)
16 | headers = next(rows)
17 | for row in rows:
18 | record = { name: func(val) for name, func, val in zip(headers, types, row) }
19 | records.append(record)
20 | return records
21 | ```
22 |
23 |
24 |
25 | [Back](ex2_6.md)
26 |
--------------------------------------------------------------------------------
/Exercises/soln3_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 3.1 - Solution
2 |
3 | ```python
4 | # stock.py
5 |
6 | class Stock:
7 | def __init__(self, name, shares, price):
8 | self.name = name
9 | self.shares = shares
10 | self.price = price
11 |
12 | def cost(self):
13 | return self.shares * self.price
14 |
15 | def sell(self, nshares):
16 | self.shares -= nshares
17 |
18 | def read_portfolio(filename):
19 | '''
20 | Read a CSV file of stock data into a list of Stocks
21 | '''
22 | import csv
23 | portfolio = []
24 | with open(filename) as f:
25 | rows = csv.reader(f)
26 | headers = next(rows)
27 | for row in rows:
28 | record = Stock(row[0], int(row[1]), float(row[2]))
29 | portfolio.append(record)
30 | return portfolio
31 |
32 | def print_portfolio(portfolio):
33 | '''
34 | Make a nicely formatted table showing stock data
35 | '''
36 | print('%10s %10s %10s' % ('name', 'shares', 'price'))
37 | print(('-'*10 + ' ')*3)
38 | for s in portfolio:
39 | print('%10s %10d %10.2f' % (s.name, s.shares, s.price))
40 |
41 | if __name__ == '__main__':
42 | portfolio = read_portfolio('Data/portfolio.csv')
43 | print_portfolio(portfolio)
44 | ```
45 |
46 |
47 |
48 | [Back](ex3_1.md)
49 |
--------------------------------------------------------------------------------
/Exercises/soln3_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 3.2 - Solution
2 |
3 | (c) Table Formatter
4 |
5 | ```python
6 | # tableformat.py
7 |
8 | # Print a table
9 | def print_table(records, fields):
10 | print(' '.join('%10s' % fieldname for fieldname in fields))
11 | print(('-'*10 + ' ')*len(fields))
12 | for record in records:
13 | print(' '.join('%10s' % getattr(record, fieldname) for fieldname in fields))
14 | ```
15 |
16 |
17 |
18 | [Back](ex3_2.md)
19 |
--------------------------------------------------------------------------------
/Exercises/soln3_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 3.3 - Solution
2 |
3 | ```python
4 | # stock.py
5 |
6 | class Stock:
7 | types = (str, int, float)
8 | def __init__(self, name, shares, price):
9 | self.name = name
10 | self.shares = shares
11 | self.price = price
12 |
13 | def cost(self):
14 | return self.shares * self.price
15 |
16 | def sell(self, nshares):
17 | self.shares -= nshares
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | values = [func(val) for func, val in zip(cls.types, row)]
22 | return cls(*values)
23 |
24 | def read_portfolio(filename):
25 | '''
26 | Read a CSV file of stock data into a list of Stocks
27 | '''
28 | import csv
29 | portfolio = []
30 | with open(filename) as f:
31 | rows = csv.reader(f)
32 | headers = next(rows)
33 | for row in rows:
34 | record = Stock.from_row(row)
35 | portfolio.append(record)
36 | return portfolio
37 |
38 | if __name__ == '__main__':
39 | import tableformat
40 | import reader
41 | portfolio = read_portfolio('Data/portfolio.csv')
42 |
43 | # Generalized version
44 | # portfolio = reader.read_csv_as_instances('Data/portfolio.csv', Stock)
45 | tableformat.print_table(portfolio, ['name', 'shares', 'price'])
46 | ```
47 |
48 | The `reader.py` file should now look like this:
49 |
50 | ```python
51 | # reader.py
52 |
53 | import csv
54 | from collections import defaultdict
55 |
56 | def read_csv_as_dicts(filename, types):
57 | '''
58 | Read a CSV file into a list of dicts with column type conversion
59 | '''
60 | records = []
61 | with open(filename) as f:
62 | rows = csv.reader(f)
63 | headers = next(rows)
64 | for row in rows:
65 | record = { name: func(val) for name, func, val in zip(headers, types, row) }
66 | records.append(record)
67 | return records
68 |
69 | def read_csv_as_instances(filename, cls):
70 | '''
71 | Read a CSV file into a list of instances
72 | '''
73 | records = []
74 | with open(filename) as f:
75 | rows = csv.reader(f)
76 | headers = next(rows)
77 | for row in rows:
78 | records.append(cls.from_row(row))
79 | return records
80 | ```
81 |
82 |
83 |
84 | [Back](ex3_3.md)
85 |
--------------------------------------------------------------------------------
/Exercises/soln3_5.md:
--------------------------------------------------------------------------------
1 | # Exercise 3.5 - Solution
2 |
3 | ```python
4 | # tableformat.py
5 |
6 | def print_table(records, fields, formatter):
7 | formatter.headings(fields)
8 | for r in records:
9 | rowdata = [getattr(r, fieldname) for fieldname in fields]
10 | formatter.row(rowdata)
11 |
12 | class TableFormatter:
13 | def headings(self, headers):
14 | raise NotImplementedError()
15 |
16 | def row(self, rowdata):
17 | raise NotImplementedError()
18 |
19 | class TextTableFormatter(TableFormatter):
20 | def headings(self, headers):
21 | print(' '.join('%10s' % h for h in headers))
22 | print(('-'*10 + ' ')*len(headers))
23 |
24 | def row(self, rowdata):
25 | print(' '.join('%10s' % d for d in rowdata))
26 |
27 | class CSVTableFormatter(TableFormatter):
28 | def headings(self, headers):
29 | print(','.join(headers))
30 |
31 | def row(self, rowdata):
32 | print(','.join(str(d) for d in rowdata))
33 |
34 | class HTMLTableFormatter(TableFormatter):
35 | def headings(self, headers):
36 | print('
', end=' ')
37 | for h in headers:
38 | print('%s | ' % h, end=' ')
39 | print('
')
40 |
41 | def row(self, rowdata):
42 | print('', end=' ')
43 | for d in rowdata:
44 | print('%s | ' % d, end=' ')
45 | print('
')
46 |
47 | def create_formatter(name):
48 | if name == 'text':
49 | formatter_cls = TextTableFormatter
50 | elif name == 'csv':
51 | formatter_cls = CSVTableFormatter
52 | elif name == 'html':
53 | formatter_cls = HTMLTableFormatter
54 | else:
55 | raise RuntimeError('Unknown format %s' % name)
56 | return formatter_cls()
57 | ```
58 |
59 |
60 |
61 | [Back](ex3_5.md)
62 |
--------------------------------------------------------------------------------
/Exercises/soln3_6.md:
--------------------------------------------------------------------------------
1 | # Exercise 3.6 - Solution
2 |
3 | ## (a) Better output for printing objects
4 |
5 | ```python
6 | # stock.py
7 |
8 | class Stock:
9 | ...
10 |
11 | def __repr__(self):
12 | # Note: The !r format code produces the repr() string
13 | return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})'
14 | ...
15 | ```
16 |
17 | ## (b) Making Objects Comparable
18 |
19 | ```python
20 | class Stock:
21 | ...
22 | def __eq__(self, other):
23 | return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
24 | (other.name, other.shares, other.price))
25 | ...
26 | ```
27 |
28 | ## (c) Context Managers
29 |
30 | Code is given in the exercise.
31 |
32 |
33 |
34 | [Back](ex3_6.md)
35 |
--------------------------------------------------------------------------------
/Exercises/soln3_7.md:
--------------------------------------------------------------------------------
1 |
2 | # Exercise 3.7 - Solution
3 |
4 | ## (a) Interfaces
5 |
6 | ```python
7 | # tableformat.py
8 |
9 | def print_table(records, fields, formatter):
10 | if not isinstance(formatter, TableFormatter):
11 | raise TypeError('Expected a TableFormatter')
12 |
13 | formatter.headings(fields)
14 | for r in records:
15 | rowdata = [getattr(r, fieldname) for fieldname in fields]
16 | formatter.row(rowdata)
17 | ...
18 | ```
19 |
20 | ## (b) Abstract Base Classes
21 |
22 | ```python
23 | # tableformat.py
24 | from abc import ABC, abstractmethod
25 |
26 | class TableFormatter(ABC):
27 | @abstractmethod
28 | def headings(self, headers):
29 | pass
30 |
31 | @abstractmethod
32 | def row(self, rowdata):
33 | pass
34 | ```
35 |
36 | ## (c) Algorithm Template Classes
37 |
38 | ```python
39 | # reader.py
40 |
41 | import csv
42 | from abc import ABC, abstractmethod
43 |
44 | class CSVParser(ABC):
45 | def parse(self, filename):
46 | records = []
47 | with open(filename) as f:
48 | rows = csv.reader(f)
49 | headers = next(rows)
50 | for row in rows:
51 | record = self.make_record(headers, row)
52 | records.append(record)
53 | return records
54 |
55 | @abstractmethod
56 | def make_record(self, headers, row):
57 | pass
58 |
59 | class DictCSVParser(CSVParser):
60 | def __init__(self, types):
61 | self.types = types
62 |
63 | def make_record(self, headers, row):
64 | return { name: func(val) for name, func, val in zip(headers, self.types, row) }
65 |
66 | class InstanceCSVParser(CSVParser):
67 | def __init__(self, cls):
68 | self.cls = cls
69 |
70 | def make_record(self, headers, row):
71 | return self.cls.from_row(row)
72 |
73 | def read_csv_as_dicts(filename, types):
74 | parser = DictCSVParser(types)
75 | return parser.parse(filename)
76 |
77 | def read_csv_as_instances(filename, cls):
78 | parser = InstanceCSVParser(cls)
79 | return parser.parse(filename)
80 | ```
81 |
82 |
83 |
84 |
85 | [Back](ex3_7.md)
86 |
--------------------------------------------------------------------------------
/Exercises/soln3_8.md:
--------------------------------------------------------------------------------
1 | # Exercise 3.8 - Solution
2 |
3 | ```python
4 | # tableformat.py
5 | from abc import ABC, abstractmethod
6 |
7 | def print_table(records, fields, formatter):
8 | if not isinstance(formatter, TableFormatter):
9 | raise RuntimeError('Expected a TableFormatter')
10 |
11 | formatter.headings(fields)
12 | for r in records:
13 | rowdata = [getattr(r, fieldname) for fieldname in fields]
14 | formatter.row(rowdata)
15 |
16 | class TableFormatter(ABC):
17 | @abstractmethod
18 | def headings(self, headers):
19 | pass
20 |
21 | @abstractmethod
22 | def row(self, rowdata):
23 | pass
24 |
25 |
26 | class TextTableFormatter(TableFormatter):
27 | def headings(self, headers):
28 | print(' '.join('%10s' % h for h in headers))
29 | print(('-'*10 + ' ')*len(headers))
30 |
31 | def row(self, rowdata):
32 | print(' '.join('%10s' % d for d in rowdata))
33 |
34 | class CSVTableFormatter(TableFormatter):
35 | def headings(self, headers):
36 | print(','.join(headers))
37 |
38 | def row(self, rowdata):
39 | print(','.join(str(d) for d in rowdata))
40 |
41 | class HTMLTableFormatter(TableFormatter):
42 | def headings(self, headers):
43 | print('', end=' ')
44 | for h in headers:
45 | print('%s | ' % h, end=' ')
46 | print('
')
47 |
48 | def row(self, rowdata):
49 | print('', end=' ')
50 | for d in rowdata:
51 | print('%s | ' % d, end=' ')
52 | print('
')
53 |
54 |
55 | class ColumnFormatMixin:
56 | formats = []
57 | def row(self, rowdata):
58 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
59 | super().row(rowdata)
60 |
61 | class UpperHeadersMixin:
62 | def headings(self, headers):
63 | super().headings([h.upper() for h in headers])
64 |
65 | def create_formatter(name, column_formats=None, upper_headers=False):
66 | if name == 'text':
67 | formatter_cls = TextTableFormatter
68 | elif name == 'csv':
69 | formatter_cls = CSVTableFormatter
70 | elif name == 'html':
71 | formatter_cls = HTMLTableFormatter
72 | else:
73 | raise RuntimeError('Unknown format %s' % name)
74 |
75 | if column_formats:
76 | class formatter_cls(ColumnFormatMixin, formatter_cls):
77 | formats = column_formats
78 |
79 | if upper_headers:
80 | class formatter_cls(UpperHeadersMixin, formatter_cls):
81 | pass
82 |
83 | return formatter_cls()
84 | ```
85 |
86 |
87 |
88 | [Back](ex3_8.md)
89 |
--------------------------------------------------------------------------------
/Exercises/soln4_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 4.1 - Solution
2 |
3 | No solution. Just follow along.
4 |
5 |
6 |
7 | [Back](ex4_1.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln4_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 4.2 - Solution
2 |
3 | Here is the validator code in its entirety:
4 |
5 | ```python
6 | class Validator:
7 | @classmethod
8 | def check(cls, value):
9 | return value
10 |
11 | class Typed(Validator):
12 | expected_type = object
13 | @classmethod
14 | def check(cls, value):
15 | if not isinstance(value, cls.expected_type):
16 | raise TypeError(f'Expected {cls.expected_type}')
17 | return super().check(value)
18 |
19 | class Integer(Typed):
20 | expected_type = int
21 |
22 | class Float(Typed):
23 | expected_type = float
24 |
25 | class String(Typed):
26 | expected_type = str
27 |
28 | class Positive(Validator):
29 | @classmethod
30 | def check(cls, value):
31 | if value < 0:
32 | raise ValueError('Must be >= 0')
33 | return super().check(value)
34 |
35 | class NonEmpty(Validator):
36 | @classmethod
37 | def check(cls, value):
38 | if len(value) == 0:
39 | raise ValueError('Must be non-empty')
40 | return super().check(value)
41 |
42 | class PositiveInteger(Integer, Positive):
43 | pass
44 |
45 | class PositiveFloat(Float, Positive):
46 | pass
47 |
48 | class NonEmptyString(String, NonEmpty):
49 | pass
50 | ```
51 |
52 | ## (c) Using the validators
53 |
54 | ```python
55 | # validate.py
56 | ...
57 |
58 | if __name__ == '__main__':
59 | class Stock:
60 | __slots__ = ('name', '_shares', '_price')
61 | def __init__(self, name, shares, price):
62 | self.name = name
63 | self.shares = shares
64 | self.price = price
65 |
66 | def __repr__(self):
67 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
68 |
69 | @property
70 | def shares(self):
71 | return self._shares
72 | @shares.setter
73 | def shares(self, value):
74 | self._shares = PositiveInteger.check(value)
75 |
76 | @property
77 | def price(self):
78 | return self._price
79 | @price.setter
80 | def price(self, value):
81 | self._price = PositiveFloat.check(value)
82 |
83 | @property
84 | def cost(self):
85 | return self.shares * self.price
86 |
87 | def sell(self, nshares):
88 | self.shares -= nshares
89 | ```
90 |
91 |
92 |
93 | [Back](ex4_2.md)
94 |
--------------------------------------------------------------------------------
/Exercises/soln4_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 4.3 - Solution
2 |
3 | ```python
4 | class Validator:
5 | def __init__(self, name=None):
6 | self.name = name
7 |
8 | def __set_name__(self, cls, name):
9 | self.name = name
10 |
11 | @classmethod
12 | def check(cls, value):
13 | return value
14 |
15 | def __set__(self, instance, value):
16 | instance.__dict__[self.name] = self.check(value)
17 |
18 | class Typed(Validator):
19 | expected_type = object
20 | @classmethod
21 | def check(cls, value):
22 | if not isinstance(value, cls.expected_type):
23 | raise TypeError(f'expected {cls.expected_type}')
24 | return super().check(value)
25 |
26 | class Integer(Typed):
27 | expected_type = int
28 |
29 | class Float(Typed):
30 | expected_type = float
31 |
32 | class String(Typed):
33 | expected_type = str
34 |
35 | class Positive(Validator):
36 | @classmethod
37 | def check(cls, value):
38 | if value < 0:
39 | raise ValueError('must be >= 0')
40 | return super().check(value)
41 |
42 | class NonEmpty(Validator):
43 | @classmethod
44 | def check(cls, value):
45 | if len(value) == 0:
46 | raise ValueError('must be non-empty')
47 | return super().check(value)
48 |
49 | class PositiveInteger(Integer, Positive):
50 | pass
51 |
52 | class PositiveFloat(Float, Positive):
53 | pass
54 |
55 | class NonEmptyString(String, NonEmpty):
56 | pass
57 |
58 | # Examples
59 | if __name__ == '__main__':
60 | def add(x, y):
61 | Integer.check(x)
62 | Integer.check(y)
63 | return x + y
64 |
65 | class Stock:
66 | name = NonEmptyString()
67 | shares = PositiveInteger()
68 | price = PositiveFloat()
69 | def __init__(self,name,shares,price):
70 | self.name = name
71 | self.shares = shares
72 | self.price = price
73 |
74 | def __repr__(self):
75 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
76 |
77 | @property
78 | def cost(self):
79 | return self.shares * self.price
80 |
81 | def sell(self, nshares):
82 | self.shares -= nshares
83 | ```
84 |
85 |
86 |
87 | [Back](ex4_3.md)
88 |
--------------------------------------------------------------------------------
/Exercises/soln4_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 4.4 - Solution
2 |
3 | Code is given in the exercise.
4 |
5 |
6 |
7 | [Back](ex4_4.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln5_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 5.1 - Solution
2 |
3 | This is a partial solution that does not include type-hints.
4 |
5 | ```python
6 | # reader.py
7 |
8 | import csv
9 |
10 | def csv_as_dicts(lines, types, *, headers=None):
11 | '''
12 | Convert lines of CSV data into a list of dictionaries
13 | '''
14 | records = []
15 | rows = csv.reader(lines)
16 | if headers is None:
17 | headers = next(rows)
18 | for row in rows:
19 | record = { name: func(val)
20 | for name, func, val in zip(headers, types, row) }
21 | records.append(record)
22 | return records
23 |
24 | def csv_as_instances(lines, cls, *, headers=None):
25 | '''
26 | Convert lines of CSV data into a list of instances
27 | '''
28 | records = []
29 | rows = csv.reader(lines)
30 | if headers is None:
31 | headers = next(rows)
32 | for row in rows:
33 | record = cls.from_row(row)
34 | records.append(record)
35 | return records
36 |
37 | def read_csv_as_dicts(filename, types, *, headers=None):
38 | '''
39 | Read CSV data into a list of dictionaries with optional type conversion
40 | '''
41 | with open(filename) as file:
42 | return csv_as_dicts(file, types, headers=headers)
43 |
44 | def read_csv_as_instances(filename, cls, *, headers=None):
45 | '''
46 | Read CSV data into a list of instances
47 | '''
48 | with open(filename) as file:
49 | return csv_as_instances(file, cls, headers=headers)
50 | ```
51 |
52 |
53 |
54 |
55 | [Back](ex5_1.md)
56 |
--------------------------------------------------------------------------------
/Exercises/soln5_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 5.2 - Solution
2 |
3 | None. Just work through the exercise.
4 |
5 |
6 |
7 | [Back](ex5_2.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln5_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 5.3 - Solution
2 |
3 | ## (a) Higher order functions
4 |
5 | ```python
6 | # reader.py
7 |
8 | import csv
9 |
10 | def convert_csv(lines, converter, *, headers=None):
11 | records = []
12 | rows = csv.reader(lines)
13 | if headers is None:
14 | headers = next(rows)
15 | for row in rows:
16 | record = converter(headers, row)
17 | records.append(record)
18 | return records
19 |
20 | def csv_as_dicts(lines, types, *, headers=None):
21 | return convert_csv(lines,
22 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
23 |
24 | def csv_as_instances(lines, cls, *, headers=None):
25 | return convert_csv(lines,
26 | lambda headers, row: cls.from_row(row))
27 |
28 | def read_csv_as_dicts(filename, types, *, headers=None):
29 | '''
30 | Read CSV data into a list of dictionaries with optional type conversion
31 | '''
32 | with open(filename) as file:
33 | return csv_as_dicts(file, types, headers=headers)
34 |
35 | def read_csv_as_instances(filename, cls, *, headers=None):
36 | '''
37 | Read CSV data into a list of instances
38 | '''
39 | with open(filename) as file:
40 | return csv_as_instances(file, cls, headers=headers)
41 | ```
42 |
43 | ## (b) Using map()
44 |
45 | ```python
46 | # reader.py
47 |
48 | import csv
49 |
50 | def convert_csv(lines, converter, *, headers=None):
51 | rows = csv.reader(lines)
52 | if headers is None:
53 | headers = next(rows)
54 | return list(map(lambda row: converter(headers, row), rows))
55 | ```
56 |
57 |
58 |
59 | [Back](ex5_3.md)
60 |
--------------------------------------------------------------------------------
/Exercises/soln5_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 5.4 - Solution
2 |
3 | ```python
4 | # typedproperty.py
5 |
6 | def typedproperty(name, expected_type):
7 | private_name = '_' + name
8 |
9 | @property
10 | def value(self):
11 | return getattr(self, private_name)
12 |
13 | @value.setter
14 | def value(self, val):
15 | if not isinstance(val, expected_type):
16 | raise TypeError(f'Expected {expected_type}')
17 | setattr(self, private_name, val)
18 |
19 | return value
20 |
21 |
22 | String = lambda name: typedproperty(name, str)
23 | Integer = lambda name: typedproperty(name, int)
24 | Float = lambda name: typedproperty(name, float)
25 |
26 | # Example
27 | if __name__ == '__main__':
28 | class Stock:
29 | name = String('name')
30 | shares = Integer('shares')
31 | price = Float('price')
32 | def __init__(self, name, shares, price):
33 | self.name = name
34 | self.shares = shares
35 | self.price = price
36 | ```
37 |
38 |
39 | [Back](ex5_4.md)
40 |
--------------------------------------------------------------------------------
/Exercises/soln5_5.md:
--------------------------------------------------------------------------------
1 | # Exercise 5.5 - Solution
2 |
3 | ```python
4 | # reader.py
5 |
6 | import csv
7 | import logging
8 |
9 | log = logging.getLogger(__name__)
10 |
11 | def convert_csv(lines, converter, *, headers=None):
12 | rows = csv.reader(lines)
13 | if headers is None:
14 | headers = next(rows)
15 |
16 | records = []
17 | for rowno, row in enumerate(rows, start=1):
18 | try:
19 | records.append(converter(headers, row))
20 | except ValueError as e:
21 | log.warning('Row %s: Bad row: %s', rowno, row)
22 | log.debug('Row %s: Reason: %s', rowno, row)
23 | return records
24 |
25 | def csv_as_dicts(lines, types, *, headers=None):
26 | return convert_csv(lines,
27 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
28 |
29 | def csv_as_instances(lines, cls, *, headers=None):
30 | return convert_csv(lines,
31 | lambda headers, row: cls.from_row(row))
32 |
33 | def read_csv_as_dicts(filename, types, *, headers=None):
34 | '''
35 | Read CSV data into a list of dictionaries with optional type conversion
36 | '''
37 | with open(filename) as file:
38 | return csv_as_dicts(file, types, headers=headers)
39 |
40 | def read_csv_as_instances(filename, cls, *, headers=None):
41 | '''
42 | Read CSV data into a list of instances
43 | '''
44 | with open(filename) as file:
45 | return csv_as_instances(file, cls, headers=headers)
46 | ```
47 |
48 |
49 |
50 | [Back](ex5_5.md)
51 |
--------------------------------------------------------------------------------
/Exercises/soln6_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 6.1 - Solution
2 |
3 | ## (a) Simplified Structures
4 |
5 | ```python
6 | # structure.py
7 |
8 | class Structure:
9 | _fields = ()
10 | def __init__(self, *args):
11 | if len(args) != len(self._fields):
12 | raise TypeError('Expected %d arguments' % len(self._fields))
13 | for name, arg in zip(self._fields,args):
14 | setattr(self, name, arg)
15 | ```
16 |
17 | ## (b) Making a Useful Representation
18 |
19 | [source, python]
20 | ```
21 | class Structure:
22 | ...
23 | def __repr__(self):
24 | return '%s(%s)' % (type(self).__name__,
25 | ', '.join(repr(getattr(self, name)) for name in self._fields))
26 | ```
27 |
28 | ## (c) Restricting Attribute Names
29 |
30 | [source, python]
31 | ```
32 | class Structure:
33 | ...
34 | def __setattr__(self, name, value):
35 | if name.startswith('_') or name in self._fields:
36 | super().__setattr__(name, value)
37 | else:
38 | raise AttributeError('No attribute %s' % name)
39 | ```
40 |
41 |
42 |
43 | [Back](ex6_1.md)
44 |
--------------------------------------------------------------------------------
/Exercises/soln6_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 6.2 - Solution
2 |
3 | ```python
4 | # structure.py
5 |
6 | import sys
7 |
8 | class Structure:
9 | _fields = ()
10 |
11 | @staticmethod
12 | def _init():
13 | locs = sys._getframe(1).f_locals
14 | self = locs.pop('self')
15 | for name, val in locs.items():
16 | setattr(self, name, val)
17 |
18 | def __setattr__(self, name, value):
19 | if name.startswith('_') or name in self._fields:
20 | super().__setattr__(name, value)
21 | else:
22 | raise AttributeError('No attribute %s' % name)
23 |
24 | def __repr__(self):
25 | return '%s(%s)' % (type(self).__name__,
26 | ', '.join(repr(getattr(self, name)) for name in self._fields))
27 | ```
28 |
29 |
30 |
31 |
32 | [Back](ex6_2.md)
33 |
--------------------------------------------------------------------------------
/Exercises/soln6_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 6.3 - Solution
2 |
3 | ```python
4 | # structure.py
5 |
6 | import sys
7 | import inspect
8 |
9 | class Structure:
10 | _fields = ()
11 |
12 | @staticmethod
13 | def _init():
14 | locs = sys._getframe(1).f_locals
15 | self = locs.pop('self')
16 | for name, val in locs.items():
17 | setattr(self, name, val)
18 |
19 | def __setattr__(self, name, value):
20 | if name.startswith('_') or name in self._fields:
21 | super().__setattr__(name, value)
22 | else:
23 | raise AttributeError('No attribute %s' % name)
24 |
25 | def __repr__(self):
26 | return '%s(%s)' % (type(self).__name__,
27 | ', '.join(repr(getattr(self, name)) for name in self._fields))
28 |
29 | @classmethod
30 | def set_fields(cls):
31 | sig = inspect.signature(cls)
32 | cls._fields = tuple(sig.parameters)
33 | ```
34 |
35 | Here is the `Stock` class in progress:
36 |
37 | ```python
38 | # stock.py
39 |
40 | from structure import Structure
41 |
42 | class Stock(Structure):
43 | def __init__(self, name, shares, price):
44 | self._init()
45 |
46 | @property
47 | def cost(self):
48 | return self.shares * self.price
49 |
50 | def sell(self, nshares):
51 | self.shares -= nshares
52 |
53 | Stock.set_fields()
54 | ```
55 |
56 |
57 |
58 | [Back](ex6_3.md)
59 |
--------------------------------------------------------------------------------
/Exercises/soln6_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 6.4 - Solution
2 |
3 | ```python
4 | # structure.py
5 |
6 | class Structure:
7 | _fields = ()
8 |
9 | def __setattr__(self, name, value):
10 | if name.startswith('_') or name in self._fields:
11 | super().__setattr__(name, value)
12 | else:
13 | raise AttributeError('No attribute %s' % name)
14 |
15 | def __repr__(self):
16 | return '%s(%s)' % (type(self).__name__,
17 | ', '.join(repr(getattr(self, name)) for name in self._fields))
18 |
19 | @classmethod
20 | def create_init(cls):
21 | args = ','.join(cls._fields)
22 | code = 'def __init__(self, {0}):\n'.format(args)
23 | statements = [ ' self.{0} = {0}'.format(name) for name in cls._fields]
24 | code += '\n'.join(statements)
25 | locs = { }
26 | exec(code, locs)
27 | cls.__init__ = locs['__init__']
28 | ```
29 |
30 | Here is the `Stock` class in progress after this change:
31 |
32 | ```python
33 | # stock.py
34 |
35 | from structure import Structure
36 |
37 | class Stock(Structure):
38 | _fields = ('name', 'shares', 'price')
39 |
40 | @property
41 | def cost(self):
42 | return self.shares * self.price
43 |
44 | def sell(self, nshares):
45 | self.shares -= nshares
46 |
47 | Stock.create_init()
48 | ```
49 |
50 |
51 |
52 | [Back](ex6_4.md)
53 |
--------------------------------------------------------------------------------
/Exercises/soln6_5.md:
--------------------------------------------------------------------------------
1 | # Exercise 6.5 - Solution
2 |
3 | ```python
4 | # validate.py
5 | ...
6 |
7 | from inspect import signature
8 |
9 | class ValidatedFunction:
10 | def __init__(self, func):
11 | self.func = func
12 | self.signature = signature(func)
13 | self.annotations = dict(func.__annotations__)
14 | self.retcheck = self.annotations.pop('return', None)
15 |
16 | def __call__(self, *args, **kwargs):
17 | bound = self.signature.bind(*args, **kwargs)
18 |
19 | for name, val in self.annotations.items():
20 | val.check(bound.arguments[name])
21 |
22 | result = self.func(*args, **kwargs)
23 |
24 | if self.retcheck:
25 | self.retcheck.check(result)
26 |
27 | return result
28 |
29 | # Examples
30 | if __name__ == '__main__':
31 | def add(x:Integer, y:Integer) -> Integer:
32 | return x + y
33 |
34 | add = ValidatedFunction(add)
35 | ```
36 |
37 |
38 |
39 | [Back](ex6_5.md)
40 |
--------------------------------------------------------------------------------
/Exercises/soln7_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 7.1 - Solution
2 |
3 | ```python
4 | # validate.py
5 | ...
6 |
7 | from inspect import signature
8 |
9 | def validated(func):
10 | sig = signature(func)
11 |
12 | # Gather the function annotations
13 | annotations = dict(func.__annotations__)
14 |
15 | # Get the return annotation (if any)
16 | retcheck = annotations.pop('return', None)
17 |
18 | def wrapper(*args, **kwargs):
19 | bound = sig.bind(*args, **kwargs)
20 | errors = []
21 |
22 | # Enforce argument checks
23 | for name, validator in annotations.items():
24 | try:
25 | validator.check(bound.arguments[name])
26 | except Exception as e:
27 | errors.append(f' {name}: {e}')
28 |
29 | if errors:
30 | raise TypeError('Bad Arguments\n' + '\n'.join(errors))
31 |
32 | result = func(*args, **kwargs)
33 |
34 | # Enforce return check (if any)
35 | if retcheck:
36 | try:
37 | retcheck.check(result)
38 | except Exception as e:
39 | raise TypeError(f'Bad return: {e}') from None
40 | return result
41 |
42 | return wrapper
43 | ```
44 |
45 |
46 |
47 | [Back](ex7_1.md)
48 |
--------------------------------------------------------------------------------
/Exercises/soln7_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 7.3 - Solution
2 |
3 | Here's a full version of the `structure.py` file.
4 |
5 | ```python
6 | # structure.py
7 |
8 | from validate import Validator, validated
9 |
10 | class Structure:
11 | _fields = ()
12 | _types = ()
13 |
14 | def __setattr__(self, name, value):
15 | if name.startswith('_') or name in self._fields:
16 | super().__setattr__(name, value)
17 | else:
18 | raise AttributeError('No attribute %s' % name)
19 |
20 | def __repr__(self):
21 | return '%s(%s)' % (type(self).__name__,
22 | ', '.join(repr(getattr(self, name)) for name in self._fields))
23 |
24 | @classmethod
25 | def from_row(cls, row):
26 | rowdata = [ func(val) for func, val in zip(cls._types, row) ]
27 | return cls(*rowdata)
28 |
29 | @classmethod
30 | def create_init(cls):
31 | '''
32 | Create an __init__ method from _fields
33 | '''
34 | args = ','.join(cls._fields)
35 | code = f'def __init__(self, {args}):\n'
36 | for name in cls._fields:
37 | code += f' self.{name} = {name}\n'
38 | locs = { }
39 | exec(code, locs)
40 | cls.__init__ = locs['__init__']
41 |
42 | @classmethod
43 | def __init_subclass__(cls):
44 | # Apply the validated decorator to subclasses
45 | validate_attributes(cls)
46 |
47 | def validate_attributes(cls):
48 | '''
49 | Class decorator that scans a class definition for Validators
50 | and builds a _fields variable that captures their definition order.
51 | '''
52 | validators = []
53 | for name, val in vars(cls).items():
54 | if isinstance(val, Validator):
55 | validators.append(val)
56 |
57 | # Apply validated decorator to any callable with annotations
58 | elif callable(val) and val.__annotations__:
59 | setattr(cls, name, validated(val))
60 |
61 | # Collect all of the field names
62 | cls._fields = tuple([v.name for v in validators])
63 |
64 | # Collect type conversions. The lambda x:x is an identity
65 | # function that's used in case no expected_type is found.
66 | cls._types = tuple([ getattr(v, 'expected_type', lambda x: x)
67 | for v in validators ])
68 |
69 | # Create the __init__ method
70 | if cls._fields:
71 | cls.create_init()
72 |
73 | return cls
74 | ```
75 |
76 |
77 |
78 | [Back](ex7_3.md)
79 |
--------------------------------------------------------------------------------
/Exercises/soln7_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 7.4 - Solution
2 |
3 | All of the solution code is given in the exercise description.
4 |
5 |
6 |
7 | [Back](ex7_4.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln7_5.md:
--------------------------------------------------------------------------------
1 | # Exercise 7.5 - Solution
2 |
3 | Nothing here. Just follow the exercise.
4 |
5 |
6 |
7 | [Back](ex7_5.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln8_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 8.1 - Solution
2 |
3 | ## (b) Adding Iteration to Objects
4 |
5 | ```python
6 | # structure.py
7 | ...
8 |
9 | class Structure(metaclass=StructureMeta):
10 | ...
11 | def __iter__(self):
12 | for name in self._fields:
13 | yield getattr(self, name)
14 | ...
15 | ```
16 |
17 | ## (c) The Surprising Power of Iteration
18 |
19 | ```python
20 | # structure.py
21 | ...
22 |
23 | class Structure(metaclass=StructureMeta):
24 | ...
25 | def __eq__(self, other):
26 | return isinstance(other, type(self)) and tuple(self) == tuple(other)
27 | ...
28 | ```
29 |
30 | ## (d) Monitoring a streaming data source
31 |
32 | ```python
33 | # follow.py
34 | import os
35 | import time
36 |
37 | def follow(filename):
38 | '''
39 | Generator that produces a sequence of lines being written at the end of a file.
40 | '''
41 | with open(filename,'r') as f:
42 | f.seek(0,os.SEEK_END)
43 | while True:
44 | line = f.readline()
45 | if line == '':
46 | time.sleep(0.1) # Sleep briefly to avoid busy wait
47 | continue
48 | yield line
49 |
50 | # Example use
51 | if __name__ == '__main__':
52 | for line in follow('Data/stocklog.csv'):
53 | row = line.split(',')
54 | name = row[0].strip('"')
55 | price = float(row[1])
56 | change = float(row[4])
57 | if change < 0:
58 | print('%10s %10.2f %10.2f' % (name, price, change))
59 | ```
60 |
61 | **Discussion**
62 |
63 | The `time.sleep()` function is being used here to avoid busy-waiting
64 | on the CPU (e.g., sitting in an infinite loop with 100% CPU use while waiting for new lines to arrive)
65 |
66 |
67 |
68 | [Back](ex8_1.md)
69 |
--------------------------------------------------------------------------------
/Exercises/soln8_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 8.2 - Solution
2 |
3 | ```python
4 | # ticker.py
5 | from structure import Structure
6 |
7 | class Ticker(Structure):
8 | name = String()
9 | price = Float()
10 | date = String()
11 | time = String()
12 | change = Float()
13 | open = Float()
14 | high = Float()
15 | low = Float()
16 | volume = Integer()
17 |
18 | if __name__ == '__main__':
19 | from follow import follow
20 | import csv
21 | from tableformat import create_formatter, print_table
22 |
23 | formatter = create_formatter('text')
24 |
25 | lines = follow('Data/stocklog.csv')
26 | rows = csv.reader(lines)
27 | records = (Ticker.from_row(row) for row in rows)
28 | negative = (rec for rec in records if rec.change < 0)
29 | print_table(negative, ['name','price','change'], formatter)
30 | ```
31 |
32 |
33 |
34 | [Back](ex8_2.md)
35 |
--------------------------------------------------------------------------------
/Exercises/soln8_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 8.3 - Solution
2 |
3 | ```python
4 | # coticker.py
5 | from structure import Structure
6 | from validate import String, Integer, Float
7 |
8 | class Ticker(Structure):
9 | name = String()
10 | price = Float()
11 | date = String()
12 | time = String()
13 | change = Float()
14 | open = Float()
15 | high = Float()
16 | low = Float()
17 | volume = Integer()
18 |
19 | from cofollow import consumer, follow
20 | from tableformat import create_formatter
21 | import csv
22 |
23 | # Coroutine that splits rows using the CSV module. This is rather
24 | # mind-bending due to the fact that the csv module only understands
25 | # iteration with the for-loop. To make it work, we wrap it around
26 | # a generator that simply produces the last item received.
27 |
28 | @consumer
29 | def to_csv(target):
30 | def producer():
31 | while True:
32 | yield line
33 |
34 | reader = csv.reader(producer())
35 | while True:
36 | line = yield
37 | target.send(next(reader))
38 |
39 | @consumer
40 | def create_ticker(target):
41 | while True:
42 | row = yield
43 | target.send(Ticker.from_row(row))
44 |
45 | @consumer
46 | def negchange(target):
47 | while True:
48 | record = yield
49 | if record.change < 0:
50 | target.send(record)
51 |
52 | @consumer
53 | def ticker(fmt, fields):
54 | formatter = create_formatter(fmt)
55 | formatter.headings(fields)
56 | while True:
57 | rec = yield
58 | row = [getattr(rec, name) for name in fields]
59 | formatter.row(row)
60 |
61 | if __name__ == '__main__':
62 | follow('Data/stocklog.csv',
63 | to_csv(
64 | create_ticker(
65 | negchange(
66 | ticker('text', ['name','price','change'])))))
67 | ```
68 |
69 |
70 |
71 | [Back](ex8_3.md)
72 |
--------------------------------------------------------------------------------
/Exercises/soln8_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 8.4 - Solution
2 |
3 | All of the code is given in the exercise description.
4 |
5 |
6 |
7 | [Back](ex8_4.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln8_5.md:
--------------------------------------------------------------------------------
1 | # Exercise 8.5 - Solution
2 |
3 | ## (b) Generators as Tasks Serving Network Connections
4 |
5 | Here's the complete code.
6 |
7 | ```python
8 | # server.py
9 |
10 | from socket import *
11 | from select import select
12 | from collections import deque
13 |
14 | tasks = deque()
15 | recv_wait = {} # sock -> task
16 | send_wait = {} # sock -> task
17 |
18 | def run():
19 | while any([tasks, recv_wait, send_wait]):
20 | while not tasks:
21 | can_recv, can_send, _ = select(recv_wait, send_wait, [])
22 | for s in can_recv:
23 | tasks.append(recv_wait.pop(s))
24 | for s in can_send:
25 | tasks.append(send_wait.pop(s))
26 | task = tasks.popleft()
27 | try:
28 | reason, resource = task.send(None)
29 | if reason == 'recv':
30 | recv_wait[resource] = task
31 | elif reason == 'send':
32 | send_wait[resource] = task
33 | else:
34 | raise RuntimeError('Unknown reason %r' % reason)
35 | except StopIteration:
36 | print('Task done')
37 |
38 | def tcp_server(address, handler):
39 | sock = socket(AF_INET, SOCK_STREAM)
40 | sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
41 | sock.bind(address)
42 | sock.listen(5)
43 | while True:
44 | yield 'recv', sock
45 | client, addr = sock.accept()
46 | tasks.append(handler(client, addr))
47 |
48 | def echo_handler(client, address):
49 | print('Connection from', address)
50 | while True:
51 | yield 'recv', client
52 | data = client.recv(1000)
53 | if not data:
54 | break
55 | yield 'send', client
56 | client.send(b'GOT:' + data)
57 | print('Connection closed')
58 |
59 | if __name__ == '__main__':
60 | tasks.append(tcp_server(('',25000), echo_handler))
61 | run()
62 | ```
63 |
64 |
65 |
66 | [Back](ex8_5.md)
67 |
--------------------------------------------------------------------------------
/Exercises/soln9_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 9.1 - Solution
2 |
3 | None. Just follow the exercise steps.
4 |
5 |
6 |
7 | [Back](ex9_1.md)
8 |
--------------------------------------------------------------------------------
/Exercises/soln9_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 9.2 - Solution
2 |
3 | ## (a) Making a package
4 |
5 | You don't need to modify much source code. Just make a directory with
6 | this structure:
7 |
8 | ```
9 | structly/
10 | __init__.py
11 | validate.py
12 | reader.py
13 | structure.py
14 | tableformat.py
15 | ```
16 |
17 | The `__init__.py` file can be empty. You need to make one small change to the `structure.py` file to make the import statement work.
18 |
19 | ```python
20 | # structure.py
21 |
22 | ...
23 | from .validate import Validator
24 | ```
25 |
26 |
27 |
28 | [Back](ex9_2.md)
29 |
--------------------------------------------------------------------------------
/Exercises/soln9_4.md:
--------------------------------------------------------------------------------
1 | # Exercise 9.4 - Solution
2 |
3 | ```python
4 | # tableformat.py
5 | from abc import ABC, abstractmethod
6 |
7 | def print_table(records, fields, formatter):
8 | if not isinstance(formatter, TableFormatter):
9 | raise RuntimeError('Expected a TableFormatter')
10 |
11 | formatter.headings(fields)
12 | for r in records:
13 | rowdata = [getattr(r, fieldname) for fieldname in fields]
14 | formatter.row(rowdata)
15 |
16 | class TableFormatter(ABC):
17 | _formats = { }
18 |
19 | @classmethod
20 | def __init_subclass__(cls):
21 | name = cls.__module__.split('.')[-1]
22 | TableFormatter._formats[name] = cls
23 |
24 | @abstractmethod
25 | def headings(self, headers):
26 | pass
27 |
28 | @abstractmethod
29 | def row(self, rowdata):
30 | pass
31 |
32 | class ColumnFormatMixin:
33 | formats = []
34 | def row(self, rowdata):
35 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
36 | super().row(rowdata)
37 |
38 | class UpperHeadersMixin:
39 | def headings(self, headers):
40 | super().headings([h.upper() for h in headers])
41 |
42 | def create_formatter(name, column_formats=None, upper_headers=False):
43 | if name not in TableFormatter._formats:
44 | __import__(f'{__package__}.formats.{name}')
45 |
46 | formatter_cls = TableFormatter._formats.get(name)
47 | if not formatter_cls:
48 | raise RuntimeError('Unknown format %s' % name)
49 |
50 | if column_formats:
51 | class formatter_cls(ColumnFormatMixin, formatter_cls):
52 | formats = column_formats
53 |
54 | if upper_headers:
55 | class formatter_cls(UpperHeadersMixin, formatter_cls):
56 | pass
57 |
58 | return formatter_cls()
59 | ```
60 |
61 |
62 |
63 | [Back](ex9_4.md)
64 |
--------------------------------------------------------------------------------
/PythonMastery.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dabeaz-course/python-mastery/dcf5e16d1fedfc9a40620141d18c3c7a75059425/PythonMastery.pdf
--------------------------------------------------------------------------------
/Solutions/1_1/art.py:
--------------------------------------------------------------------------------
1 | # art.py
2 |
3 | import sys
4 | import random
5 |
6 | chars = '\|/'
7 |
8 | def draw(rows, columns):
9 | for r in range(rows):
10 | print(''.join(random.choice(chars) for _ in range(columns)))
11 |
12 | if __name__ == '__main__':
13 | if len(sys.argv) != 3:
14 | raise SystemExit("Usage: art.py rows columns")
15 | draw(int(sys.argv[1]), int(sys.argv[2]))
16 |
--------------------------------------------------------------------------------
/Solutions/1_3/pcost.py:
--------------------------------------------------------------------------------
1 | # pcost.py
2 |
3 | total_cost = 0.0
4 |
5 | with open('../../Data/portfolio.dat', 'r') as f:
6 | for line in f:
7 | fields = line.split()
8 | nshares = int(fields[1])
9 | price = float(fields[2])
10 | total_cost = total_cost + nshares * price
11 |
12 | print(total_cost)
13 |
--------------------------------------------------------------------------------
/Solutions/1_4/pcost.py:
--------------------------------------------------------------------------------
1 | # pcost.py
2 |
3 | def portfolio_cost(filename):
4 | total_cost = 0.0
5 | with open(filename) as f:
6 | for line in f:
7 | fields = line.split()
8 | try:
9 | nshares = int(fields[1])
10 | price = float(fields[2])
11 | total_cost = total_cost + nshares*price
12 |
13 | # This catches errors in int() and float() conversions above
14 | except ValueError as e:
15 | print("Couldn't parse:", repr(line))
16 | print("Reason:", e)
17 |
18 | return total_cost
19 |
20 | print(portfolio_cost('../../Data/portfolio3.dat'))
21 |
--------------------------------------------------------------------------------
/Solutions/1_5/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | def __init__(self, name, shares, price):
5 | self.name = name
6 | self.shares = shares
7 | self.price = price
8 | def cost(self):
9 | return self.shares * self.price
10 |
--------------------------------------------------------------------------------
/Solutions/1_6/pcost.py:
--------------------------------------------------------------------------------
1 | # pcost.py
2 |
3 | def portfolio_cost(filename):
4 | total_cost = 0.0
5 | with open(filename) as f:
6 | for line in f:
7 | fields = line.split()
8 | try:
9 | nshares = int(fields[1])
10 | price = float(fields[2])
11 | total_cost = total_cost + nshares * price
12 |
13 | # This catches errors in int() and float() conversions above
14 | except ValueError as e:
15 | print("Couldn't parse:", repr(line))
16 | print("Reason:", e)
17 |
18 | return total_cost
19 |
20 |
21 | if __name__ == '__main__':
22 | print(portfolio_cost('../../Data/portfolio.dat'))
23 |
--------------------------------------------------------------------------------
/Solutions/2_1/readrides.py:
--------------------------------------------------------------------------------
1 | # readrides.py
2 |
3 | import csv
4 |
5 | def read_rides_as_tuples(filename):
6 | '''
7 | Read the bus ride data as a list of tuples
8 | '''
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headings = next(rows) # Skip headers
13 | for row in rows:
14 | route = row[0]
15 | date = row[1]
16 | daytype = row[2]
17 | rides = int(row[3])
18 | record = (route, date, daytype, rides)
19 | records.append(record)
20 | return records
21 |
22 | def read_rides_as_dicts(filename):
23 | '''
24 | Read the bus ride data as a list of dicts
25 | '''
26 | records = []
27 | with open(filename) as f:
28 | rows = csv.reader(f)
29 | headings = next(rows) # Skip headers
30 | for row in rows:
31 | route = row[0]
32 | date = row[1]
33 | daytype = row[2]
34 | rides = int(row[3])
35 | record = {
36 | 'route': route,
37 | 'date': date,
38 | 'daytype': daytype,
39 | 'rides' : rides
40 | }
41 | records.append(record)
42 | return records
43 |
44 | class Row:
45 | # Uncomment to see effect of slots
46 | # __slots__ = ('route', 'date', 'daytype', 'rides')
47 | def __init__(self, route, date, daytype, rides):
48 | self.route = route
49 | self.date = date
50 | self.daytype = daytype
51 | self.rides = rides
52 |
53 | # Uncomment to use a namedtuple instead
54 | #from collections import namedtuple
55 | #Row = namedtuple('Row',('route','date','daytype','rides'))
56 |
57 | def read_rides_as_instances(filename):
58 | '''
59 | Read the bus ride data as a list of instances
60 | '''
61 | records = []
62 | with open(filename) as f:
63 | rows = csv.reader(f)
64 | headings = next(rows) # Skip headers
65 | for row in rows:
66 | route = row[0]
67 | date = row[1]
68 | daytype = row[2]
69 | rides = int(row[3])
70 | record = Row(route, date, daytype, rides)
71 | records.append(record)
72 | return records
73 |
74 | if __name__ == '__main__':
75 | import tracemalloc
76 | tracemalloc.start()
77 | read_rides = read_rides_as_tuples # Change to as_dicts, as_instances, etc.
78 | rides = read_rides("../../Data/ctabus.csv")
79 |
80 | print('Memory Use: Current %d, Peak %d' % tracemalloc.get_traced_memory())
81 |
--------------------------------------------------------------------------------
/Solutions/2_2/cta.py:
--------------------------------------------------------------------------------
1 | # cta.py
2 |
3 | from collections import defaultdict, Counter
4 | import tracemalloc
5 | import readrides
6 |
7 | tracemalloc.start()
8 |
9 | rows = readrides.read_rides_as_dicts('../../Data/ctabus.csv')
10 |
11 | # --------------------------------------------------
12 | # Question 1: How many bus routes are in Chicago?
13 | # Solution: Use a set to get unique values.
14 |
15 | routes = set()
16 | for row in rows:
17 | routes.add(row['route'])
18 | print(len(routes), 'routes')
19 |
20 | # --------------------------------------------------
21 | # Question 2: How many people rode route 22 on February 2, 2011?
22 | # Solution: Make dictionary with composite keys
23 |
24 | by_route_date = { }
25 | for row in rows:
26 | by_route_date[row['route'], row['date']] = row['rides']
27 |
28 | print('Rides on Route 22, February 2, 2011:', by_route_date['22','02/02/2011'])
29 |
30 | # --------------------------------------------------
31 | # Question 3: Total number of rides per route
32 | # Solution: Use a counter to tabulate things
33 | rides_per_route = Counter()
34 | for row in rows:
35 | rides_per_route[row['route']] += row['rides']
36 |
37 | # Make a table showing the routes and a count ranked by popularity
38 | for route, count in rides_per_route.most_common():
39 | print('%5s %10d' % (route, count))
40 |
41 | # --------------------------------------------------
42 | # Question 4: Routes with greatest increase in ridership 2001 - 2011
43 | # Solution: Counters embedded inside a defaultdict
44 |
45 | rides_by_year = defaultdict(Counter)
46 | for row in rows:
47 | year = row['date'].split('/')[2]
48 | rides_by_year[year][row['route']] += row['rides']
49 |
50 | diffs = rides_by_year['2011'] - rides_by_year['2001']
51 | for route, diff in diffs.most_common(5):
52 | print(route, diff)
53 |
54 | # ---- Memory use
55 | print('Memory Use: Current %d, Peak %d' % tracemalloc.get_traced_memory())
56 |
--------------------------------------------------------------------------------
/Solutions/2_2/readport.py:
--------------------------------------------------------------------------------
1 | # readport.py
2 |
3 | import csv
4 |
5 | # A function that reads a file into a list of dicts
6 | def read_portfolio(filename):
7 | portfolio = []
8 | with open(filename) as f:
9 | rows = csv.reader(f)
10 | headers = next(rows)
11 | for row in rows:
12 | record = {
13 | 'name' : row[0],
14 | 'shares' : int(row[1]),
15 | 'price' : float(row[2])
16 | }
17 | portfolio.append(record)
18 | return portfolio
19 |
--------------------------------------------------------------------------------
/Solutions/2_2/readrides.py:
--------------------------------------------------------------------------------
1 | # readrides.py
2 |
3 | import csv
4 |
5 | def read_rides_as_tuples(filename):
6 | '''
7 | Read the bus ride data as a list of tuples
8 | '''
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headings = next(rows) # Skip headers
13 | for row in rows:
14 | route = row[0]
15 | date = row[1]
16 | daytype = row[2]
17 | rides = int(row[3])
18 | record = (route, date, daytype, rides)
19 | records.append(record)
20 | return records
21 |
22 | def read_rides_as_dicts(filename):
23 | '''
24 | Read the bus ride data as a list of dicts
25 | '''
26 | records = []
27 | with open(filename) as f:
28 | rows = csv.reader(f)
29 | headings = next(rows) # Skip headers
30 | for row in rows:
31 | route = row[0]
32 | date = row[1]
33 | daytype = row[2]
34 | rides = int(row[3])
35 | record = {
36 | 'route': route,
37 | 'date': date,
38 | 'daytype': daytype,
39 | 'rides' : rides
40 | }
41 | records.append(record)
42 | return records
43 |
44 | class Row:
45 | # Uncomment to see effect of slots
46 | # __slots__ = ('route', 'date', 'daytype', 'rides')
47 | def __init__(self, route, date, daytype, rides):
48 | self.route = route
49 | self.date = date
50 | self.daytype = daytype
51 | self.rides = rides
52 |
53 | # Uncomment to use a namedtuple instead
54 | #from collections import namedtuple
55 | #Row = namedtuple('Row',('route','date','daytype','rides'))
56 |
57 | def read_rides_as_instances(filename):
58 | '''
59 | Read the bus ride data as a list of instances
60 | '''
61 | records = []
62 | with open(filename) as f:
63 | rows = csv.reader(f)
64 | headings = next(rows) # Skip headers
65 | for row in rows:
66 | route = row[0]
67 | date = row[1]
68 | daytype = row[2]
69 | rides = int(row[3])
70 | record = Row(route, date, daytype, rides)
71 | records.append(record)
72 | return records
73 |
74 | if __name__ == '__main__':
75 | import tracemalloc
76 | tracemalloc.start()
77 | read_rides = read_rides_as_tuples # Change to as_dicts, as_instances, etc.
78 | rides = read_rides("../../Data/ctabus.csv")
79 |
80 | print('Memory Use: Current %d, Peak %d' % tracemalloc.get_traced_memory())
81 |
--------------------------------------------------------------------------------
/Solutions/2_4/mutint.py:
--------------------------------------------------------------------------------
1 | # mutint.py
2 | #
3 | # Mutable integers
4 |
5 | from functools import total_ordering
6 |
7 | @total_ordering
8 | class MutInt:
9 | __slots__ = ['value']
10 |
11 | def __init__(self, value):
12 | self.value = value
13 |
14 | def __str__(self):
15 | return str(self.value)
16 |
17 | def __repr__(self):
18 | return f'MutInt({self.value!r})'
19 |
20 | def __format__(self, fmt):
21 | return format(self.value, fmt)
22 |
23 | # Implement the "+" operator. Forward operands (MutInt + other)
24 | def __add__(self, other):
25 | if isinstance(other, MutInt):
26 | return MutInt(self.value + other.value)
27 | elif isinstance(other, int):
28 | return MutInt(self.value + other)
29 | else:
30 | return NotImplemented
31 |
32 | # Support for reversed operands (other + MutInt)
33 | __radd__ = __add__
34 |
35 | # Support for in-place update (MutInt += other)
36 | def __iadd__(self, other):
37 | if isinstance(other, MutInt):
38 | self.value += other.value
39 | return self
40 | elif isinstance(other, int):
41 | self.value += other
42 | return self
43 | else:
44 | return NotImplemented
45 |
46 | # Support for equality testing
47 | def __eq__(self, other):
48 | if isinstance(other, MutInt):
49 | return self.value == other.value
50 | elif isinstance(other, int):
51 | return self.value == other
52 | else:
53 | return NotImplemented
54 |
55 | # One relation is needed for @total_ordering decorator. It fills in others
56 | def __lt__(self, other):
57 | if isinstance(other, MutInt):
58 | return self.value < other.value
59 | elif isinstance(other, int):
60 | return self.value < other
61 | else:
62 | return NotImplemented
63 |
64 | # Conversions to int() and float()
65 | def __int__(self):
66 | return int(self.value)
67 |
68 | def __float__(self):
69 | return float(self.value)
70 |
71 | # Support for indexing s[MutInt]
72 | __index__ = __int__
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Solutions/2_5/cta.py:
--------------------------------------------------------------------------------
1 | # cta.py
2 |
3 | from collections import defaultdict, Counter
4 | import tracemalloc
5 | import readrides
6 |
7 | tracemalloc.start()
8 |
9 | rows = readrides.read_rides_as_dicts('../../Data/ctabus.csv')
10 |
11 | # --------------------------------------------------
12 | # Question 1: How many bus routes are in Chicago?
13 | # Solution: Use a set to get unique values.
14 |
15 | routes = set()
16 | for row in rows:
17 | routes.add(row['route'])
18 | print(len(routes), 'routes')
19 |
20 | # --------------------------------------------------
21 | # Question 2: How many people rode route 22 on February 2, 2011?
22 | # Solution: Make dictionary with composite keys
23 |
24 | by_route_date = { }
25 | for row in rows:
26 | by_route_date[row['route'], row['date']] = row['rides']
27 |
28 | print('Rides on Route 22, February 2, 2011:', by_route_date['22','02/02/2011'])
29 |
30 | # --------------------------------------------------
31 | # Question 3: Total number of rides per route
32 | # Solution: Use a counter to tabulate things
33 | rides_per_route = Counter()
34 | for row in rows:
35 | rides_per_route[row['route']] += row['rides']
36 |
37 | # Make a table showing the routes and a count ranked by popularity
38 | for route, count in rides_per_route.most_common():
39 | print('%5s %10d' % (route, count))
40 |
41 | # --------------------------------------------------
42 | # Question 4: Routes with greatest increase in ridership 2001 - 2011
43 | # Solution: Counters embedded inside a defaultdict
44 |
45 | rides_by_year = defaultdict(Counter)
46 | for row in rows:
47 | year = row['date'].split('/')[2]
48 | rides_by_year[year][row['route']] += row['rides']
49 |
50 | diffs = rides_by_year['2011'] - rides_by_year['2001']
51 | for route, diff in diffs.most_common(5):
52 | print(route, diff)
53 |
54 | # ---- Memory use
55 | print('Memory Use: Current %d, Peak %d' % tracemalloc.get_traced_memory())
56 |
--------------------------------------------------------------------------------
/Solutions/2_6/colreader.py:
--------------------------------------------------------------------------------
1 | # colreader.py
2 |
3 | import collections
4 | import csv
5 |
6 | class DataCollection(collections.abc.Sequence):
7 | def __init__(self, columns):
8 | self.column_names = list(columns)
9 | self.column_data = list(columns.values())
10 |
11 | def __len__(self):
12 | return len(self.column_data[0])
13 |
14 | def __getitem__(self, index):
15 | return dict(zip(self.column_names,
16 | (col[index] for col in self.column_data)))
17 |
18 |
19 | def read_csv_as_columns(filename, types):
20 | columns = collections.defaultdict(list)
21 | with open(filename) as f:
22 | rows = csv.reader(f)
23 | headers = next(rows)
24 | for row in rows:
25 | for name, func, val in zip(headers, types, row):
26 | columns[name].append(func(val))
27 |
28 | return DataCollection(columns)
29 |
30 | if __name__ == '__main__':
31 | import tracemalloc
32 | from sys import intern
33 |
34 | tracemalloc.start()
35 | data = read_csv_as_columns('../../Data/ctabus.csv', [intern, intern, intern, int])
36 | print(tracemalloc.get_traced_memory())
37 |
--------------------------------------------------------------------------------
/Solutions/2_6/cta.py:
--------------------------------------------------------------------------------
1 | # cta.py
2 |
3 | from collections import defaultdict, Counter
4 | import tracemalloc
5 | import csv
6 | import sys
7 |
8 | tracemalloc.start()
9 |
10 | if True:
11 | # Part (b)
12 | import reader
13 | rows = reader.read_csv_as_dicts('../../Data/ctabus.csv',
14 | [sys.intern, sys.intern, sys.intern, int])
15 | else:
16 | # Part (d) - Challenge
17 | import colreader
18 | rows = colreader.read_csv_as_columns('../../Data/ctabus.csv',
19 | [sys.intern, sys.intern, sys.intern, int])
20 |
21 | # --------------------------------------------------
22 | # Question 1: How many bus routes are in Chicago?
23 | # Solution: Use a set to get unique values.
24 |
25 | routes = set()
26 | for row in rows:
27 | routes.add(row['route'])
28 | print(len(routes), 'routes')
29 |
30 | # --------------------------------------------------
31 | # Question 2: Total number of rides per route
32 | # Solution: Use a counter to tabulate things
33 | rides_per_route = Counter()
34 | for row in rows:
35 | rides_per_route[row['route']] += row['rides']
36 |
37 | # Make a table showing the routes and a count ranked by popularity
38 | for route, count in rides_per_route.most_common():
39 | print('%5s %10d' % (route, count))
40 |
41 | # --------------------------------------------------
42 | # Question 3: Routes with greatest increase in ridership 2001 - 2011
43 | # Solution: Counters embedded inside a defaultdict
44 |
45 | rides_by_year = defaultdict(Counter)
46 | for row in rows:
47 | year = row['date'].split('/')[2]
48 | rides_by_year[year][row['route']] += row['rides']
49 |
50 | diffs = rides_by_year['2011'] - rides_by_year['2001']
51 | for route, diff in diffs.most_common(5):
52 | print(route, diff)
53 |
54 | # ---- Memory use
55 | print('Memory Use: Current %d, Peak %d' % tracemalloc.get_traced_memory())
56 |
--------------------------------------------------------------------------------
/Solutions/2_6/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 |
5 | def read_csv_as_dicts(filename, types):
6 | '''
7 | Read a CSV file into a list of dicts with column type conversion
8 | '''
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headers = next(rows)
13 | for row in rows:
14 | record = { name: func(val) for name, func, val in zip(headers, types, row) }
15 | records.append(record)
16 | return records
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Solutions/3_1/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | def __init__(self, name, shares, price):
5 | self.name = name
6 | self.shares = shares
7 | self.price = price
8 |
9 | def cost(self):
10 | return self.shares * self.price
11 |
12 | def sell(self, nshares):
13 | self.shares -= nshares
14 |
15 | def read_portfolio(filename):
16 | '''
17 | Read a CSV file of stock data into a list of Stocks
18 | '''
19 | import csv
20 | portfolio = []
21 | with open(filename) as f:
22 | rows = csv.reader(f)
23 | headers = next(rows)
24 | for row in rows:
25 | record = Stock(row[0], int(row[1]), float(row[2]))
26 | portfolio.append(record)
27 | return portfolio
28 |
29 | def print_portfolio(portfolio):
30 | '''
31 | Make a nicely formatted table showing stock data
32 | '''
33 | print('%10s %10s %10s' % ('name', 'shares', 'price'))
34 | print(('-'*10 + ' ')*3)
35 | for s in portfolio:
36 | print('%10s %10d %10.2f' % (s.name, s.shares, s.price))
37 |
38 | if __name__ == '__main__':
39 | portfolio = read_portfolio('../../Data/portfolio.csv')
40 | print_portfolio(portfolio)
41 |
42 |
--------------------------------------------------------------------------------
/Solutions/3_2/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | def __init__(self, name, shares, price):
5 | self.name = name
6 | self.shares = shares
7 | self.price = price
8 |
9 | def cost(self):
10 | return self.shares * self.price
11 |
12 | def sell(self, nshares):
13 | self.shares -= nshares
14 |
15 |
16 | def read_portfolio(filename):
17 | '''
18 | Read a CSV file of stock data into a list of Stocks
19 | '''
20 | import csv
21 | portfolio = []
22 | with open(filename) as f:
23 | rows = csv.reader(f)
24 | headers = next(rows)
25 | for row in rows:
26 | record = Stock(row[0], int(row[1]), float(row[2]))
27 | portfolio.append(record)
28 | return portfolio
29 |
30 | if __name__ == '__main__':
31 | import tableformat
32 | portfolio = read_portfolio('../../Data/portfolio.csv')
33 | tableformat.print_table(portfolio, ['name','shares','price'])
34 |
--------------------------------------------------------------------------------
/Solutions/3_2/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 |
3 | # Print a table
4 | def print_table(records, fields):
5 | print(' '.join('%10s' % fieldname for fieldname in fields))
6 | print(('-'*10 + ' ')*len(fields))
7 | for record in records:
8 | print(' '.join('%10s' % getattr(record, fieldname) for fieldname in fields))
9 |
--------------------------------------------------------------------------------
/Solutions/3_3/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 |
5 | def read_csv_as_dicts(filename, types):
6 | '''
7 | Read a CSV file into a list of dicts with column type conversion
8 | '''
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headers = next(rows)
13 | for row in rows:
14 | record = { name: func(val) for name, func, val in zip(headers, types, row) }
15 | records.append(record)
16 | return records
17 |
18 | def read_csv_as_instances(filename, cls):
19 | '''
20 | Read a CSV file into a list of instances
21 | '''
22 | records = []
23 | with open(filename) as f:
24 | rows = csv.reader(f)
25 | headers = next(rows)
26 | for row in rows:
27 | records.append(cls.from_row(row))
28 | return records
29 |
--------------------------------------------------------------------------------
/Solutions/3_3/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | types = (str, int, float)
5 | def __init__(self, name, shares, price):
6 | self.name = name
7 | self.shares = shares
8 | self.price = price
9 |
10 | @classmethod
11 | def from_row(cls, row):
12 | values = [func(val) for func, val in zip(cls.types, row)]
13 | return cls(*values)
14 |
15 | def cost(self):
16 | return self.shares * self.price
17 |
18 | def sell(self, nshares):
19 | self.shares -= nshares
20 |
21 | def read_portfolio(filename):
22 | '''
23 | Read a CSV file of stock data into a list of Stocks
24 | '''
25 | import csv
26 | portfolio = []
27 | with open(filename) as f:
28 | rows = csv.reader(f)
29 | headers = next(rows)
30 | for row in rows:
31 | record = Stock.from_row(row)
32 | portfolio.append(record)
33 | return portfolio
34 |
35 | if __name__ == '__main__':
36 | import tableformat
37 | import reader
38 | # portfolio = read_portfolio('../../Data/portfolio.csv')
39 | portfolio = reader.read_csv_as_instances('../../Data/portfolio.csv', Stock)
40 | tableformat.print_table(portfolio, ['name', 'shares', 'price'])
41 |
--------------------------------------------------------------------------------
/Solutions/3_3/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 |
3 | # Print a table
4 | def print_table(records, fields):
5 | # Print the table headers in a 10-character wide field
6 | for fieldname in fields:
7 | print('%10s' % fieldname, end=' ')
8 | print()
9 |
10 | # Print the separator bars
11 | print(('-'*10 + ' ')*len(fields))
12 |
13 | # Output the table contents
14 | for r in records:
15 | for fieldname in fields:
16 | print('%10s' % getattr(r, fieldname), end=' ')
17 | print()
18 |
--------------------------------------------------------------------------------
/Solutions/3_4/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | __slots__ = ('name','_shares','_price')
5 | _types = (str, int, float)
6 | def __init__(self, name, shares, price):
7 | self.name = name
8 | self.shares = shares
9 | self.price = price
10 |
11 | @classmethod
12 | def from_row(cls, row):
13 | values = [func(val) for func, val in zip(cls._types, row)]
14 | return cls(*values)
15 |
16 | @property
17 | def shares(self):
18 | return self._shares
19 | @shares.setter
20 | def shares(self, value):
21 | if not isinstance(value, self._types[1]):
22 | raise TypeError(f'Expected {self._types[1].__name__}')
23 | if value < 0:
24 | raise ValueError('shares must be >= 0')
25 | self._shares = value
26 |
27 | @property
28 | def price(self):
29 | return self._price
30 | @price.setter
31 | def price(self, value):
32 | if not isinstance(value, self._types[2]):
33 | raise TypeError(f'Expected {self._types[2].__name__}')
34 | if value < 0:
35 | raise ValueError('price must be >= 0')
36 | self._price = value
37 |
38 | @property
39 | def cost(self):
40 | return self.shares * self.price
41 |
42 | def sell(self, nshares):
43 | self.shares -= nshares
44 |
--------------------------------------------------------------------------------
/Solutions/3_5/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 |
5 | def read_csv_as_dicts(filename, types):
6 | '''
7 | Read a CSV file into a list of dicts with column type conversion
8 | '''
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headers = next(rows)
13 | for row in rows:
14 | record = { name: func(val) for name, func, val in zip(headers, types, row) }
15 | records.append(record)
16 | return records
17 |
18 | def read_csv_as_instances(filename, cls):
19 | '''
20 | Read a CSV file into a list of instances
21 | '''
22 | records = []
23 | with open(filename) as f:
24 | rows = csv.reader(f)
25 | headers = next(rows)
26 | for row in rows:
27 | records.append(cls.from_row(row))
28 | return records
29 |
--------------------------------------------------------------------------------
/Solutions/3_5/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | __slots__ = ('name','_shares','_price')
5 | _types = (str, int, float)
6 | def __init__(self, name, shares, price):
7 | self.name = name
8 | self.shares = shares
9 | self.price = price
10 |
11 | @classmethod
12 | def from_row(cls, row):
13 | values = [func(val) for func, val in zip(cls._types, row)]
14 | return cls(*values)
15 |
16 | @property
17 | def shares(self):
18 | return self._shares
19 | @shares.setter
20 | def shares(self, value):
21 | if not isinstance(value, self._types[1]):
22 | raise TypeError(f'Expected {self._types[1].__name__}')
23 | if value < 0:
24 | raise ValueError('shares must be >= 0')
25 | self._shares = value
26 |
27 | @property
28 | def price(self):
29 | return self._price
30 | @price.setter
31 | def price(self, value):
32 | if not isinstance(value, self._types[2]):
33 | raise TypeError(f'Expected {self._types[2].__name__}')
34 | if value < 0:
35 | raise ValueError('price must be >= 0')
36 | self._price = value
37 |
38 | @property
39 | def cost(self):
40 | return self.shares * self.price
41 |
42 | def sell(self, nshares):
43 | self.shares -= nshares
44 |
45 | # Sample
46 | if __name__ == '__main__':
47 | import reader
48 | from tableformat import create_formatter, print_table
49 |
50 | portfolio = reader.read_csv_as_instances('../../Data/portfolio.csv', Stock)
51 | formatter = create_formatter('text')
52 | print_table(portfolio,['name','shares','price'], formatter)
53 |
--------------------------------------------------------------------------------
/Solutions/3_5/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 |
3 | def print_table(records, fields, formatter):
4 | formatter.headings(fields)
5 | for r in records:
6 | rowdata = [getattr(r, fieldname) for fieldname in fields]
7 | formatter.row(rowdata)
8 |
9 | class TableFormatter:
10 | def headings(self, headers):
11 | raise NotImplementedError()
12 |
13 | def row(self, rowdata):
14 | raise NotImplementedError()
15 |
16 | class TextTableFormatter(TableFormatter):
17 | def headings(self, headers):
18 | print(' '.join('%10s' % h for h in headers))
19 | print(('-'*10 + ' ')*len(headers))
20 |
21 | def row(self, rowdata):
22 | print(' '.join('%10s' % d for d in rowdata))
23 |
24 | class CSVTableFormatter(TableFormatter):
25 | def headings(self, headers):
26 | print(','.join(headers))
27 |
28 | def row(self, rowdata):
29 | print(','.join(str(d) for d in rowdata))
30 |
31 | class HTMLTableFormatter(TableFormatter):
32 | def headings(self, headers):
33 | print('', end=' ')
34 | for h in headers:
35 | print('%s | ' % h, end=' ')
36 | print('
')
37 |
38 | def row(self, rowdata):
39 | print('', end=' ')
40 | for d in rowdata:
41 | print('%s | ' % d, end=' ')
42 | print('
')
43 |
44 | def create_formatter(name):
45 | if name == 'text':
46 | formatter_cls = TextTableFormatter
47 | elif name == 'csv':
48 | formatter_cls = CSVTableFormatter
49 | elif name == 'html':
50 | formatter_cls = HTMLTableFormatter
51 | else:
52 | raise RuntimeError('Unknown format %s' % name)
53 | return formatter_cls()
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/Solutions/3_6/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 |
5 | def read_csv_as_dicts(filename, types):
6 | '''
7 | Read a CSV file into a list of dicts with column type conversion
8 | '''
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headers = next(rows)
13 | for row in rows:
14 | record = { name: func(val) for name, func, val in zip(headers, types, row) }
15 | records.append(record)
16 | return records
17 |
18 | def read_csv_as_instances(filename, cls):
19 | '''
20 | Read a CSV file into a list of instances
21 | '''
22 | records = []
23 | with open(filename) as f:
24 | rows = csv.reader(f)
25 | headers = next(rows)
26 | for row in rows:
27 | records.append(cls.from_row(row))
28 | return records
29 |
--------------------------------------------------------------------------------
/Solutions/3_6/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | __slots__ = ('name','_shares','_price')
5 | _types = (str, int, float)
6 | def __init__(self, name, shares, price):
7 | self.name = name
8 | self.shares = shares
9 | self.price = price
10 |
11 | def __repr__(self):
12 | # Note: The !r format code produces the repr() string
13 | return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})'
14 |
15 | def __eq__(self, other):
16 | return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
17 | (other.name, other.shares, other.price))
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | values = [func(val) for func, val in zip(cls._types, row)]
22 | return cls(*values)
23 |
24 | @property
25 | def shares(self):
26 | return self._shares
27 | @shares.setter
28 | def shares(self, value):
29 | if not isinstance(value, self._types[1]):
30 | raise TypeError(f'Expected {self._types[1].__name__}')
31 | if value < 0:
32 | raise ValueError('shares must be >= 0')
33 | self._shares = value
34 |
35 | @property
36 | def price(self):
37 | return self._price
38 | @price.setter
39 | def price(self, value):
40 | if not isinstance(value, self._types[2]):
41 | raise TypeError(f'Expected {self._types[2].__name__}')
42 | if value < 0:
43 | raise ValueError('price must be >= 0')
44 | self._price = value
45 |
46 | @property
47 | def cost(self):
48 | return self.shares * self.price
49 |
50 | def sell(self, nshares):
51 | self.shares -= nshares
52 |
53 | # Sample
54 | if __name__ == '__main__':
55 | import reader
56 | from tableformat import create_formatter, print_table
57 |
58 | portfolio = reader.read_csv_as_instances('../../Data/portfolio.csv', Stock)
59 | formatter = create_formatter('text')
60 | print_table(portfolio,['name','shares','price'], formatter)
61 |
--------------------------------------------------------------------------------
/Solutions/3_6/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 |
3 | def print_table(records, fields, formatter):
4 | formatter.headings(fields)
5 | for r in records:
6 | rowdata = [getattr(r, fieldname) for fieldname in fields]
7 | formatter.row(rowdata)
8 |
9 | class TableFormatter:
10 | def headings(self, headers):
11 | raise NotImplementedError()
12 |
13 | def row(self, rowdata):
14 | raise NotImplementedError()
15 |
16 | class TextTableFormatter(TableFormatter):
17 | def headings(self, headers):
18 | print(' '.join('%10s' % h for h in headers))
19 | print(('-'*10 + ' ')*len(headers))
20 |
21 | def row(self, rowdata):
22 | print(' '.join('%10s' % d for d in rowdata))
23 |
24 | class CSVTableFormatter(TableFormatter):
25 | def headings(self, headers):
26 | print(','.join(headers))
27 |
28 | def row(self, rowdata):
29 | print(','.join(str(d) for d in rowdata))
30 |
31 | class HTMLTableFormatter(TableFormatter):
32 | def headings(self, headers):
33 | print('', end=' ')
34 | for h in headers:
35 | print('%s | ' % h, end=' ')
36 | print('
')
37 |
38 | def row(self, rowdata):
39 | print('', end=' ')
40 | for d in rowdata:
41 | print('%s | ' % d, end=' ')
42 | print('
')
43 |
44 | def create_formatter(name):
45 | if name == 'text':
46 | formatter_cls = TextTableFormatter
47 | elif name == 'csv':
48 | formatter_cls = CSVTableFormatter
49 | elif name == 'html':
50 | formatter_cls = HTMLTableFormatter
51 | else:
52 | raise RuntimeError('Unknown format %s' % name)
53 | return formatter_cls()
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/Solutions/3_7/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | from abc import ABC, abstractmethod
5 |
6 | class CSVParser(ABC):
7 |
8 | def parse(self, filename):
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headers = next(rows)
13 | for row in rows:
14 | record = self.make_record(headers, row)
15 | records.append(record)
16 | return records
17 |
18 | @abstractmethod
19 | def make_record(self, headers, row):
20 | pass
21 |
22 | class DictCSVParser(CSVParser):
23 | def __init__(self, types):
24 | self.types = types
25 |
26 | def make_record(self, headers, row):
27 | return { name: func(val) for name, func, val in zip(headers, self.types, row) }
28 |
29 | class InstanceCSVParser(CSVParser):
30 | def __init__(self, cls):
31 | self.cls = cls
32 |
33 | def make_record(self, headers, row):
34 | return self.cls.from_row(row)
35 |
36 | def read_csv_as_dicts(filename, types):
37 | parser = DictCSVParser(types)
38 | return parser.parse(filename)
39 |
40 | def read_csv_as_instances(filename, cls):
41 | parser = InstanceCSVParser(cls)
42 | return parser.parse(filename)
43 |
--------------------------------------------------------------------------------
/Solutions/3_7/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | __slots__ = ('name','_shares','_price')
5 | _types = (str, int, float)
6 | def __init__(self, name, shares, price):
7 | self.name = name
8 | self.shares = shares
9 | self.price = price
10 |
11 | def __repr__(self):
12 | # Note: The !r format code produces the repr() string
13 | return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})'
14 |
15 | def __eq__(self, other):
16 | return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
17 | (other.name, other.shares, other.price))
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | values = [func(val) for func, val in zip(cls._types, row)]
22 | return cls(*values)
23 |
24 | @property
25 | def shares(self):
26 | return self._shares
27 | @shares.setter
28 | def shares(self, value):
29 | if not isinstance(value, self._types[1]):
30 | raise TypeError(f'Expected {self._types[1].__name__}')
31 | if value < 0:
32 | raise ValueError('shares must be >= 0')
33 | self._shares = value
34 |
35 | @property
36 | def price(self):
37 | return self._price
38 | @price.setter
39 | def price(self, value):
40 | if not isinstance(value, self._types[2]):
41 | raise TypeError(f'Expected {self._types[2].__name__}')
42 | if value < 0:
43 | raise ValueError('price must be >= 0')
44 | self._price = value
45 |
46 | @property
47 | def cost(self):
48 | return self.shares * self.price
49 |
50 | def sell(self, nshares):
51 | self.shares -= nshares
52 |
53 | # Sample
54 | if __name__ == '__main__':
55 | import reader
56 | from tableformat import create_formatter, print_table
57 |
58 | portfolio = reader.read_csv_as_instances('../../Data/portfolio.csv', Stock)
59 | formatter = create_formatter('text')
60 | print_table(portfolio,['name','shares','price'], formatter)
61 |
--------------------------------------------------------------------------------
/Solutions/3_7/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise TypeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 | class TextTableFormatter(TableFormatter):
23 | def headings(self, headers):
24 | print(' '.join('%10s' % h for h in headers))
25 | print(('-'*10 + ' ')*len(headers))
26 |
27 | def row(self, rowdata):
28 | print(' '.join('%10s' % d for d in rowdata))
29 |
30 | class CSVTableFormatter(TableFormatter):
31 | def headings(self, headers):
32 | print(','.join(headers))
33 |
34 | def row(self, rowdata):
35 | print(','.join(str(d) for d in rowdata))
36 |
37 | class HTMLTableFormatter(TableFormatter):
38 | def headings(self, headers):
39 | print('', end=' ')
40 | for h in headers:
41 | print('%s | ' % h, end=' ')
42 | print('
')
43 |
44 | def row(self, rowdata):
45 | print('', end=' ')
46 | for d in rowdata:
47 | print('%s | ' % d, end=' ')
48 | print('
')
49 |
50 | def create_formatter(name):
51 | if name == 'text':
52 | formatter = TextTableFormatter
53 | elif name == 'csv':
54 | formatter = CSVTableFormatter
55 | elif name == 'html':
56 | formatter = HTMLTableFormatter
57 | else:
58 | raise RuntimeError('Unknown format %s' % name)
59 | return formatter()
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/Solutions/3_8/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | from abc import ABC, abstractmethod
5 |
6 | class CSVParser(ABC):
7 |
8 | def parse(self, filename):
9 | records = []
10 | with open(filename) as f:
11 | rows = csv.reader(f)
12 | headers = next(rows)
13 | for row in rows:
14 | record = self.make_record(headers, row)
15 | records.append(record)
16 | return records
17 |
18 | @abstractmethod
19 | def make_record(self, headers, row):
20 | pass
21 |
22 | class DictCSVParser(CSVParser):
23 | def __init__(self, types):
24 | self.types = types
25 |
26 | def make_record(self, headers, row):
27 | return { name: func(val) for name, func, val in zip(headers, self.types, row) }
28 |
29 | class InstanceCSVParser(CSVParser):
30 | def __init__(self, cls):
31 | self.cls = cls
32 |
33 | def make_record(self, headers, row):
34 | return self.cls.from_row(row)
35 |
36 | def read_csv_as_dicts(filename, types):
37 | parser = DictCSVParser(types)
38 | return parser.parse(filename)
39 |
40 | def read_csv_as_instances(filename, cls):
41 | parser = InstanceCSVParser(cls)
42 | return parser.parse(filename)
43 |
--------------------------------------------------------------------------------
/Solutions/3_8/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise TypeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 |
23 | class TextTableFormatter(TableFormatter):
24 | def headings(self, headers):
25 | print(' '.join('%10s' % h for h in headers))
26 | print(('-'*10 + ' ')*len(headers))
27 |
28 | def row(self, rowdata):
29 | print(' '.join('%10s' % d for d in rowdata))
30 |
31 | class CSVTableFormatter(TableFormatter):
32 | def headings(self, headers):
33 | print(','.join(headers))
34 |
35 | def row(self, rowdata):
36 | print(','.join(str(d) for d in rowdata))
37 |
38 | class HTMLTableFormatter(TableFormatter):
39 | def headings(self, headers):
40 | print('', end=' ')
41 | for h in headers:
42 | print('%s | ' % h, end=' ')
43 | print('
')
44 |
45 | def row(self, rowdata):
46 | print('', end=' ')
47 | for d in rowdata:
48 | print('%s | ' % d, end=' ')
49 | print('
')
50 |
51 | class ColumnFormatMixin:
52 | formats = []
53 | def row(self, rowdata):
54 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
55 | super().row(rowdata)
56 |
57 | class UpperHeadersMixin:
58 | def headings(self, headers):
59 | super().headings([h.upper() for h in headers])
60 |
61 | def create_formatter(name, column_formats=None, upper_headers=False):
62 | if name == 'text':
63 | formatter_cls = TextTableFormatter
64 | elif name == 'csv':
65 | formatter_cls = CSVTableFormatter
66 | elif name == 'html':
67 | formatter_cls = HTMLTableFormatter
68 | else:
69 | raise RuntimeError('Unknown format %s' % name)
70 |
71 | if column_formats:
72 | class formatter_cls(ColumnFormatMixin, formatter_cls):
73 | formats = column_formats
74 |
75 | if upper_headers:
76 | class formatter_cls(UpperHeadersMixin, formatter_cls):
77 | pass
78 |
79 | return formatter_cls()
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Solutions/4_2/validate.py:
--------------------------------------------------------------------------------
1 | class Validator:
2 | @classmethod
3 | def check(cls, value):
4 | return value
5 |
6 | class Typed(Validator):
7 | expected_type = object
8 | @classmethod
9 | def check(cls, value):
10 | if not isinstance(value, cls.expected_type):
11 | raise TypeError(f'Expected {cls.expected_type}')
12 | return super().check(value)
13 |
14 | class Integer(Typed):
15 | expected_type = int
16 |
17 | class Float(Typed):
18 | expected_type = float
19 |
20 | class String(Typed):
21 | expected_type = str
22 |
23 | class Positive(Validator):
24 | @classmethod
25 | def check(cls, value):
26 | if value < 0:
27 | raise ValueError('Must be >= 0')
28 | return super().check(value)
29 |
30 | class NonEmpty(Validator):
31 | @classmethod
32 | def check(cls, value):
33 | if len(value) == 0:
34 | raise ValueError('Must be non-empty')
35 | return super().check(value)
36 |
37 | class PositiveInteger(Integer, Positive):
38 | pass
39 |
40 | class PositiveFloat(Float, Positive):
41 | pass
42 |
43 | class NonEmptyString(String, NonEmpty):
44 | pass
45 |
46 | # Examples
47 | if __name__ == '__main__':
48 | def add(x, y):
49 | Integer.check(x)
50 | Integer.check(y)
51 | return x + y
52 |
53 | class Stock:
54 | __slots__ = ('name','_shares','_price')
55 | def __init__(self, name, shares, price):
56 | self.name = name
57 | self.shares = shares
58 | self.price = price
59 |
60 | def __repr__(self):
61 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
62 |
63 | @property
64 | def shares(self):
65 | return self._shares
66 | @shares.setter
67 | def shares(self, value):
68 | self._shares = PositiveInteger.check(value)
69 |
70 | @property
71 | def price(self):
72 | return self._price
73 | @price.setter
74 | def price(self, value):
75 | self._price = PositiveFloat.check(value)
76 |
77 | @property
78 | def cost(self):
79 | return self.shares * self.price
80 |
81 | def sell(self, nshares):
82 | self.shares -= nshares
83 |
--------------------------------------------------------------------------------
/Solutions/4_3/validate.py:
--------------------------------------------------------------------------------
1 | class Validator:
2 | def __init__(self, name=None):
3 | self.name = name
4 |
5 | def __set_name__(self, cls, name):
6 | self.name = name
7 |
8 | @classmethod
9 | def check(cls, value):
10 | return value
11 |
12 | def __set__(self, instance, value):
13 | instance.__dict__[self.name] = self.check(value)
14 |
15 | class Typed(Validator):
16 | expected_type = object
17 | @classmethod
18 | def check(cls, value):
19 | if not isinstance(value, cls.expected_type):
20 | raise TypeError(f'expected {cls.expected_type}')
21 | return super().check(value)
22 |
23 | class Integer(Typed):
24 | expected_type = int
25 |
26 | class Float(Typed):
27 | expected_type = float
28 |
29 | class String(Typed):
30 | expected_type = str
31 |
32 | class Positive(Validator):
33 | @classmethod
34 | def check(cls, value):
35 | if value < 0:
36 | raise ValueError('must be >= 0')
37 | return super().check(value)
38 |
39 | class NonEmpty(Validator):
40 | @classmethod
41 | def check(cls, value):
42 | if len(value) == 0:
43 | raise ValueError('must be non-empty')
44 | return super().check(value)
45 |
46 | class PositiveInteger(Integer, Positive):
47 | pass
48 |
49 | class PositiveFloat(Float, Positive):
50 | pass
51 |
52 | class NonEmptyString(String, NonEmpty):
53 | pass
54 |
55 | # Examples
56 | if __name__ == '__main__':
57 | def add(x, y):
58 | Integer.check(x)
59 | Integer.check(y)
60 | return x + y
61 |
62 | class Stock:
63 | name = NonEmptyString()
64 | shares = PositiveInteger()
65 | price = PositiveFloat()
66 | def __init__(self, name, shares, price):
67 | self.name = name
68 | self.shares = shares
69 | self.price = price
70 |
71 | def __repr__(self):
72 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
73 |
74 | @property
75 | def cost(self):
76 | return self.shares * self.price
77 |
78 | def sell(self, nshares):
79 | self.shares -= nshares
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/Solutions/5_2/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | from abc import ABC, abstractmethod
4 | import csv
5 | import logging
6 | log = logging.getLogger(__name__)
7 |
8 | class CSVParser(ABC):
9 |
10 | def parse(self, filename):
11 | records = []
12 | with open(filename) as f:
13 | rows = csv.reader(f)
14 | headers = next(rows)
15 | for rowno, row in enumerate(rows, start=1):
16 | try:
17 | record = self.make_record(headers, row)
18 | records.append(record)
19 | except ValueError as e:
20 | log.warning('Row %d: Bad row: %s', rowno, row)
21 | log.debug('Row %d: Reason: %s', rowno, e)
22 |
23 | return records
24 |
25 | @abstractmethod
26 | def make_record(self, headers, row):
27 | raise NotImplementedError()
28 |
29 | class DictCSVParser(CSVParser):
30 | def __init__(self, types):
31 | self.types = types
32 |
33 | def make_record(self, headers, row):
34 | return { name: func(val) for name, func, val in zip(headers, self.types, row) }
35 |
36 | class InstanceCSVParser(CSVParser):
37 | def __init__(self, cls):
38 | self.cls = cls
39 |
40 | def make_record(self, headers, row):
41 | return self.cls.from_row(row)
42 |
43 | def read_csv_as_dicts(filename, types):
44 | parser = DictCSVParser(types)
45 | return parser.parse(filename)
46 |
47 | def read_csv_as_instances(filename, cls):
48 | parser = InstanceCSVParser(cls)
49 | return parser.parse(filename)
50 |
--------------------------------------------------------------------------------
/Solutions/5_2/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | __slots__ = ('name','_shares','_price')
5 | _types = (str, int, float)
6 | def __init__(self, name, shares, price):
7 | self.name = name
8 | self.shares = shares
9 | self.price = price
10 |
11 | def __repr__(self):
12 | # Note: The !r format code produces the repr() string
13 | return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})'
14 |
15 | def __eq__(self, other):
16 | return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
17 | (other.name, other.shares, other.price))
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | values = [func(val) for func, val in zip(cls._types, row)]
22 | return cls(*values)
23 |
24 | @property
25 | def shares(self):
26 | return self._shares
27 | @shares.setter
28 | def shares(self, value):
29 | if not isinstance(value, self._types[1]):
30 | raise TypeError(f'Expected {self._types[1].__name__}')
31 | if value < 0:
32 | raise ValueError('shares must be >= 0')
33 | self._shares = value
34 |
35 | @property
36 | def price(self):
37 | return self._price
38 | @price.setter
39 | def price(self, value):
40 | if not isinstance(value, self._types[2]):
41 | raise TypeError(f'Expected {self._types[2].__name__}')
42 | if value < 0:
43 | raise ValueError('price must be >= 0')
44 | self._price = value
45 |
46 | @property
47 | def cost(self):
48 | return self.shares * self.price
49 |
50 | def sell(self, nshares):
51 | self.shares -= nshares
52 |
--------------------------------------------------------------------------------
/Solutions/5_3/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 |
5 | def convert_csv(lines, converter, *, headers=None):
6 | rows = csv.reader(lines)
7 | if headers is None:
8 | headers = next(rows)
9 | return list(map(lambda row: converter(headers, row), rows))
10 |
11 | def csv_as_dicts(lines, types, *, headers=None):
12 | return convert_csv(lines,
13 | lambda headers, row: {name: func(val) for name, func, val in zip(headers, types, row)},
14 | headers=headers)
15 |
16 |
17 | def csv_as_instances(lines, cls, *, headers=None):
18 | return convert_csv(lines,
19 | lambda headers, row: cls.from_row(row),
20 | headers=headers)
21 |
22 | def read_csv_as_dicts(filename, types, *, headers=None):
23 | '''
24 | Read CSV data into a list of dictionaries with optional type conversion
25 | '''
26 | with open(filename) as file:
27 | return csv_as_dicts(file, types, headers=headers)
28 |
29 | def read_csv_as_instances(filename, cls, *, headers=None):
30 | '''
31 | Read CSV data into a list of instances
32 | '''
33 | with open(filename) as file:
34 | return csv_as_instances(file, cls, headers=headers)
35 |
36 |
--------------------------------------------------------------------------------
/Solutions/5_3/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | __slots__ = ('name','_shares','_price')
5 | _types = (str, int, float)
6 | def __init__(self, name, shares, price):
7 | self.name = name
8 | self.shares = shares
9 | self.price = price
10 |
11 | def __repr__(self):
12 | # Note: The !r format code produces the repr() string
13 | return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})'
14 |
15 | def __eq__(self, other):
16 | return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
17 | (other.name, other.shares, other.price))
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | values = [func(val) for func, val in zip(cls._types, row)]
22 | return cls(*values)
23 |
24 | @property
25 | def shares(self):
26 | return self._shares
27 | @shares.setter
28 | def shares(self, value):
29 | if not isinstance(value, self._types[1]):
30 | raise TypeError(f'Expected {self._types[1].__name__}')
31 | if value < 0:
32 | raise ValueError('shares must be >= 0')
33 | self._shares = value
34 |
35 | @property
36 | def price(self):
37 | return self._price
38 | @price.setter
39 | def price(self, value):
40 | if not isinstance(value, self._types[2]):
41 | raise TypeError(f'Expected {self._types[2].__name__}')
42 | if value < 0:
43 | raise ValueError('price must be >= 0')
44 | self._price = value
45 |
46 | @property
47 | def cost(self):
48 | return self.shares * self.price
49 |
50 | def sell(self, nshares):
51 | self.shares -= nshares
52 |
--------------------------------------------------------------------------------
/Solutions/5_4/typedproperty.py:
--------------------------------------------------------------------------------
1 | # typedproperty.py
2 |
3 | def typedproperty(name, expected_type):
4 | private_name = '_' + name
5 |
6 | @property
7 | def value(self):
8 | return getattr(self, private_name)
9 |
10 | @value.setter
11 | def value(self, val):
12 | if not isinstance(val, expected_type):
13 | raise TypeError(f'Expected {expected_type}')
14 | setattr(self, private_name, val)
15 |
16 | return value
17 |
18 |
19 | String = lambda name: typedproperty(name, str)
20 | Integer = lambda name: typedproperty(name, int)
21 | Float = lambda name: typedproperty(name, float)
22 |
23 | # Example
24 | if __name__ == '__main__':
25 | class Stock:
26 | name = String('name')
27 | shares = Integer('shares')
28 | price = Float('price')
29 | def __init__(self, name, shares, price):
30 | self.name = name
31 | self.shares = shares
32 | self.price = price
33 |
34 |
--------------------------------------------------------------------------------
/Solutions/5_5/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | def convert_csv(lines, converter, *, headers=None):
9 | rows = csv.reader(lines)
10 | if headers is None:
11 | headers = next(rows)
12 |
13 | records = []
14 | for rowno, row in enumerate(rows, start=1):
15 | try:
16 | records.append(converter(headers, row))
17 | except ValueError as e:
18 | log.warning('Row %s: Bad row: %s', rowno, row)
19 | log.debug('Row %s: Reason: %s', rowno, row)
20 | return records
21 |
22 | def csv_as_dicts(lines, types, *, headers=None):
23 | return convert_csv(lines,
24 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
25 |
26 | def csv_as_instances(lines, cls, *, headers=None):
27 | return convert_csv(lines,
28 | lambda headers, row: cls.from_row(row))
29 |
30 | def read_csv_as_dicts(filename, types, *, headers=None):
31 | '''
32 | Read CSV data into a list of dictionaries with optional type conversion
33 | '''
34 | with open(filename) as file:
35 | return csv_as_dicts(file, types, headers=headers)
36 |
37 | def read_csv_as_instances(filename, cls, *, headers=None):
38 | '''
39 | Read CSV data into a list of instances
40 | '''
41 | with open(filename) as file:
42 | return csv_as_instances(file, cls, headers=headers)
43 |
44 |
--------------------------------------------------------------------------------
/Solutions/5_6/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | class Stock:
4 | __slots__ = ('name','_shares','_price')
5 | _types = (str, int, float)
6 | def __init__(self, name, shares, price):
7 | self.name = name
8 | self.shares = shares
9 | self.price = price
10 |
11 | def __repr__(self):
12 | # Note: The !r format code produces the repr() string
13 | return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})'
14 |
15 | def __eq__(self, other):
16 | return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
17 | (other.name, other.shares, other.price))
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | values = [func(val) for func, val in zip(cls._types, row)]
22 | return cls(*values)
23 |
24 | @property
25 | def shares(self):
26 | return self._shares
27 | @shares.setter
28 | def shares(self, value):
29 | if not isinstance(value, self._types[1]):
30 | raise TypeError(f'Expected {self._types[1].__name__}')
31 | if value < 0:
32 | raise ValueError('shares must be >= 0')
33 | self._shares = value
34 |
35 | @property
36 | def price(self):
37 | return self._price
38 | @price.setter
39 | def price(self, value):
40 | if not isinstance(value, self._types[2]):
41 | raise TypeError(f'Expected {self._types[2].__name__}')
42 | if value < 0:
43 | raise ValueError('price must be >= 0')
44 | self._price = value
45 |
46 | @property
47 | def cost(self):
48 | return self.shares * self.price
49 |
50 | def sell(self, nshares):
51 | self.shares -= nshares
52 |
--------------------------------------------------------------------------------
/Solutions/5_6/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/6_1/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 |
5 | class Stock(Structure):
6 | _fields = ('name', 'shares', 'price')
7 |
8 | @property
9 | def cost(self):
10 | return self.shares * self.price
11 |
12 | def sell(self, nshares):
13 | self.shares -= nshares
14 |
15 |
--------------------------------------------------------------------------------
/Solutions/6_1/structure.py:
--------------------------------------------------------------------------------
1 | # structure.py
2 |
3 | class Structure:
4 | _fields = ()
5 | def __init__(self, *args):
6 | if len(args) != len(self._fields):
7 | raise TypeError('Expected %d arguments' % len(self._fields))
8 | for name, val in zip(self._fields, args):
9 | setattr(self, name, val)
10 |
11 | def __setattr__(self, name, value):
12 | if name.startswith('_') or name in self._fields:
13 | super().__setattr__(name, value)
14 | else:
15 | raise AttributeError('No attribute %s' % name)
16 |
17 | def __repr__(self):
18 | return '%s(%s)' % (type(self).__name__,
19 | ', '.join(repr(getattr(self, name)) for name in self._fields))
20 |
--------------------------------------------------------------------------------
/Solutions/6_1/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/6_2/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 |
5 | class Stock(Structure):
6 | _fields = ('name', 'shares', 'price')
7 | def __init__(self, name, shares, price):
8 | self._init()
9 |
10 | @property
11 | def cost(self):
12 | return self.shares * self.price
13 |
14 | def sell(self, nshares):
15 | self.shares -= nshares
16 |
17 |
--------------------------------------------------------------------------------
/Solutions/6_2/structure.py:
--------------------------------------------------------------------------------
1 | # structure.py
2 |
3 | import sys
4 |
5 | class Structure:
6 | _fields = ()
7 |
8 | @staticmethod
9 | def _init():
10 | locs = sys._getframe(1).f_locals
11 | self = locs.pop('self')
12 | for name, val in locs.items():
13 | setattr(self, name, val)
14 |
15 | def __setattr__(self, name, value):
16 | if name.startswith('_') or name in self._fields:
17 | super().__setattr__(name, value)
18 | else:
19 | raise AttributeError('No attribute %s' % name)
20 |
21 | def __repr__(self):
22 | return '%s(%s)' % (type(self).__name__,
23 | ', '.join(repr(getattr(self, name)) for name in self._fields))
24 |
--------------------------------------------------------------------------------
/Solutions/6_2/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/6_3/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 |
5 | class Stock(Structure):
6 | def __init__(self, name, shares, price):
7 | self._init()
8 |
9 | @property
10 | def cost(self):
11 | return self.shares * self.price
12 |
13 | def sell(self, nshares):
14 | self.shares -= nshares
15 |
16 | Stock.set_fields()
17 |
18 |
--------------------------------------------------------------------------------
/Solutions/6_3/structure.py:
--------------------------------------------------------------------------------
1 | # structure.py
2 |
3 | import sys
4 | import inspect
5 |
6 | class Structure:
7 | _fields = ()
8 |
9 | @staticmethod
10 | def _init():
11 | locs = sys._getframe(1).f_locals
12 | self = locs.pop('self')
13 | for name, val in locs.items():
14 | setattr(self, name, val)
15 |
16 | def __setattr__(self, name, value):
17 | if name.startswith('_') or name in self._fields:
18 | super().__setattr__(name, value)
19 | else:
20 | raise AttributeError('No attribute %s' % name)
21 |
22 | def __repr__(self):
23 | return '%s(%s)' % (type(self).__name__,
24 | ', '.join(repr(getattr(self, name)) for name in self._fields))
25 |
26 | @classmethod
27 | def set_fields(cls):
28 | sig = inspect.signature(cls)
29 | cls._fields = tuple(sig.parameters)
30 |
31 |
--------------------------------------------------------------------------------
/Solutions/6_3/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/6_4/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 |
5 | class Stock(Structure):
6 | _fields = ('name', 'shares', 'price')
7 |
8 | @property
9 | def cost(self):
10 | return self.shares * self.price
11 |
12 | def sell(self, nshares):
13 | self.shares -= nshares
14 |
15 | Stock.create_init()
16 |
17 |
--------------------------------------------------------------------------------
/Solutions/6_4/structure.py:
--------------------------------------------------------------------------------
1 | # structure.py
2 |
3 | class Structure:
4 | _fields = ()
5 |
6 | def __setattr__(self, name, value):
7 | if name.startswith('_') or name in self._fields:
8 | super().__setattr__(name, value)
9 | else:
10 | raise AttributeError('No attribute %s' % name)
11 |
12 | def __repr__(self):
13 | return '%s(%s)' % (type(self).__name__,
14 | ', '.join(repr(getattr(self, name)) for name in self._fields))
15 |
16 | @classmethod
17 | def create_init(cls):
18 | '''
19 | Create an __init__ method from _fields
20 | '''
21 | args = ','.join(cls._fields)
22 | code = f'def __init__(self, {args}):\n'
23 | for name in cls._fields:
24 | code += f' self.{name} = {name}\n'
25 | locs = { }
26 | exec(code, locs)
27 | cls.__init__ = locs['__init__']
28 |
--------------------------------------------------------------------------------
/Solutions/6_4/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/7_1/logcall.py:
--------------------------------------------------------------------------------
1 | # logcall.py
2 |
3 | def logged(func):
4 | print('Adding logging to', func.__name__)
5 | def wrapper(*args,**kwargs):
6 | print('Calling', func.__name__)
7 | return func(*args,**kwargs)
8 | return wrapper
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Solutions/7_1/sample.py:
--------------------------------------------------------------------------------
1 | # sample.py
2 |
3 | from logcall import logged
4 |
5 | @logged
6 | def add(x,y):
7 | return x+y
8 |
9 | @logged
10 | def sub(x,y):
11 | return x-y
12 |
--------------------------------------------------------------------------------
/Solutions/7_2/logcall.py:
--------------------------------------------------------------------------------
1 | # logcall.py
2 |
3 | from functools import wraps
4 |
5 | def logformat(fmt):
6 | def logged(func):
7 | print('Adding logging to', func.__name__)
8 | @wraps(func)
9 | def wrapper(*args,**kwargs):
10 | print(fmt.format(func=func))
11 | return func(*args, **kwargs)
12 | return wrapper
13 | return logged
14 |
15 | # Original no-argument @logged decorator defined in terms of the more
16 | # general @logformat decorator
17 |
18 | logged = logformat('Calling {func.__name__}')
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Solutions/7_2/sample.py:
--------------------------------------------------------------------------------
1 | # sample.py
2 |
3 | from logcall import logged, logformat
4 |
5 | @logged
6 | def add(x,y):
7 | return x+y
8 |
9 | @logged
10 | def sub(x,y):
11 | return x-y
12 |
13 | @logformat('{func.__code__.co_filename}:{func.__name__}')
14 | def mul(x,y):
15 | return x*y
16 |
--------------------------------------------------------------------------------
/Solutions/7_2/spam.py:
--------------------------------------------------------------------------------
1 | from logcall import logged
2 |
3 | class Spam:
4 | @logged
5 | def instance_method(self):
6 | pass
7 |
8 | @classmethod
9 | @logged
10 | def class_method(cls):
11 | pass
12 |
13 | @staticmethod
14 | @logged
15 | def static_method():
16 | pass
17 |
18 | @property
19 | @logged
20 | def property_method(self):
21 | pass
22 |
--------------------------------------------------------------------------------
/Solutions/7_3/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | def convert_csv(lines, converter, *, headers=None):
9 | rows = csv.reader(lines)
10 | if headers is None:
11 | headers = next(rows)
12 |
13 | records = []
14 | for rowno, row in enumerate(rows, start=1):
15 | try:
16 | records.append(converter(headers, row))
17 | except ValueError as e:
18 | log.warning('Row %s: Bad row: %s', rowno, row)
19 | log.debug('Row %s: Reason: %s', rowno, row)
20 | return records
21 |
22 | def csv_as_dicts(lines, types, *, headers=None):
23 | return convert_csv(lines,
24 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
25 |
26 | def csv_as_instances(lines, cls, *, headers=None):
27 | return convert_csv(lines,
28 | lambda headers, row: cls.from_row(row))
29 |
30 | def read_csv_as_dicts(filename, types, *, headers=None):
31 | '''
32 | Read CSV data into a list of dictionaries with optional type conversion
33 | '''
34 | with open(filename) as file:
35 | return csv_as_dicts(file, types, headers=headers)
36 |
37 | def read_csv_as_instances(filename, cls, *, headers=None):
38 | '''
39 | Read CSV data into a list of instances
40 | '''
41 | with open(filename) as file:
42 | return csv_as_instances(file, cls, headers=headers)
43 |
44 |
--------------------------------------------------------------------------------
/Solutions/7_3/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 | from validate import String, PositiveInteger, PositiveFloat
5 |
6 | class Stock(Structure):
7 | name = String()
8 | shares = PositiveInteger()
9 | price = PositiveFloat()
10 |
11 | @property
12 | def cost(self):
13 | return self.shares * self.price
14 |
15 | def sell(self, nshares: PositiveInteger):
16 | self.shares -= nshares
17 |
--------------------------------------------------------------------------------
/Solutions/7_3/structure.py:
--------------------------------------------------------------------------------
1 | # structure.py
2 |
3 | from validate import Validator, validated
4 |
5 | class Structure:
6 | _fields = ()
7 | _types = ()
8 |
9 | def __setattr__(self, name, value):
10 | if name.startswith('_') or name in self._fields:
11 | super().__setattr__(name, value)
12 | else:
13 | raise AttributeError('No attribute %s' % name)
14 |
15 | def __repr__(self):
16 | return '%s(%s)' % (type(self).__name__,
17 | ', '.join(repr(getattr(self, name)) for name in self._fields))
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | rowdata = [ func(val) for func, val in zip(cls._types, row) ]
22 | return cls(*rowdata)
23 |
24 | @classmethod
25 | def create_init(cls):
26 | '''
27 | Create an __init__ method from _fields
28 | '''
29 | args = ','.join(cls._fields)
30 | code = f'def __init__(self, {args}):\n'
31 | for name in cls._fields:
32 | code += f' self.{name} = {name}\n'
33 | locs = { }
34 | exec(code, locs)
35 | cls.__init__ = locs['__init__']
36 |
37 | @classmethod
38 | def __init_subclass__(cls):
39 | # Apply the validated decorator to subclasses
40 | validate_attributes(cls)
41 |
42 | def validate_attributes(cls):
43 | '''
44 | Class decorator that scans a class definition for Validators
45 | and builds a _fields variable that captures their definition order.
46 | '''
47 | validators = []
48 | for name, val in vars(cls).items():
49 | if isinstance(val, Validator):
50 | validators.append(val)
51 |
52 | # Apply validated decorator to any callable with annotations
53 | elif callable(val) and val.__annotations__:
54 | setattr(cls, name, validated(val))
55 |
56 | # Collect all of the field names
57 | cls._fields = tuple([v.name for v in validators])
58 |
59 | # Collect type conversions. The lambda x:x is an identity
60 | # function that's used in case no expected_type is found.
61 | cls._types = tuple([ getattr(v, 'expected_type', lambda x: x)
62 | for v in validators ])
63 |
64 | # Create the __init__ method
65 | if cls._fields:
66 | cls.create_init()
67 |
68 |
69 | return cls
70 |
71 |
--------------------------------------------------------------------------------
/Solutions/7_3/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/7_4/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 | from validate import String, PositiveInteger, PositiveFloat
5 |
6 | class Stock(Structure):
7 | name = String()
8 | shares = PositiveInteger()
9 | price = PositiveFloat()
10 |
11 | @property
12 | def cost(self):
13 | return self.shares * self.price
14 |
15 | def sell(self, nshares: PositiveInteger):
16 | self.shares -= nshares
17 |
--------------------------------------------------------------------------------
/Solutions/7_4/structure.py:
--------------------------------------------------------------------------------
1 | # structure.py
2 |
3 | from validate import Validator, validated
4 |
5 | class Structure:
6 | _fields = ()
7 | _types = ()
8 |
9 | def __setattr__(self, name, value):
10 | if name.startswith('_') or name in self._fields:
11 | super().__setattr__(name, value)
12 | else:
13 | raise AttributeError('No attribute %s' % name)
14 |
15 | def __repr__(self):
16 | return '%s(%s)' % (type(self).__name__,
17 | ', '.join(repr(getattr(self, name)) for name in self._fields))
18 |
19 | @classmethod
20 | def from_row(cls, row):
21 | rowdata = [ func(val) for func, val in zip(cls._types, row) ]
22 | return cls(*rowdata)
23 |
24 | @classmethod
25 | def create_init(cls):
26 | '''
27 | Create an __init__ method from _fields
28 | '''
29 | args = ','.join(cls._fields)
30 | code = f'def __init__(self, {args}):\n'
31 | for name in cls._fields:
32 | code += f' self.{name} = {name}\n'
33 | locs = { }
34 | exec(code, locs)
35 | cls.__init__ = locs['__init__']
36 |
37 | @classmethod
38 | def __init_subclass__(cls):
39 | # Apply the validated decorator to subclasses
40 | validate_attributes(cls)
41 |
42 | def validate_attributes(cls):
43 | '''
44 | Class decorator that scans a class definition for Validators
45 | and builds a _fields variable that captures their definition order.
46 | '''
47 | validators = []
48 | for name, val in vars(cls).items():
49 | if isinstance(val, Validator):
50 | validators.append(val)
51 |
52 | # Apply validated decorator to any callable with annotations
53 | elif callable(val) and val.__annotations__:
54 | setattr(cls, name, validated(val))
55 |
56 | # Collect all of the field names
57 | cls._fields = tuple([v.name for v in validators])
58 |
59 | # Collect type conversions. The lambda x:x is an identity
60 | # function that's used in case no expected_type is found.
61 | cls._types = tuple([ getattr(v, 'expected_type', lambda x: x)
62 | for v in validators ])
63 |
64 | # Create the __init__ method
65 | if cls._fields:
66 | cls.create_init()
67 |
68 |
69 | return cls
70 |
71 | def typed_structure(clsname, **validators):
72 | cls = type(clsname, (Structure,), validators)
73 | return cls
74 |
--------------------------------------------------------------------------------
/Solutions/7_4/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/7_5/mymeta.py:
--------------------------------------------------------------------------------
1 | # mymeta.py
2 |
3 | class mytype(type):
4 | @staticmethod
5 | def __new__(meta, name, bases, __dict__):
6 | print("Creating class :", name)
7 | print("Base classes :", bases)
8 | print("Attributes :", list(__dict__.keys()))
9 | return super().__new__(meta, name, bases, __dict__)
10 |
11 | class myobject(metaclass=mytype):
12 | pass
13 |
14 | class Stock(myobject):
15 | def __init__(self,name,shares,price):
16 | self.name = name
17 | self.shares = shares
18 | self.price = price
19 | def cost(self):
20 | return self.shares*self.price
21 | def sell(self,nshares):
22 | self.shares -= nshares
23 |
24 | class MyStock(Stock):
25 | pass
26 |
--------------------------------------------------------------------------------
/Solutions/7_6/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | def convert_csv(lines, converter, *, headers=None):
9 | rows = csv.reader(lines)
10 | if headers is None:
11 | headers = next(rows)
12 |
13 | records = []
14 | for rowno, row in enumerate(rows, start=1):
15 | try:
16 | records.append(converter(headers, row))
17 | except ValueError as e:
18 | log.warning('Row %s: Bad row: %s', rowno, row)
19 | log.debug('Row %s: Reason: %s', rowno, row)
20 | return records
21 |
22 | def csv_as_dicts(lines, types, *, headers=None):
23 | return convert_csv(lines,
24 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
25 |
26 | def csv_as_instances(lines, cls, *, headers=None):
27 | return convert_csv(lines,
28 | lambda headers, row: cls.from_row(row))
29 |
30 | def read_csv_as_dicts(filename, types, *, headers=None):
31 | '''
32 | Read CSV data into a list of dictionaries with optional type conversion
33 | '''
34 | with open(filename) as file:
35 | return csv_as_dicts(file, types, headers=headers)
36 |
37 | def read_csv_as_instances(filename, cls, *, headers=None):
38 | '''
39 | Read CSV data into a list of instances
40 | '''
41 | with open(filename) as file:
42 | return csv_as_instances(file, cls, headers=headers)
43 |
44 |
--------------------------------------------------------------------------------
/Solutions/7_6/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 |
5 | class Stock(Structure):
6 | name = String()
7 | shares = PositiveInteger()
8 | price = PositiveFloat()
9 |
10 | @property
11 | def cost(self):
12 | return self.shares * self.price
13 |
14 | def sell(self, nshares: PositiveInteger):
15 | self.shares -= nshares
16 |
17 | if __name__ == '__main__':
18 | from reader import read_csv_as_instances
19 | from tableformat import create_formatter, print_table
20 |
21 | portfolio = read_csv_as_instances('../../Data/portfolio.csv', Stock)
22 | formatter = create_formatter('text')
23 | print_table(portfolio, ['name','shares','price'], formatter)
24 |
--------------------------------------------------------------------------------
/Solutions/7_6/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise RuntimeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 | class TextTableFormatter(TableFormatter):
23 | def headings(self, headers):
24 | print(' '.join('%10s' % h for h in headers))
25 | print(('-'*10 + ' ')*len(headers))
26 |
27 | def row(self, rowdata):
28 | print(' '.join('%10s' % d for d in rowdata))
29 |
30 | class CSVTableFormatter(TableFormatter):
31 | def headings(self, headers):
32 | print(','.join(headers))
33 |
34 | def row(self, rowdata):
35 | print(','.join(str(d) for d in rowdata))
36 |
37 | class HTMLTableFormatter(TableFormatter):
38 | def headings(self, headers):
39 | print('', end=' ')
40 | for h in headers:
41 | print('%s | ' % h, end=' ')
42 | print('
')
43 |
44 | def row(self, rowdata):
45 | print('', end=' ')
46 | for d in rowdata:
47 | print('%s | ' % d, end=' ')
48 | print('
')
49 |
50 | class ColumnFormatMixin:
51 | formats = []
52 | def row(self, rowdata):
53 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
54 | super().row(rowdata)
55 |
56 | class UpperHeadersMixin:
57 | def headings(self, headers):
58 | super().headings([h.upper() for h in headers])
59 |
60 | def create_formatter(name, column_formats=None, upper_headers=False):
61 | if name == 'text':
62 | formatter_cls = TextTableFormatter
63 | elif name == 'csv':
64 | formatter_cls = CSVTableFormatter
65 | elif name == 'html':
66 | formatter_cls = HTMLTableFormatter
67 | else:
68 | raise RuntimeError('Unknown format %s' % name)
69 |
70 | if column_formats:
71 | class formatter_cls(ColumnFormatMixin, formatter_cls):
72 | formats = column_formats
73 |
74 | if upper_headers:
75 | class formatter_cls(UpperHeadersMixin, formatter_cls):
76 | pass
77 |
78 | return formatter_cls()
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/Solutions/7_6/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/8_1/follow.py:
--------------------------------------------------------------------------------
1 | # follow.py
2 | import os
3 | import time
4 |
5 | def follow(filename):
6 | '''
7 | Generator that produces a sequence of lines being written at the end of a file.
8 | '''
9 | with open(filename,'r') as f:
10 | f.seek(0,os.SEEK_END)
11 | while True:
12 | line = f.readline()
13 | if line == '':
14 | time.sleep(0.1) # Sleep briefly to avoid busy wait
15 | continue
16 | yield line
17 |
18 | # Example use
19 | if __name__ == '__main__':
20 | for line in follow('../../Data/stocklog.csv'):
21 | fields = line.split(',')
22 | name = fields[0].strip('"')
23 | price = float(fields[1])
24 | change = float(fields[4])
25 | if change < 0:
26 | print('%10s %10.2f %10.2f' % (name, price, change))
27 |
28 |
--------------------------------------------------------------------------------
/Solutions/8_1/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | def convert_csv(lines, converter, *, headers=None):
9 | rows = csv.reader(lines)
10 | if headers is None:
11 | headers = next(rows)
12 |
13 | records = []
14 | for rowno, row in enumerate(rows, start=1):
15 | try:
16 | records.append(converter(headers, row))
17 | except ValueError as e:
18 | log.warning('Row %s: Bad row: %s', rowno, row)
19 | log.debug('Row %s: Reason: %s', rowno, row)
20 | return records
21 |
22 | def csv_as_dicts(lines, types, *, headers=None):
23 | return convert_csv(lines,
24 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
25 |
26 | def csv_as_instances(lines, cls, *, headers=None):
27 | return convert_csv(lines,
28 | lambda headers, row: cls.from_row(row))
29 |
30 | def read_csv_as_dicts(filename, types, *, headers=None):
31 | '''
32 | Read CSV data into a list of dictionaries with optional type conversion
33 | '''
34 | with open(filename) as file:
35 | return csv_as_dicts(file, types, headers=headers)
36 |
37 | def read_csv_as_instances(filename, cls, *, headers=None):
38 | '''
39 | Read CSV data into a list of instances
40 | '''
41 | with open(filename) as file:
42 | return csv_as_instances(file, cls, headers=headers)
43 |
44 |
--------------------------------------------------------------------------------
/Solutions/8_1/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structure import Structure
4 | from validate import String, PositiveInteger, PositiveFloat
5 |
6 | class Stock(Structure):
7 | name = String('name')
8 | shares = PositiveInteger('shares')
9 | price = PositiveFloat('price')
10 |
11 | @property
12 | def cost(self):
13 | return self.shares * self.price
14 |
15 | def sell(self, nshares):
16 | self.shares -= nshares
17 |
18 | if __name__ == '__main__':
19 | from reader import read_csv_as_instances
20 | from tableformat import create_formatter, print_table
21 |
22 | portfolio = read_csv_as_instances('../../Data/portfolio.csv', Stock)
23 | formatter = create_formatter('text')
24 | print_table(portfolio, ['name','shares','price'], formatter)
25 |
--------------------------------------------------------------------------------
/Solutions/8_1/teststock.py:
--------------------------------------------------------------------------------
1 | # teststock.py
2 |
3 | import stock
4 | import unittest
5 |
6 | class TestStock(unittest.TestCase):
7 | def test_create(self):
8 | s = stock.Stock('GOOG', 100, 490.1)
9 | self.assertEqual(s.name, 'GOOG')
10 | self.assertEqual(s.shares, 100)
11 | self.assertEqual(s.price, 490.1)
12 |
13 | def test_create_keyword(self):
14 | s = stock.Stock(name='GOOG', shares=100, price=490.1)
15 | self.assertEqual(s.name, 'GOOG')
16 | self.assertEqual(s.shares, 100)
17 | self.assertEqual(s.price, 490.1)
18 |
19 | def test_cost(self):
20 | s = stock.Stock('GOOG', 100, 490.1)
21 | self.assertEqual(s.cost, 49010.0)
22 |
23 | def test_sell(self):
24 | s = stock.Stock('GOOG', 100, 490.1)
25 | s.sell(25)
26 | self.assertEqual(s.shares, 75)
27 |
28 | def test_from_row(self):
29 | s = stock.Stock.from_row(['GOOG','100','490.1'])
30 | self.assertEqual(s.name, 'GOOG')
31 | self.assertEqual(s.shares, 100)
32 | self.assertEqual(s.price, 490.1)
33 |
34 | def test_repr(self):
35 | s = stock.Stock('GOOG', 100, 490.1)
36 | self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
37 |
38 | def test_eq(self):
39 | a = stock.Stock('GOOG', 100, 490.1)
40 | b = stock.Stock('GOOG', 100, 490.1)
41 | self.assertTrue(a==b)
42 |
43 | # Tests for failure conditions
44 | def test_shares_badtype(self):
45 | s = stock.Stock('GOOG', 100, 490.1)
46 | with self.assertRaises(TypeError):
47 | s.shares = '50'
48 |
49 | def test_shares_badvalue(self):
50 | s = stock.Stock('GOOG', 100, 490.1)
51 | with self.assertRaises(ValueError):
52 | s.shares = -50
53 |
54 | def test_price_badtype(self):
55 | s = stock.Stock('GOOG', 100, 490.1)
56 | with self.assertRaises(TypeError):
57 | s.price = '45.23'
58 |
59 | def test_price_badvalue(self):
60 | s = stock.Stock('GOOG', 100, 490.1)
61 | with self.assertRaises(ValueError):
62 | s.price = -45.23
63 |
64 | def test_bad_attribute(self):
65 | s = stock.Stock('GOOG', 100, 490.1)
66 | with self.assertRaises(AttributeError):
67 | s.share = 100
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/Solutions/8_2/follow.py:
--------------------------------------------------------------------------------
1 | # follow.py
2 | import os
3 | import time
4 | import csv
5 |
6 | def follow(filename):
7 | '''
8 | Generator that produces a sequence of lines being written at the end of a file.
9 | '''
10 | with open(filename,'r') as f:
11 | f.seek(0,os.SEEK_END)
12 | while True:
13 | line = f.readline()
14 | if line == '':
15 | time.sleep(0.1) # Sleep briefly to avoid busy wait
16 | continue
17 | yield line
18 |
--------------------------------------------------------------------------------
/Solutions/8_2/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise RuntimeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 | class TextTableFormatter(TableFormatter):
23 | def headings(self, headers):
24 | print(' '.join('%10s' % h for h in headers))
25 | print(('-'*10 + ' ')*len(headers))
26 |
27 | def row(self, rowdata):
28 | print(' '.join('%10s' % d for d in rowdata))
29 |
30 | class CSVTableFormatter(TableFormatter):
31 | def headings(self, headers):
32 | print(','.join(headers))
33 |
34 | def row(self, rowdata):
35 | print(','.join(str(d) for d in rowdata))
36 |
37 | class HTMLTableFormatter(TableFormatter):
38 | def headings(self, headers):
39 | print('', end=' ')
40 | for h in headers:
41 | print('%s | ' % h, end=' ')
42 | print('
')
43 |
44 | def row(self, rowdata):
45 | print('', end=' ')
46 | for d in rowdata:
47 | print('%s | ' % d, end=' ')
48 | print('
')
49 |
50 | class ColumnFormatMixin:
51 | formats = []
52 | def row(self, rowdata):
53 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
54 | super().row(rowdata)
55 |
56 | class UpperHeadersMixin:
57 | def headings(self, headers):
58 | super().headings([h.upper() for h in headers])
59 |
60 | def create_formatter(name, column_formats=None, upper_headers=False):
61 | if name == 'text':
62 | formatter_cls = TextTableFormatter
63 | elif name == 'csv':
64 | formatter_cls = CSVTableFormatter
65 | elif name == 'html':
66 | formatter_cls = HTMLTableFormatter
67 | else:
68 | raise RuntimeError('Unknown format %s' % name)
69 |
70 | if column_formats:
71 | class formatter_cls(ColumnFormatMixin, formatter_cls):
72 | formats = column_formats
73 |
74 | if upper_headers:
75 | class formatter_cls(UpperHeadersMixin, formatter_cls):
76 | pass
77 |
78 | return formatter_cls()
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/Solutions/8_2/ticker.py:
--------------------------------------------------------------------------------
1 | # ticker.py
2 | from structure import Structure
3 |
4 | class Ticker(Structure):
5 | name = String()
6 | price = Float()
7 | date = String()
8 | time = String()
9 | change = Float()
10 | open = Float()
11 | high = Float()
12 | low = Float()
13 | volume = Integer()
14 |
15 | if __name__ == '__main__':
16 | from follow import follow
17 | import csv
18 | from tableformat import create_formatter, print_table
19 |
20 | formatter = create_formatter('text')
21 |
22 | lines = follow('../../Data/stocklog.csv')
23 | rows = csv.reader(lines)
24 | records = (Ticker.from_row(row) for row in rows)
25 | negative = (rec for rec in records if rec.change < 0)
26 | print_table(negative, ['name','price','change'], formatter)
27 |
--------------------------------------------------------------------------------
/Solutions/8_3/cofollow.py:
--------------------------------------------------------------------------------
1 | # cofollow.py
2 | import os
3 | import time
4 |
5 | def follow(filename, target):
6 | with open(filename, 'r') as f:
7 | f.seek(0,os.SEEK_END)
8 | while True:
9 | line = f.readline()
10 | if line != '':
11 | target.send(line)
12 | else:
13 | time.sleep(0.1)
14 |
15 | # Decorator for coroutines
16 | from functools import wraps
17 |
18 | def consumer(func):
19 | @wraps(func)
20 | def start(*args,**kwargs):
21 | f = func(*args,**kwargs)
22 | f.send(None)
23 | return f
24 | return start
25 |
26 | # Sample coroutine
27 | @consumer
28 | def printer():
29 | while True:
30 | item = yield
31 | print(item)
32 |
33 | # Example use.
34 |
35 | if __name__ == '__main__':
36 | follow('../../Data/stocklog.csv', printer())
37 |
--------------------------------------------------------------------------------
/Solutions/8_3/coticker.py:
--------------------------------------------------------------------------------
1 | # coticker.py
2 | from structure import Structure
3 |
4 | class Ticker(Structure):
5 | name = String()
6 | price = Float()
7 | date = String()
8 | time = String()
9 | change = Float()
10 | open = Float()
11 | high = Float()
12 | low = Float()
13 | volume = Integer()
14 |
15 | from cofollow import consumer, follow
16 | from tableformat import create_formatter
17 | import csv
18 |
19 | @consumer
20 | def to_csv(target):
21 | def producer():
22 | while True:
23 | yield line
24 |
25 | reader = csv.reader(producer())
26 | while True:
27 | line = yield
28 | target.send(next(reader))
29 |
30 | @consumer
31 | def create_ticker(target):
32 | while True:
33 | row = yield
34 | target.send(Ticker.from_row(row))
35 |
36 | @consumer
37 | def negchange(target):
38 | while True:
39 | record = yield
40 | if record.change < 0:
41 | target.send(record)
42 |
43 | @consumer
44 | def ticker(fmt, fields):
45 | formatter = create_formatter(fmt)
46 | formatter.headings(fields)
47 | while True:
48 | rec = yield
49 | row = [getattr(rec, name) for name in fields]
50 | formatter.row(row)
51 |
52 | if __name__ == '__main__':
53 | follow('../../Data/stocklog.csv',
54 | to_csv(
55 | create_ticker(
56 | negchange(
57 | ticker('text', ['name','price','change'])))))
58 |
--------------------------------------------------------------------------------
/Solutions/8_3/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise RuntimeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 | class TextTableFormatter(TableFormatter):
23 | def headings(self, headers):
24 | print(' '.join('%10s' % h for h in headers))
25 | print(('-'*10 + ' ')*len(headers))
26 |
27 | def row(self, rowdata):
28 | print(' '.join('%10s' % d for d in rowdata))
29 |
30 | class CSVTableFormatter(TableFormatter):
31 | def headings(self, headers):
32 | print(','.join(headers))
33 |
34 | def row(self, rowdata):
35 | print(','.join(str(d) for d in rowdata))
36 |
37 | class HTMLTableFormatter(TableFormatter):
38 | def headings(self, headers):
39 | print('', end=' ')
40 | for h in headers:
41 | print('%s | ' % h, end=' ')
42 | print('
')
43 |
44 | def row(self, rowdata):
45 | print('', end=' ')
46 | for d in rowdata:
47 | print('%s | ' % d, end=' ')
48 | print('
')
49 |
50 | class ColumnFormatMixin:
51 | formats = []
52 | def row(self, rowdata):
53 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
54 | super().row(rowdata)
55 |
56 | class UpperHeadersMixin:
57 | def headings(self, headers):
58 | super().headings([h.upper() for h in headers])
59 |
60 | def create_formatter(name, column_formats=None, upper_headers=False):
61 | if name == 'text':
62 | formatter_cls = TextTableFormatter
63 | elif name == 'csv':
64 | formatter_cls = CSVTableFormatter
65 | elif name == 'html':
66 | formatter_cls = HTMLTableFormatter
67 | else:
68 | raise RuntimeError('Unknown format %s' % name)
69 |
70 | if column_formats:
71 | class formatter_cls(ColumnFormatMixin, formatter_cls):
72 | formats = column_formats
73 |
74 | if upper_headers:
75 | class formatter_cls(UpperHeadersMixin, formatter_cls):
76 | pass
77 |
78 | return formatter_cls()
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/Solutions/8_4/cofollow.py:
--------------------------------------------------------------------------------
1 | # cofollow.py
2 | import os
3 | import time
4 | import csv
5 |
6 | def follow(filename,target):
7 | with open(filename,"r") as f:
8 | f.seek(0,os.SEEK_END)
9 | while True:
10 | line = f.readline()
11 | if line != '':
12 | target.send(line)
13 | else:
14 | time.sleep(0.1)
15 |
16 | # Decorator for coroutines
17 | from functools import wraps
18 |
19 | def consumer(func):
20 | @wraps(func)
21 | def start(*args,**kwargs):
22 | f = func(*args,**kwargs)
23 | f.send(None)
24 | return f
25 | return start
26 |
27 | # Sample coroutine
28 | @consumer
29 | def printer():
30 | while True:
31 | try:
32 | item = yield
33 | print(item)
34 | except Exception as e:
35 | print('ERROR: %r' % e)
36 |
37 | # Example use.
38 |
39 | if __name__ == '__main__':
40 | follow('../../Data/stocklog.csv', printer())
41 |
--------------------------------------------------------------------------------
/Solutions/8_4/follow.py:
--------------------------------------------------------------------------------
1 | # follow.py
2 | import os
3 | import time
4 |
5 | def follow(filename):
6 | '''
7 | Generator that produces a sequence of lines being written at the end of a file.
8 | '''
9 | try:
10 | with open(filename,'r') as f:
11 | f.seek(0,os.SEEK_END)
12 | while True:
13 | line = f.readline()
14 | if line == '':
15 | time.sleep(0.1) # Sleep briefly to avoid busy wait
16 | continue
17 | yield line
18 | except GeneratorExit:
19 | print('Following Done')
20 |
21 | def splitter(lines):
22 | for line in lines:
23 | yield line.split(',')
24 |
25 |
26 | def make_records(rows,names):
27 | for row in rows:
28 | yield dict(zip(names,row))
29 |
30 | def unquote(records,keylist):
31 | for r in records:
32 | for key in keylist:
33 | r[key] = r[key].strip('"')
34 | yield r
35 |
36 | def convert(records,converter,keylist):
37 | for r in records:
38 | for key in keylist:
39 | r[key] = converter(r[key])
40 | yield r
41 |
42 | def parse_stock_data(lines):
43 | rows = splitter(lines)
44 | records = make_records(rows,['name','price','date','time',
45 | 'change','open','high','low','volume'])
46 | records = unquote(records,["name","date","time"])
47 | records = convert(records,float,['price','change','open','high','low'])
48 | records = convert(records,int,['volume'])
49 | return records
50 |
51 | # Sample use
52 | if __name__ == '__main__':
53 | lines = follow("../../Data/stocklog.dat")
54 | records = parse_stock_data(lines)
55 | for r in records:
56 | print("%(name)10s %(price)10.2f %(change)10.2f" % r)
57 |
--------------------------------------------------------------------------------
/Solutions/8_5/multitask.py:
--------------------------------------------------------------------------------
1 | # multitask.py
2 |
3 | from collections import deque
4 |
5 | tasks = deque()
6 | def run():
7 | while tasks:
8 | task = tasks.popleft()
9 | try:
10 | next(task)
11 | tasks.append(task)
12 | except StopIteration:
13 | print('Task done')
14 |
15 | def countdown(n):
16 | while n > 0:
17 | print('T-minus', n)
18 | yield
19 | n -= 1
20 |
21 | def countup(n):
22 | x = 0
23 | while x < n:
24 | print('Up we go', x)
25 | yield
26 | x += 1
27 |
28 | if __name__ == '__main__':
29 | tasks.append(countdown(10))
30 | tasks.append(countdown(5))
31 | tasks.append(countup(20))
32 | run()
33 |
34 |
--------------------------------------------------------------------------------
/Solutions/8_5/server.py:
--------------------------------------------------------------------------------
1 | # server.py
2 |
3 | from socket import *
4 | from select import select
5 | from collections import deque
6 |
7 | tasks = deque()
8 | recv_wait = {} # sock -> task
9 | send_wait = {} # sock -> task
10 |
11 | def run():
12 | while any([tasks, recv_wait, send_wait]):
13 | while not tasks:
14 | can_recv, can_send, _ = select(recv_wait, send_wait, [])
15 | for s in can_recv:
16 | tasks.append(recv_wait.pop(s))
17 | for s in can_send:
18 | tasks.append(send_wait.pop(s))
19 | task = tasks.popleft()
20 | try:
21 | reason, resource = task.send(None)
22 | if reason == 'recv':
23 | recv_wait[resource] = task
24 | elif reason == 'send':
25 | send_wait[resource] = task
26 | else:
27 | raise RuntimeError('Unknown reason %r' % reason)
28 | except StopIteration:
29 | print('Task done')
30 |
31 | def tcp_server(address, handler):
32 | sock = socket(AF_INET, SOCK_STREAM)
33 | sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
34 | sock.bind(address)
35 | sock.listen(5)
36 | while True:
37 | yield 'recv', sock
38 | client, addr = sock.accept()
39 | tasks.append(handler(client, addr))
40 |
41 | def echo_handler(client, address):
42 | print('Connection from', address)
43 | while True:
44 | yield 'recv', client
45 | data = client.recv(1000)
46 | if not data:
47 | break
48 | yield 'send', client
49 | client.send(b'GOT:' + data)
50 | print('Connection closed')
51 |
52 | if __name__ == '__main__':
53 | tasks.append(tcp_server(('',25000), echo_handler))
54 | run()
55 |
56 |
--------------------------------------------------------------------------------
/Solutions/8_6/asyncserver.py:
--------------------------------------------------------------------------------
1 | # server.py
2 |
3 | from socket import *
4 | from select import select
5 | from collections import deque
6 | from types import coroutine
7 |
8 | tasks = deque()
9 | recv_wait = {} # sock -> task
10 | send_wait = {} # sock -> task
11 |
12 | def run():
13 | while any([tasks, recv_wait, send_wait]):
14 | while not tasks:
15 | can_recv, can_send, _ = select(recv_wait, send_wait, [])
16 | for s in can_recv:
17 | tasks.append(recv_wait.pop(s))
18 | for s in can_send:
19 | tasks.append(send_wait.pop(s))
20 | task = tasks.popleft()
21 | try:
22 | reason, resource = task.send(None)
23 | if reason == 'recv':
24 | recv_wait[resource] = task
25 | elif reason == 'send':
26 | send_wait[resource] = task
27 | else:
28 | raise RuntimeError('Unknown reason %r' % reason)
29 | except StopIteration:
30 | print('Task done')
31 |
32 | class GenSocket:
33 | def __init__(self, sock):
34 | self.sock = sock
35 |
36 | @coroutine
37 | def accept(self):
38 | yield 'recv', self.sock
39 | client, addr = self.sock.accept()
40 | return GenSocket(client), addr
41 |
42 | @coroutine
43 | def recv(self, maxsize):
44 | yield 'recv', self.sock
45 | return self.sock.recv(maxsize)
46 |
47 | @coroutine
48 | def send(self, data):
49 | yield 'send', self.sock
50 | return self.sock.send(data)
51 |
52 | def __getattr__(self, name):
53 | return getattr(self.sock, name)
54 |
55 | async def tcp_server(address, handler):
56 | sock = GenSocket(socket(AF_INET, SOCK_STREAM))
57 | sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
58 | sock.bind(address)
59 | sock.listen(5)
60 | while True:
61 | client, addr = await sock.accept()
62 | tasks.append(handler(client, addr))
63 |
64 | async def echo_handler(client, address):
65 | print('Connection from', address)
66 | while True:
67 | data = await client.recv(1000)
68 | if not data:
69 | break
70 | await client.send(b'GOT:' + data)
71 | print('Connection closed')
72 |
73 | if __name__ == '__main__':
74 | tasks.append(tcp_server(('',25000), echo_handler))
75 | run()
76 |
77 |
--------------------------------------------------------------------------------
/Solutions/8_6/cofollow.py:
--------------------------------------------------------------------------------
1 | # cofollow.py
2 | import os
3 | import time
4 | import csv
5 |
6 | def follow(filename,target):
7 | with open(filename,"r") as f:
8 | f.seek(0,os.SEEK_END)
9 | while True:
10 | line = f.readline()
11 | if line != '':
12 | target.send(line)
13 | else:
14 | time.sleep(0.1)
15 |
16 | def receive(expected_type):
17 | msg = yield
18 | assert isinstance(msg, expected_type), 'Expected type %s' % (expected_type)
19 | return msg
20 |
21 | # Decorator for coroutines
22 | from functools import wraps
23 |
24 | def consumer(func):
25 | @wraps(func)
26 | def start(*args,**kwargs):
27 | f = func(*args,**kwargs)
28 | f.send(None)
29 | return f
30 | return start
31 |
32 | # Sample coroutine
33 | @consumer
34 | def printer():
35 | while True:
36 | item = yield from receive(object)
37 | print(item)
38 |
39 | # Example use.
40 | if __name__ == '__main__':
41 | follow('../../Data/stocklog.csv', printer())
42 |
--------------------------------------------------------------------------------
/Solutions/8_6/coticker.py:
--------------------------------------------------------------------------------
1 | # coticker.py
2 | from structure import Structure
3 | from validate import String, Integer, Float
4 |
5 | class Ticker(Structure):
6 | name = String()
7 | price = Float()
8 | date = String()
9 | time = String()
10 | change = Float()
11 | open = Float()
12 | high = Float()
13 | low = Float()
14 | volume = Integer()
15 |
16 | from cofollow import consumer, follow, receive
17 | from tableformat import create_formatter
18 | import csv
19 |
20 | @consumer
21 | def to_csv(target):
22 | def producer():
23 | while True:
24 | yield line
25 |
26 | reader = csv.reader(producer())
27 | while True:
28 | line = yield from receive(str)
29 | target.send(next(reader))
30 |
31 | @consumer
32 | def create_ticker(target):
33 | while True:
34 | row = yield from receive(list)
35 | target.send(Ticker.from_row(row))
36 |
37 | @consumer
38 | def negchange(target):
39 | while True:
40 | record = yield from receive(Ticker)
41 | if record.change < 0:
42 | target.send(record)
43 |
44 | @consumer
45 | def ticker(fmt, fields):
46 | formatter = create_formatter('text')
47 | formatter.headings(fields)
48 | while True:
49 | rec = yield from receive(Ticker)
50 | row = [getattr(rec, name) for name in fields]
51 | formatter.row(row)
52 |
53 | if __name__ == '__main__':
54 | follow('../../Data/stocklog.csv',
55 | to_csv(
56 | create_ticker(
57 | negchange(
58 | ticker('text', ['name','price','change'])))))
59 |
--------------------------------------------------------------------------------
/Solutions/8_6/server.py:
--------------------------------------------------------------------------------
1 | # server.py
2 |
3 | from socket import *
4 | from select import select
5 | from collections import deque
6 |
7 | tasks = deque()
8 | recv_wait = {} # sock -> task
9 | send_wait = {} # sock -> task
10 |
11 | def run():
12 | while any([tasks, recv_wait, send_wait]):
13 | while not tasks:
14 | can_recv, can_send, _ = select(recv_wait, send_wait, [])
15 | for s in can_recv:
16 | tasks.append(recv_wait.pop(s))
17 | for s in can_send:
18 | tasks.append(send_wait.pop(s))
19 | task = tasks.popleft()
20 | try:
21 | reason, resource = task.send(None)
22 | if reason == 'recv':
23 | recv_wait[resource] = task
24 | elif reason == 'send':
25 | send_wait[resource] = task
26 | else:
27 | raise RuntimeError('Unknown reason %r' % reason)
28 | except StopIteration:
29 | print('Task done')
30 |
31 | class GenSocket:
32 | def __init__(self, sock):
33 | self.sock = sock
34 |
35 | def accept(self):
36 | yield 'recv', self.sock
37 | client, addr = self.sock.accept()
38 | return GenSocket(client), addr
39 |
40 | def recv(self, maxsize):
41 | yield 'recv', self.sock
42 | return self.sock.recv(maxsize)
43 |
44 | def send(self, data):
45 | yield 'send', self.sock
46 | return self.sock.send(data)
47 |
48 | def __getattr__(self, name):
49 | return getattr(self.sock, name)
50 |
51 | def tcp_server(address, handler):
52 | sock = GenSocket(socket(AF_INET, SOCK_STREAM))
53 | sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
54 | sock.bind(address)
55 | sock.listen(5)
56 | while True:
57 | client, addr = yield from sock.accept()
58 | tasks.append(handler(client, addr))
59 |
60 | def echo_handler(client, address):
61 | print('Connection from', address)
62 | while True:
63 | data = yield from client.recv(1000)
64 | if not data:
65 | break
66 | yield from client.send(b'GOT:' + data)
67 | print('Connection closed')
68 |
69 | if __name__ == '__main__':
70 | tasks.append(tcp_server(('',25000), echo_handler))
71 | run()
72 |
73 |
--------------------------------------------------------------------------------
/Solutions/8_6/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise RuntimeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 | class TextTableFormatter(TableFormatter):
23 | def headings(self, headers):
24 | print(' '.join('%10s' % h for h in headers))
25 | print(('-'*10 + ' ')*len(headers))
26 |
27 | def row(self, rowdata):
28 | print(' '.join('%10s' % d for d in rowdata))
29 |
30 | class CSVTableFormatter(TableFormatter):
31 | def headings(self, headers):
32 | print(','.join(headers))
33 |
34 | def row(self, rowdata):
35 | print(','.join(str(d) for d in rowdata))
36 |
37 | class HTMLTableFormatter(TableFormatter):
38 | def headings(self, headers):
39 | print('', end=' ')
40 | for h in headers:
41 | print('%s | ' % h, end=' ')
42 | print('
')
43 |
44 | def row(self, rowdata):
45 | print('', end=' ')
46 | for d in rowdata:
47 | print('%s | ' % d, end=' ')
48 | print('
')
49 |
50 | class ColumnFormatMixin:
51 | formats = []
52 | def row(self, rowdata):
53 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
54 | super().row(rowdata)
55 |
56 | class UpperHeadersMixin:
57 | def headings(self, headers):
58 | super().headings([h.upper() for h in headers])
59 |
60 | def create_formatter(name, column_formats=None, upper_headers=False):
61 | if name == 'text':
62 | formatter_cls = TextTableFormatter
63 | elif name == 'csv':
64 | formatter_cls = CSVTableFormatter
65 | elif name == 'html':
66 | formatter_cls = HTMLTableFormatter
67 | else:
68 | raise RuntimeError('Unknown format %s' % name)
69 |
70 | if column_formats:
71 | class formatter_cls(ColumnFormatMixin, formatter_cls):
72 | formats = column_formats
73 |
74 | if upper_headers:
75 | class formatter_cls(UpperHeadersMixin, formatter_cls):
76 | pass
77 |
78 | return formatter_cls()
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/Solutions/9_1/simplemod.py:
--------------------------------------------------------------------------------
1 | # simplemod.py
2 |
3 | x = 42 # A global variable
4 |
5 | # A simple function
6 | def foo():
7 | print("x is %s" % x)
8 |
9 | # A simple class
10 | class Spam:
11 | def yow(self):
12 | print('Yow!')
13 |
14 | # A scripting statement
15 | print("Loaded simplemod")
16 |
--------------------------------------------------------------------------------
/Solutions/9_2/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structly.structure import Structure
4 |
5 | class Stock(Structure):
6 | name = String()
7 | shares = PositiveInteger()
8 | price = PositiveFloat()
9 |
10 | @property
11 | def cost(self):
12 | return self.shares * self.price
13 |
14 | def sell(self, nshares: PositiveInteger):
15 | self.shares -= nshares
16 |
17 | if __name__ == '__main__':
18 | from structly.reader import read_csv_as_instances
19 | from structly.tableformat import create_formatter, print_table
20 | portfolio = read_csv_as_instances('../../Data/portfolio.csv', Stock)
21 | formatter = create_formatter('text')
22 | print_table(portfolio, ['name','shares','price'], formatter)
23 |
24 |
--------------------------------------------------------------------------------
/Solutions/9_2/structly/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dabeaz-course/python-mastery/dcf5e16d1fedfc9a40620141d18c3c7a75059425/Solutions/9_2/structly/__init__.py
--------------------------------------------------------------------------------
/Solutions/9_2/structly/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | def convert_csv(lines, converter, *, headers=None):
9 | rows = csv.reader(lines)
10 | if headers is None:
11 | headers = next(rows)
12 |
13 | records = []
14 | for rowno, row in enumerate(rows, start=1):
15 | try:
16 | records.append(converter(headers, row))
17 | except ValueError as e:
18 | log.warning('Row %s: Bad row: %s', rowno, row)
19 | log.debug('Row %s: Reason: %s', rowno, row)
20 | return records
21 |
22 | def csv_as_dicts(lines, types, *, headers=None):
23 | return convert_csv(lines,
24 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
25 |
26 | def csv_as_instances(lines, cls, *, headers=None):
27 | return convert_csv(lines,
28 | lambda headers, row: cls.from_row(row))
29 |
30 | def read_csv_as_dicts(filename, types, *, headers=None):
31 | '''
32 | Read CSV data into a list of dictionaries with optional type conversion
33 | '''
34 | with open(filename) as file:
35 | return csv_as_dicts(file, types, headers=headers)
36 |
37 | def read_csv_as_instances(filename, cls, *, headers=None):
38 | '''
39 | Read CSV data into a list of instances
40 | '''
41 | with open(filename) as file:
42 | return csv_as_instances(file, cls, headers=headers)
43 |
44 |
--------------------------------------------------------------------------------
/Solutions/9_2/structly/tableformat.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise RuntimeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 |
23 | class TextTableFormatter(TableFormatter):
24 | def headings(self, headers):
25 | print(' '.join('%10s' % h for h in headers))
26 | print(('-'*10 + ' ')*len(headers))
27 |
28 | def row(self, rowdata):
29 | print(' '.join('%10s' % d for d in rowdata))
30 |
31 | class CSVTableFormatter(TableFormatter):
32 | def headings(self, headers):
33 | print(','.join(headers))
34 |
35 | def row(self, rowdata):
36 | print(','.join(str(d) for d in rowdata))
37 |
38 | class HTMLTableFormatter(TableFormatter):
39 | def headings(self, headers):
40 | print('', end=' ')
41 | for h in headers:
42 | print('%s | ' % h, end=' ')
43 | print('
')
44 |
45 | def row(self, rowdata):
46 | print('', end=' ')
47 | for d in rowdata:
48 | print('%s | ' % d, end=' ')
49 | print('
')
50 |
51 | class ColumnFormatMixin:
52 | formats = []
53 | def row(self, rowdata):
54 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
55 | super().row(rowdata)
56 |
57 | class UpperHeadersMixin:
58 | def headings(self, headers):
59 | super().headings([h.upper() for h in headers])
60 |
61 | def create_formatter(name, column_formats=None, upper_headers=False):
62 | if name == 'text':
63 | formatter_cls = TextTableFormatter
64 | elif name == 'csv':
65 | formatter_cls = CSVTableFormatter
66 | elif name == 'html':
67 | formatter_cls = HTMLTableFormatter
68 | else:
69 | raise RuntimeError('Unknown format %s' % name)
70 |
71 | if column_formats:
72 | class formatter_cls(ColumnFormatMixin, formatter_cls):
73 | formats = column_formats
74 |
75 | if upper_headers:
76 | class formatter_cls(UpperHeadersMixin, formatter_cls):
77 | pass
78 |
79 | return formatter_cls()
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Solutions/9_3/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structly import *
4 |
5 | class Stock(Structure):
6 | name = String()
7 | shares = PositiveInteger()
8 | price = PositiveFloat()
9 |
10 | @property
11 | def cost(self):
12 | return self.shares * self.price
13 |
14 | def sell(self, nshares: PositiveInteger):
15 | self.shares -= nshares
16 |
17 | if __name__ == '__main__':
18 | portfolio = read_csv_as_instances('../../Data/portfolio.csv', Stock)
19 | formatter = create_formatter('text')
20 | print_table(portfolio, ['name','shares','price'], formatter)
21 |
22 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/__init__.py:
--------------------------------------------------------------------------------
1 | # structly/__init__.py
2 |
3 | from .structure import *
4 | from .reader import *
5 | from .tableformat import *
6 |
7 | __all__ = [ *structure.__all__,
8 | *reader.__all__,
9 | *tableformat.__all__ ]
10 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | def convert_csv(lines, converter, *, headers=None):
9 | rows = csv.reader(lines)
10 | if headers is None:
11 | headers = next(rows)
12 |
13 | records = []
14 | for rowno, row in enumerate(rows, start=1):
15 | try:
16 | records.append(converter(headers, row))
17 | except ValueError as e:
18 | log.warning('Row %s: Bad row: %s', rowno, row)
19 | log.debug('Row %s: Reason: %s', rowno, row)
20 | return records
21 |
22 | def csv_as_dicts(lines, types, *, headers=None):
23 | return convert_csv(lines,
24 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
25 |
26 | def csv_as_instances(lines, cls, *, headers=None):
27 | return convert_csv(lines,
28 | lambda headers, row: cls.from_row(row))
29 |
30 | def read_csv_as_dicts(filename, types, *, headers=None):
31 | '''
32 | Read CSV data into a list of dictionaries with optional type conversion
33 | '''
34 | with open(filename) as file:
35 | return csv_as_dicts(file, types, headers=headers)
36 |
37 | def read_csv_as_instances(filename, cls, *, headers=None):
38 | '''
39 | Read CSV data into a list of instances
40 | '''
41 | with open(filename) as file:
42 | return csv_as_instances(file, cls, headers=headers)
43 |
44 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/tableformat/__init__.py:
--------------------------------------------------------------------------------
1 | # __init__.py
2 |
3 | from .formatter import print_table, create_formatter
4 |
5 | __all__ = [ 'print_table', 'create_formatter' ]
6 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/tableformat/formats/__init__.py:
--------------------------------------------------------------------------------
1 | # formats/__init__.py
2 |
3 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/tableformat/formats/csv.py:
--------------------------------------------------------------------------------
1 | # csv.py
2 |
3 | from ..formatter import TableFormatter
4 |
5 | class CSVTableFormatter(TableFormatter):
6 | def headings(self, headers):
7 | print(','.join(headers))
8 |
9 | def row(self, rowdata):
10 | print(','.join(str(d) for d in rowdata))
11 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/tableformat/formats/html.py:
--------------------------------------------------------------------------------
1 | # html.py
2 |
3 | from ..formatter import TableFormatter
4 |
5 | class HTMLTableFormatter(TableFormatter):
6 | def headings(self, headers):
7 | print('', end=' ')
8 | for h in headers:
9 | print('%s | ' % h, end=' ')
10 | print('
')
11 |
12 | def row(self, rowdata):
13 | print('', end=' ')
14 | for d in rowdata:
15 | print('%s | ' % d, end=' ')
16 | print('
')
17 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/tableformat/formats/text.py:
--------------------------------------------------------------------------------
1 | # text.py
2 |
3 | from ..formatter import TableFormatter
4 |
5 | class TextTableFormatter(TableFormatter):
6 | def headings(self, headers):
7 | print(' '.join('%10s' % h for h in headers))
8 | print(('-'*10 + ' ')*len(headers))
9 |
10 | def row(self, rowdata):
11 | print(' '.join('%10s' % d for d in rowdata))
12 |
--------------------------------------------------------------------------------
/Solutions/9_3/structly/tableformat/formatter.py:
--------------------------------------------------------------------------------
1 | # tableformat.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise RuntimeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | @abstractmethod
15 | def headings(self, headers):
16 | pass
17 |
18 | @abstractmethod
19 | def row(self, rowdata):
20 | pass
21 |
22 | from .formats.text import TextTableFormatter
23 | from .formats.csv import CSVTableFormatter
24 | from .formats.html import HTMLTableFormatter
25 |
26 | class ColumnFormatMixin:
27 | formats = []
28 | def row(self, rowdata):
29 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
30 | super().row(rowdata)
31 |
32 | class UpperHeadersMixin:
33 | def headings(self, headers):
34 | super().headings([h.upper() for h in headers])
35 |
36 | def create_formatter(name, column_formats=None, upper_headers=False):
37 | if name == 'text':
38 | formatter_cls = TextTableFormatter
39 | elif name == 'csv':
40 | formatter_cls = CSVTableFormatter
41 | elif name == 'html':
42 | formatter_cls = HTMLTableFormatter
43 | else:
44 | raise RuntimeError('Unknown format %s' % name)
45 |
46 | if column_formats:
47 | class formatter_cls(ColumnFormatMixin, formatter_cls):
48 | formats = column_formats
49 |
50 | if upper_headers:
51 | class formatter_cls(UpperHeadersMixin, formatter_cls):
52 | pass
53 |
54 | return formatter_cls()
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/Solutions/9_4/stock.py:
--------------------------------------------------------------------------------
1 | # stock.py
2 |
3 | from structly import *
4 |
5 | class Stock(Structure):
6 | name = String()
7 | shares = PositiveInteger()
8 | price = PositiveFloat()
9 |
10 | @property
11 | def cost(self):
12 | return self.shares * self.price
13 |
14 | def sell(self, nshares: PositiveInteger):
15 | self.shares -= nshares
16 |
17 | if __name__ == '__main__':
18 | portfolio = read_csv_as_instances('../../Data/portfolio.csv', Stock)
19 | formatter = create_formatter('text')
20 | print_table(portfolio, ['name','shares','price'], formatter)
21 |
22 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/__init__.py:
--------------------------------------------------------------------------------
1 | # structly/__init__.py
2 |
3 | from .structure import *
4 | from .reader import *
5 | from .tableformat import *
6 |
7 | __all__ = [ *structure.__all__,
8 | *reader.__all__,
9 | *tableformat.__all__ ]
10 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/reader.py:
--------------------------------------------------------------------------------
1 | # reader.py
2 |
3 | import csv
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | def convert_csv(lines, converter, *, headers=None):
9 | rows = csv.reader(lines)
10 | if headers is None:
11 | headers = next(rows)
12 |
13 | records = []
14 | for rowno, row in enumerate(rows, start=1):
15 | try:
16 | records.append(converter(headers, row))
17 | except ValueError as e:
18 | log.warning('Row %s: Bad row: %s', rowno, row)
19 | log.debug('Row %s: Reason: %s', rowno, row)
20 | return records
21 |
22 | def csv_as_dicts(lines, types, *, headers=None):
23 | return convert_csv(lines,
24 | lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
25 |
26 | def csv_as_instances(lines, cls, *, headers=None):
27 | return convert_csv(lines,
28 | lambda headers, row: cls.from_row(row))
29 |
30 | def read_csv_as_dicts(filename, types, *, headers=None):
31 | '''
32 | Read CSV data into a list of dictionaries with optional type conversion
33 | '''
34 | with open(filename) as file:
35 | return csv_as_dicts(file, types, headers=headers)
36 |
37 | def read_csv_as_instances(filename, cls, *, headers=None):
38 | '''
39 | Read CSV data into a list of instances
40 | '''
41 | with open(filename) as file:
42 | return csv_as_instances(file, cls, headers=headers)
43 |
44 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/tableformat/__init__.py:
--------------------------------------------------------------------------------
1 | # __init__.py
2 |
3 | from .formatter import print_table, create_formatter
4 |
5 | __all__ = [ 'print_table', 'create_formatter' ]
6 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/tableformat/formats/__init__.py:
--------------------------------------------------------------------------------
1 | # formats/__init__.py
2 |
3 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/tableformat/formats/csv.py:
--------------------------------------------------------------------------------
1 | # csv.py
2 |
3 | from ..formatter import TableFormatter
4 |
5 | class CSVTableFormatter(TableFormatter):
6 | def headings(self, headers):
7 | print(','.join(headers))
8 |
9 | def row(self, rowdata):
10 | print(','.join(str(d) for d in rowdata))
11 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/tableformat/formats/html.py:
--------------------------------------------------------------------------------
1 | # html.py
2 |
3 | from ..formatter import TableFormatter
4 |
5 | class HTMLTableFormatter(TableFormatter):
6 | def headings(self, headers):
7 | print('', end=' ')
8 | for h in headers:
9 | print('%s | ' % h, end=' ')
10 | print('
')
11 |
12 | def row(self, rowdata):
13 | print('', end=' ')
14 | for d in rowdata:
15 | print('%s | ' % d, end=' ')
16 | print('
')
17 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/tableformat/formats/text.py:
--------------------------------------------------------------------------------
1 | # text.py
2 |
3 | from ..formatter import TableFormatter
4 |
5 | class TextTableFormatter(TableFormatter):
6 | def headings(self, headers):
7 | print(' '.join('%10s' % h for h in headers))
8 | print(('-'*10 + ' ')*len(headers))
9 |
10 | def row(self, rowdata):
11 | print(' '.join('%10s' % d for d in rowdata))
12 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/tableformat/formats/tsv.py:
--------------------------------------------------------------------------------
1 | # tsv.py
2 |
3 | from ..formatter import TableFormatter
4 |
5 | class TSVTableFormatter(TableFormatter):
6 | def headings(self, headers):
7 | print('\t'.join(headers))
8 | def row(self, rowdata):
9 | print('\t'.join(str(d) for d in rowdata))
10 |
--------------------------------------------------------------------------------
/Solutions/9_4/structly/tableformat/formatter.py:
--------------------------------------------------------------------------------
1 | # formatter.py
2 | from abc import ABC, abstractmethod
3 |
4 | def print_table(records, fields, formatter):
5 | if not isinstance(formatter, TableFormatter):
6 | raise RuntimeError('Expected a TableFormatter')
7 |
8 | formatter.headings(fields)
9 | for r in records:
10 | rowdata = [getattr(r, fieldname) for fieldname in fields]
11 | formatter.row(rowdata)
12 |
13 | class TableFormatter(ABC):
14 | _formats = { }
15 |
16 | @classmethod
17 | def __init_subclass__(cls):
18 | name = cls.__module__.split('.')[-1]
19 | TableFormatter._formats[name] = cls
20 |
21 | @abstractmethod
22 | def headings(self, headers):
23 | pass
24 |
25 | @abstractmethod
26 | def row(self, rowdata):
27 | pass
28 |
29 | class ColumnFormatMixin:
30 | formats = []
31 | def row(self, rowdata):
32 | rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
33 | super().row(rowdata)
34 |
35 | class UpperHeadersMixin:
36 | def headings(self, headers):
37 | super().headings([h.upper() for h in headers])
38 |
39 | def create_formatter(name, column_formats=None, upper_headers=False):
40 | if name not in TableFormatter._formats:
41 | __import__(f'{__package__}.formats.{name}')
42 |
43 | formatter_cls = TableFormatter._formats.get(name)
44 | if not formatter_cls:
45 | raise RuntimeError('Unknown format %s' % name)
46 |
47 | if column_formats:
48 | class formatter_cls(ColumnFormatMixin, formatter_cls):
49 | formats = column_formats
50 |
51 | if upper_headers:
52 | class formatter_cls(UpperHeadersMixin, formatter_cls):
53 | pass
54 |
55 | return formatter_cls()
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/Solutions/README.md:
--------------------------------------------------------------------------------
1 | # Solutions
2 |
3 | This directory contains fully worked out solution code for every
4 | exercise. The code is written to be runnable from this directory.
5 | When taking the course, I would encourage you to try and come up
6 | with your own solution first. However, if you're stuck or if
7 | everything has become broken, you can come here to get a fresh
8 | copy of code to work with.
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------