├── .gitignore ├── LICENSE.md ├── Notes ├── 00_Setup.md ├── 01_Introduction │ ├── 00_Overview.md │ ├── 01_Python.md │ ├── 02_Hello_world.md │ ├── 03_Numbers.md │ ├── 04_Strings.md │ ├── 05_Lists.md │ ├── 06_Files.md │ └── 07_Functions.md ├── 02_Working_with_data │ ├── 00_Overview.md │ ├── 01_Datatypes.md │ ├── 02_Containers.md │ ├── 03_Formatting.md │ ├── 04_Sequences.md │ ├── 05_Collections.md │ ├── 06_List_comprehension.md │ ├── 07_Objects.md │ ├── references.png │ └── shallow.png ├── 03_Program_organization │ ├── 00_Overview.md │ ├── 01_Script.md │ ├── 02_More_functions.md │ ├── 03_Error_checking.md │ ├── 04_Modules.md │ ├── 05_Main_module.md │ └── 06_Design_discussion.md ├── 04_Classes_objects │ ├── 00_Overview.md │ ├── 01_Class.md │ ├── 02_Inheritance.md │ ├── 03_Special_methods.md │ └── 04_Defining_exceptions.md ├── 05_Object_model │ ├── 00_Overview.md │ ├── 01_Dicts_revisited.md │ └── 02_Classes_encapsulation.md ├── 06_Generators │ ├── 00_Overview.md │ ├── 01_Iteration_protocol.md │ ├── 02_Customizing_iteration.md │ ├── 03_Producers_consumers.md │ └── 04_More_generators.md ├── 07_Advanced_Topics │ ├── 00_Overview.md │ ├── 01_Variable_arguments.md │ ├── 02_Anonymous_function.md │ ├── 03_Returning_functions.md │ ├── 04_Function_decorators.md │ └── 05_Decorated_methods.md ├── 08_Testing_debugging │ ├── 00_Overview.md │ ├── 01_Testing.md │ ├── 02_Logging.md │ └── 03_Debugging.md ├── 09_Packages │ ├── 00_Overview.md │ ├── 01_Packages.md │ ├── 02_Third_party.md │ ├── 03_Distribution.md │ └── TheEnd.md ├── Contents.md └── InstructorNotes.md ├── README.md ├── Solutions ├── 1_10 │ └── mortgage.py ├── 1_27 │ └── pcost.py ├── 1_33 │ └── pcost.py ├── 1_5 │ └── bounce.py ├── 2_11 │ └── report.py ├── 2_16 │ ├── pcost.py │ └── report.py ├── 2_7 │ └── report.py ├── 3_10 │ └── fileparse.py ├── 3_14 │ ├── fileparse.py │ ├── pcost.py │ └── report.py ├── 3_16 │ ├── fileparse.py │ ├── pcost.py │ └── report.py ├── 3_18 │ ├── fileparse.py │ ├── pcost.py │ └── report.py ├── 3_2 │ └── report.py ├── 3_7 │ └── fileparse.py ├── 4_10 │ ├── fileparse.py │ ├── pcost.py │ ├── report.py │ ├── stock.py │ └── tableformat.py ├── 4_4 │ ├── fileparse.py │ ├── pcost.py │ ├── report.py │ └── stock.py ├── 5_8 │ ├── fileparse.py │ ├── pcost.py │ ├── report.py │ ├── stock.py │ └── tableformat.py ├── 6_12 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ ├── tableformat.py │ └── ticker.py ├── 6_15 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ ├── tableformat.py │ └── ticker.py ├── 6_3 │ ├── fileparse.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ └── tableformat.py ├── 6_7 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ └── tableformat.py ├── 7_10 │ └── timethis.py ├── 7_11 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ ├── tableformat.py │ ├── ticker.py │ ├── timethis.py │ └── typedproperty.py ├── 7_4 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ ├── tableformat.py │ └── ticker.py ├── 7_9 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ ├── tableformat.py │ ├── ticker.py │ └── typedproperty.py ├── 8_1 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ ├── tableformat.py │ ├── test_stock.py │ ├── ticker.py │ ├── timethis.py │ └── typedproperty.py ├── 8_2 │ ├── fileparse.py │ ├── follow.py │ ├── pcost.py │ ├── portfolio.py │ ├── report.py │ ├── stock.py │ ├── tableformat.py │ ├── test_stock.py │ ├── ticker.py │ ├── timethis.py │ └── typedproperty.py ├── 9_3 │ └── porty-app │ │ ├── README.txt │ │ ├── portfolio.csv │ │ ├── porty │ │ ├── __init__.py │ │ ├── fileparse.py │ │ ├── follow.py │ │ ├── pcost.py │ │ ├── portfolio.py │ │ ├── report.py │ │ ├── stock.py │ │ ├── tableformat.py │ │ ├── test_stock.py │ │ ├── ticker.py │ │ └── typedproperty.py │ │ ├── prices.csv │ │ └── print-report.py ├── 9_5 │ └── porty-app │ │ ├── MANIFEST.in │ │ ├── README.txt │ │ ├── portfolio.csv │ │ ├── porty │ │ ├── __init__.py │ │ ├── fileparse.py │ │ ├── follow.py │ │ ├── pcost.py │ │ ├── portfolio.py │ │ ├── report.py │ │ ├── stock.py │ │ ├── tableformat.py │ │ ├── test_stock.py │ │ ├── ticker.py │ │ └── typedproperty.py │ │ ├── prices.csv │ │ ├── print-report.py │ │ └── setup.py └── README.md ├── Work ├── Data │ ├── dowstocks.csv │ ├── missing.csv │ ├── portfolio.csv │ ├── portfolio.csv.gz │ ├── portfolio.dat │ ├── portfolio2.csv │ ├── portfolioblank.csv │ ├── portfoliodate.csv │ ├── prices.csv │ └── stocksim.py ├── README.md ├── bounce.py ├── fileparse.py ├── mortgage.py ├── pcost.py └── report.py ├── _config.yml ├── _layouts └── default.html └── 翻译说明.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Notes/00_Setup.md: -------------------------------------------------------------------------------- 1 | # 课程设置与概述 2 | 3 | 欢迎访问本课程(Practical Python Programming)。这个页面包含一些关于课程设置的重要信息。 4 | 5 | ## 课程周期和时间要求 6 | 7 | 该课程最初是作为一个由讲师主导的,持续 3 -4 天的现场培训。要全部完成本课程,您应该最少计划用25-35小时进行学习。大部分的参与者发现,在不浏览题解代码的情况下,学习材料是相当具有挑战性的(见下文) 8 | 9 | ## 设置和 Python 安装 10 | 11 | 您只需要安装 3.6 或者更新版本的 Python 即可。不依赖于任何特定的操作系统,编辑器,IDE,或者其它与 Python 相关的工具。没有第三方依赖。 12 | 13 | 也就是说,本课程大部分内容涉及学习如何编写脚本与小型程序,这些脚本与小型程序涉及从文件中读取数据。因此,您需要确保您处在一个可以轻松处理文件的环境中。这包括使用编辑器创建 Python 程序,并能够从 shell 或终端运行这些程序。 14 | 15 | 您可能倾向于使用更具交互性的环境来学习本课程,例如 Jupyter Notebooks。**我建议不要这样做**。尽管 Jupyter Notebooks 非常棒,但本课程中的许多练习教授与程序组织的相关的观念,包括使用函数,模块,导入语句以及重构源代码跨越多个文件的程序。以我的经验,很难在 Jupyter Notebooks 环境中重复这样的环境。 16 | 17 | ## 派生(Forking)/克隆(Cloning) 课程仓库 18 | 19 | 为了准备本课程的环境,我推荐您从本课程的仓库 [https://github.com/dabeaz-course/practical-python](https://github.com/dabeaz-course/practical-python) 派生您自己的 GitHub 仓库。完成后,您可以将其克隆到本地计算机上: 20 | 21 | ``` 22 | bash % git clone https://github.com/yourname/practical-python 23 | bash % cd practical-python 24 | bash % 25 | ``` 26 | 27 | 请在 `practical-python/` 目录下完成所有的练习。如果将解题代码提交回派生的仓库,那么您的所有代码会保存到一个地方。完成后,您将拥有良好的学习记录。 28 | 29 | 如果您不想派生一个自己的 GitHub 仓库或者您没有 GitHub 账号,您仍然可以将本课程的仓库克隆到您自己的计算机上: 30 | 31 | ``` 32 | bash % git clone https://github.com/dabeaz-course/practical-python 33 | bash % cd practical-python 34 | bash % 35 | ``` 36 | 37 | 如果这样做,除了对计算机的本地副本进行更改外,您将无法提交代码更改到 GitHub 上。 38 | 39 | ## 课程排版 40 | 41 | 在 Work/ 目录下完成所有的编程工作。在Work/ 目录里面,有一个 Data/ 目录。 Data/ 目录包含各类在课程中使用的数据文件及其它脚本。您将会经常访问位于 Data/ 目录下的文件。课程练习假定您在 Work/ 目录下创建程序。 42 | 43 | ## 课程顺序 44 | 45 | 课程材料应该从第 1 节开始,按章节顺序完成。后面章节中的课程练习以前面章节中编写的代码为基础。后面的许多练习涉及对现有代码的微小重构。 46 | 47 | ## 题解代码 48 | 49 | [`Solutions/`](../Solutions) 目录包含选定练习的题解代码。如果您需要一些提示,请随时查看。为了最大限度地利用课程,您应该首先尝试创建自己的题解。 50 | 51 | [目录](Contents.md) \| [下一节 (1 Python 简介)](01_Introduction/00_Overview.md) 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Notes/01_Introduction/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [下一节 (2_处理数据)](../02_Working_with_data/00_Overview.md) 2 | 3 | ## 1. Python 简介 4 | 本章是第一章,将会从头开始介绍 Python 基础知识,让你从零开始,学会怎么编写、运行、调试一个简单的程序。最后,你可以运用这些 Python 基础知识,去编写一个简短的脚本,读取 csv 数据并执行一些简单的计算。 5 | * [1.1 Python简介](01_Python.md) 6 | * [1.2 第一个程序](02_Hello_world.md) 7 | * [1.3 数字](03_Numbers.md) 8 | * [1.4 字符串](04_Strings.md) 9 | * [1.5 列表](05_Lists.md) 10 | * [1.6 文件](06_Files.md) 11 | * [1.7 函数](07_Functions.md) 12 | 13 | [目录](../Contents.md) \| [下一节 (2_处理数据)](../02_Working_with_data/00_Overview.md) -------------------------------------------------------------------------------- /Notes/02_Working_with_data/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (1 Python 简介)](../01_Introduction/00_Overview.md) \| [下一节(3 程序组织)](../03_Program_organization/00_Overview.md) 2 | 3 | # 2. 处理数据 4 | 5 | 要编写有用的程序,您需要能够处理数据。本节介绍 Python的核心数据结构: 元组(tuple),列表(list),集合(set)和字典(dict),并讨论常见的数据处理习惯用法。本节的最后一部分深入探讨 Python 的底层对象模型。 6 | 7 | * [2.1 数据类型和数据结构](01_Datatypes.md) 8 | * [2.2 容器](02_Containers.md) 9 | * [2.3 格式化输出](03_Formatting.md) 10 | * [2.4 序列](04_Sequences.md) 11 | * [2.5 collections 模块](05_Collections.md) 12 | * [2.6 列表推导式](06_List_comprehension.md) 13 | * [2.7 对象模型](07_Objects.md) 14 | 15 | [目录](../Contents.md) \| [上一节 (1 Python 简介)](../01_Introduction/00_Overview.md) \| [下一节(3 程序组织)](../03_Program_organization/00_Overview.md) 16 | 17 | -------------------------------------------------------------------------------- /Notes/02_Working_with_data/references.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codists/practical-python-zh/b34b5dfb215d76b8bd4845f60803398f67ab8471/Notes/02_Working_with_data/references.png -------------------------------------------------------------------------------- /Notes/02_Working_with_data/shallow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codists/practical-python-zh/b34b5dfb215d76b8bd4845f60803398f67ab8471/Notes/02_Working_with_data/shallow.png -------------------------------------------------------------------------------- /Notes/03_Program_organization/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (2 处理数据)](../02_Working_with_data/00_Overview.md) \| [下一节 (4 类和对象)](../04_Classes_objects/00_Overview.md) 2 | 3 | # 3. 程序组织 4 | 5 | 到目前为止,我们已经学习了一些 Python 基础知识并编写了一些简短的脚本。但是,当开始编写更大的程序时,我们会想要有条理地组织这些程序。本节将深入讨论关于函数编写,错误处理以及模块的更多细节。最后,您应该能够编写跨多个文件的,并细分为函数的程序。我们还将提供一些有用的代码模板,以编写更有用的脚本。 6 | 7 | * [3.1 函数和脚本编写](01_Script.md) 8 | * [3.2 关于函数的更多细节](02_More_functions.md) 9 | * [3.3 异常处理](03_Error_checking.md) 10 | * [3.4 模块](04_Modules.md) 11 | * [3.5 主模块](05_Main_module.md) 12 | * [3.6 关于拥抱灵活性的设计讨论](06_Design_discussion.md) 13 | 14 | [目录](../Contents.md) \| [上一节 (2 处理数据)](../02_Working_with_data/00_Overview.md) \| [下一节 (4 类和对象)](../04_Classes_objects/00_Overview.md) 15 | 16 | -------------------------------------------------------------------------------- /Notes/04_Classes_objects/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (3 程序组织)](../03_Program_organization/00_Overview.md) \| [下一节 (5 Python对象的内部工作机制)](../05_Object_model/00_Overview.md) 2 | 3 | # 4. 类和对象 4 | 5 | 到目前为止,我们的程序仅使用了内置的 Python 数据类型。本节,我们介绍类(class)和对象(object)的概念。你将学习 `class` 语句,该语句允许你创建新的对象。我们还将介绍继承(inheritance)的概念,继承通常用于构建可扩展的程序。最后,我们将研究类的一些其它特性,包括特殊方法,动态属性查找以及自定义异常。 6 | 7 | * [4.1 类简介](01_Class.md) 8 | * [4.2 继承](02_Inheritance.md) 9 | * [4.3 特殊方法](03_Special_methods.md) 10 | * [4.4 自定义异常](04_Defining_exceptions.md) 11 | 12 | [目录](../Contents.md) \| [上一节 (3 程序组织)](../03_Program_organization/00_Overview.md) \| [下一节 (5 Python对象的内部工作机制)](../05_Object_model/00_Overview.md) -------------------------------------------------------------------------------- /Notes/04_Classes_objects/04_Defining_exceptions.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (4.3 特殊方法)](03_Special_methods.md) \| [下一节 (5 对象模型)](../05_Object_model/00_Overview.md) 2 | 3 | # 4.4 定义异常 4 | 5 | 用户可以通过类实现自定义异常: 6 | 7 | ```python 8 | class NetworkError(Exception): 9 | pass 10 | ``` 11 | 12 | **异常类始终继承自 `Exception` ** 13 | 14 | 它们通常是空类。空类内部使用 `pass` 表示。 15 | 16 | 你也可以对异常进行分层: 17 | 18 | ```python 19 | class AuthenticationError(NetworkError): 20 | pass 21 | 22 | class ProtocolError(NetworkError): 23 | pass 24 | ``` 25 | 26 | ## 练习 27 | 28 | ### 练习 4.11:自定义异常 29 | 30 | 通常情况下,为库定义自己的异常是一种良好的习惯。 31 | 32 | 这样可以更容易区分异常是常见编程错误触发的,还是库为了提示特定问题而有意触发的。 33 | 34 | 请修改上次练习中的 `create_formatter()` 函数,当用户提供错误的格式名时,触发自定义的 `FormatError` 异常。 35 | 36 | 示例: 37 | 38 | ```python 39 | >>> from tableformat import create_formatter 40 | >>> formatter = create_formatter('xls') 41 | Traceback (most recent call last): 42 | File "", line 1, in 43 | File "tableformat.py", line 71, in create_formatter 44 | raise FormatError('Unknown table format %s' % name) 45 | FormatError: Unknown table format xls 46 | >>> 47 | ``` 48 | 49 | [目录](../Contents.md) \| [上一节 (4.3 特殊方法)](03_Special_methods.md) \| [下一节 (5 对象模型)](../05_Object_model/00_Overview.md) -------------------------------------------------------------------------------- /Notes/05_Object_model/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (4 类和对象)](../04_Classes_objects/00_Overview.md) \| [下一节 (6 生成器)](../06_Generators/00_Overview.md) 2 | 3 | # 5. Python 对象的内部工作原理 4 | 5 | 本节介绍 Python 对象的内部工作原理。来自其它语言的程序员通常会发现 Python 的类概念缺乏特性。例如,没有访问控制(access-control)的概念(如:private,protected),`self` 参数让人感觉很奇怪,并且,坦白地说,使用对象有时候让人感到“一切都是开放的”。虽然某种程度上来说也许是这样,但是我们要了解其工作原理,以及一些常见的编程习惯,以便更好的封装对象的内部。 6 | 7 | 虽然没有必要担心内部细节会影响效率,但是,大多数 Python 程序员对类的工作原理都有基本了解。这就是为什么要介绍 Python 对象的内部工作原理的原因。 8 | 9 | * [5.1 再谈字典 (对象实现)](01_Dicts_revisited.md) 10 | * [5.2 封装技术](02_Classes_encapsulation.md) 11 | 12 | [目录](../Contents.md) \| [上一节 (4 类和对象)](../04_Classes_objects/00_Overview.md) \| [下一节 (6 生成器)](../06_Generators/00_Overview.md) 13 | 14 | -------------------------------------------------------------------------------- /Notes/06_Generators/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (5 Python对象的内部工作原理)](../05_Object_model/00_Overview.md) \| [下一节 (7 高级主题)](../07_Advanced_Topics/00_Overview.md) 2 | 3 | # 6. 生成器 4 | 5 | 在 Python 语言中,迭代(`for` 循环)是最常用的编程模式之一。程序执行大量的迭代来处理列表、读取文件、查询数据库......Python 最强大的特性之一就是能够以所谓的“生成器函数(generator function)”形式自定义和重新定义迭代。本节将对生成器主题进行介绍。最后,你将学习以一种有趣的方式编写程序来处理实时流数据。 6 | 7 | * [6.1 迭代协议](01_Iteration_protocol.md) 8 | * [6.2 使用生成器自定义迭代](02_Customizing_iteration.md) 9 | * [6.3 生产者/消费者问题和工作流](03_Producers_consumers.md) 10 | * [6.4 生成器表达式](04_More_generators.md) 11 | 12 | [目录](../Contents.md) \| [上一节 (5 Python对象的内部工作原理)](../05_Object_model/00_Overview.md) \| [下一节 (7 高级主题)](../07_Advanced_Topics/00_Overview.md) -------------------------------------------------------------------------------- /Notes/07_Advanced_Topics/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (6 生成器)](../06_Generators/00_Overview.md) \| [下一节 (8 测试和调试)](../08_Testing_debugging/00_Overview.md) 2 | 3 | # 7. 高级主题 4 | 5 | 本节,我们将探究一些高级的 Python 特性。你可能会在日常编程中遇到这些特性。虽然许多特性本可以在前面的章节中介绍,但是却没有介绍并不是为了让你在当时避免肝脑涂地。 6 | 7 | 应该强调的是:本节主题旨在对这些(特性的)思想做基础介绍。你需要查看更高级的材料来了解它们的细节。 8 | 9 | * [7.1 可变参数函数](01_Variable_arguments.md) 10 | * [7.2 匿名函数和 lambda](02_Anonymous_function.md) 11 | * [7.3 返回函数和闭包](03_Returning_functions.md) 12 | * [7.4 函数装饰器](04_Function_decorators.md) 13 | * [7.5 静态方法和类方法](05_Decorated_methods.md) 14 | 15 | [目录](../Contents.md) \| [上一节 (6 生成器)](../06_Generators/00_Overview.md) \| [下一节 (8 测试和调试)](../08_Testing_debugging/00_Overview.md) -------------------------------------------------------------------------------- /Notes/08_Testing_debugging/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (7 高级主题)](../07_Advanced_Topics/00_Overview.md) \| [下一节 (9 包)](../09_Packages/00_Overview.md) 2 | 3 | # 8. 测试和调试 4 | 5 | 本节介绍与测试、日志和调试有关的基本主题。 6 | 7 | * [8.1 测试](01_Testing.md) 8 | * [8.2 日志,错误处理和诊断](02_Logging.md) 9 | * [8.3 调试](03_Debugging.md) 10 | 11 | [目录](../Contents.md) \| [上一节 (7 高级主题)](../07_Advanced_Topics/00_Overview.md) \| [下一节 (9 包)](../09_Packages/00_Overview.md) 12 | 13 | -------------------------------------------------------------------------------- /Notes/09_Packages/00_Overview.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (8 测试和调试)](../08_Testing_debugging/00_Overview.md) 2 | 3 | # 9 包 4 | 5 | 我们将以“如何把代码组织成包结构”这一主题结束本课程。同时,也将对第三方包的安装、如何将自己的代码提供给其他人使用进行讨论。 6 | 7 | 在 Python 开发中,打包(packaging)是一个演进的,复杂的主题。本节主要关注一些通用的代码组织原则,而不是特定的工具。无论以后使用那种工具来分发代码或者管理依赖,这些通用的代码组织原则都是有用的。 8 | 9 | * [9.1 包](01_Packages.md) 10 | * [9.2 第三方模块](02_Third_party.md) 11 | * [9.3 将代码提供给其他人(使用)](03_Distribution.md) 12 | 13 | [目录](../Contents.md) \| [上一节 (8 测试和调试)](../08_Testing_debugging/00_Overview.md) 14 | 15 | -------------------------------------------------------------------------------- /Notes/09_Packages/02_Third_party.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (9.1 包)](01_Packages.md) \| [下一节 (9.3 版本分发)](03_Distribution.md) 2 | 3 | # 9.2 第三方模块 4 | 5 | Python 拥有一个包含各种内置模块的大型库(*自带电池(batteries included)*)(译注:“自带电池”来自于官方文档的翻译,意为“功能齐全”)。 6 | 7 | 甚至还有很多第三方模块(可以使用)。请到 [Python 包索引( PyPi )](https://pypi.org/) 查看,或者使用谷歌搜索特定主题。 8 | 9 | 对于 Python 而言,如何处理第三方依赖关系是一个不断演化的主题。本节仅仅涵盖一些基础知识,帮助你了解它们是如何工作的。 10 | 11 | ### 模块搜索路径 12 | 13 | `sys.path` 是一个列表,包含所有 `import` 语句要检查的目录。查看 `sys.path` : 14 | 15 | ```python 16 | >>> import sys 17 | >>> sys.path 18 | ... look at the result ... 19 | >>> 20 | ``` 21 | 22 | 如果导入的内容不在目录中。那么将会触发 `ImportError` 异常。 23 | 24 | ### 标准库模块 25 | 26 | Python 标准库中的模块通常来自于 `/usr/local/lib/python3.6' 之类的位置。可以通过一个简短的测试来确定模块来自于哪里: 27 | 28 | ```python 29 | >>> import re 30 | >>> re 31 | 32 | >>> 33 | ``` 34 | 35 | 在交互式解释器(REPL)中查看模块的位置是一个很好的调试技巧。交互式解释器将会显示模块所在的位置。 36 | 37 | ### 第三方模块 38 | 39 | 第三方模块通常位于专门的目录 `site-packages` 中。如果执行与上述相同的步骤,可以看到第三方模块所在的位置: 40 | 41 | ```python 42 | >>> import numpy 43 | >>> numpy 44 | 45 | >>> 46 | ``` 47 | 48 | 同样,如果要弄清楚导入的第三方模块为什么没有像预期那样工作,那么查看模块的位置是一个很好的调试技巧。 49 | 50 | ### 安装模块 51 | 52 | 安装第三方模块最常用的技术是使用 `pip`。示例: 53 | 54 | ```bash 55 | bash % python3 -m pip install packagename 56 | ``` 57 | 58 | 此命令会下载包并将包安装到 `site-packages` 目录中。 59 | 60 | ### 问题 61 | 62 | * 你可能正在使用你不能直接控制的 Python。 63 | * 企业认可的 Python 64 | * 操作系统自带的 Python 65 | * 你可能没有权限在计算机中安装全局包 66 | * 可能还有其它依赖 67 | 68 | ### 虚拟环境 69 | 70 | 对于包安装问题,一个常见的解决方案是为自己创建所谓的“虚拟环境”。自然,创建虚拟环境的方式不止这一种——实际上,有好几种相互竞争的工具和技术。如果你正在使用标准方式安装的 Python,可以尝试输入以下代码创建虚拟环境: 71 | 72 | ```bash 73 | bash % python -m venv mypython 74 | bash % 75 | ``` 76 | 77 | 稍等片刻后,你将拥有一个新目录 `mypython`,该目录安装了你自己的 Python。在 `mypython` 目录中,可以找到 `bin/` 目录(Unix 系统)或者 `Scripts/` 目录(Windows 系统)。运行 `bin/` 目录下的 `activate` 脚本会“激活“ 对应的 Python,使其成为 shell 的默认 `python` 命令。示例: 78 | 79 | ```bash 80 | bash % source mypython/bin/activate 81 | (mypython) bash % 82 | ``` 83 | 84 | 现在,你可以从这里开始安装自己的 Python 包了。示例: 85 | 86 | ``` 87 | (mypython) bash % python -m pip install pandas 88 | ... 89 | ``` 90 | 91 | 通常,为了试验和试用不同的包,可以使用虚拟环境。另一方面,如果你正在创建一个应用并且它有特定的包依赖关系,那么问题会稍有不同。 92 | 93 | ### 处理应用中的第三方依赖 94 | 95 | 如果你已经编写了一个应用,并且该应用具有特定的第三方依赖,那么创建并保存一个包括你自己的代码及其依赖的环境将会是一个挑战。可悲的是,这一直是一个非常混乱和频繁变化的领域。即使到现在,它仍在继续演变。 96 | 97 | 我不想向你提供必定会过时的信息,建议查阅 [Python 打包用户指南](https://packaging.python.org) 。 98 | 99 | ## 练习 100 | 101 | ### 练习 9.4:创建虚拟环境 102 | 103 | 看看你是否可以像上面一样创建一个虚拟环境并在虚拟环境中安装 pandas。 104 | 105 | [目录](../Contents.md) \| [上一节 (9.1 包)](01_Packages.md) \| [下一节 (9.3 版本分发)](03_Distribution.md) 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Notes/09_Packages/03_Distribution.md: -------------------------------------------------------------------------------- 1 | [目录](../Contents.md) \| [上一节 (9.2 第三方包)](02_Third_party.md) \| [下一节 (完结)](TheEnd.md) 2 | 3 | # 9.3 Distribution版本分发 4 | 5 | 在某些时候,你可能想要将自己的代码提供给其他人——可能只是同事(使用)。本节给出执行此操作的最基本技术。更多详细信息,请参考 [Python 打包用户指南](https://packaging.python.org)。 6 | 7 | ### 创建 setup.py 文件 8 | 9 | 请添加一个 `setup.py` 到项目目录的顶层。 10 | 11 | ```python 12 | # setup.py 13 | import setuptools 14 | 15 | setuptools.setup( 16 | name="porty", 17 | version="0.0.1", 18 | author="Your Name", 19 | author_email="you@example.com", 20 | description="Practical Python Code", 21 | packages=setuptools.find_packages(), 22 | ) 23 | ``` 24 | 25 | ### 创建 MANIFEST.in 文件 26 | 27 | 如果有其它文件与你的项目相关联,请使用一个 `MANIFEST.in` 文件指定这些关联的文件。示例: 28 | 29 | ``` 30 | # MANIFEST.in 31 | include *.csv 32 | ``` 33 | 34 | 请将 `MANIFEST.in` 文件放到 `setup.py` 所在的目录。 35 | 36 | ### 创建源码发行版 37 | 38 | 要创建源码发行版,请使用 `setup.py` 文件。示例: 39 | 40 | ``` 41 | bash % python setup.py sdist 42 | ``` 43 | 44 | 这将在 `dist/` 目录中创建 `.tar.gz` 或者 `.zip` 文件。该文件就是你要提供给其他人使用的文件。 45 | 46 | ### 安装你的代码 47 | 48 | 其他人可以使用 `pip` 像安装其它软件包一样安装你的 Python 代码。他们仅仅需要提供在之前的步骤中创建的文件即可。示例: 49 | 50 | ``` 51 | bash % python -m pip install porty-0.0.1.tar.gz 52 | ``` 53 | 54 | ### 说明 55 | 56 | 上面的步骤描述了创建 Python 代码包的最基本的知识,您可以将这些代码包提供给其他人使用。实际上,根据第三方的依赖关系,它可能要复杂得多,无论你的应用是否包含外部代码(例如 C 或 C++)。这已经超出了本课程的范围,我们只作初步了解。 57 | 58 | ## 练习 59 | 60 | ### 练习 9.5:创建软件包 61 | 62 | 使用练习 9.3 中创建的 `porty-app/` 代码,看看是否可以重新创建此前描述的步骤。具体来说,添加一个 `setup.py` 文件和一个 `MANIFEST.in` 文件到顶级目录中。然后通过运行 `python setup.py sdist` 创建源码发行版。 63 | 64 | 最后,看看是否可以在 Python 虚拟环境中安装你的软件包。 65 | 66 | [目录](../Contents.md) \| [上一节 (9.2 第三方包)](02_Third_party.md) \| [下一节 (完结)](TheEnd.md) 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Notes/09_Packages/TheEnd.md: -------------------------------------------------------------------------------- 1 | # 完结! 2 | 3 | 你已经完成了本课程。感谢你的关注,感谢你花时间和精力学习本课程。祝你在未来的 Python 编程中快乐并有所收获。 4 | 5 | 我总是很高兴能够收到反馈。你可以在 [https://dabeaz.com](https://dabeaz.com) 6 | 或者推特( [@dabeaz](https://twitter.com/dabeaz) )上找到我。- 戴维·比兹利(David Beazley) 7 | 8 | [目录](../Contents.md) \| [根目录](../..) 9 | 10 | -------------------------------------------------------------------------------- /Notes/Contents.md: -------------------------------------------------------------------------------- 1 | # 实用的 Python 编程 2 | 3 | ## 目录 4 | 5 | * [0.课程设置 (请先阅读!)](00_Setup.md) 6 | * [1.Python简介](01_Introduction/00_Overview.md) 7 | * [2.处理数据](02_Working_with_data/00_Overview.md) 8 | * [3.程序组织](03_Program_organization/00_Overview.md) 9 | * [4.类和对象](04_Classes_objects/00_Overview.md) 10 | * [5.Python 对象的内部工作原理](05_Object_model/00_Overview.md) 11 | * [6.生成器](06_Generators/00_Overview.md) 12 | * [7.高级主题](07_Advanced_Topics/00_Overview.md) 13 | * [8.测试_日志__调试](08_Testing_debugging/00_Overview.md) 14 | * [9. 包](09_Packages/00_Overview.md) 15 | 16 | 如果您计划教授本课程,请查看[讲师笔记](InstructorNotes.md)。 17 | 18 | [首页](../README.md) 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 欢迎光临 2 | 3 | 大约 25 年前,当我第一次学习 Python 时,发现 Python 竟然可以被高效地应用到各种混乱的工作项目上,我立即被震惊了。15 年前,我自己也将这种乐趣教授给别人。教学的结果就是本课程——一门实用的学习 Python的课程。本课程自 2007 年面世以来,已经被应用到400多个团体的面对面教学中。这些团体中包含了交易员、系统管理员、天文学家、修理工以及上百名使用 Python 帮助火星探测器着陆的火箭科学家。现在,我很高兴能够在知识共享协议下将这份课程共享给大家。 4 | 5 | [GitHub Pages](https://dabeaz-course.github.io/practical-python) | [GitHub Repo](https://github.com/dabeaz-course/practical-python). 6 | 7 | --David Beazley ([https://dabeaz.com](https://dabeaz.com)), [@dabeaz](https://twitter.com/dabeaz) 8 | 9 | ## 这是什么? 10 | 11 | 你在这里看到的这份材料是以讲师为主导的 Python 培训课程的核心内容。该课程用于企业培训和职业发展。自 2007 年以来,这门课程一直在不断地完善,并且在现实课堂中经受了实战的检验。一般来说,这门课程在3至4天内被面授给学习者——大约需要25至35个小时的高强度学习。这包括完成大约 130 道动手编程练习。 12 | 13 | ## 课程受众 14 | 15 | 本课程的学员通常是专业的科学家、工程师和程序员,他们已经有至少一种其它编程语言的经验。不需要事先掌握 Python 的相关知识,但是需要具备通用的编程知识。大部分学员觉得这门课程具有挑战性——尽管他们已经使用过 Python 进行编程。 16 | 17 | ## 课程目标 18 | 19 | 本课程的目标是涵盖 Python 编程的基础方面,重点是脚本编写、数据操作和程序组织。在本课程结束时,学员应该能够开始独立地编写有用的 Python 程序,或者能够理解和修改合作者编写的代码。 20 | 21 | ## 课程要求 22 | 23 | 要完成本门课程,需要安装 Python 3.6 或者更高的版本,以及腾出学习本课程的时间。 24 | 25 | ## 本课程不包含的内容 26 | 27 | 这不是一门为计算机编程初学者开设的课程。本课程假设你已经有 Python 语言或者其它语言的编程经验。 28 | 29 | 这不是一门关于 web 开发的课程。那是另一个“马戏团”,然而,如果你留在这个“马戏团”,你仍会看到一些有趣的表演——只是没有动物。(译者注:原文这里使用的是 circus,可以理解为:本课程不涉及 web 开发, web 开发是另一个主题,但是如果你仍留下来学习本课程,你也会有所收获。) 30 | 31 | 这不是一门为软件工程师开设的关于如何编写和维护一个百万行 Python 应用程序的课程。我不写那样的程序,大多数使用 Python 的公司也不这样做,同样,你也不需要。这样的内容已经删除了。 32 | 33 | ## 马上带我访问课程 34 | 35 | 请点击 [这里](Notes/Contents.md)。 36 | 37 | ## 社区讨论 38 | 39 | 想要讨论课程吗?你可以在 [Gitter](https://gitter.im/dabeaz-course/practical-python) 上加入会话。我不一定能够回复每个人,但其他人也许可以帮上忙。 40 | 41 | ## 致谢 42 | 43 | Llorenç Muntaner 在将课程内容从 Apple Keynote 转换为你在这里看到的在线版本方面发挥了重要作用。 44 | 45 | 在过去的 12 年里,不同的讲师曾在不同的时间讲过这门课程。这其中包括(按字母顺序):Ned 46 | Batchelder, Juan Pablo Claude, Mark Fenner, Michael Foord, Matt Harrison, Raymond Hettinger, Daniel Klein, Travis Oliphant, James Powell, Michael Selik, Hugo Shi, Ian Stokes-Rees, Yarko Tymciurak,Bryan Van de ven, Peter Wang, and Mark Wiebe。 47 | 48 | 还要感谢成千上万学习过本门课程的学员,你们对本课程进行反馈以及讨论,为本课程的成功做出了贡献。 49 | 50 | ## 问题和解答 51 | 52 | ### 问题:有课程视频可以观看吗? 53 | 54 | 没有,这门课程是让你写代码的,而不是看别人写代码。 55 | 56 | ### 问题: 这门课程是如何授权的? 57 | 58 | 本课程采用知识共享协议 ShareAlike 4.0 国际许可协议进行授权。 59 | 60 | ### 问题: 我教自己的 Python 课程时可以使用这份材料吗? 61 | 62 | 可以,只要注明出处即可。 63 | 64 | ### 问题: 我可以制作衍生作品吗? 65 | 66 | 可以,只要这些作品遵循相同的许可条款并且提供出处。 67 | 68 | ### 问题:我可以把这门课程翻译成其它语言吗? 69 | 70 | 可以,这非常棒。完成后请给我发送一个链接。 71 | 72 | ### 问题: 我可以直播课程或者制作视频吗? 73 | 74 | 可以,去做吧。如果你这样做,你将会学到很多 Python 知识。 75 | 76 | ### 问题: 为什么没有包含某个内容? 77 | 78 | 在 3 -4 天的时间里,只能安排这么多内容了。如果没有讲某个内容,很可能是因为曾经讲了这个内容,但是大家头都炸了,或者一开始就没有足够到时间去讲。另外,这是一门课程,而不是 Python 参考手册。 79 | 80 | ### 问题: 你接受拉取请求(pull requests)吗? 81 | 82 | 如果你能为本课程报告 Bug,我们将不胜感激,可以通过 [issue tracker](https://github.com/dabeaz-course/practical-python/issues) 进行提交。 83 | 84 | 除非是被邀请,否则不接受拉取请求,请先提交 issue。 85 | 86 | -------------------------------------------------------------------------------- /Solutions/1_10/mortgage.py: -------------------------------------------------------------------------------- 1 | # mortgage.py 2 | 3 | principal = 500000.0 4 | rate = 0.05 5 | payment = 2684.11 6 | total_paid = 0.0 7 | month = 0 8 | 9 | extra_payment = 1000.0 10 | extra_payment_start_month = 61 11 | extra_payment_end_month = 108 12 | 13 | while principal > 0: 14 | month = month + 1 15 | principal = principal * (1+rate/12) - payment 16 | total_paid = total_paid + payment 17 | 18 | if month >= extra_payment_start_month and month <= extra_payment_end_month: 19 | principal = principal - extra_payment 20 | total_paid = total_paid + extra_payment 21 | 22 | print(month, round(total_paid,2), round(principal, 2)) 23 | 24 | print('Total paid', round(total_paid, 2)) 25 | print('Months', month) 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Solutions/1_27/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | total_cost = 0.0 4 | 5 | with open('../../Work/Data/portfolio.csv', 'rt') as f: 6 | headers = next(f) 7 | for line in f: 8 | row = line.split(',') 9 | nshares = int(row[1]) 10 | price = float(row[2]) 11 | total_cost += nshares * price 12 | 13 | print('Total cost', total_cost) 14 | -------------------------------------------------------------------------------- /Solutions/1_33/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import csv 4 | def portfolio_cost(filename): 5 | ''' 6 | Computes the total cost (shares*price) of a portfolio file 7 | ''' 8 | total_cost = 0.0 9 | 10 | with open(filename, 'rt') as f: 11 | rows = csv.reader(f) 12 | headers = next(rows) 13 | for row in rows: 14 | try: 15 | nshares = int(row[1]) 16 | price = float(row[2]) 17 | total_cost += nshares * price 18 | # This catches errors in int() and float() conversions above 19 | except ValueError: 20 | print('Bad row:', row) 21 | 22 | return total_cost 23 | 24 | import sys 25 | if len(sys.argv) == 2: 26 | filename = sys.argv[1] 27 | else: 28 | filename = input('Enter a filename:') 29 | 30 | cost = portfolio_cost(filename) 31 | print('Total cost:', cost) 32 | -------------------------------------------------------------------------------- /Solutions/1_5/bounce.py: -------------------------------------------------------------------------------- 1 | # bounce.py 2 | 3 | height = 100 4 | bounce = 1 5 | while bounce <= 10: 6 | height = height * (3/5) 7 | print(bounce, round(height, 4)) 8 | bounce += 1 9 | -------------------------------------------------------------------------------- /Solutions/2_11/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | import csv 3 | 4 | def read_portfolio(filename): 5 | ''' 6 | Read a stock portfolio file into a list of dictionaries with keys 7 | name, shares, and price. 8 | ''' 9 | portfolio = [] 10 | with open(filename) as f: 11 | rows = csv.reader(f) 12 | headers = next(rows) 13 | 14 | for row in rows: 15 | stock = { 16 | 'name' : row[0], 17 | 'shares' : int(row[1]), 18 | 'price' : float(row[2]) 19 | } 20 | portfolio.append(stock) 21 | 22 | return portfolio 23 | 24 | def read_prices(filename): 25 | ''' 26 | Read a CSV file of price data into a dict mapping names to prices. 27 | ''' 28 | prices = {} 29 | with open(filename) as f: 30 | rows = csv.reader(f) 31 | for row in rows: 32 | try: 33 | prices[row[0]] = float(row[1]) 34 | except IndexError: 35 | pass 36 | 37 | return prices 38 | 39 | def make_report_data(portfolio, prices): 40 | ''' 41 | Make a list of (name, shares, price, change) tuples given a portfolio list 42 | and prices dictionary. 43 | ''' 44 | rows = [] 45 | for stock in portfolio: 46 | current_price = prices[stock['name']] 47 | change = current_price - stock['price'] 48 | summary = (stock['name'], stock['shares'], current_price, change) 49 | rows.append(summary) 50 | return rows 51 | 52 | # Read data files and create the report data 53 | 54 | portfolio = read_portfolio('../../Work/Data/portfolio.csv') 55 | prices = read_prices('../../Work/Data/prices.csv') 56 | 57 | # Generate the report data 58 | 59 | report = make_report_data(portfolio, prices) 60 | 61 | # Output the report 62 | headers = ('Name', 'Shares', 'Price', 'Change') 63 | print('%10s %10s %10s %10s' % headers) 64 | print(('-' * 10 + ' ') * len(headers)) 65 | for row in report: 66 | print('%10s %10d %10.2f %10.2f' % row) 67 | -------------------------------------------------------------------------------- /Solutions/2_16/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import csv 4 | def portfolio_cost(filename): 5 | ''' 6 | Computes the total cost (shares*price) of a portfolio file 7 | ''' 8 | total_cost = 0.0 9 | 10 | with open(filename) as f: 11 | rows = csv.reader(f) 12 | headers = next(rows) 13 | for rowno, row in enumerate(rows, start=1): 14 | record = dict(zip(headers, row)) 15 | try: 16 | nshares = int(record['shares']) 17 | price = float(record['price']) 18 | total_cost += nshares * price 19 | # This catches errors in int() and float() conversions above 20 | except ValueError: 21 | print(f'Row {rowno}: Bad row: {row}') 22 | 23 | return total_cost 24 | 25 | import sys 26 | if len(sys.argv) == 2: 27 | filename = sys.argv[1] 28 | else: 29 | filename = input('Enter a filename:') 30 | 31 | cost = portfolio_cost(filename) 32 | print('Total cost:', cost) 33 | -------------------------------------------------------------------------------- /Solutions/2_16/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | import csv 3 | 4 | def read_portfolio(filename): 5 | ''' 6 | Read a stock portfolio file into a list of dictionaries with keys 7 | name, shares, and price. 8 | ''' 9 | portfolio = [] 10 | with open(filename) as f: 11 | rows = csv.reader(f) 12 | headers = next(rows) 13 | 14 | for row in rows: 15 | record = dict(zip(headers, row)) 16 | stock = { 17 | 'name' : record['name'], 18 | 'shares' : int(record['shares']), 19 | 'price' : float(record['price']) 20 | } 21 | portfolio.append(stock) 22 | 23 | return portfolio 24 | 25 | def read_prices(filename): 26 | ''' 27 | Read a CSV file of price data into a dict mapping names to prices. 28 | ''' 29 | prices = {} 30 | with open(filename) as f: 31 | rows = csv.reader(f) 32 | for row in rows: 33 | try: 34 | prices[row[0]] = float(row[1]) 35 | except IndexError: 36 | pass 37 | 38 | return prices 39 | 40 | def make_report_data(portfolio, prices): 41 | ''' 42 | Make a list of (name, shares, price, change) tuples given a portfolio list 43 | and prices dictionary. 44 | ''' 45 | rows = [] 46 | for stock in portfolio: 47 | current_price = prices[stock['name']] 48 | change = current_price - stock['price'] 49 | summary = (stock['name'], stock['shares'], current_price, change) 50 | rows.append(summary) 51 | return rows 52 | 53 | # Read data files and create the report data 54 | 55 | portfolio = read_portfolio('../../Work/Data/portfolio.csv') 56 | prices = read_prices('../../Work/Data/prices.csv') 57 | 58 | # Generate the report data 59 | 60 | report = make_report_data(portfolio, prices) 61 | 62 | # Output the report 63 | headers = ('Name', 'Shares', 'Price', 'Change') 64 | print('%10s %10s %10s %10s' % headers) 65 | print(('-' * 10 + ' ') * len(headers)) 66 | for row in report: 67 | print('%10s %10d %10.2f %10.2f' % row) 68 | -------------------------------------------------------------------------------- /Solutions/2_7/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | import csv 3 | 4 | def read_portfolio(filename): 5 | ''' 6 | Read a stock portfolio file into a list of dictionaries with keys 7 | name, shares, and price. 8 | ''' 9 | portfolio = [] 10 | with open(filename) as f: 11 | rows = csv.reader(f) 12 | headers = next(rows) 13 | 14 | for row in rows: 15 | stock = { 16 | 'name' : row[0], 17 | 'shares' : int(row[1]), 18 | 'price' : float(row[2]) 19 | } 20 | portfolio.append(stock) 21 | 22 | return portfolio 23 | 24 | def read_prices(filename): 25 | ''' 26 | Read a CSV file of price data into a dict mapping names to prices. 27 | ''' 28 | prices = {} 29 | with open(filename) as f: 30 | rows = csv.reader(f) 31 | for row in rows: 32 | try: 33 | prices[row[0]] = float(row[1]) 34 | except IndexError: 35 | pass 36 | 37 | return prices 38 | 39 | portfolio = read_portfolio('../../Work/Data/portfolio.csv') 40 | prices = read_prices('../../Work/Data/prices.csv') 41 | 42 | # Calculate the total cost of the portfolio 43 | total_cost = 0.0 44 | for s in portfolio: 45 | total_cost += s['shares']*s['price'] 46 | 47 | print('Total cost', total_cost) 48 | 49 | # Compute the current value of the portfolio 50 | total_value = 0.0 51 | for s in portfolio: 52 | total_value += s['shares']*prices[s['name']] 53 | 54 | print('Current value', total_value) 55 | print('Gain', total_value - total_cost) 56 | -------------------------------------------------------------------------------- /Solutions/3_10/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | with open(filename) as f: 12 | rows = csv.reader(f, delimiter=delimiter) 13 | 14 | # Read the file headers (if any) 15 | headers = next(rows) if has_headers else [] 16 | 17 | # If specific columns have been selected, make indices for filtering and set output columns 18 | if select: 19 | indices = [ headers.index(colname) for colname in select ] 20 | headers = select 21 | 22 | records = [] 23 | for rowno, row in enumerate(rows, 1): 24 | if not row: # Skip rows with no data 25 | continue 26 | 27 | # If specific column indices are selected, pick them out 28 | if select: 29 | row = [ row[index] for index in indices] 30 | 31 | # Apply type conversion to the row 32 | if types: 33 | try: 34 | row = [func(val) for func, val in zip(types, row)] 35 | except ValueError as e: 36 | if not silence_errors: 37 | print(f"Row {rowno}: Couldn't convert {row}") 38 | print(f"Row {rowno}: Reason {e}") 39 | continue 40 | 41 | # Make a dictionary or a tuple 42 | if headers: 43 | record = dict(zip(headers, row)) 44 | else: 45 | record = tuple(row) 46 | records.append(record) 47 | 48 | return records 49 | -------------------------------------------------------------------------------- /Solutions/3_14/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | with open(filename) as f: 12 | rows = csv.reader(f, delimiter=delimiter) 13 | 14 | # Read the file headers (if any) 15 | headers = next(rows) if has_headers else [] 16 | 17 | # If specific columns have been selected, make indices for filtering and set output columns 18 | if select: 19 | indices = [ headers.index(colname) for colname in select ] 20 | headers = select 21 | 22 | records = [] 23 | for rowno, row in enumerate(rows, 1): 24 | if not row: # Skip rows with no data 25 | continue 26 | 27 | # If specific column indices are selected, pick them out 28 | if select: 29 | row = [ row[index] for index in indices] 30 | 31 | # Apply type conversion to the row 32 | if types: 33 | try: 34 | row = [func(val) for func, val in zip(types, row)] 35 | except ValueError as e: 36 | if not silence_errors: 37 | print(f"Row {rowno}: Couldn't convert {row}") 38 | print(f"Row {rowno}: Reason {e}") 39 | continue 40 | 41 | # Make a dictionary or a tuple 42 | if headers: 43 | record = dict(zip(headers, row)) 44 | else: 45 | record = tuple(row) 46 | records.append(record) 47 | 48 | return records 49 | -------------------------------------------------------------------------------- /Solutions/3_14/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return sum([s['shares']*s['price'] for s in portfolio]) 11 | 12 | import sys 13 | if len(sys.argv) == 2: 14 | filename = sys.argv[1] 15 | else: 16 | filename = input('Enter a filename:') 17 | 18 | cost = portfolio_cost(filename) 19 | print('Total cost:', cost) 20 | -------------------------------------------------------------------------------- /Solutions/3_14/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | 5 | def read_portfolio(filename): 6 | ''' 7 | Read a stock portfolio file into a list of dictionaries with keys 8 | name, shares, and price. 9 | ''' 10 | return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float]) 11 | 12 | def read_prices(filename): 13 | ''' 14 | Read a CSV file of price data into a dict mapping names to prices. 15 | ''' 16 | return dict(fileparse.parse_csv(filename,types=[str,float], has_headers=False)) 17 | 18 | def make_report_data(portfolio,prices): 19 | ''' 20 | Make a list of (name, shares, price, change) tuples given a portfolio list 21 | and prices dictionary. 22 | ''' 23 | rows = [] 24 | for stock in portfolio: 25 | current_price = prices[stock['name']] 26 | change = current_price - stock['price'] 27 | summary = (stock['name'], stock['shares'], current_price, change) 28 | rows.append(summary) 29 | return rows 30 | 31 | def print_report(reportdata): 32 | ''' 33 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 34 | ''' 35 | headers = ('Name','Shares','Price','Change') 36 | print('%10s %10s %10s %10s' % headers) 37 | print(('-'*10 + ' ')*len(headers)) 38 | for row in reportdata: 39 | print('%10s %10d %10.2f %10.2f' % row) 40 | 41 | def portfolio_report(portfoliofile,pricefile): 42 | ''' 43 | Make a stock report given portfolio and price data files. 44 | ''' 45 | # Read data files 46 | portfolio = read_portfolio(portfoliofile) 47 | prices = read_prices(pricefile) 48 | 49 | # Create the report data 50 | report = make_report_data(portfolio, prices) 51 | 52 | # Print it out 53 | print_report(report) 54 | 55 | portfolio_report('../../Work/Data/portfolio.csv', 56 | '../../Work/Data/prices.csv') 57 | -------------------------------------------------------------------------------- /Solutions/3_16/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | with open(filename) as f: 12 | rows = csv.reader(f, delimiter=delimiter) 13 | 14 | # Read the file headers (if any) 15 | headers = next(rows) if has_headers else [] 16 | 17 | # If specific columns have been selected, make indices for filtering and set output columns 18 | if select: 19 | indices = [ headers.index(colname) for colname in select ] 20 | headers = select 21 | 22 | records = [] 23 | for rowno, row in enumerate(rows, 1): 24 | if not row: # Skip rows with no data 25 | continue 26 | 27 | # If specific column indices are selected, pick them out 28 | if select: 29 | row = [ row[index] for index in indices] 30 | 31 | # Apply type conversion to the row 32 | if types: 33 | try: 34 | row = [func(val) for func, val in zip(types, row)] 35 | except ValueError as e: 36 | if not silence_errors: 37 | print(f"Row {rowno}: Couldn't convert {row}") 38 | print(f"Row {rowno}: Reason {e}") 39 | continue 40 | 41 | # Make a dictionary or a tuple 42 | if headers: 43 | record = dict(zip(headers, row)) 44 | else: 45 | record = tuple(row) 46 | records.append(record) 47 | 48 | return records 49 | -------------------------------------------------------------------------------- /Solutions/3_16/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return sum([s['shares'] * s['price'] for s in portfolio]) 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/3_16/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | 5 | def read_portfolio(filename): 6 | ''' 7 | Read a stock portfolio file into a list of dictionaries with keys 8 | name, shares, and price. 9 | ''' 10 | return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float]) 11 | 12 | def read_prices(filename): 13 | ''' 14 | Read a CSV file of price data into a dict mapping names to prices. 15 | ''' 16 | return dict(fileparse.parse_csv(filename,types=[str,float], has_headers=False)) 17 | 18 | def make_report_data(portfolio,prices): 19 | ''' 20 | Make a list of (name, shares, price, change) tuples given a portfolio list 21 | and prices dictionary. 22 | ''' 23 | rows = [] 24 | for stock in portfolio: 25 | current_price = prices[stock['name']] 26 | change = current_price - stock['price'] 27 | summary = (stock['name'], stock['shares'], current_price, change) 28 | rows.append(summary) 29 | return rows 30 | 31 | def print_report(reportdata): 32 | ''' 33 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 34 | ''' 35 | headers = ('Name','Shares','Price','Change') 36 | print('%10s %10s %10s %10s' % headers) 37 | print(('-'*10 + ' ')*len(headers)) 38 | for row in reportdata: 39 | print('%10s %10d %10.2f %10.2f' % row) 40 | 41 | def portfolio_report(portfoliofile, pricefile): 42 | ''' 43 | Make a stock report given portfolio and price data files. 44 | ''' 45 | # Read data files 46 | portfolio = read_portfolio(portfoliofile) 47 | prices = read_prices(pricefile) 48 | 49 | # Create the report data 50 | report = make_report_data(portfolio, prices) 51 | 52 | # Print it out 53 | print_report(report) 54 | 55 | def main(args): 56 | if len(args) != 3: 57 | raise SystemExit('Usage: %s portfile pricefile' % args[0]) 58 | portfolio_report(args[1], args[2]) 59 | 60 | if __name__ == '__main__': 61 | import sys 62 | main(sys.argv) 63 | -------------------------------------------------------------------------------- /Solutions/3_18/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/3_18/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return sum([s['shares'] * s['price'] for s in portfolio]) 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/3_18/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | 5 | def read_portfolio(filename): 6 | ''' 7 | Read a stock portfolio file into a list of dictionaries with keys 8 | name, shares, and price. 9 | ''' 10 | with open(filename) as lines: 11 | return fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float]) 12 | 13 | def read_prices(filename): 14 | ''' 15 | Read a CSV file of price data into a dict mapping names to prices. 16 | ''' 17 | with open(filename) as lines: 18 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 19 | 20 | def make_report_data(portfolio,prices): 21 | ''' 22 | Make a list of (name, shares, price, change) tuples given a portfolio list 23 | and prices dictionary. 24 | ''' 25 | rows = [] 26 | for stock in portfolio: 27 | current_price = prices[stock['name']] 28 | change = current_price - stock['price'] 29 | summary = (stock['name'], stock['shares'], current_price, change) 30 | rows.append(summary) 31 | return rows 32 | 33 | def print_report(reportdata): 34 | ''' 35 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 36 | ''' 37 | headers = ('Name','Shares','Price','Change') 38 | print('%10s %10s %10s %10s' % headers) 39 | print(('-'*10 + ' ')*len(headers)) 40 | for row in reportdata: 41 | print('%10s %10d %10.2f %10.2f' % row) 42 | 43 | def portfolio_report(portfoliofile, pricefile): 44 | ''' 45 | Make a stock report given portfolio and price data files. 46 | ''' 47 | # Read data files 48 | portfolio = read_portfolio(portfoliofile) 49 | prices = read_prices(pricefile) 50 | 51 | # Create the report data 52 | report = make_report_data(portfolio, prices) 53 | 54 | # Print it out 55 | print_report(report) 56 | 57 | def main(args): 58 | if len(args) != 3: 59 | raise SystemExit('Usage: %s portfile pricefile' % args[0]) 60 | portfolio_report(args[1], args[2]) 61 | 62 | if __name__ == '__main__': 63 | import sys 64 | main(sys.argv) 65 | -------------------------------------------------------------------------------- /Solutions/3_2/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | import csv 3 | 4 | def read_portfolio(filename): 5 | ''' 6 | Read a stock portfolio file into a list of dictionaries with keys 7 | name, shares, and price. 8 | ''' 9 | portfolio = [] 10 | with open(filename) as f: 11 | rows = csv.reader(f) 12 | headers = next(rows) 13 | 14 | for row in rows: 15 | record = dict(zip(headers, row)) 16 | stock = { 17 | 'name' : record['name'], 18 | 'shares' : int(record['shares']), 19 | 'price' : float(record['price']) 20 | } 21 | portfolio.append(stock) 22 | 23 | return portfolio 24 | 25 | def read_prices(filename): 26 | ''' 27 | Read a CSV file of price data into a dict mapping names to prices. 28 | ''' 29 | prices = {} 30 | with open(filename) as f: 31 | rows = csv.reader(f) 32 | for row in rows: 33 | try: 34 | prices[row[0]] = float(row[1]) 35 | except IndexError: 36 | pass 37 | 38 | return prices 39 | 40 | def make_report_data(portfolio,prices): 41 | ''' 42 | Make a list of (name, shares, price, change) tuples given a portfolio list 43 | and prices dictionary. 44 | ''' 45 | rows = [] 46 | for stock in portfolio: 47 | current_price = prices[stock['name']] 48 | change = current_price - stock['price'] 49 | summary = (stock['name'], stock['shares'], current_price, change) 50 | rows.append(summary) 51 | return rows 52 | 53 | def print_report(reportdata): 54 | ''' 55 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 56 | ''' 57 | headers = ('Name','Shares','Price','Change') 58 | print('%10s %10s %10s %10s' % headers) 59 | print(('-'*10 + ' ')*len(headers)) 60 | for row in reportdata: 61 | print('%10s %10d %10.2f %10.2f' % row) 62 | 63 | def portfolio_report(portfoliofile,pricefile): 64 | ''' 65 | Make a stock report given portfolio and price data files. 66 | ''' 67 | # Read data files 68 | portfolio = read_portfolio(portfoliofile) 69 | prices = read_prices(pricefile) 70 | 71 | # Create the report data 72 | report = make_report_data(portfolio,prices) 73 | 74 | # Print it out 75 | print_report(report) 76 | 77 | portfolio_report('../../Work/Data/portfolio.csv', 78 | '../../Work/Data/prices.csv') 79 | -------------------------------------------------------------------------------- /Solutions/3_7/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=','): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | with open(filename) as f: 9 | rows = csv.reader(f, delimiter=delimiter) 10 | 11 | # Read the file headers (if any) 12 | headers = next(rows) if has_headers else [] 13 | 14 | # If specific columns have been selected, make indices for filtering 15 | if select: 16 | indices = [ headers.index(colname) for colname in select ] 17 | headers = select 18 | 19 | records = [] 20 | for row in rows: 21 | if not row: # Skip rows with no data 22 | continue 23 | 24 | # If specific column indices are selected, pick them out 25 | if select: 26 | row = [ row[index] for index in indices] 27 | 28 | # Apply type conversion to the row 29 | if types: 30 | row = [func(val) for func, val in zip(types, row)] 31 | 32 | # Make a dictionary or a tuple 33 | if headers: 34 | record = dict(zip(headers, row)) 35 | else: 36 | record = tuple(row) 37 | records.append(record) 38 | 39 | return records 40 | -------------------------------------------------------------------------------- /Solutions/4_10/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/4_10/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return sum([s.cost() for s in portfolio]) 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/4_10/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | 7 | def read_portfolio(filename): 8 | ''' 9 | Read a stock portfolio file into a list of dictionaries with keys 10 | name, shares, and price. 11 | ''' 12 | with open(filename) as lines: 13 | portdicts = fileparse.parse_csv(lines, 14 | select=['name','shares','price'], 15 | types=[str,int,float]) 16 | 17 | portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] 18 | return portfolio 19 | 20 | def read_prices(filename): 21 | ''' 22 | Read a CSV file of price data into a dict mapping names to prices. 23 | ''' 24 | with open(filename) as lines: 25 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 26 | 27 | def make_report_data(portfolio, prices): 28 | ''' 29 | Make a list of (name, shares, price, change) tuples given a portfolio list 30 | and prices dictionary. 31 | ''' 32 | rows = [] 33 | for s in portfolio: 34 | current_price = prices[s.name] 35 | change = current_price - s.price 36 | summary = (s.name, s.shares, current_price, change) 37 | rows.append(summary) 38 | return rows 39 | 40 | def print_report(reportdata, formatter): 41 | ''' 42 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 43 | ''' 44 | formatter.headings(['Name','Shares','Price','Change']) 45 | for name, shares, price, change in reportdata: 46 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 47 | formatter.row(rowdata) 48 | 49 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 50 | ''' 51 | Make a stock report given portfolio and price data files. 52 | ''' 53 | # Read data files 54 | portfolio = read_portfolio(portfoliofile) 55 | prices = read_prices(pricefile) 56 | 57 | # Create the report data 58 | report = make_report_data(portfolio, prices) 59 | 60 | # Print it out 61 | formatter = tableformat.create_formatter(fmt) 62 | print_report(report, formatter) 63 | 64 | def main(args): 65 | if len(args) != 4: 66 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 67 | portfolio_report(args[1], args[2], args[3]) 68 | 69 | if __name__ == '__main__': 70 | import sys 71 | main(sys.argv) 72 | -------------------------------------------------------------------------------- /Solutions/4_10/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 7 | def __init__(self, name, shares, price): 8 | self.name = name 9 | self.shares = shares 10 | self.price = price 11 | 12 | def __repr__(self): 13 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 14 | 15 | def cost(self): 16 | ''' 17 | Return the cost as shares*price 18 | ''' 19 | return self.shares * self.price 20 | 21 | def sell(self, nshares): 22 | ''' 23 | Sell a number of shares 24 | ''' 25 | self.shares -= nshares 26 | 27 | -------------------------------------------------------------------------------- /Solutions/4_10/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/4_4/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/4_4/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return sum([s.cost() for s in portfolio]) 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/4_4/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | 6 | def read_portfolio(filename): 7 | ''' 8 | Read a stock portfolio file into a list of dictionaries with keys 9 | name, shares, and price. 10 | ''' 11 | with open(filename) as lines: 12 | portdicts = fileparse.parse_csv(lines, 13 | select=['name','shares','price'], 14 | types=[str,int,float]) 15 | 16 | portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] 17 | return portfolio 18 | 19 | def read_prices(filename): 20 | ''' 21 | Read a CSV file of price data into a dict mapping names to prices. 22 | ''' 23 | with open(filename) as lines: 24 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 25 | 26 | def make_report_data(portfolio, prices): 27 | ''' 28 | Make a list of (name, shares, price, change) tuples given a portfolio list 29 | and prices dictionary. 30 | ''' 31 | rows = [] 32 | for s in portfolio: 33 | current_price = prices[s.name] 34 | change = current_price - s.price 35 | summary = (s.name, s.shares, current_price, change) 36 | rows.append(summary) 37 | return rows 38 | 39 | def print_report(reportdata): 40 | ''' 41 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 42 | ''' 43 | headers = ('Name','Shares','Price','Change') 44 | print('%10s %10s %10s %10s' % headers) 45 | print(('-'*10 + ' ')*len(headers)) 46 | for row in reportdata: 47 | print('%10s %10d %10.2f %10.2f' % row) 48 | 49 | def portfolio_report(portfoliofile, pricefile): 50 | ''' 51 | Make a stock report given portfolio and price data files. 52 | ''' 53 | # Read data files 54 | portfolio = read_portfolio(portfoliofile) 55 | prices = read_prices(pricefile) 56 | 57 | # Create the report data 58 | report = make_report_data(portfolio, prices) 59 | 60 | # Print it out 61 | print_report(report) 62 | 63 | def main(args): 64 | if len(args) != 3: 65 | raise SystemExit('Usage: %s portfile pricefile' % args[0]) 66 | portfolio_report(args[1], args[2]) 67 | 68 | if __name__ == '__main__': 69 | import sys 70 | main(sys.argv) 71 | -------------------------------------------------------------------------------- /Solutions/4_4/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 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 | ''' 14 | Return the cost as shares*price 15 | ''' 16 | return self.shares * self.price 17 | 18 | def sell(self, nshares): 19 | ''' 20 | Sell a number of shares 21 | ''' 22 | self.shares -= nshares 23 | 24 | -------------------------------------------------------------------------------- /Solutions/5_8/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/5_8/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return sum([s.cost() for s in portfolio]) 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/5_8/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | 7 | def read_portfolio(filename): 8 | ''' 9 | Read a stock portfolio file into a list of dictionaries with keys 10 | name, shares, and price. 11 | ''' 12 | with open(filename) as lines: 13 | portdicts = fileparse.parse_csv(lines, 14 | select=['name','shares','price'], 15 | types=[str,int,float]) 16 | 17 | portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] 18 | return portfolio 19 | 20 | def read_prices(filename): 21 | ''' 22 | Read a CSV file of price data into a dict mapping names to prices. 23 | ''' 24 | with open(filename) as lines: 25 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 26 | 27 | def make_report_data(portfolio, prices): 28 | ''' 29 | Make a list of (name, shares, price, change) tuples given a portfolio list 30 | and prices dictionary. 31 | ''' 32 | rows = [] 33 | for s in portfolio: 34 | current_price = prices[s.name] 35 | change = current_price - s.price 36 | summary = (s.name, s.shares, current_price, change) 37 | rows.append(summary) 38 | return rows 39 | 40 | def print_report(reportdata, formatter): 41 | ''' 42 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 43 | ''' 44 | formatter.headings(['Name','Shares','Price','Change']) 45 | for name, shares, price, change in reportdata: 46 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 47 | formatter.row(rowdata) 48 | 49 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 50 | ''' 51 | Make a stock report given portfolio and price data files. 52 | ''' 53 | # Read data files 54 | portfolio = read_portfolio(portfoliofile) 55 | prices = read_prices(pricefile) 56 | 57 | # Create the report data 58 | report = make_report_data(portfolio, prices) 59 | 60 | # Print it out 61 | formatter = tableformat.create_formatter(fmt) 62 | print_report(report, formatter) 63 | 64 | def main(args): 65 | if len(args) != 4: 66 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 67 | portfolio_report(args[1], args[2], args[3]) 68 | 69 | if __name__ == '__main__': 70 | import sys 71 | main(sys.argv) 72 | -------------------------------------------------------------------------------- /Solutions/5_8/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 7 | __slots__ = ('name','_shares','price') 8 | def __init__(self,name, shares, price): 9 | self.name = name 10 | self.shares = shares 11 | self.price = price 12 | 13 | def __repr__(self): 14 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 15 | 16 | @property 17 | def shares(self): 18 | return self._shares 19 | 20 | @shares.setter 21 | def shares(self, value): 22 | if not isinstance(value,int): 23 | raise TypeError("Must be integer") 24 | self._shares = value 25 | 26 | @property 27 | def cost(self): 28 | ''' 29 | Return the cost as shares*price 30 | ''' 31 | return self.shares * self.price 32 | 33 | def sell(self, nshares): 34 | ''' 35 | Sell a number of shares and return the remaining number. 36 | ''' 37 | self.shares -= nshares 38 | -------------------------------------------------------------------------------- /Solutions/5_8/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/6_12/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/6_12/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 | f = open(filename, 'r') 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 | import report 21 | 22 | portfolio = report.read_portfolio('../../Data/portfolio.csv') 23 | 24 | for line in follow('../../Data/stocklog.csv'): 25 | row = line.split(',') 26 | name = row[0].strip('"') 27 | price = float(row[1]) 28 | change = float(row[4]) 29 | if name in portfolio: 30 | print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') 31 | -------------------------------------------------------------------------------- /Solutions/6_12/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/6_12/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | class Portfolio: 4 | def __init__(self, holdings): 5 | self._holdings = holdings 6 | 7 | def __iter__(self): 8 | return self._holdings.__iter__() 9 | 10 | def __len__(self): 11 | return len(self._holdings) 12 | 13 | def __getitem__(self, index): 14 | return self._holdings[index] 15 | 16 | def __contains__(self, name): 17 | return any([s.name == name for s in self._holdings]) 18 | 19 | @property 20 | def total_cost(self): 21 | return sum([s.shares * s.price for s in self._holdings]) 22 | 23 | def tabulate_shares(self): 24 | from collections import Counter 25 | total_shares = Counter() 26 | for s in self._holdings: 27 | total_shares[s.name] += s.shares 28 | return total_shares 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Solutions/6_12/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | from portfolio import Portfolio 7 | 8 | def read_portfolio(filename): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | portdicts = fileparse.parse_csv(lines, 15 | select=['name','shares','price'], 16 | types=[str,int,float]) 17 | 18 | portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] 19 | return Portfolio(portfolio) 20 | 21 | def read_prices(filename): 22 | ''' 23 | Read a CSV file of price data into a dict mapping names to prices. 24 | ''' 25 | with open(filename) as lines: 26 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 27 | 28 | def make_report_data(portfolio, prices): 29 | ''' 30 | Make a list of (name, shares, price, change) tuples given a portfolio list 31 | and prices dictionary. 32 | ''' 33 | rows = [] 34 | for s in portfolio: 35 | current_price = prices[s.name] 36 | change = current_price - s.price 37 | summary = (s.name, s.shares, current_price, change) 38 | rows.append(summary) 39 | return rows 40 | 41 | def print_report(reportdata, formatter): 42 | ''' 43 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 44 | ''' 45 | formatter.headings(['Name','Shares','Price','Change']) 46 | for name, shares, price, change in reportdata: 47 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 48 | formatter.row(rowdata) 49 | 50 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 51 | ''' 52 | Make a stock report given portfolio and price data files. 53 | ''' 54 | # Read data files 55 | portfolio = read_portfolio(portfoliofile) 56 | prices = read_prices(pricefile) 57 | 58 | # Create the report data 59 | report = make_report_data(portfolio, prices) 60 | 61 | # Print it out 62 | formatter = tableformat.create_formatter(fmt) 63 | print_report(report, formatter) 64 | 65 | def main(args): 66 | if len(args) != 4: 67 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 68 | portfolio_report(args[1], args[2], args[3]) 69 | 70 | if __name__ == '__main__': 71 | import sys 72 | main(sys.argv) 73 | -------------------------------------------------------------------------------- /Solutions/6_12/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 7 | __slots__ = ('name','_shares','price') 8 | def __init__(self,name, shares, price): 9 | self.name = name 10 | self.shares = shares 11 | self.price = price 12 | 13 | def __repr__(self): 14 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 15 | 16 | @property 17 | def shares(self): 18 | return self._shares 19 | 20 | @shares.setter 21 | def shares(self, value): 22 | if not isinstance(value,int): 23 | raise TypeError("Must be integer") 24 | self._shares = value 25 | 26 | @property 27 | def cost(self): 28 | ''' 29 | Return the cost as shares*price 30 | ''' 31 | return self.shares * self.price 32 | 33 | def sell(self, nshares): 34 | ''' 35 | Sell a number of shares and return the remaining number. 36 | ''' 37 | self.shares -= nshares 38 | -------------------------------------------------------------------------------- /Solutions/6_12/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/6_12/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | import report 5 | import tableformat 6 | from follow import follow 7 | import time 8 | 9 | def select_columns(rows, indices): 10 | for row in rows: 11 | yield [row[index] for index in indices] 12 | 13 | def convert_types(rows, types): 14 | for row in rows: 15 | yield [func(val) for func, val in zip(types, row)] 16 | 17 | def make_dicts(rows, headers): 18 | for row in rows: 19 | yield dict(zip(headers, row)) 20 | 21 | def parse_stock_data(lines): 22 | rows = csv.reader(lines) 23 | rows = select_columns(rows, [0, 1, 4]) 24 | rows = convert_types(rows, [str,float,float]) 25 | rows = make_dicts(rows, ['name','price','change']) 26 | return rows 27 | 28 | def filter_symbols(rows, names): 29 | for row in rows: 30 | if row['name'] in names: 31 | yield row 32 | 33 | def ticker(portfile, logfile, fmt): 34 | portfolio = report.read_portfolio(portfile) 35 | lines = follow(logfile) 36 | rows = parse_stock_data(lines) 37 | rows = filter_symbols(rows, portfolio) 38 | formatter = tableformat.create_formatter(fmt) 39 | formatter.headings(['Name','Price','Change']) 40 | for row in rows: 41 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 42 | 43 | def main(args): 44 | if len(args) != 4: 45 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 46 | ticker(args[1], args[2], args[3]) 47 | 48 | if __name__ == '__main__': 49 | import sys 50 | main(sys.argv) 51 | -------------------------------------------------------------------------------- /Solutions/6_15/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/6_15/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/6_15/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/6_15/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | class Portfolio: 4 | def __init__(self, holdings): 5 | self._holdings = holdings 6 | 7 | def __iter__(self): 8 | return self._holdings.__iter__() 9 | 10 | def __len__(self): 11 | return len(self._holdings) 12 | 13 | def __getitem__(self, index): 14 | return self._holdings[index] 15 | 16 | def __contains__(self, name): 17 | return any(s.name == name for s in self._holdings) 18 | 19 | @property 20 | def total_cost(self): 21 | return sum(s.shares * s.price for s in self._holdings) 22 | 23 | def tabulate_shares(self): 24 | from collections import Counter 25 | total_shares = Counter() 26 | for s in self._holdings: 27 | total_shares[s.name] += s.shares 28 | return total_shares 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Solutions/6_15/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | from portfolio import Portfolio 7 | 8 | def read_portfolio(filename): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | portdicts = fileparse.parse_csv(lines, 15 | select=['name','shares','price'], 16 | types=[str,int,float]) 17 | 18 | portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] 19 | return Portfolio(portfolio) 20 | 21 | def read_prices(filename): 22 | ''' 23 | Read a CSV file of price data into a dict mapping names to prices. 24 | ''' 25 | with open(filename) as lines: 26 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 27 | 28 | def make_report_data(portfolio, prices): 29 | ''' 30 | Make a list of (name, shares, price, change) tuples given a portfolio list 31 | and prices dictionary. 32 | ''' 33 | rows = [] 34 | for s in portfolio: 35 | current_price = prices[s.name] 36 | change = current_price - s.price 37 | summary = (s.name, s.shares, current_price, change) 38 | rows.append(summary) 39 | return rows 40 | 41 | def print_report(reportdata, formatter): 42 | ''' 43 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 44 | ''' 45 | formatter.headings(['Name','Shares','Price','Change']) 46 | for name, shares, price, change in reportdata: 47 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 48 | formatter.row(rowdata) 49 | 50 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 51 | ''' 52 | Make a stock report given portfolio and price data files. 53 | ''' 54 | # Read data files 55 | portfolio = read_portfolio(portfoliofile) 56 | prices = read_prices(pricefile) 57 | 58 | # Create the report data 59 | report = make_report_data(portfolio, prices) 60 | 61 | # Print it out 62 | formatter = tableformat.create_formatter(fmt) 63 | print_report(report, formatter) 64 | 65 | def main(args): 66 | if len(args) != 4: 67 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 68 | portfolio_report(args[1], args[2], args[3]) 69 | 70 | if __name__ == '__main__': 71 | import sys 72 | main(sys.argv) 73 | -------------------------------------------------------------------------------- /Solutions/6_15/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 7 | __slots__ = ('name','_shares','price') 8 | def __init__(self,name, shares, price): 9 | self.name = name 10 | self.shares = shares 11 | self.price = price 12 | 13 | def __repr__(self): 14 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 15 | 16 | @property 17 | def shares(self): 18 | return self._shares 19 | 20 | @shares.setter 21 | def shares(self, value): 22 | if not isinstance(value,int): 23 | raise TypeError("Must be integer") 24 | self._shares = value 25 | 26 | @property 27 | def cost(self): 28 | ''' 29 | Return the cost as shares*price 30 | ''' 31 | return self.shares * self.price 32 | 33 | def sell(self, nshares): 34 | ''' 35 | Sell a number of shares and return the remaining number. 36 | ''' 37 | self.shares -= nshares 38 | -------------------------------------------------------------------------------- /Solutions/6_15/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/6_15/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | import report 5 | import tableformat 6 | from follow import follow 7 | import time 8 | 9 | def select_columns(rows, indices): 10 | for row in rows: 11 | yield [row[index] for index in indices] 12 | 13 | def convert_types(rows, types): 14 | for row in rows: 15 | yield [func(val) for func, val in zip(types, row)] 16 | 17 | def make_dicts(rows, headers): 18 | return (dict(zip(headers,row)) for row in rows) 19 | 20 | def parse_stock_data(lines): 21 | rows = csv.reader(lines) 22 | rows = select_columns(rows, [0, 1, 4]) 23 | rows = convert_types(rows, [str,float,float]) 24 | rows = make_dicts(rows, ['name','price','change']) 25 | return rows 26 | 27 | def ticker(portfile, logfile, fmt): 28 | portfolio = report.read_portfolio(portfile) 29 | lines = follow(logfile) 30 | rows = parse_stock_data(lines) 31 | rows = (row for row in rows if row['name'] in portfolio) 32 | formatter = tableformat.create_formatter(fmt) 33 | formatter.headings(['Name','Price','Change']) 34 | for row in rows: 35 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 36 | 37 | def main(args): 38 | if len(args) != 4: 39 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 40 | ticker(args[1], args[2], args[3]) 41 | 42 | if __name__ == '__main__': 43 | import sys 44 | main(sys.argv) 45 | -------------------------------------------------------------------------------- /Solutions/6_3/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/6_3/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/6_3/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | class Portfolio: 4 | def __init__(self, holdings): 5 | self._holdings = holdings 6 | 7 | def __iter__(self): 8 | return self._holdings.__iter__() 9 | 10 | def __len__(self): 11 | return len(self._holdings) 12 | 13 | def __getitem__(self, index): 14 | return self._holdings[index] 15 | 16 | def __contains__(self, name): 17 | return any([s.name == name for s in self._holdings]) 18 | 19 | @property 20 | def total_cost(self): 21 | return sum([s.shares * s.price for s in self._holdings]) 22 | 23 | def tabulate_shares(self): 24 | from collections import Counter 25 | total_shares = Counter() 26 | for s in self._holdings: 27 | total_shares[s.name] += s.shares 28 | return total_shares 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Solutions/6_3/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | from portfolio import Portfolio 7 | 8 | def read_portfolio(filename): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | portdicts = fileparse.parse_csv(lines, 15 | select=['name','shares','price'], 16 | types=[str,int,float]) 17 | 18 | portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] 19 | return Portfolio(portfolio) 20 | 21 | def read_prices(filename): 22 | ''' 23 | Read a CSV file of price data into a dict mapping names to prices. 24 | ''' 25 | with open(filename) as lines: 26 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 27 | 28 | def make_report_data(portfolio, prices): 29 | ''' 30 | Make a list of (name, shares, price, change) tuples given a portfolio list 31 | and prices dictionary. 32 | ''' 33 | rows = [] 34 | for s in portfolio: 35 | current_price = prices[s.name] 36 | change = current_price - s.price 37 | summary = (s.name, s.shares, current_price, change) 38 | rows.append(summary) 39 | return rows 40 | 41 | def print_report(reportdata, formatter): 42 | ''' 43 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 44 | ''' 45 | formatter.headings(['Name','Shares','Price','Change']) 46 | for name, shares, price, change in reportdata: 47 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 48 | formatter.row(rowdata) 49 | 50 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 51 | ''' 52 | Make a stock report given portfolio and price data files. 53 | ''' 54 | # Read data files 55 | portfolio = read_portfolio(portfoliofile) 56 | prices = read_prices(pricefile) 57 | 58 | # Create the report data 59 | report = make_report_data(portfolio, prices) 60 | 61 | # Print it out 62 | formatter = tableformat.create_formatter(fmt) 63 | print_report(report, formatter) 64 | 65 | def main(args): 66 | if len(args) != 4: 67 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 68 | portfolio_report(args[1], args[2], args[3]) 69 | 70 | if __name__ == '__main__': 71 | import sys 72 | main(sys.argv) 73 | -------------------------------------------------------------------------------- /Solutions/6_3/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 7 | __slots__ = ('name','_shares','price') 8 | def __init__(self,name, shares, price): 9 | self.name = name 10 | self.shares = shares 11 | self.price = price 12 | 13 | def __repr__(self): 14 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 15 | 16 | @property 17 | def shares(self): 18 | return self._shares 19 | 20 | @shares.setter 21 | def shares(self, value): 22 | if not isinstance(value,int): 23 | raise TypeError("Must be integer") 24 | self._shares = value 25 | 26 | @property 27 | def cost(self): 28 | ''' 29 | Return the cost as shares*price 30 | ''' 31 | return self.shares * self.price 32 | 33 | def sell(self, nshares): 34 | ''' 35 | Sell a number of shares and return the remaining number. 36 | ''' 37 | self.shares -= nshares 38 | -------------------------------------------------------------------------------- /Solutions/6_3/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/6_7/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/6_7/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 | f = open(filename, 'r') 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 | import report 21 | 22 | portfolio = report.read_portfolio('../../Data/portfolio.csv') 23 | 24 | for line in follow('../../Data/stocklog.csv'): 25 | row = line.split(',') 26 | name = row[0].strip('"') 27 | price = float(row[1]) 28 | change = float(row[4]) 29 | if name in portfolio: 30 | print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') 31 | -------------------------------------------------------------------------------- /Solutions/6_7/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/6_7/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | class Portfolio: 4 | def __init__(self, holdings): 5 | self._holdings = holdings 6 | 7 | def __iter__(self): 8 | return self._holdings.__iter__() 9 | 10 | def __len__(self): 11 | return len(self._holdings) 12 | 13 | def __getitem__(self, index): 14 | return self._holdings[index] 15 | 16 | def __contains__(self, name): 17 | return any([s.name == name for s in self._holdings]) 18 | 19 | @property 20 | def total_cost(self): 21 | return sum([s.shares * s.price for s in self._holdings]) 22 | 23 | def tabulate_shares(self): 24 | from collections import Counter 25 | total_shares = Counter() 26 | for s in self._holdings: 27 | total_shares[s.name] += s.shares 28 | return total_shares 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Solutions/6_7/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | from portfolio import Portfolio 7 | 8 | def read_portfolio(filename): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | portdicts = fileparse.parse_csv(lines, 15 | select=['name','shares','price'], 16 | types=[str,int,float]) 17 | 18 | portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] 19 | return Portfolio(portfolio) 20 | 21 | def read_prices(filename): 22 | ''' 23 | Read a CSV file of price data into a dict mapping names to prices. 24 | ''' 25 | with open(filename) as lines: 26 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) 27 | 28 | def make_report_data(portfolio, prices): 29 | ''' 30 | Make a list of (name, shares, price, change) tuples given a portfolio list 31 | and prices dictionary. 32 | ''' 33 | rows = [] 34 | for s in portfolio: 35 | current_price = prices[s.name] 36 | change = current_price - s.price 37 | summary = (s.name, s.shares, current_price, change) 38 | rows.append(summary) 39 | return rows 40 | 41 | def print_report(reportdata, formatter): 42 | ''' 43 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 44 | ''' 45 | formatter.headings(['Name','Shares','Price','Change']) 46 | for name, shares, price, change in reportdata: 47 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 48 | formatter.row(rowdata) 49 | 50 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 51 | ''' 52 | Make a stock report given portfolio and price data files. 53 | ''' 54 | # Read data files 55 | portfolio = read_portfolio(portfoliofile) 56 | prices = read_prices(pricefile) 57 | 58 | # Create the report data 59 | report = make_report_data(portfolio, prices) 60 | 61 | # Print it out 62 | formatter = tableformat.create_formatter(fmt) 63 | print_report(report, formatter) 64 | 65 | def main(args): 66 | if len(args) != 4: 67 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 68 | portfolio_report(args[1], args[2], args[3]) 69 | 70 | if __name__ == '__main__': 71 | import sys 72 | main(sys.argv) 73 | -------------------------------------------------------------------------------- /Solutions/6_7/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 7 | __slots__ = ('name','_shares','price') 8 | def __init__(self,name, shares, price): 9 | self.name = name 10 | self.shares = shares 11 | self.price = price 12 | 13 | def __repr__(self): 14 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 15 | 16 | @property 17 | def shares(self): 18 | return self._shares 19 | 20 | @shares.setter 21 | def shares(self, value): 22 | if not isinstance(value,int): 23 | raise TypeError("Must be integer") 24 | self._shares = value 25 | 26 | @property 27 | def cost(self): 28 | ''' 29 | Return the cost as shares*price 30 | ''' 31 | return self.shares * self.price 32 | 33 | def sell(self, nshares): 34 | ''' 35 | Sell a number of shares and return the remaining number. 36 | ''' 37 | self.shares -= nshares 38 | -------------------------------------------------------------------------------- /Solutions/6_7/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/7_10/timethis.py: -------------------------------------------------------------------------------- 1 | # timethis.py 2 | 3 | import time 4 | 5 | def timethis(func): 6 | def wrapper(*args, **kwargs): 7 | start = time.time() 8 | try: 9 | return func(*args,**kwargs) 10 | finally: 11 | end = time.time() 12 | print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) 13 | return wrapper 14 | 15 | if __name__ == '__main__': 16 | @timethis 17 | def countdown(n): 18 | while n > 0: 19 | n-= 1 20 | 21 | countdown(1000000) 22 | -------------------------------------------------------------------------------- /Solutions/7_11/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/7_11/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/7_11/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/7_11/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | import fileparse 4 | import stock 5 | 6 | class Portfolio: 7 | def __init__(self): 8 | self._holdings = [] 9 | 10 | @classmethod 11 | def from_csv(cls, lines, **opts): 12 | self = cls() 13 | portdicts = fileparse.parse_csv(lines, 14 | select=['name','shares','price'], 15 | types=[str,int,float], 16 | **opts) 17 | 18 | for d in portdicts: 19 | self.append(stock.Stock(**d)) 20 | 21 | return self 22 | 23 | def append(self, holding): 24 | self._holdings.append(holding) 25 | 26 | def __iter__(self): 27 | return self._holdings.__iter__() 28 | 29 | def __len__(self): 30 | return len(self._holdings) 31 | 32 | def __getitem__(self, index): 33 | return self._holdings[index] 34 | 35 | def __contains__(self, name): 36 | return any(s.name == name for s in self._holdings) 37 | 38 | @property 39 | def total_cost(self): 40 | return sum(s.shares * s.price for s in self._holdings) 41 | 42 | def tabulate_shares(self): 43 | from collections import Counter 44 | total_shares = Counter() 45 | for s in self._holdings: 46 | total_shares[s.name] += s.shares 47 | return total_shares 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Solutions/7_11/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | from portfolio import Portfolio 6 | import tableformat 7 | 8 | def read_portfolio(filename, **opts): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | return Portfolio.from_csv(lines, **opts) 15 | 16 | def read_prices(filename, **opts): 17 | ''' 18 | Read a CSV file of price data into a dict mapping names to prices. 19 | ''' 20 | with open(filename) as lines: 21 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) 22 | 23 | def make_report(portfolio, prices): 24 | ''' 25 | Make a list of (name, shares, price, change) tuples given a portfolio list 26 | and prices dictionary. 27 | ''' 28 | rows = [] 29 | for s in portfolio: 30 | current_price = prices[s.name] 31 | change = current_price - s.price 32 | summary = (s.name, s.shares, current_price, change) 33 | rows.append(summary) 34 | return rows 35 | 36 | def print_report(reportdata, formatter): 37 | ''' 38 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 39 | ''' 40 | formatter.headings(['Name','Shares','Price','Change']) 41 | for name, shares, price, change in reportdata: 42 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 43 | formatter.row(rowdata) 44 | 45 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 46 | ''' 47 | Make a stock report given portfolio and price data files. 48 | ''' 49 | # Read data files 50 | portfolio = read_portfolio(portfoliofile) 51 | prices = read_prices(pricefile) 52 | 53 | # Create the report data 54 | report = make_report(portfolio, prices) 55 | 56 | # Print it out 57 | formatter = tableformat.create_formatter(fmt) 58 | print_report(report, formatter) 59 | 60 | def main(args): 61 | if len(args) != 4: 62 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 63 | portfolio_report(args[1], args[2], args[3]) 64 | 65 | if __name__ == '__main__': 66 | import sys 67 | main(sys.argv) 68 | -------------------------------------------------------------------------------- /Solutions/7_11/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | from typedproperty import String, Integer, Float 4 | 5 | class Stock: 6 | ''' 7 | An instance of a stock holding consisting of name, shares, and price. 8 | ''' 9 | name = String('name') 10 | shares = Integer('shares') 11 | price = Float('price') 12 | 13 | def __init__(self,name, shares, price): 14 | self.name = name 15 | self.shares = shares 16 | self.price = price 17 | 18 | def __repr__(self): 19 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 20 | 21 | @property 22 | def cost(self): 23 | ''' 24 | Return the cost as shares*price 25 | ''' 26 | return self.shares * self.price 27 | 28 | def sell(self, nshares): 29 | ''' 30 | Sell a number of shares and return the remaining number. 31 | ''' 32 | self.shares -= nshares 33 | -------------------------------------------------------------------------------- /Solutions/7_11/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/7_11/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | import report 5 | import tableformat 6 | from follow import follow 7 | 8 | def select_columns(rows, indices): 9 | for row in rows: 10 | yield [row[index] for index in indices] 11 | 12 | def convert_types(rows, types): 13 | for row in rows: 14 | yield [func(val) for func, val in zip(types, row)] 15 | 16 | def make_dicts(rows, headers): 17 | return (dict(zip(headers,row)) for row in rows) 18 | 19 | def parse_stock_data(lines): 20 | rows = csv.reader(lines) 21 | rows = select_columns(rows, [0, 1, 4]) 22 | rows = convert_types(rows, [str,float,float]) 23 | rows = make_dicts(rows, ['name','price','change']) 24 | return rows 25 | 26 | def ticker(portfile, logfile, fmt): 27 | portfolio = report.read_portfolio(portfile) 28 | lines = follow(logfile) 29 | rows = parse_stock_data(lines) 30 | rows = (row for row in rows if row['name'] in portfolio) 31 | formatter = tableformat.create_formatter(fmt) 32 | formatter.headings(['Name','Price','Change']) 33 | for row in rows: 34 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 35 | 36 | def main(args): 37 | if len(args) != 4: 38 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 39 | ticker(args[1], args[2], args[3]) 40 | 41 | if __name__ == '__main__': 42 | import sys 43 | main(sys.argv) 44 | -------------------------------------------------------------------------------- /Solutions/7_11/timethis.py: -------------------------------------------------------------------------------- 1 | # timethis.py 2 | 3 | import time 4 | 5 | def timethis(func): 6 | def wrapper(*args, **kwargs): 7 | start = time.time() 8 | try: 9 | return func(*args,**kwargs) 10 | finally: 11 | end = time.time() 12 | print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) 13 | return wrapper 14 | 15 | if __name__ == '__main__': 16 | @timethis 17 | def countdown(n): 18 | while n > 0: 19 | n-= 1 20 | 21 | countdown(1000000) 22 | -------------------------------------------------------------------------------- /Solutions/7_11/typedproperty.py: -------------------------------------------------------------------------------- 1 | # typedproperty.py 2 | 3 | def typedproperty(name, expected_type): 4 | private_name = '_' + name 5 | @property 6 | def prop(self): 7 | return getattr(self, private_name) 8 | 9 | @prop.setter 10 | def prop(self, value): 11 | if not isinstance(value, expected_type): 12 | raise TypeError(f'Expected {expected_type}') 13 | setattr(self, private_name, value) 14 | 15 | return prop 16 | 17 | String = lambda name: typedproperty(name, str) 18 | Integer = lambda name: typedproperty(name, int) 19 | Float = lambda name: typedproperty(name, float) 20 | 21 | # Example 22 | if __name__ == '__main__': 23 | class Stock: 24 | name = typedproperty('name', str) 25 | shares = typedproperty('shares', int) 26 | price = typedproperty('price', float) 27 | 28 | def __init__(self, name, shares, price): 29 | self.name = name 30 | self.shares = shares 31 | self.price = price 32 | 33 | 34 | class Stock2: 35 | name = String('name') 36 | shares = Integer('shares') 37 | price = Float('price') 38 | 39 | def __init__(self, name, shares, price): 40 | self.name = name 41 | self.shares = shares 42 | self.price = price 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Solutions/7_4/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/7_4/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/7_4/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/7_4/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | class Portfolio: 4 | def __init__(self, holdings): 5 | self._holdings = holdings 6 | 7 | def __iter__(self): 8 | return self._holdings.__iter__() 9 | 10 | def __len__(self): 11 | return len(self._holdings) 12 | 13 | def __getitem__(self, index): 14 | return self._holdings[index] 15 | 16 | def __contains__(self, name): 17 | return any(s.name == name for s in self._holdings) 18 | 19 | @property 20 | def total_cost(self): 21 | return sum(s.shares * s.price for s in self._holdings) 22 | 23 | def tabulate_shares(self): 24 | from collections import Counter 25 | total_shares = Counter() 26 | for s in self._holdings: 27 | total_shares[s.name] += s.shares 28 | return total_shares 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Solutions/7_4/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | from portfolio import Portfolio 7 | 8 | def read_portfolio(filename, **opts): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | portdicts = fileparse.parse_csv(lines, 15 | select=['name','shares','price'], 16 | types=[str,int,float], 17 | **opts) 18 | 19 | portfolio = [ Stock(**d) for d in portdicts ] 20 | return Portfolio(portfolio) 21 | 22 | def read_prices(filename, **opts): 23 | ''' 24 | Read a CSV file of price data into a dict mapping names to prices. 25 | ''' 26 | with open(filename) as lines: 27 | return dict(fileparse.parse_csv(lines,types=[str,float], has_headers=False, **opts)) 28 | 29 | def make_report_data(portfolio, prices): 30 | ''' 31 | Make a list of (name, shares, price, change) tuples given a portfolio list 32 | and prices dictionary. 33 | ''' 34 | rows = [] 35 | for s in portfolio: 36 | current_price = prices[s.name] 37 | change = current_price - s.price 38 | summary = (s.name, s.shares, current_price, change) 39 | rows.append(summary) 40 | return rows 41 | 42 | def print_report(reportdata, formatter): 43 | ''' 44 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 45 | ''' 46 | formatter.headings(['Name','Shares','Price','Change']) 47 | for name, shares, price, change in reportdata: 48 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 49 | formatter.row(rowdata) 50 | 51 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 52 | ''' 53 | Make a stock report given portfolio and price data files. 54 | ''' 55 | # Read data files 56 | portfolio = read_portfolio(portfoliofile) 57 | prices = read_prices(pricefile) 58 | 59 | # Create the report data 60 | report = make_report_data(portfolio, prices) 61 | 62 | # Print it out 63 | formatter = tableformat.create_formatter(fmt) 64 | print_report(report, formatter) 65 | 66 | def main(args): 67 | if len(args) != 4: 68 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 69 | portfolio_report(args[1], args[2], args[3]) 70 | 71 | if __name__ == '__main__': 72 | import sys 73 | main(sys.argv) 74 | -------------------------------------------------------------------------------- /Solutions/7_4/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | class Stock: 4 | ''' 5 | An instance of a stock holding consisting of name, shares, and price. 6 | ''' 7 | __slots__ = ('name','_shares','price') 8 | def __init__(self,name, shares, price): 9 | self.name = name 10 | self.shares = shares 11 | self.price = price 12 | 13 | def __repr__(self): 14 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 15 | 16 | @property 17 | def shares(self): 18 | return self._shares 19 | 20 | @shares.setter 21 | def shares(self, value): 22 | if not isinstance(value,int): 23 | raise TypeError("Must be integer") 24 | self._shares = value 25 | 26 | @property 27 | def cost(self): 28 | ''' 29 | Return the cost as shares*price 30 | ''' 31 | return self.shares * self.price 32 | 33 | def sell(self, nshares): 34 | ''' 35 | Sell a number of shares and return the remaining number. 36 | ''' 37 | self.shares -= nshares 38 | -------------------------------------------------------------------------------- /Solutions/7_4/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/7_4/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | import report 5 | import tableformat 6 | from follow import follow 7 | import time 8 | 9 | def select_columns(rows, indices): 10 | for row in rows: 11 | yield [row[index] for index in indices] 12 | 13 | def convert_types(rows, types): 14 | for row in rows: 15 | yield [func(val) for func, val in zip(types, row)] 16 | 17 | def make_dicts(rows, headers): 18 | return (dict(zip(headers,row)) for row in rows) 19 | 20 | def parse_stock_data(lines): 21 | rows = csv.reader(lines) 22 | rows = select_columns(rows, [0, 1, 4]) 23 | rows = convert_types(rows, [str,float,float]) 24 | rows = make_dicts(rows, ['name','price','change']) 25 | return rows 26 | 27 | def ticker(portfile, logfile, fmt): 28 | portfolio = report.read_portfolio(portfile) 29 | lines = follow(logfile) 30 | rows = parse_stock_data(lines) 31 | rows = (row for row in rows if row['name'] in portfolio) 32 | formatter = tableformat.create_formatter(fmt) 33 | formatter.headings(['Name','Price','Change']) 34 | for row in rows: 35 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 36 | 37 | def main(args): 38 | if len(args) != 4: 39 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 40 | ticker(args[1], args[2], args[3]) 41 | 42 | if __name__ == '__main__': 43 | import sys 44 | main(sys.argv) 45 | -------------------------------------------------------------------------------- /Solutions/7_9/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/7_9/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/7_9/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/7_9/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | class Portfolio: 4 | def __init__(self, holdings): 5 | self._holdings = holdings 6 | 7 | def __iter__(self): 8 | return self._holdings.__iter__() 9 | 10 | def __len__(self): 11 | return len(self._holdings) 12 | 13 | def __getitem__(self, index): 14 | return self._holdings[index] 15 | 16 | def __contains__(self, name): 17 | return any(s.name == name for s in self._holdings) 18 | 19 | @property 20 | def total_cost(self): 21 | return sum(s.shares * s.price for s in self._holdings) 22 | 23 | def tabulate_shares(self): 24 | from collections import Counter 25 | total_shares = Counter() 26 | for s in self._holdings: 27 | total_shares[s.name] += s.shares 28 | return total_shares 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Solutions/7_9/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | import tableformat 6 | from portfolio import Portfolio 7 | 8 | def read_portfolio(filename, **opts): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | portdicts = fileparse.parse_csv(lines, 15 | select=['name','shares','price'], 16 | types=[str,int,float], 17 | **opts) 18 | 19 | portfolio = [ Stock(**d) for d in portdicts ] 20 | return Portfolio(portfolio) 21 | 22 | def read_prices(filename, **opts): 23 | ''' 24 | Read a CSV file of price data into a dict mapping names to prices. 25 | ''' 26 | with open(filename) as lines: 27 | return dict(fileparse.parse_csv(lines,types=[str,float], has_headers=False, **opts)) 28 | 29 | def make_report_data(portfolio, prices): 30 | ''' 31 | Make a list of (name, shares, price, change) tuples given a portfolio list 32 | and prices dictionary. 33 | ''' 34 | rows = [] 35 | for s in portfolio: 36 | current_price = prices[s.name] 37 | change = current_price - s.price 38 | summary = (s.name, s.shares, current_price, change) 39 | rows.append(summary) 40 | return rows 41 | 42 | def print_report(reportdata, formatter): 43 | ''' 44 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 45 | ''' 46 | formatter.headings(['Name','Shares','Price','Change']) 47 | for name, shares, price, change in reportdata: 48 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 49 | formatter.row(rowdata) 50 | 51 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 52 | ''' 53 | Make a stock report given portfolio and price data files. 54 | ''' 55 | # Read data files 56 | portfolio = read_portfolio(portfoliofile) 57 | prices = read_prices(pricefile) 58 | 59 | # Create the report data 60 | report = make_report_data(portfolio, prices) 61 | 62 | # Print it out 63 | formatter = tableformat.create_formatter(fmt) 64 | print_report(report, formatter) 65 | 66 | def main(args): 67 | if len(args) != 4: 68 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 69 | portfolio_report(args[1], args[2], args[3]) 70 | 71 | if __name__ == '__main__': 72 | import sys 73 | main(sys.argv) 74 | -------------------------------------------------------------------------------- /Solutions/7_9/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | from typedproperty import String, Integer, Float 4 | 5 | class Stock: 6 | ''' 7 | An instance of a stock holding consisting of name, shares, and price. 8 | ''' 9 | name = String('name') 10 | shares = Integer('shares') 11 | price = Float('price') 12 | 13 | def __init__(self,name, shares, price): 14 | self.name = name 15 | self.shares = shares 16 | self.price = price 17 | 18 | def __repr__(self): 19 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 20 | 21 | @property 22 | def cost(self): 23 | ''' 24 | Return the cost as shares*price 25 | ''' 26 | return self.shares * self.price 27 | 28 | def sell(self, nshares): 29 | ''' 30 | Sell a number of shares and return the remaining number. 31 | ''' 32 | self.shares -= nshares 33 | -------------------------------------------------------------------------------- /Solutions/7_9/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/7_9/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | import report 5 | import tableformat 6 | from follow import follow 7 | import time 8 | 9 | def select_columns(rows, indices): 10 | for row in rows: 11 | yield [row[index] for index in indices] 12 | 13 | def convert_types(rows, types): 14 | for row in rows: 15 | yield [func(val) for func, val in zip(types, row)] 16 | 17 | def make_dicts(rows, headers): 18 | return (dict(zip(headers,row)) for row in rows) 19 | 20 | def parse_stock_data(lines): 21 | rows = csv.reader(lines) 22 | rows = select_columns(rows, [0, 1, 4]) 23 | rows = convert_types(rows, [str,float,float]) 24 | rows = make_dicts(rows, ['name','price','change']) 25 | return rows 26 | 27 | def ticker(portfile, logfile, fmt): 28 | portfolio = report.read_portfolio(portfile) 29 | lines = follow(logfile) 30 | rows = parse_stock_data(lines) 31 | rows = (row for row in rows if row['name'] in portfolio) 32 | formatter = tableformat.create_formatter(fmt) 33 | formatter.headings(['Name','Price','Change']) 34 | for row in rows: 35 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 36 | 37 | def main(args): 38 | if len(args) != 4: 39 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 40 | ticker(args[1], args[2], args[3]) 41 | 42 | if __name__ == '__main__': 43 | import sys 44 | main(sys.argv) 45 | -------------------------------------------------------------------------------- /Solutions/7_9/typedproperty.py: -------------------------------------------------------------------------------- 1 | # typedproperty.py 2 | 3 | def typedproperty(name, expected_type): 4 | private_name = '_' + name 5 | @property 6 | def prop(self): 7 | return getattr(self, private_name) 8 | 9 | @prop.setter 10 | def prop(self, value): 11 | if not isinstance(value, expected_type): 12 | raise TypeError(f'Expected {expected_type}') 13 | setattr(self, private_name, value) 14 | 15 | return prop 16 | 17 | String = lambda name: typedproperty(name, str) 18 | Integer = lambda name: typedproperty(name, int) 19 | Float = lambda name: typedproperty(name, float) 20 | 21 | # Example 22 | if __name__ == '__main__': 23 | class Stock: 24 | name = typedproperty('name', str) 25 | shares = typedproperty('shares', int) 26 | price = typedproperty('price', float) 27 | 28 | def __init__(self, name, shares, price): 29 | self.name = name 30 | self.shares = shares 31 | self.price = price 32 | 33 | class Stock2: 34 | name = String('name') 35 | shares = Integer('shares') 36 | price = Float('price') 37 | 38 | def __init__(self, name, shares, price): 39 | self.name = name 40 | self.shares = shares 41 | self.price = price 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Solutions/8_1/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | 4 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 5 | ''' 6 | Parse a CSV file into a list of records with type conversion. 7 | ''' 8 | if select and not has_headers: 9 | raise RuntimeError('select requires column headers') 10 | 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | print(f"Row {rowno}: Couldn't convert {row}") 37 | print(f"Row {rowno}: Reason {e}") 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/8_1/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/8_1/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/8_1/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | import fileparse 4 | import stock 5 | 6 | class Portfolio: 7 | def __init__(self): 8 | self._holdings = [] 9 | 10 | @classmethod 11 | def from_csv(cls, lines, **opts): 12 | self = cls() 13 | portdicts = fileparse.parse_csv(lines, 14 | select=['name','shares','price'], 15 | types=[str,int,float], 16 | **opts) 17 | 18 | for d in portdicts: 19 | self.append(stock.Stock(**d)) 20 | 21 | return self 22 | 23 | def append(self, holding): 24 | self._holdings.append(holding) 25 | 26 | def __iter__(self): 27 | return self._holdings.__iter__() 28 | 29 | def __len__(self): 30 | return len(self._holdings) 31 | 32 | def __getitem__(self, index): 33 | return self._holdings[index] 34 | 35 | def __contains__(self, name): 36 | return any(s.name == name for s in self._holdings) 37 | 38 | @property 39 | def total_cost(self): 40 | return sum(s.shares * s.price for s in self._holdings) 41 | 42 | def tabulate_shares(self): 43 | from collections import Counter 44 | total_shares = Counter() 45 | for s in self._holdings: 46 | total_shares[s.name] += s.shares 47 | return total_shares 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Solutions/8_1/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | from portfolio import Portfolio 6 | import tableformat 7 | 8 | def read_portfolio(filename, **opts): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | return Portfolio.from_csv(lines, **opts) 15 | 16 | def read_prices(filename, **opts): 17 | ''' 18 | Read a CSV file of price data into a dict mapping names to prices. 19 | ''' 20 | with open(filename) as lines: 21 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) 22 | 23 | def make_report(portfolio, prices): 24 | ''' 25 | Make a list of (name, shares, price, change) tuples given a portfolio list 26 | and prices dictionary. 27 | ''' 28 | rows = [] 29 | for s in portfolio: 30 | current_price = prices[s.name] 31 | change = current_price - s.price 32 | summary = (s.name, s.shares, current_price, change) 33 | rows.append(summary) 34 | return rows 35 | 36 | def print_report(reportdata, formatter): 37 | ''' 38 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 39 | ''' 40 | formatter.headings(['Name','Shares','Price','Change']) 41 | for name, shares, price, change in reportdata: 42 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 43 | formatter.row(rowdata) 44 | 45 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 46 | ''' 47 | Make a stock report given portfolio and price data files. 48 | ''' 49 | # Read data files 50 | portfolio = read_portfolio(portfoliofile) 51 | prices = read_prices(pricefile) 52 | 53 | # Create the report data 54 | report = make_report(portfolio, prices) 55 | 56 | # Print it out 57 | formatter = tableformat.create_formatter(fmt) 58 | print_report(report, formatter) 59 | 60 | def main(args): 61 | if len(args) != 4: 62 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 63 | portfolio_report(args[1], args[2], args[3]) 64 | 65 | if __name__ == '__main__': 66 | import sys 67 | main(sys.argv) 68 | -------------------------------------------------------------------------------- /Solutions/8_1/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | from typedproperty import String, Integer, Float 4 | 5 | class Stock: 6 | ''' 7 | An instance of a stock holding consisting of name, shares, and price. 8 | ''' 9 | name = String('name') 10 | shares = Integer('shares') 11 | price = Float('price') 12 | 13 | def __init__(self,name, shares, price): 14 | self.name = name 15 | self.shares = shares 16 | self.price = price 17 | 18 | def __repr__(self): 19 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 20 | 21 | @property 22 | def cost(self): 23 | ''' 24 | Return the cost as shares*price 25 | ''' 26 | return self.shares * self.price 27 | 28 | def sell(self, nshares): 29 | ''' 30 | Sell a number of shares and return the remaining number. 31 | ''' 32 | self.shares -= nshares 33 | -------------------------------------------------------------------------------- /Solutions/8_1/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/8_1/test_stock.py: -------------------------------------------------------------------------------- 1 | # test_stock.py 2 | 3 | import unittest 4 | import stock 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_cost(self): 14 | s = stock.Stock('GOOG', 100, 490.1) 15 | self.assertEqual(s.cost, 49010.0) 16 | 17 | def test_sell(self): 18 | s = stock.Stock('GOOG', 100, 490.1) 19 | s.sell(25) 20 | self.assertEqual(s.shares, 75) 21 | 22 | def test_shares_check(self): 23 | s = stock.Stock('GOOG', 100, 490.1) 24 | with self.assertRaises(TypeError): 25 | s.shares = '100' 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /Solutions/8_1/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | import report 5 | import tableformat 6 | from follow import follow 7 | 8 | def select_columns(rows, indices): 9 | for row in rows: 10 | yield [row[index] for index in indices] 11 | 12 | def convert_types(rows, types): 13 | for row in rows: 14 | yield [func(val) for func, val in zip(types, row)] 15 | 16 | def make_dicts(rows, headers): 17 | return (dict(zip(headers,row)) for row in rows) 18 | 19 | def parse_stock_data(lines): 20 | rows = csv.reader(lines) 21 | rows = select_columns(rows, [0, 1, 4]) 22 | rows = convert_types(rows, [str,float,float]) 23 | rows = make_dicts(rows, ['name','price','change']) 24 | return rows 25 | 26 | def ticker(portfile, logfile, fmt): 27 | portfolio = report.read_portfolio(portfile) 28 | lines = follow(logfile) 29 | rows = parse_stock_data(lines) 30 | rows = (row for row in rows if row['name'] in portfolio) 31 | formatter = tableformat.create_formatter(fmt) 32 | formatter.headings(['Name','Price','Change']) 33 | for row in rows: 34 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 35 | 36 | def main(args): 37 | if len(args) != 4: 38 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 39 | ticker(args[1], args[2], args[3]) 40 | 41 | if __name__ == '__main__': 42 | import sys 43 | main(sys.argv) 44 | -------------------------------------------------------------------------------- /Solutions/8_1/timethis.py: -------------------------------------------------------------------------------- 1 | # timethis.py 2 | 3 | import time 4 | 5 | def timethis(func): 6 | def wrapper(*args, **kwargs): 7 | start = time.time() 8 | try: 9 | return func(*args,**kwargs) 10 | finally: 11 | end = time.time() 12 | print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) 13 | return wrapper 14 | 15 | if __name__ == '__main__': 16 | @timethis 17 | def countdown(n): 18 | while n > 0: 19 | n-= 1 20 | 21 | countdown(1000000) 22 | -------------------------------------------------------------------------------- /Solutions/8_1/typedproperty.py: -------------------------------------------------------------------------------- 1 | # typedproperty.py 2 | 3 | def typedproperty(name, expected_type): 4 | private_name = '_' + name 5 | @property 6 | def prop(self): 7 | return getattr(self, private_name) 8 | 9 | @prop.setter 10 | def prop(self, value): 11 | if not isinstance(value, expected_type): 12 | raise TypeError(f'Expected {expected_type}') 13 | setattr(self, private_name, value) 14 | 15 | return prop 16 | 17 | String = lambda name: typedproperty(name, str) 18 | Integer = lambda name: typedproperty(name, int) 19 | Float = lambda name: typedproperty(name, float) 20 | 21 | # Example 22 | if __name__ == '__main__': 23 | class Stock: 24 | name = typedproperty('name', str) 25 | shares = typedproperty('shares', int) 26 | price = typedproperty('price', float) 27 | 28 | def __init__(self, name, shares, price): 29 | self.name = name 30 | self.shares = shares 31 | self.price = price 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Solutions/8_2/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | import logging 4 | log = logging.getLogger(__name__) 5 | 6 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 7 | ''' 8 | Parse a CSV file into a list of records with type conversion. 9 | ''' 10 | if select and not has_headers: 11 | raise RuntimeError('select requires column headers') 12 | 13 | rows = csv.reader(lines, delimiter=delimiter) 14 | 15 | # Read the file headers (if any) 16 | headers = next(rows) if has_headers else [] 17 | 18 | # If specific columns have been selected, make indices for filtering and set output columns 19 | if select: 20 | indices = [ headers.index(colname) for colname in select ] 21 | headers = select 22 | 23 | records = [] 24 | for rowno, row in enumerate(rows, 1): 25 | if not row: # Skip rows with no data 26 | continue 27 | 28 | # If specific column indices are selected, pick them out 29 | if select: 30 | row = [ row[index] for index in indices] 31 | 32 | # Apply type conversion to the row 33 | if types: 34 | try: 35 | row = [func(val) for func, val in zip(types, row)] 36 | except ValueError as e: 37 | if not silence_errors: 38 | log.warning("Row %d: Couldn't convert %s", rowno, row) 39 | log.debug("Row %d: Reason %s", rowno, e) 40 | continue 41 | 42 | # Make a dictionary or a tuple 43 | if headers: 44 | record = dict(zip(headers, row)) 45 | else: 46 | record = tuple(row) 47 | records.append(record) 48 | 49 | return records 50 | -------------------------------------------------------------------------------- /Solutions/8_2/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/8_2/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/8_2/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | import fileparse 4 | import stock 5 | 6 | class Portfolio: 7 | def __init__(self): 8 | self._holdings = [] 9 | 10 | @classmethod 11 | def from_csv(cls, lines, **opts): 12 | self = cls() 13 | portdicts = fileparse.parse_csv(lines, 14 | select=['name','shares','price'], 15 | types=[str,int,float], 16 | **opts) 17 | 18 | for d in portdicts: 19 | self.append(stock.Stock(**d)) 20 | 21 | return self 22 | 23 | def append(self, holding): 24 | self._holdings.append(holding) 25 | 26 | def __iter__(self): 27 | return self._holdings.__iter__() 28 | 29 | def __len__(self): 30 | return len(self._holdings) 31 | 32 | def __getitem__(self, index): 33 | return self._holdings[index] 34 | 35 | def __contains__(self, name): 36 | return any(s.name == name for s in self._holdings) 37 | 38 | @property 39 | def total_cost(self): 40 | return sum(s.shares * s.price for s in self._holdings) 41 | 42 | def tabulate_shares(self): 43 | from collections import Counter 44 | total_shares = Counter() 45 | for s in self._holdings: 46 | total_shares[s.name] += s.shares 47 | return total_shares 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Solutions/8_2/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | import fileparse 4 | from stock import Stock 5 | from portfolio import Portfolio 6 | import tableformat 7 | 8 | def read_portfolio(filename, **opts): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | return Portfolio.from_csv(lines, **opts) 15 | 16 | def read_prices(filename, **opts): 17 | ''' 18 | Read a CSV file of price data into a dict mapping names to prices. 19 | ''' 20 | with open(filename) as lines: 21 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) 22 | 23 | def make_report(portfolio, prices): 24 | ''' 25 | Make a list of (name, shares, price, change) tuples given a portfolio list 26 | and prices dictionary. 27 | ''' 28 | rows = [] 29 | for s in portfolio: 30 | current_price = prices[s.name] 31 | change = current_price - s.price 32 | summary = (s.name, s.shares, current_price, change) 33 | rows.append(summary) 34 | return rows 35 | 36 | def print_report(reportdata, formatter): 37 | ''' 38 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 39 | ''' 40 | formatter.headings(['Name','Shares','Price','Change']) 41 | for name, shares, price, change in reportdata: 42 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 43 | formatter.row(rowdata) 44 | 45 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 46 | ''' 47 | Make a stock report given portfolio and price data files. 48 | ''' 49 | # Read data files 50 | portfolio = read_portfolio(portfoliofile) 51 | prices = read_prices(pricefile) 52 | 53 | # Create the report data 54 | report = make_report(portfolio, prices) 55 | 56 | # Print it out 57 | formatter = tableformat.create_formatter(fmt) 58 | print_report(report, formatter) 59 | 60 | def main(args): 61 | if len(args) != 4: 62 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 63 | portfolio_report(args[1], args[2], args[3]) 64 | 65 | if __name__ == '__main__': 66 | import sys 67 | main(sys.argv) 68 | -------------------------------------------------------------------------------- /Solutions/8_2/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | from typedproperty import String, Integer, Float 4 | 5 | class Stock: 6 | ''' 7 | An instance of a stock holding consisting of name, shares, and price. 8 | ''' 9 | name = String('name') 10 | shares = Integer('shares') 11 | price = Float('price') 12 | 13 | def __init__(self,name, shares, price): 14 | self.name = name 15 | self.shares = shares 16 | self.price = price 17 | 18 | def __repr__(self): 19 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 20 | 21 | @property 22 | def cost(self): 23 | ''' 24 | Return the cost as shares*price 25 | ''' 26 | return self.shares * self.price 27 | 28 | def sell(self, nshares): 29 | ''' 30 | Sell a number of shares and return the remaining number. 31 | ''' 32 | self.shares -= nshares 33 | -------------------------------------------------------------------------------- /Solutions/8_2/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/8_2/test_stock.py: -------------------------------------------------------------------------------- 1 | # test_stock.py 2 | 3 | import unittest 4 | import stock 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_cost(self): 14 | s = stock.Stock('GOOG', 100, 490.1) 15 | self.assertEqual(s.cost, 49010.0) 16 | 17 | def test_sell(self): 18 | s = stock.Stock('GOOG', 100, 490.1) 19 | s.sell(25) 20 | self.assertEqual(s.shares, 75) 21 | 22 | def test_shares_check(self): 23 | s = stock.Stock('GOOG', 100, 490.1) 24 | with self.assertRaises(TypeError): 25 | s.shares = '100' 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /Solutions/8_2/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | import report 5 | import tableformat 6 | from follow import follow 7 | 8 | def select_columns(rows, indices): 9 | for row in rows: 10 | yield [row[index] for index in indices] 11 | 12 | def convert_types(rows, types): 13 | for row in rows: 14 | yield [func(val) for func, val in zip(types, row)] 15 | 16 | def make_dicts(rows, headers): 17 | return (dict(zip(headers,row)) for row in rows) 18 | 19 | def parse_stock_data(lines): 20 | rows = csv.reader(lines) 21 | rows = select_columns(rows, [0, 1, 4]) 22 | rows = convert_types(rows, [str,float,float]) 23 | rows = make_dicts(rows, ['name','price','change']) 24 | return rows 25 | 26 | def ticker(portfile, logfile, fmt): 27 | portfolio = report.read_portfolio(portfile) 28 | lines = follow(logfile) 29 | rows = parse_stock_data(lines) 30 | rows = (row for row in rows if row['name'] in portfolio) 31 | formatter = tableformat.create_formatter(fmt) 32 | formatter.headings(['Name','Price','Change']) 33 | for row in rows: 34 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 35 | 36 | def main(args): 37 | if len(args) != 4: 38 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 39 | ticker(args[1], args[2], args[3]) 40 | 41 | if __name__ == '__main__': 42 | import sys 43 | main(sys.argv) 44 | -------------------------------------------------------------------------------- /Solutions/8_2/timethis.py: -------------------------------------------------------------------------------- 1 | # timethis.py 2 | 3 | import time 4 | 5 | def timethis(func): 6 | def wrapper(*args, **kwargs): 7 | start = time.time() 8 | try: 9 | return func(*args,**kwargs) 10 | finally: 11 | end = time.time() 12 | print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) 13 | return wrapper 14 | 15 | if __name__ == '__main__': 16 | @timethis 17 | def countdown(n): 18 | while n > 0: 19 | n-= 1 20 | 21 | countdown(1000000) 22 | -------------------------------------------------------------------------------- /Solutions/8_2/typedproperty.py: -------------------------------------------------------------------------------- 1 | # typedproperty.py 2 | 3 | def typedproperty(name, expected_type): 4 | private_name = '_' + name 5 | @property 6 | def prop(self): 7 | return getattr(self, private_name) 8 | 9 | @prop.setter 10 | def prop(self, value): 11 | if not isinstance(value, expected_type): 12 | raise TypeError(f'Expected {expected_type}') 13 | setattr(self, private_name, value) 14 | 15 | return prop 16 | 17 | String = lambda name: typedproperty(name, str) 18 | Integer = lambda name: typedproperty(name, int) 19 | Float = lambda name: typedproperty(name, float) 20 | 21 | # Example 22 | if __name__ == '__main__': 23 | class Stock: 24 | name = typedproperty('name', str) 25 | shares = typedproperty('shares', int) 26 | price = typedproperty('price', float) 27 | 28 | def __init__(self, name, shares, price): 29 | self.name = name 30 | self.shares = shares 31 | self.price = price 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/README.txt: -------------------------------------------------------------------------------- 1 | Code from Practical Python. 2 | 3 | The "porty" directory is a Python package of code that's loaded via 4 | import. The "print-report.py" program is a top-level script that 5 | produces a report. Try it: 6 | 7 | shell % python3 print-report.py portfolio.csv prices.csv txt 8 | Name Shares Price Change 9 | ---------- ---------- ---------- ---------- 10 | AA 100 9.22 -22.98 11 | IBM 50 106.28 15.18 12 | CAT 150 35.46 -47.98 13 | MSFT 200 20.89 -30.34 14 | GE 95 13.48 -26.89 15 | MSFT 50 20.89 -44.21 16 | IBM 100 106.28 35.84 17 | shell % 18 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/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 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codists/practical-python-zh/b34b5dfb215d76b8bd4845f60803398f67ab8471/Solutions/9_3/porty-app/porty/__init__.py -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | import logging 4 | log = logging.getLogger(__name__) 5 | 6 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 7 | ''' 8 | Parse a CSV file into a list of records with type conversion. 9 | ''' 10 | assert not (select and not has_headers), 'select requires column headers' 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | log.warning("Row %d: Couldn't convert %s", rowno, row) 37 | log.debug("Row %d: Reason %s", rowno, e) 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | from . import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | from . import fileparse 4 | from . import stock 5 | 6 | class Portfolio: 7 | def __init__(self): 8 | self._holdings = [] 9 | 10 | @classmethod 11 | def from_csv(cls, lines, **opts): 12 | self = cls() 13 | portdicts = fileparse.parse_csv(lines, 14 | select=['name','shares','price'], 15 | types=[str,int,float], 16 | **opts) 17 | 18 | for d in portdicts: 19 | self.append(stock.Stock(**d)) 20 | 21 | return self 22 | 23 | def append(self, holding): 24 | self._holdings.append(holding) 25 | 26 | def __iter__(self): 27 | return self._holdings.__iter__() 28 | 29 | def __len__(self): 30 | return len(self._holdings) 31 | 32 | def __getitem__(self, index): 33 | return self._holdings[index] 34 | 35 | def __contains__(self, name): 36 | return any(s.name == name for s in self._holdings) 37 | 38 | @property 39 | def total_cost(self): 40 | return sum(s.shares * s.price for s in self._holdings) 41 | 42 | def tabulate_shares(self): 43 | from collections import Counter 44 | total_shares = Counter() 45 | for s in self._holdings: 46 | total_shares[s.name] += s.shares 47 | return total_shares 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | from . import fileparse 4 | from .stock import Stock 5 | from .portfolio import Portfolio 6 | from . import tableformat 7 | 8 | def read_portfolio(filename, **opts): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | return Portfolio.from_csv(lines, **opts) 15 | 16 | def read_prices(filename, **opts): 17 | ''' 18 | Read a CSV file of price data into a dict mapping names to prices. 19 | ''' 20 | with open(filename) as lines: 21 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) 22 | 23 | def make_report(portfolio, prices): 24 | ''' 25 | Make a list of (name, shares, price, change) tuples given a portfolio list 26 | and prices dictionary. 27 | ''' 28 | rows = [] 29 | for s in portfolio: 30 | current_price = prices[s.name] 31 | change = current_price - s.price 32 | summary = (s.name, s.shares, current_price, change) 33 | rows.append(summary) 34 | return rows 35 | 36 | def print_report(reportdata, formatter): 37 | ''' 38 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 39 | ''' 40 | formatter.headings(['Name','Shares','Price','Change']) 41 | for name, shares, price, change in reportdata: 42 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 43 | formatter.row(rowdata) 44 | 45 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 46 | ''' 47 | Make a stock report given portfolio and price data files. 48 | ''' 49 | # Read data files 50 | portfolio = read_portfolio(portfoliofile) 51 | prices = read_prices(pricefile) 52 | 53 | # Create the report data 54 | report = make_report(portfolio, prices) 55 | 56 | # Print it out 57 | formatter = tableformat.create_formatter(fmt) 58 | print_report(report, formatter) 59 | 60 | def main(args): 61 | if len(args) != 4: 62 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 63 | portfolio_report(args[1], args[2], args[3]) 64 | 65 | if __name__ == '__main__': 66 | import sys 67 | main(sys.argv) 68 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | from .typedproperty import String, Integer, Float 4 | 5 | class Stock: 6 | ''' 7 | An instance of a stock holding consisting of name, shares, and price. 8 | ''' 9 | if __debug__: 10 | name = String('name') 11 | shares = Integer('shares') 12 | price = Float('price') 13 | 14 | def __init__(self,name, shares, price): 15 | self.name = name 16 | self.shares = shares 17 | self.price = price 18 | 19 | def __repr__(self): 20 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 21 | 22 | @property 23 | def cost(self): 24 | ''' 25 | Return the cost as shares*price 26 | ''' 27 | return self.shares * self.price 28 | 29 | def sell(self, nshares): 30 | ''' 31 | Sell a number of shares and return the remaining number. 32 | ''' 33 | self.shares -= nshares 34 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/test_stock.py: -------------------------------------------------------------------------------- 1 | # test_stock.py 2 | 3 | import unittest 4 | from . import stock 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_cost(self): 14 | s = stock.Stock('GOOG', 100, 490.1) 15 | self.assertEqual(s.cost, 49010.0) 16 | 17 | def test_sell(self): 18 | s = stock.Stock('GOOG', 100, 490.1) 19 | s.sell(25) 20 | self.assertEqual(s.shares, 75) 21 | 22 | def test_shares_check(self): 23 | s = stock.Stock('GOOG', 100, 490.1) 24 | with self.assertRaises(TypeError): 25 | s.shares = '100' 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | from . import report 5 | from . import tableformat 6 | from .follow import follow 7 | 8 | def select_columns(rows, indices): 9 | for row in rows: 10 | yield [row[index] for index in indices] 11 | 12 | def convert_types(rows, types): 13 | for row in rows: 14 | yield [func(val) for func, val in zip(types, row)] 15 | 16 | def make_dicts(rows, headers): 17 | return (dict(zip(headers,row)) for row in rows) 18 | 19 | def parse_stock_data(lines): 20 | rows = csv.reader(lines) 21 | rows = select_columns(rows, [0, 1, 4]) 22 | rows = convert_types(rows, [str,float,float]) 23 | rows = make_dicts(rows, ['name','price','change']) 24 | return rows 25 | 26 | def ticker(portfile, logfile, fmt): 27 | portfolio = report.read_portfolio(portfile) 28 | lines = follow(logfile) 29 | rows = parse_stock_data(lines) 30 | rows = (row for row in rows if row['name'] in portfolio) 31 | formatter = tableformat.create_formatter(fmt) 32 | formatter.headings(['Name','Price','Change']) 33 | for row in rows: 34 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 35 | 36 | def main(args): 37 | if len(args) != 4: 38 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 39 | ticker(args[1], args[2], args[3]) 40 | 41 | if __name__ == '__main__': 42 | import sys 43 | main(sys.argv) 44 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/porty/typedproperty.py: -------------------------------------------------------------------------------- 1 | # typedproperty.py 2 | 3 | def typedproperty(name, expected_type): 4 | private_name = '_' + name 5 | @property 6 | def prop(self): 7 | return getattr(self, private_name) 8 | 9 | @prop.setter 10 | def prop(self, value): 11 | if not isinstance(value, expected_type): 12 | raise TypeError(f'Expected {expected_type}') 13 | setattr(self, private_name, value) 14 | 15 | return prop 16 | 17 | String = lambda name: typedproperty(name, str) 18 | Integer = lambda name: typedproperty(name, int) 19 | Float = lambda name: typedproperty(name, float) 20 | 21 | # Example 22 | if __name__ == '__main__': 23 | class Stock: 24 | name = typedproperty('name', str) 25 | shares = typedproperty('shares', int) 26 | price = typedproperty('price', float) 27 | 28 | def __init__(self, name, shares, price): 29 | self.name = name 30 | self.shares = shares 31 | self.price = price 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/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 | 32 | -------------------------------------------------------------------------------- /Solutions/9_3/porty-app/print-report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # print-report.py 3 | 4 | import sys 5 | from porty.report import main 6 | main(sys.argv) 7 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.csv 2 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/README.txt: -------------------------------------------------------------------------------- 1 | Code from Practical Python. 2 | 3 | The "porty" directory is a Python package of code that's loaded via 4 | import. The "print-report.py" program is a top-level script that 5 | produces a report. Try it: 6 | 7 | shell % python3 print-report.py portfolio.csv prices.csv txt 8 | Name Shares Price Change 9 | ---------- ---------- ---------- ---------- 10 | AA 100 9.22 -22.98 11 | IBM 50 106.28 15.18 12 | CAT 150 35.46 -47.98 13 | MSFT 200 20.89 -30.34 14 | GE 95 13.48 -26.89 15 | MSFT 50 20.89 -44.21 16 | IBM 100 106.28 35.84 17 | shell % 18 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/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 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codists/practical-python-zh/b34b5dfb215d76b8bd4845f60803398f67ab8471/Solutions/9_5/porty-app/porty/__init__.py -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | import csv 3 | import logging 4 | log = logging.getLogger(__name__) 5 | 6 | def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): 7 | ''' 8 | Parse a CSV file into a list of records with type conversion. 9 | ''' 10 | assert not (select and not has_headers), 'select requires column headers' 11 | rows = csv.reader(lines, delimiter=delimiter) 12 | 13 | # Read the file headers (if any) 14 | headers = next(rows) if has_headers else [] 15 | 16 | # If specific columns have been selected, make indices for filtering and set output columns 17 | if select: 18 | indices = [ headers.index(colname) for colname in select ] 19 | headers = select 20 | 21 | records = [] 22 | for rowno, row in enumerate(rows, 1): 23 | if not row: # Skip rows with no data 24 | continue 25 | 26 | # If specific column indices are selected, pick them out 27 | if select: 28 | row = [ row[index] for index in indices] 29 | 30 | # Apply type conversion to the row 31 | if types: 32 | try: 33 | row = [func(val) for func, val in zip(types, row)] 34 | except ValueError as e: 35 | if not silence_errors: 36 | log.warning("Row %d: Couldn't convert %s", rowno, row) 37 | log.debug("Row %d: Reason %s", rowno, e) 38 | continue 39 | 40 | # Make a dictionary or a tuple 41 | if headers: 42 | record = dict(zip(headers, row)) 43 | else: 44 | record = tuple(row) 45 | records.append(record) 46 | 47 | return records 48 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/follow.py: -------------------------------------------------------------------------------- 1 | # follow.py 2 | 3 | import time 4 | import os 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 | yield line 16 | else: 17 | time.sleep(0.1) # Sleep briefly to avoid busy wait 18 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | 3 | from . import report 4 | 5 | def portfolio_cost(filename): 6 | ''' 7 | Computes the total cost (shares*price) of a portfolio file 8 | ''' 9 | portfolio = report.read_portfolio(filename) 10 | return portfolio.total_cost 11 | 12 | def main(args): 13 | if len(args) != 2: 14 | raise SystemExit('Usage: %s portfoliofile' % args[0]) 15 | filename = args[1] 16 | print('Total cost:', portfolio_cost(filename)) 17 | 18 | if __name__ == '__main__': 19 | import sys 20 | main(sys.argv) 21 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/portfolio.py: -------------------------------------------------------------------------------- 1 | # portfolio.py 2 | 3 | from . import fileparse 4 | from . import stock 5 | 6 | class Portfolio: 7 | def __init__(self): 8 | self._holdings = [] 9 | 10 | @classmethod 11 | def from_csv(cls, lines, **opts): 12 | self = cls() 13 | portdicts = fileparse.parse_csv(lines, 14 | select=['name','shares','price'], 15 | types=[str,int,float], 16 | **opts) 17 | 18 | for d in portdicts: 19 | self.append(stock.Stock(**d)) 20 | 21 | return self 22 | 23 | def append(self, holding): 24 | self._holdings.append(holding) 25 | 26 | def __iter__(self): 27 | return self._holdings.__iter__() 28 | 29 | def __len__(self): 30 | return len(self._holdings) 31 | 32 | def __getitem__(self, index): 33 | return self._holdings[index] 34 | 35 | def __contains__(self, name): 36 | return any(s.name == name for s in self._holdings) 37 | 38 | @property 39 | def total_cost(self): 40 | return sum(s.shares * s.price for s in self._holdings) 41 | 42 | def tabulate_shares(self): 43 | from collections import Counter 44 | total_shares = Counter() 45 | for s in self._holdings: 46 | total_shares[s.name] += s.shares 47 | return total_shares 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | 3 | from . import fileparse 4 | from .stock import Stock 5 | from .portfolio import Portfolio 6 | from . import tableformat 7 | 8 | def read_portfolio(filename, **opts): 9 | ''' 10 | Read a stock portfolio file into a list of dictionaries with keys 11 | name, shares, and price. 12 | ''' 13 | with open(filename) as lines: 14 | return Portfolio.from_csv(lines, **opts) 15 | 16 | def read_prices(filename, **opts): 17 | ''' 18 | Read a CSV file of price data into a dict mapping names to prices. 19 | ''' 20 | with open(filename) as lines: 21 | return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) 22 | 23 | def make_report(portfolio, prices): 24 | ''' 25 | Make a list of (name, shares, price, change) tuples given a portfolio list 26 | and prices dictionary. 27 | ''' 28 | rows = [] 29 | for s in portfolio: 30 | current_price = prices[s.name] 31 | change = current_price - s.price 32 | summary = (s.name, s.shares, current_price, change) 33 | rows.append(summary) 34 | return rows 35 | 36 | def print_report(reportdata, formatter): 37 | ''' 38 | Print a nicely formated table from a list of (name, shares, price, change) tuples. 39 | ''' 40 | formatter.headings(['Name','Shares','Price','Change']) 41 | for name, shares, price, change in reportdata: 42 | rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] 43 | formatter.row(rowdata) 44 | 45 | def portfolio_report(portfoliofile, pricefile, fmt='txt'): 46 | ''' 47 | Make a stock report given portfolio and price data files. 48 | ''' 49 | # Read data files 50 | portfolio = read_portfolio(portfoliofile) 51 | prices = read_prices(pricefile) 52 | 53 | # Create the report data 54 | report = make_report(portfolio, prices) 55 | 56 | # Print it out 57 | formatter = tableformat.create_formatter(fmt) 58 | print_report(report, formatter) 59 | 60 | def main(args): 61 | if len(args) != 4: 62 | raise SystemExit('Usage: %s portfile pricefile format' % args[0]) 63 | portfolio_report(args[1], args[2], args[3]) 64 | 65 | if __name__ == '__main__': 66 | import sys 67 | main(sys.argv) 68 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/stock.py: -------------------------------------------------------------------------------- 1 | # stock.py 2 | 3 | from .typedproperty import String, Integer, Float 4 | 5 | class Stock: 6 | ''' 7 | An instance of a stock holding consisting of name, shares, and price. 8 | ''' 9 | if __debug__: 10 | name = String('name') 11 | shares = Integer('shares') 12 | price = Float('price') 13 | 14 | def __init__(self,name, shares, price): 15 | self.name = name 16 | self.shares = shares 17 | self.price = price 18 | 19 | def __repr__(self): 20 | return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' 21 | 22 | @property 23 | def cost(self): 24 | ''' 25 | Return the cost as shares*price 26 | ''' 27 | return self.shares * self.price 28 | 29 | def sell(self, nshares): 30 | ''' 31 | Sell a number of shares and return the remaining number. 32 | ''' 33 | self.shares -= nshares 34 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/tableformat.py: -------------------------------------------------------------------------------- 1 | # tableformat.py 2 | 3 | class TableFormatter: 4 | def headings(self, headers): 5 | ''' 6 | Emit the table headers 7 | ''' 8 | raise NotImplementedError() 9 | 10 | def row(self, rowdata): 11 | ''' 12 | Emit a single row of table data 13 | ''' 14 | raise NotImplementedError() 15 | 16 | class TextTableFormatter(TableFormatter): 17 | ''' 18 | Output data in plain-text format. 19 | ''' 20 | def headings(self, headers): 21 | for h in headers: 22 | print(f'{h:>10s}', end=' ') 23 | print() 24 | print(('-'*10 + ' ')*len(headers)) 25 | 26 | def row(self, rowdata): 27 | for d in rowdata: 28 | print(f'{d:>10s}', end=' ') 29 | print() 30 | 31 | class CSVTableFormatter(TableFormatter): 32 | ''' 33 | Output data in CSV format. 34 | ''' 35 | def headings(self, headers): 36 | print(','.join(headers)) 37 | 38 | def row(self, rowdata): 39 | print(','.join(rowdata)) 40 | 41 | class HTMLTableFormatter(TableFormatter): 42 | ''' 43 | Output data in HTML format. 44 | ''' 45 | def headings(self, headers): 46 | print('', end='') 47 | for h in headers: 48 | print(f'{h}', end='') 49 | print('') 50 | 51 | def row(self, rowdata): 52 | print('', end='') 53 | for d in rowdata: 54 | print(f'{d}', end='') 55 | print('') 56 | 57 | class FormatError(Exception): 58 | pass 59 | 60 | def create_formatter(name): 61 | ''' 62 | Create an appropriate formatter given an output format name 63 | ''' 64 | if name == 'txt': 65 | return TextTableFormatter() 66 | elif name == 'csv': 67 | return CSVTableFormatter() 68 | elif name == 'html': 69 | return HTMLTableFormatter() 70 | else: 71 | raise FormatError(f'Unknown table format {name}') 72 | 73 | def print_table(objects, columns, formatter): 74 | ''' 75 | Make a nicely formatted table from a list of objects and attribute names. 76 | ''' 77 | formatter.headings(columns) 78 | for obj in objects: 79 | rowdata = [ str(getattr(obj, name)) for name in columns ] 80 | formatter.row(rowdata) 81 | 82 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/test_stock.py: -------------------------------------------------------------------------------- 1 | # test_stock.py 2 | 3 | import unittest 4 | from . import stock 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_cost(self): 14 | s = stock.Stock('GOOG', 100, 490.1) 15 | self.assertEqual(s.cost, 49010.0) 16 | 17 | def test_sell(self): 18 | s = stock.Stock('GOOG', 100, 490.1) 19 | s.sell(25) 20 | self.assertEqual(s.shares, 75) 21 | 22 | def test_shares_check(self): 23 | s = stock.Stock('GOOG', 100, 490.1) 24 | with self.assertRaises(TypeError): 25 | s.shares = '100' 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/ticker.py: -------------------------------------------------------------------------------- 1 | # ticker.py 2 | 3 | import csv 4 | from . import report 5 | from . import tableformat 6 | from .follow import follow 7 | 8 | def select_columns(rows, indices): 9 | for row in rows: 10 | yield [row[index] for index in indices] 11 | 12 | def convert_types(rows, types): 13 | for row in rows: 14 | yield [func(val) for func, val in zip(types, row)] 15 | 16 | def make_dicts(rows, headers): 17 | return (dict(zip(headers,row)) for row in rows) 18 | 19 | def parse_stock_data(lines): 20 | rows = csv.reader(lines) 21 | rows = select_columns(rows, [0, 1, 4]) 22 | rows = convert_types(rows, [str,float,float]) 23 | rows = make_dicts(rows, ['name','price','change']) 24 | return rows 25 | 26 | def ticker(portfile, logfile, fmt): 27 | portfolio = report.read_portfolio(portfile) 28 | lines = follow(logfile) 29 | rows = parse_stock_data(lines) 30 | rows = (row for row in rows if row['name'] in portfolio) 31 | formatter = tableformat.create_formatter(fmt) 32 | formatter.headings(['Name','Price','Change']) 33 | for row in rows: 34 | formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) 35 | 36 | def main(args): 37 | if len(args) != 4: 38 | raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) 39 | ticker(args[1], args[2], args[3]) 40 | 41 | if __name__ == '__main__': 42 | import sys 43 | main(sys.argv) 44 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/porty/typedproperty.py: -------------------------------------------------------------------------------- 1 | # typedproperty.py 2 | 3 | def typedproperty(name, expected_type): 4 | private_name = '_' + name 5 | @property 6 | def prop(self): 7 | return getattr(self, private_name) 8 | 9 | @prop.setter 10 | def prop(self, value): 11 | if not isinstance(value, expected_type): 12 | raise TypeError(f'Expected {expected_type}') 13 | setattr(self, private_name, value) 14 | 15 | return prop 16 | 17 | String = lambda name: typedproperty(name, str) 18 | Integer = lambda name: typedproperty(name, int) 19 | Float = lambda name: typedproperty(name, float) 20 | 21 | # Example 22 | if __name__ == '__main__': 23 | class Stock: 24 | name = typedproperty('name', str) 25 | shares = typedproperty('shares', int) 26 | price = typedproperty('price', float) 27 | 28 | def __init__(self, name, shares, price): 29 | self.name = name 30 | self.shares = shares 31 | self.price = price 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/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 | 32 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/print-report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # print-report.py 3 | 4 | import sys 5 | from porty.report import main 6 | main(sys.argv) 7 | -------------------------------------------------------------------------------- /Solutions/9_5/porty-app/setup.py: -------------------------------------------------------------------------------- 1 | # setup.py 2 | 3 | import setuptools 4 | 5 | setuptools.setup( 6 | name="porty", 7 | version="0.0.1", 8 | author="Your Name", 9 | author_email="you@example.com", 10 | description="Practical Python Code", 11 | packages=setuptools.find_packages(), 12 | scripts=['print-report.py'], 13 | ) 14 | -------------------------------------------------------------------------------- /Solutions/README.md: -------------------------------------------------------------------------------- 1 | # Solutions 2 | 3 | This directory contains solutions to selected exercises. The code is 4 | written to run within this directory and has file paths set 5 | accordingly. If you copy any of the code to the `Work/` directory, 6 | you might need to adjust filenames. 7 | 8 | -------------------------------------------------------------------------------- /Work/Data/missing.csv: -------------------------------------------------------------------------------- 1 | name,shares,price 2 | "AA",100,32.20 3 | "IBM",50,91.10 4 | "CAT",150,83.44 5 | "MSFT",,51.23 6 | "GE",95,40.37 7 | "MSFT",50,65.10 8 | "IBM",,70.44 9 | -------------------------------------------------------------------------------- /Work/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 | -------------------------------------------------------------------------------- /Work/Data/portfolio.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codists/practical-python-zh/b34b5dfb215d76b8bd4845f60803398f67ab8471/Work/Data/portfolio.csv.gz -------------------------------------------------------------------------------- /Work/Data/portfolio.dat: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Work/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 | -------------------------------------------------------------------------------- /Work/Data/portfolioblank.csv: -------------------------------------------------------------------------------- 1 | name,shares,price 2 | 3 | "AA",100,32.20 4 | 5 | "IBM",50,91.10 6 | 7 | "CAT",150,83.44 8 | 9 | "MSFT",200,51.23 10 | 11 | "GE",95,40.37 12 | 13 | "MSFT",50,65.10 14 | 15 | "IBM",100,70.44 16 | 17 | -------------------------------------------------------------------------------- /Work/Data/portfoliodate.csv: -------------------------------------------------------------------------------- 1 | name,date,time,shares,price 2 | "AA","6/11/2007","9:50am",100,32.20 3 | "IBM","5/13/2007","4:20pm",50,91.10 4 | "CAT","9/23/2006","1:30pm",150,83.44 5 | "MSFT","5/17/2007","10:30am",200,51.23 6 | "GE","2/1/2006","10:45am",95,40.37 7 | "MSFT","10/31/2006","12:05pm",50,65.10 8 | "IBM","7/9/2006","3:15pm",100,70.44 9 | -------------------------------------------------------------------------------- /Work/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 | 32 | -------------------------------------------------------------------------------- /Work/README.md: -------------------------------------------------------------------------------- 1 | # Work Area 2 | 3 | Do all of your coding work here, in the `Work/` directory. A number of starting 4 | files have been given (`bounce.py`, `mortgage.py`, `pcost.py`, etc.) along with 5 | their corresponding exercise number. 6 | 7 | Many of the programs you write reference files found in the `Data/` directory. 8 | That is also located here. 9 | -------------------------------------------------------------------------------- /Work/bounce.py: -------------------------------------------------------------------------------- 1 | # bounce.py 2 | # 3 | # Exercise 1.5 4 | -------------------------------------------------------------------------------- /Work/fileparse.py: -------------------------------------------------------------------------------- 1 | # fileparse.py 2 | # 3 | # Exercise 3.3 4 | -------------------------------------------------------------------------------- /Work/mortgage.py: -------------------------------------------------------------------------------- 1 | # mortgage.py 2 | # 3 | # Exercise 1.7 4 | -------------------------------------------------------------------------------- /Work/pcost.py: -------------------------------------------------------------------------------- 1 | # pcost.py 2 | # 3 | # Exercise 1.27 4 | -------------------------------------------------------------------------------- /Work/report.py: -------------------------------------------------------------------------------- 1 | # report.py 2 | # 3 | # Exercise 2.4 4 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if site.google_analytics %} 6 | 7 | 13 | {% endif %} 14 | 15 | 16 | {% seo %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 37 | 38 |
39 | {{ content }} 40 | 41 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /翻译说明.md: -------------------------------------------------------------------------------- 1 | 翻译说明 2 | --- 3 | 4 | # 原项目 5 | 6 | David Beazley : [practical-python](https://github.com/dabeaz-course/practical-python) 7 | 8 | # 联系方式 9 | 10 | 如果想对翻译的内容进行反馈,或者对技术问题进行探讨,欢迎联系: 11 | 12 | 1、QQ:2130658383 13 | 14 | 2、微信:codists 15 | 16 | 3、邮箱:2130658383@qq.com 17 | 18 | # 致谢 19 | 20 | 在本项目的翻译过程中,以下人员也做出了巨大的贡献,在此一一列出,以示感谢。 21 | 22 | 1、[hsfzxjy](https://github.com/hsfzxjy) 23 | 24 | 2、[whinc](https://github.com/whinc) 25 | 26 | --------------------------------------------------------------------------------