├── .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 | ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). 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 | ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). 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 | ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). 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 | ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). 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 | ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). 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 | --------------------------------------------------------------------------------