├── .gitignore ├── 01-attribute-basics.ipynb ├── 01-code └── 01-attribute-basics.py ├── 02-code └── 02-mutable-caveats.py ├── 02-mutable-caveats.ipynb ├── 03-classes.ipynb ├── 03-code └── 03-classes.py ├── 04-code └── 04-data-classes.py ├── 04-data-classes.ipynb ├── 05-code └── 05-inheritance.py ├── 05-inheritance.ipynb ├── 06-big-ideas.ipynb ├── LICENSE ├── README.md ├── demos.ipynb ├── examples ├── 8queens │ ├── README.md │ ├── drive_random_queens.py │ ├── queens.py │ ├── queens_and_guard.py │ ├── random_queens_and_guard.py │ ├── references │ │ ├── Chapter 6, Slide 1.htm │ │ ├── Chapter 6, Slide 1_files │ │ │ ├── slide01.gif │ │ │ ├── slide04.gif │ │ │ ├── slide05.gif │ │ │ └── slide06.gif │ │ ├── QSolve.java │ │ ├── QueenSolver.java │ │ ├── queen.cpp │ │ └── queen.pas │ ├── test_queens.py │ ├── test_queens_and_guard.py │ └── test_random_queens_and_guard.py ├── bingo │ ├── README.rst │ └── bingo.py ├── bitset │ ├── bitops.py │ ├── solution │ │ ├── bitops.py │ │ ├── empty.py │ │ ├── natural.py │ │ ├── test_bitops.py │ │ ├── test_empty.py │ │ ├── test_natural.py │ │ ├── test_uintset.py │ │ └── uintset.py │ └── test_bitops.py ├── camping │ ├── data_class │ │ ├── README.rst │ │ └── camping.py │ ├── one_class │ │ ├── README.rst │ │ └── camping.py │ └── two_classes │ │ ├── README.rst │ │ └── camping.py ├── coordinate.py ├── dc │ └── resource.py ├── finstory │ ├── README.rst │ ├── deducstory.py │ ├── deducstory.rst │ ├── deducstory2.py │ └── finstory.py └── tombola │ ├── README.rst │ ├── bingo.py │ ├── drum.py │ ├── lotto.py │ ├── tombola.py │ ├── tombola_runner.py │ ├── tombola_subhook.py │ ├── tombola_tests.rst │ └── tombolist.py ├── experiments ├── 01-objects.ipynb ├── 02-classes.ipynb ├── 03-subclassing.ipynb └── notebook-hacks.ipynb ├── geohash.py ├── img ├── array-of-floats.png ├── camping-UML.graffle ├── camping-UML.png ├── concepts-bicycleObject.gif ├── concepts-object.gif ├── dog_st_bernard.jpg ├── finhist-browser.png ├── finstory-UML.graffle ├── finstory-UML.png ├── finstory-spec.png ├── intro-oop-budd.jpg ├── list-of-floats.png ├── pyob-mindmap.png ├── safety-switch.jpg ├── thoughtworks.png └── title-card.png ├── labs ├── 1 │ ├── README.rst │ ├── coordinate.py │ ├── geohash.py │ └── solution │ │ └── coordinate.py └── 2 │ ├── README.rst │ └── camping.py ├── references.md └── slides ├── pythonic-objects.key └── pythonic-objects.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /01-code/01-attribute-basics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # 5 | 6 | # # Attribute basics 7 | # 8 | # ## Smalltalk class declaration 9 | # 10 | # 11 | # 12 | # **Figure 17.17 from Smalltalk-80, the language** 13 | #
Class browser showing definition of a ``FinancialHistory`` class with three instance variables: ``cashOnHand``, ``incomes``, and ``expenditures``. 14 | # 15 | # ## Official Java tutorial 16 | # 17 | # The next two figures are from the **Java Tutorial (Sun/Oracle)**, section [What is an object?](https://docs.oracle.com/javase/tutorial/java/concepts/object.html). 18 | # 19 | # An object is depicted as fields surrounded by methods: 20 | # 21 | # 22 | # 23 | # Quoting from [What is an object?](https://docs.oracle.com/javase/tutorial/java/concepts/object.html): 24 | # 25 | # > Methods operate on an object's internal state and serve as the primary mechanism for object-to-object communication. Hiding internal state and requiring all interaction to be performed through an object's methods is known as *data encapsulation* — a fundamental principle of object-oriented programming. 26 | # 27 | # An object representing a bicyle has methods such as *Change gear* and *Brake*, and fields such as *speed* and *cadence*: 28 | # 29 | # 30 | # 31 | # Code from section [What is a class?](https://docs.oracle.com/javase/tutorial/java/concepts/class.html) from the **Java Tutorial**. 32 | # 33 | # ```java 34 | # class Bicycle { 35 | # 36 | # int cadence = 0; 37 | # int speed = 0; 38 | # int gear = 1; 39 | # 40 | # void changeCadence(int newValue) { 41 | # cadence = newValue; 42 | # } 43 | # 44 | # //... 45 | # } 46 | # ``` 47 | 48 | # ## What about Python? 49 | # 50 | # ### Python terms 51 | # 52 | # From the **Python tutorial**, section [9.3.3. Instance Objects](https://docs.python.org/3.7/tutorial/classes.html#instance-objects) 53 | # 54 | # > There are two kinds of valid attribute names, data attributes and methods. 55 | # > 56 | # > *Data attributes* correspond to “instance variables” in Smalltalk, and to “data members” in C++. 57 | # 58 | # In Python, the generic term *attribute* refers to both *fields* and *methods* in Java: 59 | # 60 | # Python term |Java concept 61 | # :---------- |:----------- 62 | # attribute | fields and methods 63 | # data attribute | field 64 | # method | method 65 | 66 | # ## Hands on 67 | # 68 | # Check the version of Python we are using: 69 | 70 | # In[1]: 71 | 72 | 73 | import sys 74 | print(sys.version) 75 | 76 | 77 | # ## A simplistic class 78 | 79 | # In[2]: 80 | 81 | 82 | class Coordinate: 83 | '''Coordinate on Earth''' 84 | 85 | 86 | # In[3]: 87 | 88 | 89 | cle = Coordinate() 90 | cle.lat = 41.4 91 | cle.long = -81.8 92 | cle 93 | 94 | 95 | # In[4]: 96 | 97 | 98 | cle.lat 99 | 100 | 101 | # ### First method: ``__repr__`` 102 | 103 | # In[5]: 104 | 105 | 106 | class Coordinate: 107 | '''Coordinate on Earth''' 108 | 109 | def __repr__(self): 110 | return f'Coordinate({self.lat}, {self.long})' 111 | 112 | 113 | # In[6]: 114 | 115 | 116 | cle = Coordinate() 117 | cle.lat = 41.4 118 | cle.long = -81.8 119 | cle 120 | 121 | 122 | # In[7]: 123 | 124 | 125 | cle.__repr__() 126 | 127 | 128 | # In[8]: 129 | 130 | 131 | repr(cle) 132 | 133 | 134 | # ### About ``__repr__`` 135 | # 136 | # * Good for exploratory programming, documentation, doctests, and debugging. 137 | # * Best practice: if viable, make ``__repr__`` return string with syntax required to create a new instance like the one inspected (i.e. ``eval(repr(x)) == x``) 138 | # * If not viable, use ```` with some ``...`` that identifies the particular instance. 139 | # 140 | # 141 | # ### ``__repr__`` v. ``__str__`` 142 | # 143 | # * ``__repr__`` is for programming displays. 144 | # * ``__str__`` is for end-user displays. 145 | # 146 | # ### ``__str__`` example 147 | 148 | # In[9]: 149 | 150 | 151 | class Coordinate: 152 | '''Coordinate on Earth''' 153 | 154 | def __repr__(self): 155 | return f'Coordinate({self.lat}, {self.long})' 156 | 157 | def __str__(self): 158 | ns = 'NS'[self.lat < 0] 159 | we = 'EW'[self.long < 0] 160 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}' 161 | 162 | 163 | # In[10]: 164 | 165 | 166 | cle = Coordinate() 167 | cle.lat = 41.4 168 | cle.long = -81.8 169 | print(cle) 170 | 171 | 172 | # ### But... 173 | 174 | # In[11]: 175 | 176 | 177 | gulf_of_guinea = Coordinate() 178 | try: 179 | print(gulf_of_guinea) 180 | except AttributeError as e: 181 | print(e) 182 | 183 | 184 | # > **Quick fix**: add class attributes to provide defaults. 185 | 186 | # ## Class attributes as defaults 187 | 188 | # In[12]: 189 | 190 | 191 | class Pizza: 192 | 193 | diameter = 40 # cm 194 | slices = 8 195 | 196 | flavor = 'Cheese' 197 | flavor2 = None 198 | 199 | 200 | # In[13]: 201 | 202 | 203 | p = Pizza() 204 | p.slices 205 | 206 | 207 | # In[14]: 208 | 209 | 210 | p.flavor 211 | 212 | 213 | # In[15]: 214 | 215 | 216 | p.__dict__ 217 | 218 | 219 | # In[16]: 220 | 221 | 222 | p.flavor = 'Sausage' 223 | p.__dict__ 224 | 225 | 226 | # In[17]: 227 | 228 | 229 | p2 = Pizza() 230 | p2.flavor 231 | 232 | 233 | # In[18]: 234 | 235 | 236 | Pizza.__dict__ 237 | 238 | 239 | # ## A better pizza 240 | 241 | # In[19]: 242 | 243 | 244 | class Pizza: 245 | 246 | diameter = 40 # cm 247 | slices = 8 248 | 249 | def __init__(self, flavor='Cheese', flavor2=None): 250 | self.flavor = flavor 251 | self.flavor2 = flavor2 252 | 253 | 254 | # Good practices shown here: 255 | # 256 | # * use of *class attributes* for attributes shared by all instances; 257 | # * attributes that are expected to vary among instances are *instance attributes*; 258 | # * instance attributes are *all* assigned in ``__init__``; 259 | # * default values for instance attributes are ``__init__`` argument defaults. 260 | # 261 | # [PEP 412 — Key-Sharing Dictionary](https://www.python.org/dev/peps/pep-0412/) introduced an optimization that saves memory when instances of a class have the same instance attribute names set on ``__init__``. 262 | 263 | # ## Lab #1: enhancing ``Coordinate`` 264 | # 265 | # 266 | # Follow instructions at [labs/1/README.rst](https://github.com/fluentpython/pyob2019/blob/master/labs/1/README.rst). 267 | 268 | # In[20]: 269 | 270 | 271 | import geohash 272 | 273 | class Coordinate: 274 | '''Coordinate on Earth''' 275 | 276 | reference_system = 'WGS84' 277 | 278 | def __init__(self, lat, long): 279 | self.lat = lat 280 | self.long = long 281 | 282 | def __repr__(self): 283 | return f'Coordinate({self.lat}, {self.long})' 284 | 285 | def __str__(self): 286 | ns = 'NS'[self.lat < 0] 287 | we = 'WE'[self.long < 0] 288 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}' 289 | 290 | def geohash(self): 291 | return geohash.encode(self.lat, self.long) 292 | 293 | 294 | 295 | # In[21]: 296 | 297 | 298 | cle = Coordinate(41.5, -81.7) 299 | cle.geohash() 300 | 301 | 302 | # In[22]: 303 | 304 | 305 | from dataclasses import InitVar 306 | from typing import ClassVar 307 | 308 | 309 | # In[23]: 310 | 311 | 312 | default_flavor = 'Cheese' 313 | 314 | class Pizza: 315 | 316 | def __init__(self, flavor1=default_flavor, flavor2=None): 317 | self.flavor1 = flavor1 318 | self.flavor2 = flavor2 319 | 320 | 321 | # ### Python < 3.6 322 | # 323 | # * No way to declare instance variables without assigning. 324 | # * No way to declare variables at all (except function arguments). 325 | # * First assignment is the "declaration". 326 | # * Attributes defined in a class body are *class attributes*. 327 | # 328 | # 329 | # ### Descriptors 330 | # 331 | # Descriptors are defined in a class body, so they are *class attributes*. 332 | # 333 | # #### Descriptor examples 334 | # 335 | # From [Django Models](https://docs.djangoproject.com/en/2.2/topics/db/models/): 336 | # 337 | # ```python 338 | # from django.db import models 339 | # 340 | # class Musician(models.Model): 341 | # first_name = models.CharField(max_length=50) 342 | # last_name = models.CharField(max_length=50) 343 | # instrument = models.CharField(max_length=100) 344 | # 345 | # class Album(models.Model): 346 | # artist = models.ForeignKey(Musician, on_delete=models.CASCADE) 347 | # name = models.CharField(max_length=100) 348 | # release_date = models.DateField() 349 | # num_stars = models.IntegerField() 350 | # ``` 351 | # 352 | # ORMs use *descriptors* to declare fields (eg. Django, SQLAlchemy) that manage the persistency of the data attributes of instances that are database records. 353 | # 354 | # Such data-oriented descriptors are not part of the Python Standard Library—they are provided by external framweorks. 355 | 356 | # 357 | # 358 | -------------------------------------------------------------------------------- /02-code/02-mutable-caveats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # # Caveats with mutable attributes and arguments 5 | 6 | # ## HauntedBus 7 | # 8 | # A simple class to illustrate the danger of a mutable class attribute used as a default value for an instance attribute. Based on _Example 8-12_ of [Fluent Python, 1e](https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008). 9 | 10 | # In[1]: 11 | 12 | 13 | class HauntedBus: 14 | """A bus haunted by ghost passengers""" 15 | 16 | passengers = [] # 🐛 17 | 18 | def pick(self, name): 19 | self.passengers.append(name) 20 | 21 | def drop(self, name): 22 | self.passengers.remove(name) 23 | 24 | 25 | # In[2]: 26 | 27 | 28 | bus1 = HauntedBus() 29 | bus1.passengers 30 | 31 | 32 | # In[3]: 33 | 34 | 35 | bus1.pick('Ann') 36 | bus1.pick('Bob') 37 | bus1.passengers 38 | 39 | 40 | # In[4]: 41 | 42 | 43 | bus2 = HauntedBus() 44 | bus2.passengers 45 | 46 | 47 | # Ghost passengers! 48 | # 49 | # The `.pick` and `.drop` methods were changing the `HauntedBus.passengers` class attribute. 50 | 51 | # ## HauntedBus_v2 52 | 53 | # In[5]: 54 | 55 | 56 | class HauntedBus_v2: 57 | """Another bus haunted by ghost passengers""" 58 | 59 | def __init__(self, passengers=[]): # 🐛 60 | self.passengers = passengers 61 | 62 | def pick(self, name): 63 | self.passengers.append(name) 64 | 65 | def drop(self, name): 66 | self.passengers.remove(name) 67 | 68 | 69 | # In[6]: 70 | 71 | 72 | bus3 = HauntedBus_v2() 73 | bus3.passengers 74 | 75 | 76 | # In[7]: 77 | 78 | 79 | bus3.pick('Charlie') 80 | bus3.pick('Debbie') 81 | bus3.passengers 82 | 83 | 84 | # In[8]: 85 | 86 | 87 | bus4 = HauntedBus_v2() 88 | bus4.passengers 89 | 90 | 91 | # Ghost passengers!! 92 | # 93 | # The `.pick` and `.drop` methods were changing the default value for the passengers argument in the `__init__` method. 94 | # 95 | # The argument defaults are also class attributes (indirectly, because `__init__` is a class attribute). 96 | # 97 | # Check it out: 98 | 99 | # In[9]: 100 | 101 | 102 | HauntedBus_v2.__init__.__defaults__ 103 | 104 | 105 | # ## TwilightBus 106 | 107 | # In[10]: 108 | 109 | 110 | class TwilightBus: 111 | """A bus model that makes passengers vanish""" 112 | 113 | def __init__(self, passengers=None): 114 | if passengers is None: 115 | self.passengers = [] 116 | else: 117 | self.passengers = list(passengers) 118 | 119 | def pick(self, name): 120 | self.passengers.append(name) 121 | 122 | def drop(self, name): 123 | self.passengers.remove(name) 124 | 125 | 126 | # In[11]: 127 | 128 | 129 | hockey_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat', 'Alice'] 130 | bus5 = TwilightBus(hockey_team) 131 | bus5.passengers 132 | 133 | 134 | # In[12]: 135 | 136 | 137 | bus5.drop('Sue') 138 | bus5.drop('Pat') 139 | bus5.passengers 140 | 141 | 142 | # In[13]: 143 | 144 | 145 | hockey_team 146 | 147 | 148 | # The assignment on line 8, `self.passengers = passengers`, creates an _alias_ to the `hockey_team` list. 149 | # 150 | # Therefore, the `.drop` method removes names from the `hockey_team` list. 151 | 152 | # ## Bus 153 | 154 | # In[14]: 155 | 156 | 157 | class Bus: 158 | """The bus we wanted all along""" 159 | 160 | def __init__(self, passengers=None): 161 | self.passengers = list(passengers) if passengers else [] 162 | 163 | def pick(self, name): 164 | self.passengers.append(name) 165 | 166 | def drop(self, name): 167 | self.passengers.remove(name) 168 | 169 | 170 | # In[15]: 171 | 172 | 173 | hockey_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat', 'Alice'] 174 | bus6 = Bus(hockey_team) 175 | bus6.passengers 176 | 177 | 178 | # In[16]: 179 | 180 | 181 | bus6.drop('Sue') 182 | bus6.drop('Pat') 183 | bus6.passengers 184 | 185 | 186 | # In[17]: 187 | 188 | 189 | hockey_team 190 | 191 | 192 | # On line 5, the expression `list(passengers)` builds a new list from the `passengers` argument. 193 | # 194 | # If `passengers` is a list, `list(passengers)` makes a shallow copy of it. 195 | # 196 | # If `passengers` is an other iterable object (`tuple`, `set`, generator, etc...), then `list(passengers)` builds a new list from it. 197 | # 198 | # > Be conservative in what you send, be liberal in what you accept — _Postel's Law_ 199 | 200 | # 201 | -------------------------------------------------------------------------------- /02-mutable-caveats.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Caveats with mutable attributes and arguments" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## HauntedBus\n", 15 | "\n", 16 | "A simple class to illustrate the danger of a mutable class attribute used as a default value for an instance attribute. Based on _Example 8-12_ of [Fluent Python, 1e](https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008)." 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "class HauntedBus:\n", 26 | " \"\"\"A bus haunted by ghost passengers\"\"\"\n", 27 | " \n", 28 | " passengers = [] # 🐛\n", 29 | " \n", 30 | " def pick(self, name):\n", 31 | " self.passengers.append(name)\n", 32 | " \n", 33 | " def drop(self, name):\n", 34 | " self.passengers.remove(name)" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 2, 40 | "metadata": {}, 41 | "outputs": [ 42 | { 43 | "data": { 44 | "text/plain": [ 45 | "[]" 46 | ] 47 | }, 48 | "execution_count": 2, 49 | "metadata": {}, 50 | "output_type": "execute_result" 51 | } 52 | ], 53 | "source": [ 54 | "bus1 = HauntedBus()\n", 55 | "bus1.passengers" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 3, 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "data": { 65 | "text/plain": [ 66 | "['Ann', 'Bob']" 67 | ] 68 | }, 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "output_type": "execute_result" 72 | } 73 | ], 74 | "source": [ 75 | "bus1.pick('Ann')\n", 76 | "bus1.pick('Bob')\n", 77 | "bus1.passengers" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 4, 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "data": { 87 | "text/plain": [ 88 | "['Ann', 'Bob']" 89 | ] 90 | }, 91 | "execution_count": 4, 92 | "metadata": {}, 93 | "output_type": "execute_result" 94 | } 95 | ], 96 | "source": [ 97 | "bus2 = HauntedBus()\n", 98 | "bus2.passengers" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "Ghost passengers!\n", 106 | "\n", 107 | "The `.pick` and `.drop` methods were changing the `HauntedBus.passengers` class attribute." 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "## HauntedBus_v2" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 5, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "class HauntedBus_v2:\n", 124 | " \"\"\"Another bus haunted by ghost passengers\"\"\"\n", 125 | " \n", 126 | " def __init__(self, passengers=[]): # 🐛\n", 127 | " self.passengers = passengers\n", 128 | " \n", 129 | " def pick(self, name):\n", 130 | " self.passengers.append(name)\n", 131 | " \n", 132 | " def drop(self, name):\n", 133 | " self.passengers.remove(name)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 6, 139 | "metadata": {}, 140 | "outputs": [ 141 | { 142 | "data": { 143 | "text/plain": [ 144 | "[]" 145 | ] 146 | }, 147 | "execution_count": 6, 148 | "metadata": {}, 149 | "output_type": "execute_result" 150 | } 151 | ], 152 | "source": [ 153 | "bus3 = HauntedBus_v2()\n", 154 | "bus3.passengers" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 7, 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "data": { 164 | "text/plain": [ 165 | "['Charlie', 'Debbie']" 166 | ] 167 | }, 168 | "execution_count": 7, 169 | "metadata": {}, 170 | "output_type": "execute_result" 171 | } 172 | ], 173 | "source": [ 174 | "bus3.pick('Charlie')\n", 175 | "bus3.pick('Debbie')\n", 176 | "bus3.passengers" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 8, 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "data": { 186 | "text/plain": [ 187 | "['Charlie', 'Debbie']" 188 | ] 189 | }, 190 | "execution_count": 8, 191 | "metadata": {}, 192 | "output_type": "execute_result" 193 | } 194 | ], 195 | "source": [ 196 | "bus4 = HauntedBus_v2()\n", 197 | "bus4.passengers" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "Ghost passengers!!\n", 205 | "\n", 206 | "The `.pick` and `.drop` methods were changing the default value for the passengers argument in the `__init__` method.\n", 207 | "\n", 208 | "The argument defaults are also class attributes (indirectly, because `__init__` is a class attribute).\n", 209 | "\n", 210 | "Check it out:" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": 9, 216 | "metadata": {}, 217 | "outputs": [ 218 | { 219 | "data": { 220 | "text/plain": [ 221 | "(['Charlie', 'Debbie'],)" 222 | ] 223 | }, 224 | "execution_count": 9, 225 | "metadata": {}, 226 | "output_type": "execute_result" 227 | } 228 | ], 229 | "source": [ 230 | "HauntedBus_v2.__init__.__defaults__" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "## TwilightBus" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 14, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "# Twilight Zone\n", 247 | "\n", 248 | "class TwilightBus:\n", 249 | " \"\"\"A bus model that makes passengers vanish\"\"\"\n", 250 | "\n", 251 | " def __init__(self, passengers=None):\n", 252 | " if passengers is None:\n", 253 | " self.passengers = []\n", 254 | " else:\n", 255 | " self.passengers = list(passengers)\n", 256 | "\n", 257 | " def pick(self, name):\n", 258 | " self.passengers.append(name)\n", 259 | " \n", 260 | " def drop(self, name):\n", 261 | " self.passengers.remove(name)" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 15, 267 | "metadata": {}, 268 | "outputs": [ 269 | { 270 | "data": { 271 | "text/plain": [ 272 | "['Sue', 'Tina', 'Maya', 'Diana', 'Pat', 'Alice']" 273 | ] 274 | }, 275 | "execution_count": 15, 276 | "metadata": {}, 277 | "output_type": "execute_result" 278 | } 279 | ], 280 | "source": [ 281 | "hockey_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat', 'Alice']\n", 282 | "bus5 = TwilightBus(hockey_team)\n", 283 | "bus5.passengers" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": 16, 289 | "metadata": {}, 290 | "outputs": [ 291 | { 292 | "data": { 293 | "text/plain": [ 294 | "['Tina', 'Maya', 'Diana', 'Alice']" 295 | ] 296 | }, 297 | "execution_count": 16, 298 | "metadata": {}, 299 | "output_type": "execute_result" 300 | } 301 | ], 302 | "source": [ 303 | "bus5.drop('Sue')\n", 304 | "bus5.drop('Pat')\n", 305 | "bus5.passengers" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": 17, 311 | "metadata": {}, 312 | "outputs": [ 313 | { 314 | "data": { 315 | "text/plain": [ 316 | "['Sue', 'Tina', 'Maya', 'Diana', 'Pat', 'Alice']" 317 | ] 318 | }, 319 | "execution_count": 17, 320 | "metadata": {}, 321 | "output_type": "execute_result" 322 | } 323 | ], 324 | "source": [ 325 | "hockey_team" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": {}, 331 | "source": [ 332 | "The assignment on line 8, `self.passengers = passengers`, creates an _alias_ to the `hockey_team` list.\n", 333 | "\n", 334 | "Therefore, the `.drop` method removes names from the `hockey_team` list." 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "metadata": {}, 340 | "source": [ 341 | "## Bus" 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": null, 347 | "metadata": {}, 348 | "outputs": [], 349 | "source": [ 350 | "class Bus:\n", 351 | " \"\"\"The bus we wanted all along\"\"\"\n", 352 | "\n", 353 | " def __init__(self, passengers=None):\n", 354 | " self.passengers = list(passengers) if passengers else []\n", 355 | "\n", 356 | " def pick(self, name):\n", 357 | " self.passengers.append(name)\n", 358 | " \n", 359 | " def drop(self, name):\n", 360 | " self.passengers.remove(name)" 361 | ] 362 | }, 363 | { 364 | "cell_type": "code", 365 | "execution_count": null, 366 | "metadata": {}, 367 | "outputs": [], 368 | "source": [ 369 | "hockey_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat', 'Alice']\n", 370 | "bus6 = Bus(hockey_team)\n", 371 | "bus6.passengers" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": null, 377 | "metadata": {}, 378 | "outputs": [], 379 | "source": [ 380 | "bus6.drop('Sue')\n", 381 | "bus6.drop('Pat')\n", 382 | "bus6.passengers" 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": null, 388 | "metadata": {}, 389 | "outputs": [], 390 | "source": [ 391 | "hockey_team" 392 | ] 393 | }, 394 | { 395 | "cell_type": "markdown", 396 | "metadata": {}, 397 | "source": [ 398 | "On line 5, the expression `list(passengers)` builds a new list from the `passengers` argument.\n", 399 | "\n", 400 | "If `passengers` is a list, `list(passengers)` makes a shallow copy of it.\n", 401 | "\n", 402 | "If `passengers` is an other iterable object (`tuple`, `set`, generator, etc...), then `list(passengers)` builds a new list from it.\n", 403 | "\n", 404 | "> Be conservative in what you send, be liberal in what you accept — _Postel's Law_" 405 | ] 406 | }, 407 | { 408 | "cell_type": "markdown", 409 | "metadata": {}, 410 | "source": [ 411 | "" 412 | ] 413 | } 414 | ], 415 | "metadata": { 416 | "kernelspec": { 417 | "display_name": "Python 3", 418 | "language": "python", 419 | "name": "python3" 420 | }, 421 | "language_info": { 422 | "codemirror_mode": { 423 | "name": "ipython", 424 | "version": 3 425 | }, 426 | "file_extension": ".py", 427 | "mimetype": "text/x-python", 428 | "name": "python", 429 | "nbconvert_exporter": "python", 430 | "pygments_lexer": "ipython3", 431 | "version": "3.7.1" 432 | } 433 | }, 434 | "nbformat": 4, 435 | "nbformat_minor": 2 436 | } 437 | -------------------------------------------------------------------------------- /03-classes.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Classes with encapsulated state" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Camping budget\n", 15 | "\n", 16 | "Source code for 3 variations: [examples/camping/](https://github.com/fluentpython/pyob2019/tree/master/examples/camping)\n", 17 | "\n", 18 | "\n", 19 | "\n", 20 | "### Camper class\n", 21 | "\n", 22 | "Class ``camping.Camper`` represents a contributor to the budget of a camping trip.\n", 23 | "\n", 24 | "```python\n", 25 | ">>> from camping import Camper\n", 26 | ">>> a = Camper('Anna')\n", 27 | ">>> a.pay(33)\n", 28 | ">>> a.display()\n", 29 | "'Anna paid $ 33.00'\n", 30 | "```\n", 31 | "\n", 32 | "A camper can be created with an initial balance:\n", 33 | "\n", 34 | "```python\n", 35 | ">>> c = Camper('Charlie', 9)\n", 36 | "```\n", 37 | "The ``.display()`` method right-justifies the names taking into account the longest name so far, so that multiple calls show aligned columns:\n", 38 | "\n", 39 | "```\n", 40 | ">>> for camper in [a, c]:\n", 41 | "... print(camper.display())\n", 42 | " Anna paid $ 33.00\n", 43 | "Charlie paid $ 9.00\n", 44 | "```\n", 45 | "\n", 46 | "> The [examples/camping/dataclass](https://github.com/fluentpython/pyob2019/tree/master/examples/camping/dataclass) example shows the use of the `__post_init__` method in a dataclass.\n", 47 | "\n", 48 | "### Budget class\n", 49 | "\n", 50 | "Class ``camping.Budget`` represents the budget for a camping trip in which campers who pitched in more than average need to be reimbursed by the others.\n", 51 | "\n", 52 | "```python\n", 53 | ">>> from camping import Budget\n", 54 | ">>> b = Budget('Debbie', 'Ann', 'Bob', 'Charlie')\n", 55 | "```\n", 56 | "\n", 57 | "The ``__init__`` method takes a variable number of names, so it can be invoked as above, but also like this:\n", 58 | "\n", 59 | "\n", 60 | "```python\n", 61 | ">>> friends = ['Debbie', 'Ann', 'Bob', 'Charlie']\n", 62 | ">>> b = Budget(*friends)\n", 63 | "```\n", 64 | "\n", 65 | "Demonstration of the remaining methods:\n", 66 | "\n", 67 | "```python\n", 68 | ">>> b.total()\n", 69 | "0.0\n", 70 | ">>> b.people()\n", 71 | "['Ann', 'Bob', 'Charlie', 'Debbie']\n", 72 | ">>> b.contribute(\"Bob\", 50.00)\n", 73 | ">>> b.contribute(\"Debbie\", 40.00)\n", 74 | ">>> b.contribute(\"Ann\", 10.00)\n", 75 | ">>> b.total()\n", 76 | "100.0\n", 77 | "```\n", 78 | "\n", 79 | "The ``.report()`` method lists who should receive or pay, and the respective amounts.\n", 80 | "\n", 81 | "```\n", 82 | ">>> b.report()\n", 83 | "Total: $ 100.00; individual share: $ 25.00\n", 84 | "------------------------------------------\n", 85 | "Charlie paid $ 0.00, balance: $ -25.00\n", 86 | " Ann paid $ 10.00, balance: $ -15.00\n", 87 | " Debbie paid $ 40.00, balance: $ 15.00\n", 88 | " Bob paid $ 50.00, balance: $ 25.00\n", 89 | "```" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "### Sidebar: private attributes" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 1, 102 | "metadata": {}, 103 | "outputs": [ 104 | { 105 | "data": { 106 | "text/plain": [ 107 | "'gold'" 108 | ] 109 | }, 110 | "execution_count": 1, 111 | "metadata": {}, 112 | "output_type": "execute_result" 113 | } 114 | ], 115 | "source": [ 116 | "class BlackBox:\n", 117 | " \n", 118 | " def __init__(self, top_content, bottom_content):\n", 119 | " self._top = top_content\n", 120 | " self.__bottom = bottom_content\n", 121 | " \n", 122 | "b = BlackBox('gold', 'diamonds')\n", 123 | "\n", 124 | "b._top" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 2, 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "data": { 134 | "text/plain": [ 135 | "True" 136 | ] 137 | }, 138 | "execution_count": 2, 139 | "metadata": {}, 140 | "output_type": "execute_result" 141 | } 142 | ], 143 | "source": [ 144 | "hasattr(b, '_top')" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 3, 150 | "metadata": {}, 151 | "outputs": [ 152 | { 153 | "data": { 154 | "text/plain": [ 155 | "False" 156 | ] 157 | }, 158 | "execution_count": 3, 159 | "metadata": {}, 160 | "output_type": "execute_result" 161 | } 162 | ], 163 | "source": [ 164 | "hasattr(b, '__bottom')" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 4, 170 | "metadata": {}, 171 | "outputs": [ 172 | { 173 | "data": { 174 | "text/plain": [ 175 | "{'_top': 'gold', '_BlackBox__bottom': 'diamonds'}" 176 | ] 177 | }, 178 | "execution_count": 4, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "b.__dict__" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": 5, 190 | "metadata": {}, 191 | "outputs": [ 192 | { 193 | "data": { 194 | "text/plain": [ 195 | "'diamonds'" 196 | ] 197 | }, 198 | "execution_count": 5, 199 | "metadata": {}, 200 | "output_type": "execute_result" 201 | } 202 | ], 203 | "source": [ 204 | "b._BlackBox__bottom" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "### Private attributes takeaways\n", 212 | "\n", 213 | "\n", 214 | "\n", 215 | "* Python's ``__private`` attributes are a safety feature, not a security feature.\n", 216 | "* Pythonistas are divided: some use ``__private``, others prefer the convention ``_private``.\n", 217 | "* It's always possible to start with a public attribute, then transform it into a property.\n", 218 | "* Excessive use of getters/setters is actually weak encapsulation: the class is exposing how it keeps its state." 219 | ] 220 | }, 221 | { 222 | "cell_type": "markdown", 223 | "metadata": {}, 224 | "source": [ 225 | "" 226 | ] 227 | } 228 | ], 229 | "metadata": { 230 | "kernelspec": { 231 | "display_name": "Python 3", 232 | "language": "python", 233 | "name": "python3" 234 | }, 235 | "language_info": { 236 | "codemirror_mode": { 237 | "name": "ipython", 238 | "version": 3 239 | }, 240 | "file_extension": ".py", 241 | "mimetype": "text/x-python", 242 | "name": "python", 243 | "nbconvert_exporter": "python", 244 | "pygments_lexer": "ipython3", 245 | "version": "3.8.1" 246 | } 247 | }, 248 | "nbformat": 4, 249 | "nbformat_minor": 2 250 | } 251 | -------------------------------------------------------------------------------- /03-code/03-classes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # # Classes with encapsulated state 5 | 6 | # ## Camping budget 7 | # 8 | # Source code for 3 variations: [examples/camping/](https://github.com/fluentpython/pyob2019/tree/master/examples/camping) 9 | # 10 | # 11 | # 12 | # ### Camper class 13 | # 14 | # Class ``camping.Camper`` represents a contributor to the budget of a camping trip. 15 | # 16 | # ```python 17 | # >>> from camping import Camper 18 | # >>> a = Camper('Anna') 19 | # >>> a.pay(33) 20 | # >>> a.display() 21 | # 'Anna paid $ 33.00' 22 | # ``` 23 | # 24 | # A camper can be created with an initial balance: 25 | # 26 | # ```python 27 | # >>> c = Camper('Charlie', 9) 28 | # ``` 29 | # The ``.display()`` method right-justifies the names taking into account the longest name so far, so that multiple calls show aligned columns: 30 | # 31 | # ``` 32 | # >>> for camper in [a, c]: 33 | # ... print(camper.display()) 34 | # Anna paid $ 33.00 35 | # Charlie paid $ 9.00 36 | # ``` 37 | # 38 | # > The [examples/camping/dataclass](https://github.com/fluentpython/pyob2019/tree/master/examples/camping/dataclass) example shows the use of the `__post_init__` method in a dataclass. 39 | # 40 | # ### Budget class 41 | # 42 | # Class ``camping.Budget`` represents the budget for a camping trip in which campers who pitched in more than average need to be reimbursed by the others. 43 | # 44 | # ```python 45 | # >>> from camping import Budget 46 | # >>> b = Budget('Debbie', 'Ann', 'Bob', 'Charlie') 47 | # ``` 48 | # 49 | # The ``__init__`` method takes a variable number of names, so it can be invoked as above, but also like this: 50 | # 51 | # 52 | # ```python 53 | # >>> friends = ['Debbie', 'Ann', 'Bob', 'Charlie'] 54 | # >>> b = Budget(*friends) 55 | # ``` 56 | # 57 | # Demonstration of the remaining methods: 58 | # 59 | # ```python 60 | # >>> b.total() 61 | # 0.0 62 | # >>> b.people() 63 | # ['Ann', 'Bob', 'Charlie', 'Debbie'] 64 | # >>> b.contribute("Bob", 50.00) 65 | # >>> b.contribute("Debbie", 40.00) 66 | # >>> b.contribute("Ann", 10.00) 67 | # >>> b.total() 68 | # 100.0 69 | # ``` 70 | # 71 | # The ``.report()`` method lists who should receive or pay, and the respective amounts. 72 | # 73 | # ``` 74 | # >>> b.report() 75 | # Total: $ 100.00; individual share: $ 25.00 76 | # ------------------------------------------ 77 | # Charlie paid $ 0.00, balance: $ -25.00 78 | # Ann paid $ 10.00, balance: $ -15.00 79 | # Debbie paid $ 40.00, balance: $ 15.00 80 | # Bob paid $ 50.00, balance: $ 25.00 81 | # ``` 82 | 83 | # ### Sidebar: private attributes 84 | 85 | # In[1]: 86 | 87 | 88 | class BlackBox: 89 | 90 | def __init__(self, top_content, bottom_content): 91 | self._top = top_content 92 | self.__bottom = bottom_content 93 | 94 | b = BlackBox('gold', 'diamonds') 95 | 96 | b._top 97 | 98 | 99 | # In[2]: 100 | 101 | 102 | hasattr(b, '_top') 103 | 104 | 105 | # In[3]: 106 | 107 | 108 | hasattr(b, '__bottom') 109 | 110 | 111 | # In[4]: 112 | 113 | 114 | b.__dict__ 115 | 116 | 117 | # In[5]: 118 | 119 | 120 | b._BlackBox__bottom 121 | 122 | 123 | # ### Private attributes takeaways 124 | # 125 | # 126 | # 127 | # * Python's ``__private`` attributes are a safety feature, not a security feature. 128 | # * Pythonistas are divided: some use ``__private``, others prefer the convention ``_private``. 129 | # * It's always possible to start with a public attribute, then transform it into a property. 130 | # * Excessive use of getters/setters is actually weak encapsulation: the class is exposing how it keeps its state. 131 | 132 | # 133 | -------------------------------------------------------------------------------- /04-code/04-data-classes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # # Data classes 5 | # 6 | # In this section we will see Python features to avoid boilerplate when creating classes that are essentially collections of fields, similar to a C struct or a database record. 7 | # 8 | # * ``collections.namedtuple`` 9 | # * ``typing.NamedTuple`` 10 | # * ``dataclasses.dataclass`` 11 | 12 | # ## collections.nametuple 13 | 14 | # In[1]: 15 | 16 | 17 | from collections import namedtuple 18 | 19 | Coordinate = namedtuple('Coordinate', 'lat long') 20 | cle = Coordinate(41.40, -81.85) 21 | cle 22 | 23 | 24 | # Simple to use, and is a tuple, so you can do this: 25 | 26 | # In[2]: 27 | 28 | 29 | latitude, longitude = cle 30 | latitude 31 | 32 | 33 | # In[3]: 34 | 35 | 36 | longitude 37 | 38 | 39 | # Includes ``__eq__`` that knows how to compare with tuples: 40 | 41 | # In[4]: 42 | 43 | 44 | (latitude, longitude) == cle 45 | 46 | 47 | # ## namedtuple limitations 48 | # 49 | # * instances are immutable; 50 | # * no simple way to implement custom methods. 51 | 52 | # ## typing.NamedTuple 53 | # 54 | # Introduced in Python 3.5, with [PEP 526](https://www.python.org/dev/peps/pep-0526) variable annotation syntax added in Python 3.6. 55 | 56 | # In[5]: 57 | 58 | 59 | from typing import NamedTuple, ClassVar 60 | 61 | class Coordinate(NamedTuple): 62 | 63 | lat: float = 0 64 | long: float = 0 65 | 66 | reference_system = 'WGS84' 67 | 68 | def __str__(self): 69 | ns = 'NS'[self.lat < 0] 70 | we = 'EW'[self.long < 0] 71 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}' 72 | 73 | 74 | # In[6]: 75 | 76 | 77 | gulf_of_guinea = Coordinate() 78 | gulf_of_guinea 79 | 80 | 81 | # In[7]: 82 | 83 | 84 | Coordinate.__dict__ 85 | 86 | 87 | # In[8]: 88 | 89 | 90 | for k, v in Coordinate.__dict__.items(): 91 | if not k.startswith('_'): 92 | print(k,':', v) 93 | 94 | 95 | # In[9]: 96 | 97 | 98 | cle = Coordinate(41.40, -81.85) 99 | print(cle) 100 | 101 | 102 | # In[10]: 103 | 104 | 105 | try: 106 | cle.lat = 0 107 | except AttributeError as e: 108 | print(e) 109 | 110 | 111 | # In[11]: 112 | 113 | 114 | cle.reference_system 115 | 116 | 117 | # In[12]: 118 | 119 | 120 | try: 121 | cle.reference_system = 'X' 122 | except AttributeError as e: 123 | print(e) 124 | 125 | 126 | # ## @dataclass 127 | # 128 | # ### Coordinate as dataclass 129 | 130 | # In[13]: 131 | 132 | 133 | from dataclasses import dataclass 134 | 135 | from typing import ClassVar 136 | 137 | @dataclass 138 | class Coordinate: 139 | lat: float 140 | long: float = 0 141 | 142 | reference_system: ClassVar[str] = 'WGS84' 143 | 144 | def __str__(self): 145 | ns = 'NS'[self.lat < 0] 146 | we = 'EW'[self.long < 0] 147 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}' 148 | 149 | 150 | # In[14]: 151 | 152 | 153 | for k, v in Coordinate.__dict__.items(): 154 | if not k.startswith('_'): 155 | print(k,':', v) 156 | 157 | 158 | # In[15]: 159 | 160 | 161 | cle = Coordinate(41.40, -81.85) 162 | cle 163 | 164 | 165 | # In[16]: 166 | 167 | 168 | print(cle) 169 | 170 | 171 | # ### @dataclass options 172 | # 173 | # ``` 174 | # @dataclasses.dataclass(*, 175 | # init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False) 176 | # ``` 177 | # 178 | # 179 | # 180 | # 181 | # 182 | # 183 | # 184 | # 185 | # 186 | # 187 | # 188 | # 189 | # 190 | # 191 | # 192 | #
optiondefaultmeaning
initTruegenerate __init__¹
reprTruegenerate __repr__¹
eqTruegenerate __eq__¹
orderFalsegenerate __lt__, __le__, __gt__, __ge__²
unsafe_hashFalsegenerate __hash__³
frozenFalsemake instances "immutable" ⁴
193 | # 194 | # **Notes** 195 | # 196 | # ¹ Ignored if the special method is implemented by user.
197 | # ² Raises exceptions if ``eq=False`` or any of the listed special methods are implemented by user.
198 | # ³ Complex semantics and several caveats — see: [dataclass documentation](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
199 | # ⁴ Not really immutable — imutability is emulated generating ``__setattr__`` and ``__delattr__`` which raise ``dataclass.FrozenInstanceError`` (a subclass of ``AttributeError``). 200 | 201 | # ### Example: a Dublin Core resource dataclass 202 | 203 | # In[17]: 204 | 205 | 206 | from dataclasses import dataclass, field, fields 207 | from typing import List 208 | 209 | @dataclass 210 | class Resource: 211 | """Media resource description.""" 212 | identifier: str = "0" * 13 213 | title: str = "" 214 | creators: List[str] = field(default_factory=list) 215 | date: str = "" 216 | type: str = "" 217 | description: str = "" 218 | language: str = "" 219 | subjects: List[str] = field(default_factory=list) 220 | 221 | 222 | def __repr__(self): 223 | cls = self.__class__ 224 | cls_name = cls.__name__ 225 | res = [f'{cls_name}('] 226 | for field in fields(cls): 227 | value = getattr(self, field.name) 228 | res.append(f' {field.name} = {value!r},') 229 | res.append(f')') 230 | return '\n'.join(res) 231 | 232 | 233 | # In[18]: 234 | 235 | 236 | description = 'A hands-on guide to idiomatic Python code.' 237 | book = Resource('9781491946008', 'Fluent Python', 238 | ['Luciano Ramalho'], '2015-08-20', 'book', description, 239 | 'EN', ['computer programming', 'Python']) 240 | book 241 | 242 | 243 | # In[19]: 244 | 245 | 246 | empty = Resource() 247 | empty 248 | 249 | 250 | # ### See docs for the field function 251 | 252 | # In[20]: 253 | 254 | 255 | get_ipython().run_line_magic('pinfo', 'field') 256 | 257 | 258 | # 259 | -------------------------------------------------------------------------------- /05-code/05-inheritance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # # Inheritance 5 | # 6 | # ## A classic example 7 | # 8 | # 9 | 10 | # 11 | 12 | # ### Code review 13 | 14 | # See source code at [/examples/finstory/](https://github.com/fluentpython/pyob2019/tree/master/examples/finstory) 15 | 16 | # ## Sidebar: floats & decimals 17 | 18 | # In[1]: 19 | 20 | 21 | x = 1.1 22 | 23 | 24 | # In[2]: 25 | 26 | 27 | print(x) 28 | 29 | 30 | # In[3]: 31 | 32 | 33 | print(f'{x:.20f}') 34 | 35 | 36 | # In[4]: 37 | 38 | 39 | from decimal import Decimal 40 | Decimal(x) 41 | 42 | 43 | # ### A solution for business applications 44 | # 45 | # We want to accept `float` as input and we want to use `Decimal` in our own calculations, but we don't want to preserve the IEEE 754 errors with full precision. 46 | # 47 | # Therefore, we will covert any `float` to `str`: 48 | 49 | # In[5]: 50 | 51 | 52 | Decimal(str(x)) 53 | 54 | 55 | # To handle inputs, we will use this function: 56 | 57 | # In[6]: 58 | 59 | 60 | import decimal 61 | 62 | def new_decimal(value): 63 | """Builds a Decimal using the cleaner float `repr`""" 64 | if isinstance(value, float): 65 | value = repr(value) 66 | return decimal.Decimal(value) 67 | 68 | 69 | new_decimal(1.1) 70 | 71 | 72 | # ## More examples 73 | # 74 | # ### Variations on a bingo machine 75 | # 76 | # * [Bingo](https://github.com/fluentpython/pyob2019/tree/master/examples/bingo): a simple bingo machine. 77 | # * [Tombola ABC](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/tombola.py): Abstract Base Class for bingo machines. 78 | # * [BingoCage](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/bingo.py): an implementation of ``Tombola`` using composition. 79 | # * [TumblingDrum](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/drum.py): another implementation of ``Tombola`` using composition. 80 | # * [LotteryBlower](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/lotto.py): yet another implementation of ``Tombola`` using composition. 81 | # * [Tombolist](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/tombolist.py): a ``list`` subclass registered as a virtual subclass of ``Tombola`` 82 | 83 | # ## Key takeaways 84 | # 85 | # * Understand the difference between *interface inheritance* and *implementation inheritance*. 86 | # 87 | # * Understand that *interface inheritance* and *implementation inheritance* happen at the same time when you subclass a concrete class. 88 | # 89 | # * Avoid inheriting from concrete classes. 90 | # 91 | # * Beware of non-virtual calls in built-ins. To be safe, use [collections.UserList](https://docs.python.org/3/library/collections.html#collections.UserList) & co. 92 | # 93 | # * *Favor object composition over class inheritance. (Gang of Four)* 94 | 95 | # 96 | -------------------------------------------------------------------------------- /05-inheritance.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Inheritance\n", 8 | "\n", 9 | "## A classic example\n", 10 | "\n", 11 | "" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "### Code review" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "See source code at [/examples/finstory/](https://github.com/fluentpython/pyob2019/tree/master/examples/finstory)" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "## Sidebar: floats & decimals" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 1, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "x = 1.1" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 2, 54 | "metadata": {}, 55 | "outputs": [ 56 | { 57 | "name": "stdout", 58 | "output_type": "stream", 59 | "text": [ 60 | "1.1\n" 61 | ] 62 | } 63 | ], 64 | "source": [ 65 | "print(x)" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 3, 71 | "metadata": {}, 72 | "outputs": [ 73 | { 74 | "name": "stdout", 75 | "output_type": "stream", 76 | "text": [ 77 | "1.10000000000000008882\n" 78 | ] 79 | } 80 | ], 81 | "source": [ 82 | "print(f'{x:.20f}')" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 4, 88 | "metadata": {}, 89 | "outputs": [ 90 | { 91 | "data": { 92 | "text/plain": [ 93 | "Decimal('1.100000000000000088817841970012523233890533447265625')" 94 | ] 95 | }, 96 | "execution_count": 4, 97 | "metadata": {}, 98 | "output_type": "execute_result" 99 | } 100 | ], 101 | "source": [ 102 | "from decimal import Decimal\n", 103 | "Decimal(x)" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "### A solution for business applications\n", 111 | "\n", 112 | "We want to accept `float` as input and we want to use `Decimal` in our own calculations, but we don't want to preserve the IEEE 754 errors with full precision.\n", 113 | "\n", 114 | "Therefore, we will covert any `float` to `str`:" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 5, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "data": { 124 | "text/plain": [ 125 | "Decimal('1.1')" 126 | ] 127 | }, 128 | "execution_count": 5, 129 | "metadata": {}, 130 | "output_type": "execute_result" 131 | } 132 | ], 133 | "source": [ 134 | "Decimal(str(x))" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "To handle inputs, we will use this function:" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 6, 147 | "metadata": {}, 148 | "outputs": [ 149 | { 150 | "data": { 151 | "text/plain": [ 152 | "Decimal('1.1')" 153 | ] 154 | }, 155 | "execution_count": 6, 156 | "metadata": {}, 157 | "output_type": "execute_result" 158 | } 159 | ], 160 | "source": [ 161 | "import decimal\n", 162 | "\n", 163 | "def new_decimal(value):\n", 164 | " \"\"\"Builds a Decimal using the cleaner float `repr`\"\"\"\n", 165 | " if isinstance(value, float):\n", 166 | " value = repr(value)\n", 167 | " return decimal.Decimal(value)\n", 168 | "\n", 169 | "\n", 170 | "new_decimal(1.1)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "## More examples\n", 178 | "\n", 179 | "### Variations on a bingo machine\n", 180 | "\n", 181 | "* [Bingo](https://github.com/fluentpython/pyob2019/tree/master/examples/bingo): a simple bingo machine.\n", 182 | "* [Tombola ABC](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/tombola.py): Abstract Base Class for bingo machines.\n", 183 | "* [BingoCage](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/bingo.py): an implementation of ``Tombola`` using composition.\n", 184 | "* [TumblingDrum](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/drum.py): another implementation of ``Tombola`` using composition.\n", 185 | "* [LotteryBlower](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/lotto.py): yet another implementation of ``Tombola`` using composition.\n", 186 | "* [Tombolist](https://github.com/fluentpython/pyob2019/blob/master/examples/tombola/tombolist.py): a ``list`` subclass registered as a virtual subclass of ``Tombola``" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": {}, 192 | "source": [ 193 | "## Key takeaways\n", 194 | "\n", 195 | "* Understand the difference between *interface inheritance* and *implementation inheritance*.\n", 196 | "\n", 197 | "* Understand that *interface inheritance* and *implementation inheritance* happen at the same time when you subclass a concrete class.\n", 198 | "\n", 199 | "* Avoid inheriting from concrete classes.\n", 200 | "\n", 201 | "* Beware of non-virtual calls in built-ins. To be safe, use [collections.UserList](https://docs.python.org/3/library/collections.html#collections.UserList) & co.\n", 202 | "\n", 203 | "* *Favor object composition over class inheritance. (Gang of Four)*" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "" 211 | ] 212 | } 213 | ], 214 | "metadata": { 215 | "kernelspec": { 216 | "display_name": "Python 3", 217 | "language": "python", 218 | "name": "python3" 219 | }, 220 | "language_info": { 221 | "codemirror_mode": { 222 | "name": "ipython", 223 | "version": 3 224 | }, 225 | "file_extension": ".py", 226 | "mimetype": "text/x-python", 227 | "name": "python", 228 | "nbconvert_exporter": "python", 229 | "pygments_lexer": "ipython3", 230 | "version": "3.7.1" 231 | } 232 | }, 233 | "nbformat": 4, 234 | "nbformat_minor": 2 235 | } 236 | -------------------------------------------------------------------------------- /06-big-ideas.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Big ideas\n", 8 | "\n", 9 | "## Alan Kay defines OOP\n", 10 | "\n", 11 | "[E-mail exchange](http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en) between Stefan Ram and Alan Kay in July, 2003. \n", 12 | "\n", 13 | ">> SR: What does \"object-oriented [programming]\" mean to you?\n", 14 | "\n", 15 | "> AK: OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them." 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## A polyglot OOP book\n", 23 | "\n", 24 | "\n", 25 | "\n", 26 | "Tim Budd — [An Introduction to Object-Oriented Programming, 3e](http://web.engr.oregonstate.edu/~budd/Books/oopintro3e/info/ReadMe.html)\n", 27 | "\n", 28 | "Discussion and examples in:\n", 29 | "\n", 30 | "* C++\n", 31 | "* C#\n", 32 | "* CLOS (Lisp)\n", 33 | "* Delphi/Object Pascal\n", 34 | "* Java\n", 35 | "* Python\n", 36 | "* Ruby\n", 37 | "* Smalltalk" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "### The Object-Oriented 8 Queens\n", 45 | "\n", 46 | "* no \"God class\"\n", 47 | "* ``Queen`` objects collaborate to find the solution\n", 48 | "* ``Guard`` leverages duck typing and polymorphism to simplify the solution\n", 49 | "* solutions in [Java, C++, and Object Pascal](http://web.engr.oregonstate.edu/~budd/Books/oopintro2e/info/queens/)\n", 50 | "* solution in [Python](https://github.com/fluentpython/pyob2019/tree/master/examples/8queens)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "" 58 | ] 59 | } 60 | ], 61 | "metadata": { 62 | "kernelspec": { 63 | "display_name": "Python 3", 64 | "language": "python", 65 | "name": "python3" 66 | }, 67 | "language_info": { 68 | "codemirror_mode": { 69 | "name": "ipython", 70 | "version": 3 71 | }, 72 | "file_extension": ".py", 73 | "mimetype": "text/x-python", 74 | "name": "python", 75 | "nbconvert_exporter": "python", 76 | "pygments_lexer": "ipython3", 77 | "version": "3.7.1" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 2 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pythonic Objects 2 | 3 | This repository contains all the material used to present the tutorial [Pythonic Objects: idiomatic OOP in Python](https://us.pycon.org/2019/schedule/presentation/76/) at PyCon US 2019. 4 | 5 | The material consists of Python 3 Jupyter Notebooks, example code, and doctests for two labs (only the first was used at PyCon 2019). 6 | 7 | There's more content in the [video](https://www.youtube.com/watch?v=mUu_4k6a5-I), including spoken words, hand waving, comments and Q&A with the participants. 8 | 9 | ---- 10 | 11 | Creative Commons License
Pythonic Objects by Luciano Ramalho is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License -------------------------------------------------------------------------------- /demos.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "class Coordinate:\n", 10 | " '''Coordinate on Earth'''\n", 11 | " \n", 12 | " reference_system = 'WGS84'\n", 13 | " \n", 14 | " def __init__(self, lat, long):\n", 15 | " self.lat = lat\n", 16 | " self.long = long\n", 17 | " \n", 18 | " def __repr__(self):\n", 19 | " return f'Coordinate({self.lat}, {self.long})'\n", 20 | " \n", 21 | " def __str__(self):\n", 22 | " ns = 'NS'[self.lat < 0]\n", 23 | " we = 'WE'[self.long < 0]\n", 24 | " return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}'" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "c = Coordinate(26, 46)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "metadata": {}, 40 | "outputs": [ 41 | { 42 | "data": { 43 | "text/plain": [ 44 | "Coordinate(26, 46)" 45 | ] 46 | }, 47 | "execution_count": 3, 48 | "metadata": {}, 49 | "output_type": "execute_result" 50 | } 51 | ], 52 | "source": [ 53 | "c" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 4, 59 | "metadata": {}, 60 | "outputs": [ 61 | { 62 | "name": "stdout", 63 | "output_type": "stream", 64 | "text": [ 65 | "26.0°N, 46.0°W\n" 66 | ] 67 | } 68 | ], 69 | "source": [ 70 | "print(c)" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 5, 76 | "metadata": {}, 77 | "outputs": [ 78 | { 79 | "data": { 80 | "text/plain": [ 81 | "CoordinateT(lat=22, long=44)" 82 | ] 83 | }, 84 | "execution_count": 5, 85 | "metadata": {}, 86 | "output_type": "execute_result" 87 | } 88 | ], 89 | "source": [ 90 | "from collections import namedtuple\n", 91 | "\n", 92 | "CoordinateT = namedtuple('CoordinateT', 'lat long')\n", 93 | "\n", 94 | "c = CoordinateT(22, 44)\n", 95 | "c" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 6, 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "data": { 105 | "text/plain": [ 106 | "44" 107 | ] 108 | }, 109 | "execution_count": 6, 110 | "metadata": {}, 111 | "output_type": "execute_result" 112 | } 113 | ], 114 | "source": [ 115 | "lat, long = c\n", 116 | "long" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "From [SQL Alquemy Object Relational Tutorial](https://docs.sqlalchemy.org/en/13/orm/tutorial.html):\n", 124 | "\n", 125 | "```python\n", 126 | "from sqlalchemy.ext.declarative import declarative_base\n", 127 | "from sqlalchemy import Column, Integer, String\n", 128 | "\n", 129 | "Base = declarative_base()\n", 130 | "\n", 131 | "class User(Base):\n", 132 | " __tablename__ = 'users'\n", 133 | "\n", 134 | " id = Column(Integer, primary_key=True)\n", 135 | " name = Column(String)\n", 136 | " fullname = Column(String)\n", 137 | " nickname = Column(String)\n", 138 | "\n", 139 | " def __repr__(self):\n", 140 | " return \"\" % (\n", 141 | " self.name, self.fullname, self.nickname)\n", 142 | "```\n" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [] 151 | } 152 | ], 153 | "metadata": { 154 | "kernelspec": { 155 | "display_name": "Python 3", 156 | "language": "python", 157 | "name": "python3" 158 | }, 159 | "language_info": { 160 | "codemirror_mode": { 161 | "name": "ipython", 162 | "version": 3 163 | }, 164 | "file_extension": ".py", 165 | "mimetype": "text/x-python", 166 | "name": "python", 167 | "nbconvert_exporter": "python", 168 | "pygments_lexer": "ipython3", 169 | "version": "3.7.3" 170 | } 171 | }, 172 | "nbformat": 4, 173 | "nbformat_minor": 2 174 | } 175 | -------------------------------------------------------------------------------- /examples/8queens/README.md: -------------------------------------------------------------------------------- 1 | # The 8 Queens Problem 2 | 3 | This directory contains Object-Oriented solutions to the 8 Queens Problem, ported from examples in chapter 6 of Tim Budd's *Introduction to Object-Oriented Programming, 3e* (Addison-Wesley, 2002). 4 | 5 | The key characteristic of these examples is that there is no central control. Each queen is assigned to a column, and moves in its column searching for a row where it cannot be attacked by any other queen. When it cannot find a safe row, it asks its neighboring queen to move and starts over. This produces the backtracking behavior that makes the 8 Queens problem famous in computing. 6 | 7 | Here are notes about each of the programs in this directory. 8 | 9 | ## `queens.py` 10 | 11 | This is the implementation to study first. If executed without arguments, it computes and displays a solution with 8 Queens: 12 | 13 | ```bash 14 | $ python3 queens.py 15 | [(1, 1), (2, 7), (3, 5), (4, 8), (5, 2), (6, 4), (7, 6), (8, 3)] 16 | ┌───┬───┬───┬───┬───┬───┬───┬───┐ 17 | │ ♛ │ │ │ │ │ │ │ │ 18 | ├───┼───┼───┼───┼───┼───┼───┼───┤ 19 | │ │ │ │ │ │ │ ♛ │ │ 20 | ├───┼───┼───┼───┼───┼───┼───┼───┤ 21 | │ │ │ │ │ ♛ │ │ │ │ 22 | ├───┼───┼───┼───┼───┼───┼───┼───┤ 23 | │ │ │ │ │ │ │ │ ♛ │ 24 | ├───┼───┼───┼───┼───┼───┼───┼───┤ 25 | │ │ ♛ │ │ │ │ │ │ │ 26 | ├───┼───┼───┼───┼───┼───┼───┼───┤ 27 | │ │ │ │ ♛ │ │ │ │ │ 28 | ├───┼───┼───┼───┼───┼───┼───┼───┤ 29 | │ │ │ │ │ │ ♛ │ │ │ 30 | ├───┼───┼───┼───┼───┼───┼───┼───┤ 31 | │ │ │ ♛ │ │ │ │ │ │ 32 | └───┴───┴───┴───┴───┴───┴───┴───┘ 33 | ``` 34 | 35 | > **Note:** the grid may or may not appear jagged, depending on the width of the BLACK CHESS QUEEN Unicode character (U+265B) in the display font. If it is jagged on your machine, change the value of `queen` in the first line of the `draw_row` function. 36 | 37 | You may provide an integer argument to see a solution for a different number of queens. For example, `10`: 38 | 39 | ```bash 40 | $ python3 queens.py 10 41 | [(1, 1), (2, 8), (3, 2), (4, 9), (5, 6), (6, 3), (7, 10), (8, 4), (9, 7), (10, 5)] 42 | ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ 43 | │ ♛ │ │ │ │ │ │ │ │ │ │ 44 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 45 | │ │ │ │ │ │ │ │ ♛ │ │ │ 46 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 47 | │ │ ♛ │ │ │ │ │ │ │ │ │ 48 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 49 | │ │ │ │ │ │ │ │ │ ♛ │ │ 50 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 51 | │ │ │ │ │ │ ♛ │ │ │ │ │ 52 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 53 | │ │ │ ♛ │ │ │ │ │ │ │ │ 54 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 55 | │ │ │ │ │ │ │ │ │ │ ♛ │ 56 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 57 | │ │ │ │ ♛ │ │ │ │ │ │ │ 58 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 59 | │ │ │ │ │ │ │ ♛ │ │ │ │ 60 | ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ 61 | │ │ │ │ │ ♛ │ │ │ │ │ │ 62 | └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ 63 | ``` 64 | 65 | Because all queens start in row 1, and all move in the same way, the solution presented for each number of queens is always the same. 66 | 67 | Interestingly, running `queens.py` with 14 queens raises `RecursionError` (using Python's default recursion limit of 1000), but with 15 queens there's no problem. This is due to the backtracking behavior of the queens, which is sensitive to the order in which they search for a safe a square in their columns. 68 | 69 | ## `queens_and_guard.py` 70 | 71 | The `queens_and_guard.py` version uses a `Guard` instance as a sentinel: it is the neighbor of the first `Queen`. The `Guard` class implements three methods with simple hard-coded responses. This leverages polymorphism to avoid several checks that `queens.py` uses to handle the special case of the first `Queen`, which has no royal neighbor. 72 | 73 | ## `random_queens_and_guard.py` 74 | 75 | In this implementation, each `Queen` moves back and forth through a random but fixed sequence of rows, so each run can produce a different solution. For example, it is known that for 8 queens there are 92 solutions, but with 4 queens there are only 2. 76 | 77 | Because of the backtracking nature of the algorithm, different sequences of moves can produce more or less recursive calls. Starting with 10 queens, some runs do not conclude because Python raises a `RecursionError` (using the default recursion limit of 1000). 78 | 79 | ## `drive_random_queens.py` 80 | 81 | This script calls the `solve` function of the `random_queens_and_guard.py` module 500 times for each value of N Queens from 8 to 20. Each call either produces a solution or raises `RecursionError`. The succesful calls are counted and displayed as a percentage. This is a sample run, which took about 95 seconds on a Core i7 machine: 82 | 83 | ```bash 84 | $ time python3 drive_random_queens.py 85 | 8: 100.0% 86 | 9: 100.0% 87 | 10: 100.0% 88 | 11: 98.8% 89 | 12: 99.2% 90 | 13: 99.0% 91 | 14: 94.4% 92 | 15: 94.4% 93 | 16: 89.6% 94 | 17: 85.6% 95 | 18: 83.6% 96 | 19: 77.6% 97 | 20: 74.8% 98 | 99 | real 1m34.955s 100 | user 1m34.735s 101 | sys 0m0.040s 102 | ``` 103 | 104 | In the example above, 100% of the attempts with 10 queens were successful, but for 20 queens the success rate was 74.8% — meaning that 25.2% of the calls hit Python's recursion limit and did not complete. 105 | 106 | The table below shows results for 5 runs of `drive_random_queens.py`, demonstrating that most of the time `random_queens_and_guard.py` can solve for 10 queens, but sometimes it fails. 107 | 108 | 109 | | N | run 1 | run 2 | run 3 | run 4 | run 5 | 110 | | ---: | ---: | ---: | ---: | ---: | ---: | 111 | | **8**| 100.0%| 100.0%| 100.0%| 100.0%| 100.0%| 112 | | **9**| 100.0%| 100.0%| 100.0%| 100.0%| 100.0%| 113 | | **10**| 100.0%| 100.0%| 100.0%| 100.0%| 99.8%| 114 | | **11**| 100.0%| 99.6%| 99.4%| 98.0%| 99.2%| 115 | | **12**| 99.2%| 99.4%| 99.0%| 99.4%| 99.6%| 116 | | **13**| 98.2%| 98.6%| 97.8%| 98.2%| 98.8%| 117 | | **14**| 96.2%| 96.6%| 95.8%| 95.8%| 96.0%| 118 | | **15**| 95.4%| 96.0%| 94.4%| 92.2%| 95.4%| 119 | | **16**| 92.8%| 90.2%| 91.6%| 90.8%| 92.2%| 120 | | **17**| 85.4%| 88.4%| 88.8%| 86.6%| 88.6%| 121 | | **18**| 85.2%| 84.6%| 83.0%| 85.4%| 82.2%| 122 | | **19**| 76.4%| 76.2%| 82.0%| 77.6%| 78.2%| 123 | | **20**| 73.6%| 70.0%| 74.0%| 76.8%| 72.6%| 124 | -------------------------------------------------------------------------------- /examples/8queens/drive_random_queens.py: -------------------------------------------------------------------------------- 1 | from random_queens_and_guard import solve 2 | 3 | TRIES = 500 4 | 5 | for size in range(8, 21): 6 | count = 0 7 | for _ in range(TRIES): 8 | try: 9 | solve(size) 10 | except RecursionError: 11 | pass 12 | else: 13 | count += 1 14 | 15 | success = count / TRIES * 100 16 | print(f'{size:2d}: {success:5.1f}%') 17 | -------------------------------------------------------------------------------- /examples/8queens/queens.py: -------------------------------------------------------------------------------- 1 | # Object-oriented solution to the 8 Queens puzzle ported from Object Pascal 2 | # example in chapter 6 of "An Introduction to Object-Oriented Programming" 3 | # (3rd ed.) by Timothy Budd 4 | 5 | 6 | def aligned(source: tuple, target: tuple) -> bool: 7 | """True if positions are aligned orthogonally or diagonally.""" 8 | row, col = source 9 | target_row, target_col = target 10 | if row == target_row or col == target_col: 11 | return True 12 | # test diagonals 13 | delta = row - target_row 14 | if (col + delta == target_col) or (col - delta == target_col): 15 | return True 16 | return False 17 | 18 | 19 | def all_safe(positions: list) -> bool: 20 | """True if none of the positions are aligned.""" 21 | for i in range(len(positions) - 1): 22 | for j in range(i + 1, len(positions)): 23 | if aligned(positions[i], positions[j]): 24 | return False 25 | return True 26 | 27 | 28 | class Queen: 29 | 30 | def __init__(self, size, column, neighbor): 31 | self.size = size 32 | self.column = column 33 | self.neighbor = neighbor 34 | self.row = 1 35 | 36 | def can_attack(self, test_row, test_column) -> bool: 37 | """True if self or any neighbor can attack.""" 38 | if aligned((self.row, self.column), (test_row, test_column)): 39 | return True 40 | # test neighbors 41 | if self.neighbor: 42 | return self.neighbor.can_attack(test_row, test_column) 43 | return False 44 | 45 | def advance(self) -> bool: 46 | print(f'advance Queen #{self.column}: ({self.row}, {self.column})', end='') 47 | if self.row < self.size: # try next row 48 | self.row += 1 49 | print(f' → ({self.row}, {self.column})') 50 | return self.find_solution() 51 | print(' ×') 52 | if self.neighbor: 53 | if not self.neighbor.advance(): 54 | return False 55 | else: 56 | self.row = 1 57 | return self.find_solution() 58 | return False 59 | 60 | def find_solution(self): 61 | if self.neighbor: 62 | while self.neighbor.can_attack(self.row, self.column): 63 | if not self.advance(): 64 | return False 65 | return True 66 | 67 | def locate(self): 68 | if not self.neighbor: 69 | return [(self.row, self.column)] 70 | else: 71 | return self.neighbor.locate() + [(self.row, self.column)] 72 | 73 | 74 | def draw_row(size, row, column): 75 | queen = '│ \N{black chess queen} ' 76 | square = '│ ' 77 | if row == 1: 78 | print('┌───' + '┬───' * (size-1) + '┐') 79 | else: 80 | print('├───' + '┼───' * (size-1) + '┤') 81 | print(square * (column-1), queen, square * (size-column), '│', sep='') 82 | if row == size: 83 | print('└───' + '┴───' * (size-1) + '┘') 84 | 85 | 86 | class NoSolution(BaseException): 87 | """No solution found.""" 88 | 89 | 90 | def solve(size): 91 | neighbor = None 92 | for i in range(1, size+1): 93 | neighbor = Queen(size, i, neighbor) 94 | found = neighbor.find_solution() 95 | if not found: 96 | raise NoSolution() 97 | 98 | return neighbor.locate() 99 | 100 | 101 | def main(size): 102 | try: 103 | result = sorted(solve(size)) 104 | except NoSolution as exc: 105 | print(exc.__doc__) 106 | else: 107 | print(result) 108 | for cell in result: 109 | draw_row(size, *cell) 110 | 111 | 112 | if __name__ == '__main__': 113 | import sys 114 | if len(sys.argv) == 2: 115 | size = int(sys.argv[1]) 116 | else: 117 | size = 8 118 | main(size) 119 | -------------------------------------------------------------------------------- /examples/8queens/queens_and_guard.py: -------------------------------------------------------------------------------- 1 | # Object-oriented solution to the 8 Queens puzzle ported from Smalltalk 2 | # example in chapter 6 of "An Introduction to Object-Oriented Programming" 3 | # (3rd ed.) by Timothy Budd 4 | 5 | 6 | def aligned(source: tuple, target: tuple) -> bool: 7 | """True if positions are aligned orthogonally or diagonally.""" 8 | row, col = source 9 | target_row, target_col = target 10 | if row == target_row or col == target_col: 11 | return True 12 | # test diagonals 13 | delta = row - target_row 14 | if (col + delta == target_col) or (col - delta == target_col): 15 | return True 16 | return False 17 | 18 | 19 | class Queen: 20 | 21 | def __init__(self, size, column, neighbor): 22 | self.size = size 23 | self.column = column 24 | self.neighbor = neighbor 25 | self.row = 1 26 | 27 | def can_attack(self, test_row, test_column) -> bool: 28 | """True if self or any neighbor can attack.""" 29 | if aligned((self.row, self.column), (test_row, test_column)): 30 | return True 31 | # test neighbors 32 | return self.neighbor.can_attack(test_row, test_column) 33 | 34 | def advance(self) -> bool: 35 | if self.row < self.size: # try next row 36 | self.row += 1 37 | return self.find_solution() 38 | # cannot go further, move neighbor 39 | if not self.neighbor.advance(): 40 | return False 41 | self.row = 1 42 | return self.find_solution() 43 | 44 | def find_solution(self) -> bool: 45 | while self.neighbor.can_attack(self.row, self.column): 46 | if not self.advance(): 47 | return False 48 | return True 49 | 50 | def locate(self) -> list: 51 | return self.neighbor.locate() + [(self.row, self.column)] 52 | 53 | 54 | class Guard: 55 | """A sentinel object.""" 56 | 57 | def advance(self) -> bool: 58 | return False 59 | 60 | def can_attack(self, row, column) -> bool: 61 | return False 62 | 63 | def locate(self) -> list: 64 | return [] 65 | 66 | 67 | def draw_row(size, row, column): 68 | queen = '│ \N{black chess queen} ' 69 | square = '│ ' 70 | if row == 1: 71 | print('┌───' + '┬───' * (size-1) + '┐') 72 | else: 73 | print('├───' + '┼───' * (size-1) + '┤') 74 | print(square * (column-1), queen, square * (size-column), '│', sep='') 75 | if row == size: 76 | print('└───' + '┴───' * (size-1) + '┘') 77 | 78 | 79 | class NoSolution(BaseException): 80 | """No solution found.""" 81 | 82 | 83 | def solve(size): 84 | figure = Guard() 85 | for i in range(1, size+1): 86 | figure = Queen(size, i, figure) 87 | found = figure.find_solution() 88 | if not found: 89 | raise NoSolution() 90 | 91 | return figure.locate() 92 | 93 | 94 | def main(size): 95 | try: 96 | result = sorted(solve(size)) 97 | except NoSolution as exc: 98 | print(exc.__doc__) 99 | else: 100 | print(result) 101 | for cell in result: 102 | draw_row(size, *cell) 103 | 104 | 105 | if __name__ == '__main__': 106 | import sys 107 | if len(sys.argv) == 2: 108 | size = int(sys.argv[1]) 109 | else: 110 | size = 8 111 | main(size) 112 | -------------------------------------------------------------------------------- /examples/8queens/random_queens_and_guard.py: -------------------------------------------------------------------------------- 1 | # Object-oriented solution to the 8 Queens puzzle ported from Smalltalk 2 | # example in chapter 6 of "An Introduction to Object-Oriented Programming" 3 | # (3rd ed.) by Timothy Budd 4 | 5 | from random import shuffle 6 | 7 | 8 | def aligned(source: tuple, target: tuple) -> bool: 9 | """True if positions are aligned orthogonally or diagonally.""" 10 | row, col = source 11 | target_row, target_col = target 12 | if row == target_row or col == target_col: 13 | return True 14 | # test diagonals 15 | delta = row - target_row 16 | if (col + delta == target_col) or (col - delta == target_col): 17 | return True 18 | return False 19 | 20 | 21 | class Queen: 22 | 23 | def __init__(self, size, column, neighbor): 24 | self.size = size 25 | self.column = column 26 | self.neighbor = neighbor 27 | self.row_sequence = list(range(1, size+1)) 28 | shuffle(self.row_sequence) 29 | self.rows = self.row_sequence[:] 30 | self.row = self.rows.pop() 31 | 32 | def can_attack(self, test_row, test_column) -> bool: 33 | """True if self or any neighbor can attack.""" 34 | if aligned((self.row, self.column), (test_row, test_column)): 35 | return True 36 | # test neighbors 37 | return self.neighbor.can_attack(test_row, test_column) 38 | 39 | def advance(self) -> bool: 40 | if self.rows: # try next row 41 | self.row = self.rows.pop() 42 | return self.find_solution() 43 | # cannot go further, move neighbor 44 | if not self.neighbor.advance(): 45 | return False 46 | # restart 47 | self.rows = self.row_sequence[:] 48 | self.row = self.rows.pop() 49 | return self.find_solution() 50 | 51 | def find_solution(self) -> bool: 52 | while self.neighbor.can_attack(self.row, self.column): 53 | if not self.advance(): 54 | return False 55 | return True 56 | 57 | def locate(self) -> list: 58 | return self.neighbor.locate() + [(self.row, self.column)] 59 | 60 | 61 | class Guard: 62 | """A sentinel object.""" 63 | 64 | def advance(self) -> bool: 65 | return False 66 | 67 | def can_attack(self, row, column) -> bool: 68 | return False 69 | 70 | def locate(self) -> list: 71 | return [] 72 | 73 | 74 | def draw_row(size, row, column): 75 | queen = '│ \N{black chess queen} ' 76 | square = '│ ' 77 | if row == 1: 78 | print('┌───' + '┬───' * (size-1) + '┐') 79 | else: 80 | print('├───' + '┼───' * (size-1) + '┤') 81 | print(square * (column-1), queen, square * (size-column), '│', sep='') 82 | if row == size: 83 | print('└───' + '┴───' * (size-1) + '┘') 84 | 85 | 86 | class NoSolution(BaseException): 87 | """No solution found.""" 88 | 89 | 90 | def solve(size): 91 | figure = Guard() 92 | for i in range(1, size+1): 93 | figure = Queen(size, i, figure) 94 | found = figure.find_solution() 95 | if not found: 96 | raise NoSolution() 97 | 98 | return figure.locate() 99 | 100 | 101 | def main(size): 102 | try: 103 | result = sorted(solve(size)) 104 | except NoSolution as exc: 105 | print(exc.__doc__) 106 | else: 107 | print(result) 108 | for cell in result: 109 | draw_row(size, *cell) 110 | 111 | 112 | if __name__ == '__main__': 113 | import sys 114 | if len(sys.argv) == 2: 115 | size = int(sys.argv[1]) 116 | else: 117 | size = 8 118 | main(size) 119 | -------------------------------------------------------------------------------- /examples/8queens/references/Chapter 6, Slide 1.htm: -------------------------------------------------------------------------------- 1 | 2 | Chapter 6, Slide 1 3 | 4 |

Introduction to Object Oriented Programming, 3rd Ed

5 |

Timothy A. Budd

6 |

Chapter 6

7 |

A Case Study : Eight Queens

8 |

9 | Outline 10 |

    11 |
  1. Statement of the Problem 12 |
  2. OOP Approach 13 |
  3. Observations 14 |
  4. Pointers 15 |
  5. CRC Card for Queen 16 |
  6. CRC Card for Queen - Backside 17 |
  7. Initialization 18 |
  8. Finding First Solution 19 |
  9. Advancing to Next Position 20 |
  10. Printing Solution 21 |
  11. Can Attack 22 |
  12. The Last Queen 23 |
  13. Chapter Summary 24 |
25 |

26 | Source Code 27 |

33 |

34 | Other Material 35 |

    36 |
  • A printer friendly version of all slides 37 |
  • HTML page for queen program in Java 38 | (not available on CD, see explanation) 39 |
  • A puzzle related to the 8-queens is the knights-tour problem. 40 | While slightly more advanced, in Chapter 18 we will introduce 41 | the idea of frameworks. In another book I have written an 42 | example backtracking framework illustrated 43 | using this puzzle. 44 |
45 | 46 |
Intro OOP, Chapter 6, Slide 1
47 |
48 | 49 | 50 |

51 |

Statement of the Problem

52 |

53 | Problem - how to place eight queens on a chessboard so that no two queens 54 | can attack each other: 55 | 56 |

57 | chessboard with 8 queens 58 |

Intro OOP, Chapter 6, Slide 1
59 |
60 | 61 | 62 | 63 |
64 |

OOP Approach

65 |

66 | More than just solving the problem, we want to solve the problem in an OOP 67 | manner. 68 |

    69 |
  • 70 | Structure the program so that the data values themselves discover the 71 | solution. 72 |

    73 |

  • 74 | Similar to creating a universe and setting it in motion. 75 |

    76 |

  • 77 | No single controlling manager. 78 |
79 | 80 |
Intro OOP, Chapter 6, Slide 2
81 |
82 | 83 | 84 | 85 |
86 |

Observations

87 |

88 | Here are a few observations we can make concerning this problem: 89 |

    90 |
  • 91 | Queens can be assigned a column, problem is then to find row positions. 92 |

    93 |

  • 94 | One obvious behavior is for a queen to tell if it can attack a given position. 95 |

    96 |

  • 97 | Can structure the problem using generators - each queen can be asked to 98 | find one solution, then later be asked to find another solution. 99 |
100 | 101 |
Intro OOP, Chapter 6, Slide 3
102 |
103 | 104 | 105 | 106 |
107 |

Pointers

108 |

109 | We can make each queen point to the next on the left, then send messages only 110 | to the rightmost queen. Each queen will in turn send messages only to the 111 | neighbor it points to. 112 | 113 |

114 | picture of queens pointing to each other 115 |
116 |

Intro OOP, Chapter 6, Slide 4
117 |
118 | 119 | 120 | 121 |
122 |

CRC Card for Queen

123 | CRC card for queen class 124 |
Intro OOP, Chapter 6, Slide 5
125 |
126 | 127 | 128 | 129 |
130 |

CRC Card for Queen - Backside

131 |
132 | back side of CRC card 133 |
134 |
Intro OOP, Chapter 6, Slide 6
135 |
136 | 137 | 138 | 139 |
140 |

Initialization

141 |

142 | Initialization will set each queen to point to a neighbor, and set column 143 | value. C++ version is shown: 144 |

145 | main() {
146 | 	Queen * lastQueen = 0;
147 | 
148 | 	for (int i = 1; i <= 8; i++) {
149 | 		lastQueen = new Queen(i, lastQueen);
150 | 		if (! lastQueen->findSolution())
151 | 			cout << "no solution";
152 | 	}
153 | 	
154 | 	if (lastQueen->first()) 
155 | 		lastQueen->print();
156 | }
157 | 
158 | Queen::Queen (int col, Queen * ngh)
159 | {
160 | 	column = col;
161 | 	neighbor = ngh;
162 | 	row = 1;
163 | }
164 | 
165 | 
166 | 167 |
Intro OOP, Chapter 6, Slide 7
168 |
169 | 170 | 171 | 172 |
173 |

Finding First Solution

174 |

175 | Finding first solution, in pseudo-code: 176 |

177 | function queen.findSolution -> boolean
178 | 	
179 | 	while neighbor.canAttack (row, column) do
180 | 		if not self.advance then
181 | 			return false;
182 | 
183 | 		// found a solution
184 | 	return true;
185 | end
186 | 
187 | 
188 | We ignore for the moment the question of what to do if you don't have 189 | a neighbor 190 | 191 |
Intro OOP, Chapter 6, Slide 8
192 |
193 | 194 | 195 | 196 |
197 |

Advancing to Next Position

198 |
199 | function queen.advance -> boolean
200 | 
201 | 	if row < 8 then begin
202 | 		row := row + 1;
203 | 		return self.findSolution
204 | 	end
205 | 
206 | 		// cannot go further, move neighbor
207 | 	if not neighbor.advance then
208 | 		return false
209 | 
210 | 	row := 1
211 | 	return self findSolution
212 | 
213 | end
214 | 
215 | 216 |
Intro OOP, Chapter 6, Slide 9
217 |
218 | 219 | 220 | 221 |
222 |

Printing Solution

223 | Just recursively ripple down the list of queens, asking each to 224 | print itself. 225 |
226 | procedure print
227 | 	neighbor.print
228 | 	write row, column
229 | end
230 | 
231 | 
232 | 233 |
Intro OOP, Chapter 6, Slide 10
234 |
235 | 236 | 237 | 238 |
239 |

Can Attack

240 |
241 | function canAttack(r, c)
242 | 	if r = row then
243 | 		return true
244 | 	cd := column - c;
245 | 	if (row + cd = r) or (row - cd = r) then
246 | 		return true;
247 | 	return neighbor.canAttack(r, c)
248 | end
249 | 
250 | For a diagonal, the difference in row must equal the difference 251 | in columns. 252 | 253 |
Intro OOP, Chapter 6, Slide 11
254 |
255 | 256 | 257 | 258 |
259 |

The Last Queen

260 |

261 | Two approaches to handling the leftmost queen - 262 |

    263 |
  • 264 | Null pointers - each queen must then test for null pointers before sending 265 | a message 266 |

    267 |

  • 268 | Special ``sentinel'' value - indicates end of line for queens 269 |
270 |

271 | Both versions are described in text. 272 | 273 |

Intro OOP, Chapter 6, Slide 12
274 |
275 | The complete solutions in each language are not described in the slides, 276 | but are presented in detail in the text. 277 |
278 | A Java applet version is available. 279 | 280 |
281 |

Chapter Summary

282 |

283 | Important not for the problem being solved, but how it is solved. 284 |

    285 |
  • 286 | Solution is the result of community of agents working together 287 |

    288 |

  • 289 | No single controlling program - control is decentralized 290 |

    291 |

  • 292 | Active objects determine their own actions and behavior. 293 |
294 | 295 |
Intro OOP, Chapter 6, Slide 13
296 |
297 | 298 | 299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /examples/8queens/references/Chapter 6, Slide 1_files/slide01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/examples/8queens/references/Chapter 6, Slide 1_files/slide01.gif -------------------------------------------------------------------------------- /examples/8queens/references/Chapter 6, Slide 1_files/slide04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/examples/8queens/references/Chapter 6, Slide 1_files/slide04.gif -------------------------------------------------------------------------------- /examples/8queens/references/Chapter 6, Slide 1_files/slide05.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/examples/8queens/references/Chapter 6, Slide 1_files/slide05.gif -------------------------------------------------------------------------------- /examples/8queens/references/Chapter 6, Slide 1_files/slide06.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/examples/8queens/references/Chapter 6, Slide 1_files/slide06.gif -------------------------------------------------------------------------------- /examples/8queens/references/QSolve.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.applet.*; 3 | 4 | class Queen { 5 | // data fields 6 | private int row; 7 | private int column; 8 | private Queen neighbor; 9 | 10 | // constructor 11 | Queen (int c, Queen n) { 12 | // initialize data fields 13 | row = 1; 14 | column = c; 15 | neighbor = n; 16 | } 17 | 18 | public boolean findSolution() { 19 | while (neighbor != null && neighbor.canAttach(row, column)) 20 | if (! advance()) 21 | return false; 22 | return true; 23 | } 24 | 25 | public boolean advance() { 26 | if (row < 8) { 27 | row++; 28 | return findSolution(); 29 | } 30 | if (neighbor != null) { 31 | if (! neighbor.advance()) 32 | return false; 33 | if (! neighbor.findSolution()) 34 | return false; 35 | } 36 | else 37 | return false; 38 | row = 1; 39 | return findSolution(); 40 | 41 | } 42 | 43 | private boolean canAttach(int testRow, int testColumn) { 44 | int columnDifference = testColumn - column; 45 | if ((row == testRow) || 46 | (row + columnDifference == testRow) || 47 | (row - columnDifference == testRow)) 48 | return true; 49 | if (neighbor != null) 50 | return neighbor.canAttach(testRow, testColumn); 51 | return false; 52 | } 53 | 54 | public void paint (Graphics g) { 55 | // first draw neighbor 56 | if (neighbor != null) 57 | neighbor.paint(g); 58 | // then draw ourself 59 | // x, y is upper left corner 60 | int x = (row - 1) * 50; 61 | int y = (column - 1) * 50; 62 | g.drawLine(x+5, y+45, x+45, y+45); 63 | g.drawLine(x+5, y+45, x+5, y+5); 64 | g.drawLine(x+45, y+45, x+45, y+5); 65 | g.drawLine(x+5, y+35, x+45, y+35); 66 | g.drawLine(x+5, y+5, x+15, y+20); 67 | g.drawLine(x+15, y+20, x+25, y+5); 68 | g.drawLine(x+25, y+5, x+35, y+20); 69 | g.drawLine(x+35, y+20, x+45, y+5); 70 | g.drawOval(x+20, y+20, 10, 10); 71 | } 72 | 73 | public void foo(Queen arg, Graphics g) { 74 | if (arg.row == 3) 75 | g.setColor(Color.red); 76 | } 77 | } 78 | 79 | public class QSolve extends Applet { 80 | 81 | private Queen lastQueen; 82 | 83 | public void init() { 84 | int i; 85 | lastQueen = null; 86 | for (i = 1; i <= 8; i++) { 87 | lastQueen = new Queen(i, lastQueen); 88 | lastQueen.findSolution(); 89 | } 90 | } 91 | 92 | public void paint(Graphics g) { 93 | // draw board 94 | for (int i = 0; i <= 8; i++) { 95 | g.drawLine(50 * i, 0, 50*i, 400); 96 | g.drawLine(0, 50 * i, 400, 50*i); 97 | } 98 | // draw queens 99 | lastQueen.paint(g); 100 | } 101 | 102 | public boolean mouseDown(java.awt.Event evt, int x, int y) { 103 | lastQueen.advance(); 104 | repaint(); 105 | return true; 106 | } 107 | 108 | } 109 | 110 | -------------------------------------------------------------------------------- /examples/8queens/references/QueenSolver.java: -------------------------------------------------------------------------------- 1 | // 2 | // Eight Queens puzzle written in Java 3 | // Written by Tim Budd, January 1996 4 | // revised for 1.3 event model July 2001 5 | // 6 | 7 | import java.awt.*; 8 | import java.awt.event.*; 9 | import javax.swing.*; 10 | 11 | class Queen { 12 | // data fields 13 | private int row; 14 | private int column; 15 | private Queen neighbor; 16 | 17 | // constructor 18 | Queen (int c, Queen n) { 19 | // initialize data fields 20 | row = 1; 21 | column = c; 22 | neighbor = n; 23 | } 24 | 25 | public boolean findSolution() { 26 | while (neighbor != null && neighbor.canAttach(row, column)) 27 | if (! advance()) 28 | return false; 29 | return true; 30 | } 31 | 32 | public boolean advance() { 33 | if (row < 8) { 34 | row++; 35 | return findSolution(); 36 | } 37 | if (neighbor != null) { 38 | if (! neighbor.advance()) 39 | return false; 40 | if (! neighbor.findSolution()) 41 | return false; 42 | } 43 | else 44 | return false; 45 | row = 1; 46 | return findSolution(); 47 | 48 | } 49 | 50 | private boolean canAttach(int testRow, int testColumn) { 51 | int columnDifference = testColumn - column; 52 | if ((row == testRow) || 53 | (row + columnDifference == testRow) || 54 | (row - columnDifference == testRow)) 55 | return true; 56 | if (neighbor != null) 57 | return neighbor.canAttach(testRow, testColumn); 58 | return false; 59 | } 60 | 61 | public void paint (Graphics g) { 62 | // first draw neighbor 63 | if (neighbor != null) 64 | neighbor.paint(g); 65 | // then draw ourself 66 | // x, y is upper left corner 67 | int x = (row - 1) * 50 + 10; 68 | int y = (column - 1) * 50 + 40; 69 | g.drawLine(x+5, y+45, x+45, y+45); 70 | g.drawLine(x+5, y+45, x+5, y+5); 71 | g.drawLine(x+45, y+45, x+45, y+5); 72 | g.drawLine(x+5, y+35, x+45, y+35); 73 | g.drawLine(x+5, y+5, x+15, y+20); 74 | g.drawLine(x+15, y+20, x+25, y+5); 75 | g.drawLine(x+25, y+5, x+35, y+20); 76 | g.drawLine(x+35, y+20, x+45, y+5); 77 | g.drawOval(x+20, y+20, 10, 10); 78 | } 79 | 80 | public void foo(Queen arg, Graphics g) { 81 | if (arg.row == 3) 82 | g.setColor(Color.red); 83 | } 84 | } 85 | 86 | public class QueenSolver extends JFrame { 87 | 88 | public static void main(String [ ] args) { 89 | QueenSolver world = new QueenSolver(); 90 | world.show(); 91 | } 92 | 93 | private Queen lastQueen = null; 94 | 95 | public QueenSolver() { 96 | setTitle("8 queens"); 97 | setSize(600, 500); 98 | for (int i = 1; i <= 8; i++) { 99 | lastQueen = new Queen(i, lastQueen); 100 | lastQueen.findSolution(); 101 | } 102 | addMouseListener(new MouseKeeper()); 103 | addWindowListener(new CloseQuit()); 104 | } 105 | 106 | public void paint(Graphics g) { 107 | super.paint(g); 108 | // draw board 109 | for (int i = 0; i <= 8; i++) { 110 | g.drawLine(50 * i + 10, 40, 50*i + 10, 440); 111 | g.drawLine(10, 50 * i + 40, 410, 50*i + 40); 112 | } 113 | g.drawString("Click Mouse for Next Solution", 20, 470); 114 | // draw queens 115 | lastQueen.paint(g); 116 | } 117 | 118 | private class CloseQuit extends WindowAdapter { 119 | public void windowClosing (WindowEvent e) { 120 | System.exit(0); 121 | } 122 | } 123 | 124 | private class MouseKeeper extends MouseAdapter { 125 | public void mousePressed (MouseEvent e) { 126 | lastQueen.advance(); 127 | repaint(); 128 | } 129 | } 130 | } 131 | 132 | -------------------------------------------------------------------------------- /examples/8queens/references/queen.cpp: -------------------------------------------------------------------------------- 1 | // eight queens puzzle in C++ 2 | // written by Tim Budd, Oregon State University, 1996 3 | // 4 | 5 | # include 6 | # define bool int // not all compilers yet support booleans 7 | 8 | class queen { 9 | public: 10 | // constructor 11 | queen (int, queen *); 12 | 13 | // find and print solutions 14 | bool findSolution(); 15 | bool advance(); 16 | void print(); 17 | 18 | private: 19 | // data fields 20 | int row; 21 | const int column; 22 | queen * neighbor; 23 | 24 | // internal method 25 | bool canAttack (int, int); 26 | }; 27 | 28 | queen::queen(int col, queen * ngh) 29 | : column(col), neighbor(ngh) 30 | { 31 | row = 1; 32 | } 33 | 34 | bool queen::canAttack (int testRow, int testColumn) 35 | { 36 | // test rows 37 | if (row == testRow) 38 | return true; 39 | 40 | // test diagonals 41 | int columnDifference = testColumn - column; 42 | if ((row + columnDifference == testRow) || 43 | (row - columnDifference == testRow)) 44 | return true; 45 | 46 | // try neighbor 47 | return neighbor && neighbor->canAttack(testRow, testColumn); 48 | } 49 | 50 | bool queen::findSolution() 51 | { 52 | // test position against neighbors 53 | while (neighbor && neighbor->canAttack (row, column)) 54 | if (! advance()) 55 | return false; 56 | 57 | // found a solution 58 | return true; 59 | } 60 | 61 | bool queen::advance() 62 | { 63 | if (row < 8) { 64 | row++; 65 | return findSolution(); 66 | } 67 | 68 | if (neighbor && ! neighbor->advance()) 69 | return false; 70 | 71 | row = 1; 72 | return findSolution(); 73 | } 74 | 75 | void queen::print() 76 | { 77 | if (neighbor) 78 | neighbor->print(); 79 | cout << "column " << column << " row " << row << '\n'; 80 | } 81 | 82 | void main() { 83 | queen * lastQueen = 0; 84 | 85 | for (int i = 1; i <= 8; i++) { 86 | lastQueen = new queen(i, lastQueen); 87 | if (! lastQueen->findSolution()) 88 | cout << "no solution\n"; 89 | } 90 | 91 | lastQueen->print(); 92 | } -------------------------------------------------------------------------------- /examples/8queens/references/queen.pas: -------------------------------------------------------------------------------- 1 | (* 2 | Eight Queens puzzle in Object Pascal 3 | Written by Tim Budd, Oregon State University, 1996 4 | *) 5 | Program EightQueen; 6 | 7 | type 8 | Queen = object 9 | (* data fields *) 10 | row : integer; 11 | column : integer; 12 | neighbor : Queen; 13 | 14 | (* initialization *) 15 | procedure initialize (col : integer; ngh : Queen); 16 | 17 | (* operations *) 18 | function canAttack (testRow, testColumn : integer) : boolean; 19 | function findSolution : boolean; 20 | function advance : boolean; 21 | procedure print; 22 | end; 23 | 24 | var 25 | neighbor, lastQueen : Queen; 26 | i : integer; 27 | 28 | procedure Queen.initialize (col : integer; ngh : Queen); 29 | begin 30 | (* initialize our column and neighbor values *) 31 | column := col; 32 | neighbor := ngh; 33 | 34 | (* start in row 1 *) 35 | row := 1; 36 | end; 37 | 38 | function Queen.canAttack (testRow, testColumn : integer) : boolean; 39 | var 40 | can : boolean; 41 | columnDifference : integer; 42 | begin 43 | (* first see if rows are equal *) 44 | can := (row = testRow); 45 | 46 | if not can then begin 47 | columnDifference := testColumn - column; 48 | if (row + columnDifference = testRow) or 49 | (row - columnDifference = testRow) then 50 | can := true; 51 | end; 52 | 53 | if (not can) and (neighbor <> nil) then 54 | can := neighbor.canAttack(testRow, testColumn); 55 | canAttack := can; 56 | end; 57 | 58 | function queen.findSolution : boolean; 59 | var 60 | done : boolean; 61 | begin 62 | done := false; 63 | findSolution := true; 64 | 65 | (* test positions *) 66 | if neighbor <> nil then 67 | while not done and neighbor.canAttack(row, column) do 68 | if not self.advance then begin 69 | findSolution := false; 70 | done := true; 71 | end; 72 | end; 73 | 74 | function queen.advance : boolean; 75 | begin 76 | advance := false; 77 | 78 | (* try next row *) 79 | if row < 8 then begin 80 | row := row + 1; 81 | advance := self.findSolution; 82 | end 83 | else begin 84 | 85 | (* can not go further *) 86 | (* move neighbor to next solution *) 87 | if neighbor <> nil then 88 | if not neighbor.advance then 89 | advance := false 90 | else begin 91 | (* start again in row 1 *) 92 | row := 1; 93 | advance := self.findSolution; 94 | end; 95 | end; 96 | end; 97 | 98 | procedure queen.print; 99 | begin 100 | if neighbor <> nil then 101 | neighbor.print; 102 | writeln('row ', row , ' column ', column); 103 | end; 104 | 105 | begin 106 | neighbor := nil; 107 | for i := 1 to 8 do begin 108 | (* create and initialize new queen *) 109 | new (lastqueen); 110 | lastQueen.initialize (i, neighbor); 111 | if not lastQueen.findSolution then 112 | writeln('no solution'); 113 | (* newest queen is next queen neighbor *) 114 | neighbor := lastQueen; 115 | end; 116 | 117 | lastQueen.print; 118 | 119 | for i := 1 to 8 do begin 120 | neighbor := lastQueen.neighbor; 121 | dispose (lastQueen); 122 | lastQueen := neighbor; 123 | end; 124 | end. -------------------------------------------------------------------------------- /examples/8queens/test_queens.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from queens import aligned, all_safe 4 | 5 | 6 | @mark.parametrize("source, target, expected", [ 7 | ((1, 1), (1, 2), True), 8 | ((1, 1), (2, 2), True), 9 | ((1, 1), (2, 1), True), 10 | ((1, 1), (1, 3), True), 11 | ((1, 1), (2, 3), False), 12 | ((1, 1), (3, 3), True), 13 | ((1, 1), (3, 1), True), 14 | ((1, 1), (3, 2), False), 15 | ]) 16 | def test_aligned(source, target, expected): 17 | assert expected == aligned(source, target) 18 | assert expected == aligned(target, source) 19 | 20 | 21 | @mark.parametrize("positions, expected", [ 22 | ([(1, 1)], True), 23 | ([(1, 1), (1, 2)], False), 24 | ([(1, 1), (2, 3)], True), 25 | ([(1, 1), (1, 2), (2, 3)], False), 26 | ([(1, 1), (2, 3), (3, 2)], False), 27 | ([(1, 1), (2, 3), (3, 5)], True), 28 | ([(4, 5), (1, 1), (3, 2), (2, 4)], True), 29 | ]) 30 | def test_all_safe(positions, expected): 31 | assert expected == all_safe(positions) 32 | -------------------------------------------------------------------------------- /examples/8queens/test_queens_and_guard.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from queens_and_guard import aligned 4 | 5 | 6 | @mark.parametrize("source, target, expected", [ 7 | ((1, 1), (1, 2), True), 8 | ((1, 1), (2, 2), True), 9 | ((1, 1), (2, 1), True), 10 | ((1, 1), (1, 3), True), 11 | ((1, 1), (2, 3), False), 12 | ((1, 1), (3, 3), True), 13 | ((1, 1), (3, 1), True), 14 | ((1, 1), (3, 2), False), 15 | ]) 16 | def test_aligned(source, target, expected): 17 | assert expected == aligned(source, target) 18 | assert expected == aligned(target, source) 19 | -------------------------------------------------------------------------------- /examples/8queens/test_random_queens_and_guard.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from random_queens_and_guard import aligned 4 | 5 | 6 | @mark.parametrize("source, target, expected", [ 7 | ((1, 1), (1, 2), True), 8 | ((1, 1), (2, 2), True), 9 | ((1, 1), (2, 1), True), 10 | ((1, 1), (1, 3), True), 11 | ((1, 1), (2, 3), False), 12 | ((1, 1), (3, 3), True), 13 | ((1, 1), (3, 1), True), 14 | ((1, 1), (3, 2), False), 15 | ]) 16 | def test_aligned(source, target, expected): 17 | assert expected == aligned(source, target) 18 | assert expected == aligned(target, source) 19 | -------------------------------------------------------------------------------- /examples/bingo/README.rst: -------------------------------------------------------------------------------- 1 | Class ``Bingo`` picks an item at random from a collection of items, 2 | removing the item from the collection, like to a bingo cage. 3 | 4 | To create a loaded instance, provide an iterable with the items:: 5 | 6 | >>> from bingo import Bingo 7 | >>> balls = set(range(3)) 8 | >>> cage = Bingo(balls) 9 | 10 | The ``pop`` method retrieves an item at random, removing it:: 11 | 12 | >>> b1 = cage.pop() 13 | >>> b2 = cage.pop() 14 | >>> b3 = cage.pop() 15 | >>> balls == {b1, b2, b3} 16 | True 17 | 18 | The ``__len__`` method reports the number of items available, 19 | which makes ``Bingo`` instances compatible with the ``len`` and 20 | ``bool`` built-in functions:: 21 | 22 | >>> len(cage) 23 | 0 24 | 25 | In boolean contexts, such as ``if`` or ``while`` conditions, 26 | Python uses ``bool`` to convert objects to booleans: 27 | 28 | >>> bool(cage) 29 | False 30 | 31 | That implicit use of ``bool`` allows this code to work:: 32 | 33 | >>> while cage: 34 | ... print('Next item:', cage.pop()) 35 | ... else: 36 | ... print('No more items available.') 37 | ... 38 | No more items available. 39 | 40 | 41 | Now testing ``__len__`` with a loaded ``cage``:: 42 | 43 | >>> cage = Bingo(balls) 44 | >>> len(cage) 45 | 3 46 | >>> results = [] 47 | >>> while cage: 48 | ... results.append(cage.pop()) 49 | ... 50 | >>> len(cage) 51 | 0 52 | >>> len(results) 53 | 3 54 | -------------------------------------------------------------------------------- /examples/bingo/bingo.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class Bingo: 5 | def __init__(self, items): 6 | self._items = list(items) 7 | random.shuffle(self._items) 8 | 9 | def pop(self): 10 | return self._items.pop() 11 | 12 | def __len__(self): 13 | return len(self._items) 14 | -------------------------------------------------------------------------------- /examples/bitset/bitops.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def count_ones(bigint): 5 | count = 0 6 | while bigint: 7 | count += bigint & 1 8 | bigint >>= 1 9 | return count 10 | 11 | 12 | def get_bit(bigint, index): 13 | return bool(bigint & (1 << index)) 14 | 15 | 16 | def set_bit(bigint, index): 17 | return bigint | (1 << index) 18 | 19 | 20 | def unset_bit(bigint, index): 21 | if get_bit(bigint, index): 22 | return bigint ^ (1 << index) 23 | return bigint 24 | 25 | 26 | def find_ones(bigint): 27 | index = 0 28 | while True: 29 | if bigint & 1: 30 | yield index 31 | bigint >>= 1 32 | if bigint == 0: 33 | break 34 | index += 1 35 | -------------------------------------------------------------------------------- /examples/bitset/solution/bitops.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def count_ones(bigint): 5 | count = 0 6 | while bigint: 7 | count += bigint & 1 8 | bigint >>= 1 9 | return count 10 | 11 | 12 | def get_bit(bigint, index): 13 | return bool(bigint & (1 << index)) 14 | 15 | 16 | def set_bit(bigint, index): 17 | return bigint | (1 << index) 18 | 19 | 20 | def unset_bit(bigint, index): 21 | if get_bit(bigint, index): 22 | return bigint ^ (1 << index) 23 | return bigint 24 | 25 | 26 | def find_ones(bigint): 27 | index = 0 28 | while True: 29 | if bigint & 1: 30 | yield index 31 | bigint >>= 1 32 | if bigint == 0: 33 | break 34 | index += 1 35 | -------------------------------------------------------------------------------- /examples/bitset/solution/empty.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import uintset 4 | 5 | class EmptySet: 6 | 7 | def __len__(self): 8 | return 0 9 | 10 | def __contains__(self, element): 11 | return False 12 | 13 | def __or__(self, other): 14 | return other 15 | 16 | __ror__ = __or__ 17 | 18 | def __and__(self, other): 19 | return self 20 | 21 | __rand__ = __and__ 22 | 23 | def __repr__(self): 24 | return 'EmptySet()' 25 | 26 | 27 | 28 | Empty = EmptySet() 29 | -------------------------------------------------------------------------------- /examples/bitset/solution/natural.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | class NaturalSet: 4 | 5 | def __len__(self): 6 | return sys.maxsize 7 | 8 | def __contains__(self, element): 9 | return element >= 0 10 | 11 | def __or__(self, other): 12 | return N 13 | 14 | __ror__ = __or__ 15 | 16 | def __and__(self, other): 17 | return other 18 | 19 | __rand__ = __and__ 20 | 21 | def __repr__(self): 22 | return 'NaturalSet()' 23 | 24 | 25 | 26 | N = NaturalSet() 27 | -------------------------------------------------------------------------------- /examples/bitset/solution/test_bitops.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bitops import count_ones, get_bit, set_bit, unset_bit, find_ones 4 | 5 | 6 | @pytest.mark.parametrize('bigint, want', [ 7 | (0, 0), 8 | (1, 1), 9 | (0b10, 1), 10 | (0b11, 2), 11 | (0b1_0101_0101, 5), 12 | (2**64, 1), 13 | (2**64 - 1, 64), 14 | ]) 15 | def test_count_ones(bigint, want): 16 | got = count_ones(bigint) 17 | assert got == want 18 | 19 | 20 | @pytest.mark.parametrize('bigint, index, want', [ 21 | (0, 0, 0), 22 | (0, 1, 0), 23 | (0, 100, 0), 24 | (1, 0, 1), 25 | (1, 1, 0), 26 | (0b10, 0, 0), 27 | (0b10, 1, 1), 28 | (0b1_0101_0101, 2, 1), 29 | (0b1_0101_0101, 3, 0), 30 | (0b1_0101_0101, 7, 0), 31 | (0b1_0101_0101, 8, 1), 32 | (2**64, 0, 0), 33 | (2**64, 64, 1), 34 | (2**64 - 1, 0, 1), 35 | (2**64 - 1, 1, 1), 36 | (2**64 - 1, 63, 1), 37 | ]) 38 | def test_get_bit(bigint, index, want): 39 | got = get_bit(bigint, index) 40 | assert got == want 41 | 42 | 43 | @pytest.mark.parametrize('bigint, index, want', [ 44 | (0, 0, 1), 45 | (1, 0, 1), 46 | (0, 8, 0b1_0000_0000), 47 | (1, 8, 0b1_0000_0001), 48 | (0b10, 0, 0b11), 49 | (0b11, 1, 0b11), 50 | (0b1_0101_0101, 1, 0b1_0101_0111), 51 | (0b1_0101_0101, 2, 0b1_0101_0101), 52 | (0b1_0101_0101, 7, 0b1_1101_0101), 53 | (0b1_0101_0101, 9, 0b11_0101_0101), 54 | (2**64, 0, 2**64 + 1), 55 | (2**64, 64, 2**64), 56 | (2**64, 65, 2**65 + 2**64), 57 | ]) 58 | def test_set_bit(bigint, index, want): 59 | got = set_bit(bigint, index) 60 | assert got == want 61 | 62 | 63 | @pytest.mark.parametrize('bigint, index, want', [ 64 | (0, 0, 0), 65 | (1, 0, 0), 66 | (0, 8, 0), 67 | (0b10, 0, 0b10), 68 | (0b11, 1, 0b01), 69 | (0b1_0101_0101, 0, 0b1_0101_0100), 70 | (0b1_0101_0101, 1, 0b1_0101_0101), 71 | (0b1_0101_0101, 2, 0b1_0101_0001), 72 | (0b1_0101_0101, 8, 0b0_0101_0101), 73 | (0b1_0101_0101, 9, 0b1_0101_0101), 74 | (2**64, 0, 2**64), 75 | (2**64 + 1, 0, 2**64), 76 | (2**64, 64, 0), 77 | ]) 78 | def test_unset_bit(bigint, index, want): 79 | got = unset_bit(bigint, index) 80 | assert got == want 81 | 82 | 83 | @pytest.mark.parametrize('bigint, want', [ 84 | (0, []), 85 | (1, [0]), 86 | (0b10, [1]), 87 | (0b11, [0, 1]), 88 | (0b1_0101_0101, [0, 2, 4, 6, 8]), 89 | (2**64, [64]), 90 | (2**64 - 1, list(range(0, 64))), 91 | ]) 92 | def test_find_ones(bigint, want): 93 | got = list(find_ones(bigint)) 94 | assert got == want 95 | -------------------------------------------------------------------------------- /examples/bitset/solution/test_empty.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from empty import Empty 6 | from uintset import UintSet 7 | 8 | 9 | def test_len(): 10 | assert len(Empty) == 0 11 | 12 | 13 | def test_contains(): 14 | assert 0 not in Empty 15 | assert 1 not in Empty 16 | assert -1 not in Empty 17 | assert 42 not in Empty 18 | assert sys.maxsize not in Empty 19 | 20 | 21 | union_cases = [ 22 | (Empty, UintSet(), UintSet()), 23 | (Empty, UintSet([1]), UintSet([1])), 24 | (UintSet([1]), Empty, UintSet([1])), 25 | (UintSet([1, 100]), Empty, UintSet([1, 100])), 26 | ] 27 | 28 | @pytest.mark.parametrize("first, second, want", union_cases) 29 | def test_or_op(first, second, want): 30 | got = first | second 31 | assert got == want 32 | 33 | 34 | intersection_cases = [ 35 | (Empty, UintSet()), 36 | (Empty, UintSet([1])), 37 | (UintSet([1]), Empty), 38 | (UintSet([1, 100]), Empty), 39 | ] 40 | 41 | 42 | @pytest.mark.parametrize("first, second", intersection_cases) 43 | def test_and_op(first, second): 44 | got = first & second 45 | assert got == Empty 46 | -------------------------------------------------------------------------------- /examples/bitset/solution/test_natural.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from natural import N 6 | from uintset import UintSet 7 | 8 | 9 | def test_len(): 10 | assert len(N) == sys.maxsize 11 | 12 | 13 | def test_contains(): 14 | assert 0 in N 15 | assert 1 in N 16 | assert -1 not in N 17 | assert 42 in N 18 | assert sys.maxsize in N 19 | 20 | 21 | union_cases = [ 22 | (N, UintSet()), 23 | (N, UintSet([1])), 24 | (UintSet([1]), N), 25 | (UintSet([1, 100]), N), 26 | ] 27 | 28 | @pytest.mark.parametrize("first, second", union_cases) 29 | def test_or_op(first, second): 30 | got = first | second 31 | assert got == N 32 | 33 | 34 | intersection_cases = [ 35 | (N, UintSet(), UintSet()), 36 | (N, UintSet([1]), UintSet([1])), 37 | (UintSet([1]), N, UintSet([1])), 38 | (UintSet([1, 100]), N, UintSet([1, 100])), 39 | ] 40 | 41 | @pytest.mark.parametrize("first, second, want", intersection_cases) 42 | def test_and_op(first, second, want): 43 | got = first & second 44 | assert got == want 45 | -------------------------------------------------------------------------------- /examples/bitset/solution/test_uintset.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from uintset import UintSet 4 | from uintset import INVALID_ELEMENT_MSG, INVALID_ITER_ARG_MSG 5 | 6 | 7 | def test_len(): 8 | s = UintSet() 9 | assert len(s) == 0 10 | 11 | 12 | def test_add(): 13 | s = UintSet() 14 | s.add(0) 15 | assert len(s) == 1 16 | 17 | 18 | def test_add_multiple(): 19 | s = UintSet() 20 | s.add(1) 21 | s.add(3) 22 | s.add(1) 23 | assert len(s) == 2 24 | 25 | 26 | def test_not_number(): 27 | s = UintSet() 28 | with pytest.raises(TypeError) as e: 29 | s.add('A') 30 | assert e.value.args[0] == INVALID_ELEMENT_MSG 31 | 32 | 33 | def test_add_negative(): 34 | s = UintSet() 35 | with pytest.raises(ValueError) as e: 36 | s.add(-1) 37 | assert e.value.args[0] == INVALID_ELEMENT_MSG 38 | 39 | 40 | def test_contains_zero_not(): 41 | s = UintSet() 42 | assert 0 not in s 43 | 44 | 45 | def test_contains_zero(): 46 | s = UintSet() 47 | s.add(0) 48 | assert 0 in s 49 | 50 | 51 | def test_new_from_iterable(): 52 | s = UintSet([1, 100, 3]) # beyond word 0 53 | assert len(s) == 3 54 | assert 1 in s 55 | assert 3 in s 56 | assert 100 in s 57 | 58 | 59 | def test_new_from_empty_iterable(): 60 | s = UintSet([]) 61 | assert len(s) == 0 62 | 63 | 64 | def test_iter(): 65 | s = UintSet([1, 5, 0, 3, 2, 4]) 66 | assert list(s) == [0, 1, 2, 3, 4, 5] 67 | 68 | 69 | def test_repr_empty(): 70 | s = UintSet() 71 | assert repr(s) == 'UintSet()' 72 | 73 | 74 | def test_repr(): 75 | s = UintSet([1, 5, 0, 3, 2, 4]) 76 | assert repr(s) == 'UintSet({0, 1, 2, 3, 4, 5})' 77 | 78 | 79 | @pytest.mark.parametrize("first, second, want", [ 80 | (UintSet(), UintSet(), True), 81 | (UintSet([1]), UintSet(), False), 82 | (UintSet(), UintSet([1]), False), 83 | (UintSet([1, 100]), UintSet([1, 101]), False), 84 | (UintSet([1, 100]), [1, 101], False), 85 | ]) 86 | def test_eq(first, second, want): 87 | assert (first == second) is want 88 | 89 | 90 | union_cases = [ 91 | (UintSet(), UintSet(), UintSet()), 92 | (UintSet([1]), UintSet(), UintSet([1])), 93 | (UintSet(), UintSet([1]), UintSet([1])), 94 | (UintSet([1, 100]), UintSet([100, 1]), UintSet([100, 1])), # beyond word 0 95 | (UintSet([1, 100]), UintSet([2]), UintSet([1, 2, 100])), 96 | ] 97 | 98 | 99 | @pytest.mark.parametrize("first, second, want", union_cases) 100 | def test_or_op(first, second, want): 101 | got = first | second 102 | assert got == want 103 | 104 | 105 | @pytest.mark.parametrize("first, second, want", union_cases) 106 | def test_union(first, second, want): 107 | got = first.union(second) 108 | assert got == want 109 | 110 | 111 | @pytest.mark.parametrize("first, second, want", union_cases) 112 | def test_union_iterable(first, second, want): 113 | it = list(second) 114 | got = first.union(it) 115 | assert got == want 116 | 117 | 118 | def test_union_not_iterable(): 119 | first = UintSet() 120 | with pytest.raises(TypeError) as e: 121 | first.union(1) 122 | assert e.value.args[0] == INVALID_ITER_ARG_MSG 123 | 124 | 125 | def test_union_iterable_multiple(): 126 | s = UintSet([1, 3, 5]) 127 | it1 = [2, 4, 6] 128 | it2 = {10, 11, 12} 129 | want = UintSet({1, 2, 3, 4, 5, 6, 10, 11, 12}) 130 | got = s.union(it1, it2) 131 | assert got == want 132 | 133 | 134 | intersection_cases = [ 135 | (UintSet(), UintSet(), UintSet()), 136 | (UintSet([1]), UintSet(), UintSet()), 137 | (UintSet([1]), UintSet([1]), UintSet([1])), 138 | (UintSet([1, 100]), UintSet([100, 1]), UintSet([100, 1])), # beyond word 0 139 | (UintSet([1, 100]), UintSet([2]), UintSet()), 140 | (UintSet([1, 2, 3, 4]), UintSet([2, 3, 5]), UintSet([2, 3])), 141 | ] 142 | 143 | 144 | @pytest.mark.parametrize("first, second, want", intersection_cases) 145 | def test_and_op(first, second, want): 146 | got = first & second 147 | assert got == want 148 | 149 | 150 | @pytest.mark.parametrize("first, second, want", intersection_cases) 151 | def test_intersection(first, second, want): 152 | got = first.intersection(second) 153 | assert got == want 154 | 155 | ''' 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | @pytest.fixture 168 | def symmetric_diff_cases(): 169 | return [ 170 | (UintSet(), UintSet(), UintSet()), 171 | (UintSet([1]), UintSet(), UintSet([1])), 172 | (UintSet([1]), UintSet([1]), UintSet()), 173 | (UintSet([1, 100]), UintSet([100, 1]), UintSet()), # beyond word 0 174 | (UintSet([1, 100]), UintSet([2]), UintSet([1, 100, 2])), 175 | (UintSet([1, 2, 3, 4]), UintSet([2, 3, 5]), UintSet([1, 4, 5])), 176 | ] 177 | 178 | 179 | def test_xor_op(symmetric_diff_cases): 180 | for s1, s2, want in symmetric_diff_cases: 181 | got = s1 ^ s2 182 | assert len(got) == len(want) 183 | assert got == want 184 | 185 | 186 | def test_symmetric_difference(symmetric_diff_cases): 187 | for s1, s2, want in symmetric_diff_cases: 188 | got = s1.symmetric_difference(s2) 189 | assert len(got) == len(want) 190 | assert got == want 191 | 192 | 193 | @pytest.fixture 194 | def difference_cases(): 195 | return [ 196 | (UintSet(), UintSet(), UintSet()), 197 | (UintSet([1]), UintSet(), UintSet([1])), 198 | (UintSet([1]), UintSet([1]), UintSet()), 199 | (UintSet([1, 100]), UintSet([100, 1]), UintSet()), # beyond word 0 200 | (UintSet([1, 100]), UintSet([2]), UintSet([1, 100])), 201 | (UintSet([1, 2, 3, 4]), UintSet([2, 3, 5]), UintSet([1, 4])), 202 | ] 203 | 204 | 205 | def test_sub_op(difference_cases): 206 | for s1, s2, want in difference_cases: 207 | got = s1 - s2 208 | assert len(got) == len(want) 209 | assert got == want 210 | 211 | 212 | def test_difference(difference_cases): 213 | for s1, s2, want in difference_cases: 214 | got = s1.difference(s2) 215 | assert len(got) == len(want) 216 | assert got == want 217 | 218 | 219 | def test_remove(): 220 | test_cases = [ 221 | (UintSet([0]), 0, UintSet()), 222 | (UintSet([1, 2, 3]), 2, UintSet([1, 3])), 223 | ] 224 | for s, elem, want in test_cases: 225 | s.remove(elem) 226 | assert s == want 227 | 228 | 229 | def test_remove_all(): 230 | elems = [1, 2, 3] 231 | set = UintSet(elems) 232 | for e in elems: 233 | set.remove(e) 234 | assert len(set) == 0 235 | 236 | 237 | def test_remove_not_found(): 238 | s = UintSet() 239 | elem = 1 240 | with pytest.raises(KeyError) as excinfo: 241 | s.remove(elem) 242 | assert str(excinfo.value) == str(elem) 243 | 244 | 245 | def test_remove_not_found_2(): 246 | s = UintSet([1, 3]) 247 | elem = 2 248 | with pytest.raises(KeyError) as excinfo: 249 | s.remove(elem) 250 | assert str(excinfo.value) == str(elem) 251 | 252 | 253 | def test_pop_not_found(): 254 | s = UintSet() 255 | with pytest.raises(KeyError) as excinfo: 256 | s.pop() 257 | assert 'pop from an empty set' in str(excinfo.value) 258 | 259 | 260 | def test_pop(): 261 | test_cases = [0, 1, WORD_SIZE-1, WORD_SIZE, WORD_SIZE+1, 100] 262 | for want in test_cases: 263 | s = UintSet([want]) 264 | got = s.pop() 265 | assert got == want 266 | assert len(s) == 0 267 | 268 | 269 | def test_pop_all(): 270 | want = [100, 1, 0] 271 | s = UintSet(want) 272 | got = [] 273 | while s: 274 | got.append(s.pop()) 275 | assert len(s) == (len(want) - len(got)) 276 | assert got == want 277 | ''' 278 | -------------------------------------------------------------------------------- /examples/bitset/solution/uintset.py: -------------------------------------------------------------------------------- 1 | import bitops 2 | 3 | 4 | INVALID_ELEMENT_MSG = "'UintSet' elements must be integers >= 0" 5 | INVALID_ITER_ARG_MSG = "expected UintSet or iterable argument" 6 | 7 | class UintSet: 8 | 9 | def __init__(self, elements=None): 10 | self._bigint = 0 11 | if elements: 12 | for e in elements: 13 | self.add(e) 14 | 15 | def __len__(self): 16 | return bitops.count_ones(self._bigint) 17 | 18 | def add(self, elem): 19 | try: 20 | self._bigint = bitops.set_bit(self._bigint, elem) 21 | except TypeError: 22 | raise TypeError(INVALID_ELEMENT_MSG) 23 | except ValueError: 24 | raise ValueError(INVALID_ELEMENT_MSG) 25 | 26 | def __contains__(self, elem): 27 | try: 28 | return bitops.get_bit(self._bigint, elem) 29 | except TypeError: 30 | raise TypeError(INVALID_ELEMENT_MSG) 31 | except ValueError: 32 | raise ValueError(INVALID_ELEMENT_MSG) 33 | 34 | def __iter__(self): 35 | return bitops.find_ones(self._bigint) 36 | 37 | def __repr__(self): 38 | elements = ', '.join(str(e) for e in self) 39 | if elements: 40 | elements = '{' + elements + '}' 41 | return f'UintSet({elements})' 42 | 43 | def __eq__(self, other): 44 | return isinstance(other, self.__class__) and self._bigint == other._bigint 45 | 46 | def __or__(self, other): 47 | cls = self.__class__ 48 | if isinstance(other, cls): 49 | res = cls() 50 | res._bigint = self._bigint | other._bigint 51 | return res 52 | return NotImplemented 53 | 54 | def union(self, *others): 55 | cls = self.__class__ 56 | res = cls() 57 | res._bigint = self._bigint 58 | for other in others: 59 | if isinstance(other, cls): 60 | res._bigint |= other._bigint 61 | try: 62 | second = cls(other) 63 | except TypeError: 64 | raise TypeError(INVALID_ITER_ARG_MSG) 65 | else: 66 | res._bigint |= second._bigint 67 | return res 68 | 69 | def __and__(self, other): 70 | cls = self.__class__ 71 | if isinstance(other, cls): 72 | res = cls() 73 | res._bigint = self._bigint & other._bigint 74 | return res 75 | return NotImplemented 76 | 77 | def intersection(self, *others): 78 | cls = self.__class__ 79 | res = cls() 80 | res._bigint = self._bigint 81 | for other in others: 82 | if isinstance(other, cls): 83 | res._bigint &= other._bigint 84 | try: 85 | second = cls(other) 86 | except TypeError: 87 | raise TypeError(INVALID_ITER_ARG_MSG) 88 | else: 89 | res._bigint &= second._bigint 90 | return res 91 | 92 | -------------------------------------------------------------------------------- /examples/bitset/test_bitops.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bitops import count_ones, get_bit, set_bit, unset_bit, find_ones 4 | 5 | 6 | @pytest.mark.parametrize('bigint, want', [ 7 | (0, 0), 8 | (1, 1), 9 | (0b10, 1), 10 | (0b11, 2), 11 | (0b1_0101_0101, 5), 12 | (2**64, 1), 13 | (2**64 - 1, 64), 14 | ]) 15 | def test_count_ones(bigint, want): 16 | got = count_ones(bigint) 17 | assert got == want 18 | 19 | 20 | @pytest.mark.parametrize('bigint, index, want', [ 21 | (0, 0, 0), 22 | (0, 1, 0), 23 | (0, 100, 0), 24 | (1, 0, 1), 25 | (1, 1, 0), 26 | (0b10, 0, 0), 27 | (0b10, 1, 1), 28 | (0b1_0101_0101, 2, 1), 29 | (0b1_0101_0101, 3, 0), 30 | (0b1_0101_0101, 7, 0), 31 | (0b1_0101_0101, 8, 1), 32 | (2**64, 0, 0), 33 | (2**64, 64, 1), 34 | (2**64 - 1, 0, 1), 35 | (2**64 - 1, 1, 1), 36 | (2**64 - 1, 63, 1), 37 | ]) 38 | def test_get_bit(bigint, index, want): 39 | got = get_bit(bigint, index) 40 | assert got == want 41 | 42 | 43 | @pytest.mark.parametrize('bigint, index, want', [ 44 | (0, 0, 1), 45 | (1, 0, 1), 46 | (0, 8, 0b1_0000_0000), 47 | (1, 8, 0b1_0000_0001), 48 | (0b10, 0, 0b11), 49 | (0b11, 1, 0b11), 50 | (0b1_0101_0101, 1, 0b1_0101_0111), 51 | (0b1_0101_0101, 2, 0b1_0101_0101), 52 | (0b1_0101_0101, 7, 0b1_1101_0101), 53 | (0b1_0101_0101, 9, 0b11_0101_0101), 54 | (2**64, 0, 2**64 + 1), 55 | (2**64, 64, 2**64), 56 | (2**64, 65, 2**65 + 2**64), 57 | ]) 58 | def test_set_bit(bigint, index, want): 59 | got = set_bit(bigint, index) 60 | assert got == want 61 | 62 | 63 | @pytest.mark.parametrize('bigint, index, want', [ 64 | (0, 0, 0), 65 | (1, 0, 0), 66 | (0, 8, 0), 67 | (0b10, 0, 0b10), 68 | (0b11, 1, 0b01), 69 | (0b1_0101_0101, 0, 0b1_0101_0100), 70 | (0b1_0101_0101, 1, 0b1_0101_0101), 71 | (0b1_0101_0101, 2, 0b1_0101_0001), 72 | (0b1_0101_0101, 8, 0b0_0101_0101), 73 | (0b1_0101_0101, 9, 0b1_0101_0101), 74 | (2**64, 0, 2**64), 75 | (2**64 + 1, 0, 2**64), 76 | (2**64, 64, 0), 77 | ]) 78 | def test_unset_bit(bigint, index, want): 79 | got = unset_bit(bigint, index) 80 | assert got == want 81 | 82 | 83 | @pytest.mark.parametrize('bigint, want', [ 84 | (0, []), 85 | (1, [0]), 86 | (0b10, [1]), 87 | (0b11, [0, 1]), 88 | (0b1_0101_0101, [0, 2, 4, 6, 8]), 89 | (2**64, [64]), 90 | (2**64 - 1, list(range(0, 64))), 91 | ]) 92 | def test_find_ones(bigint, want): 93 | got = list(find_ones(bigint)) 94 | assert got == want 95 | -------------------------------------------------------------------------------- /examples/camping/data_class/README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Camping Budget Example 3 | ====================== 4 | 5 | Class ``camping.Camper`` represents a contributor to the budget of a camping trip. 6 | 7 | >>> from camping import Camper 8 | >>> a = Camper('Anna') 9 | >>> a.pay(33) 10 | >>> a.display() 11 | 'Anna paid $ 33.00' 12 | 13 | A camper can be created with an initial balance: 14 | 15 | >>> c = Camper('Charlie', 9) 16 | 17 | The ``.display()`` method right-justifies the names taking into account the 18 | longest name so far, so that multiple calls show aligned columns: 19 | 20 | >>> for camper in [a, c]: 21 | ... print(camper.display()) 22 | Anna paid $ 33.00 23 | Charlie paid $ 9.00 24 | 25 | 26 | Class ``camping.Budget`` represents the budget for a camping trip 27 | in which campers who pitched in more than average need to be 28 | reimbursed by the others. 29 | 30 | >>> from camping import Budget 31 | >>> b = Budget('Debbie', 'Ann', 'Bob', 'Charlie') 32 | >>> b.total() 33 | 0.0 34 | >>> b.people() 35 | ['Ann', 'Bob', 'Charlie', 'Debbie'] 36 | >>> b.contribute("Bob", 50.00) 37 | >>> b.contribute("Debbie", 40.00) 38 | >>> b.contribute("Ann", 10.00) 39 | >>> b.total() 40 | 100.0 41 | 42 | The ``report`` method lists who should receive or pay, and the 43 | respective amounts. 44 | 45 | >>> b.report() 46 | Total: $ 100.00; individual share: $ 25.00 47 | ------------------------------------------ 48 | Charlie paid $ 0.00, balance: $ -25.00 49 | Ann paid $ 10.00, balance: $ -15.00 50 | Debbie paid $ 40.00, balance: $ 15.00 51 | Bob paid $ 50.00, balance: $ 25.00 52 | 53 | 54 | 55 | ------------- 56 | Running tests 57 | ------------- 58 | 59 | To run these doctests on **bash** use this command line:: 60 | 61 | $ python3 -m doctest README.rst 62 | 63 | 64 | -------- 65 | Exercise 66 | -------- 67 | 68 | .. tip:: To practice TDD with doctests, this is a good option to run the tests:: 69 | 70 | $ python3 -m doctest README.rst -o REPORT_ONLY_FIRST_FAILURE 71 | 72 | 73 | 1. Allow adding contributors later 74 | ---------------------------------- 75 | 76 | As implemented, ``camping.Budget`` does not allow adding contributor names after the budget is created. 77 | Implement a method to allow adding a contributor with an optional contribution. 78 | 79 | An alternative to such a method would be to change the ``contribute`` method, 80 | removing the code that tests whether the contributor's name is found in ``self._campers``. 81 | This would be simpler, but is there a drawback to this approach? Discuss. 82 | -------------------------------------------------------------------------------- /examples/camping/data_class/camping.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import ClassVar 3 | import operator 4 | 5 | @dataclass 6 | class Camper: 7 | 8 | name: str 9 | paid: float = 0.0 10 | 11 | max_name_len: ClassVar[int] = 0 12 | template = '{name:>{name_len}} paid ${paid:7.2f}' 13 | 14 | def __post_init__(self): 15 | if len(self.name) > Camper.max_name_len: 16 | Camper.max_name_len = len(self.name) 17 | 18 | def pay(self, amount): 19 | self.paid += float(amount) 20 | 21 | def display(self): 22 | return Camper.template.format( 23 | name = self.name, 24 | name_len = self.max_name_len, 25 | paid = self.paid, 26 | ) 27 | 28 | class Budget: 29 | """ 30 | Class ``camping.Budget`` represents the budget for a camping trip. 31 | """ 32 | 33 | def __init__(self, *names): 34 | self._campers = {name: Camper(name) for name in names} 35 | 36 | def total(self): 37 | return sum(c.paid for c in self._campers.values()) 38 | 39 | def people(self): 40 | return sorted(self._campers) 41 | 42 | def contribute(self, name, amount): 43 | if name not in self._campers: 44 | raise LookupError("Name not in budget") 45 | self._campers[name].pay(amount) 46 | 47 | def individual_share(self): 48 | return self.total() / len(self._campers) 49 | 50 | def report(self): 51 | """report displays names and amounts due or owed""" 52 | share = self.individual_share() 53 | heading_tpl = 'Total: $ {:.2f}; individual share: $ {:.2f}' 54 | print(heading_tpl.format(self.total(), share)) 55 | print("-"* 42) 56 | sorted_campers = sorted(self._campers.values(), key=operator.attrgetter('paid')) 57 | for camper in sorted_campers: 58 | balance = f'balance: $ {camper.paid - share:7.2f}' 59 | print(camper.display(), balance, sep=', ') 60 | -------------------------------------------------------------------------------- /examples/camping/one_class/README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Camping Budget Example 3 | ====================== 4 | 5 | Class ``camping.Budget`` represents the budget for a camping trip 6 | in which the people who pitched in more than average need to be 7 | reimbursed by the others. 8 | 9 | >>> from camping import Budget 10 | >>> b = Budget('Debbie', 'Ann', 'Bob', 'Charlie') 11 | >>> b.total() 12 | 0.0 13 | >>> b.people() 14 | ['Ann', 'Bob', 'Charlie', 'Debbie'] 15 | >>> b.contribute("Bob", 50.00) 16 | >>> b.contribute("Debbie", 40.00) 17 | >>> b.contribute("Ann", 10.00) 18 | >>> b.total() 19 | 100.0 20 | 21 | The ``report`` method lists who should receive or pay, and the 22 | respective amounts. 23 | 24 | >>> b.report() 25 | Total: $ 100.00; individual share: $ 25.00 26 | ------------------------------------------ 27 | Charlie paid $ 0.00, balance: $ -25.00 28 | Ann paid $ 10.00, balance: $ -15.00 29 | Debbie paid $ 40.00, balance: $ 15.00 30 | Bob paid $ 50.00, balance: $ 25.00 31 | 32 | 33 | The data used by ``report`` is computed by the `balances` method: 34 | 35 | >>> b.balances() 36 | [(-15.0, 'Ann', 10.0), (25.0, 'Bob', 50.0), (-25.0, 'Charlie', 0.0), (15.0, 'Debbie', 40.0)] 37 | 38 | 39 | ------------- 40 | Running tests 41 | ------------- 42 | 43 | To run these doctests on **bash** use this command line:: 44 | 45 | $ python3 -m doctest README.rst 46 | 47 | 48 | -------- 49 | Exercise 50 | -------- 51 | 52 | .. tip:: To practice TDD with doctests, this is a good option to run the tests:: 53 | 54 | $ python3 -m doctest README.rst -o REPORT_ONLY_FIRST_FAILURE 55 | 56 | 57 | 1. Allow adding contributors later 58 | ---------------------------------- 59 | 60 | As implemented, ``camping.Budget`` does not allow adding contributor names after the budget is created. 61 | Implement a method to allow adding a contributor with an optional contribution. 62 | 63 | An alternative to such a method would be to change the ``contribute`` method, 64 | removing the code that tests whether the contributor's name is found in ``_contributions``. 65 | This would be simpler, but is there a drawback to this approach? Discuss. 66 | -------------------------------------------------------------------------------- /examples/camping/one_class/camping.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class ``camping.Budget`` represents the budget for a camping trip. 3 | """ 4 | 5 | class Budget: 6 | 7 | def __init__(self, *names): 8 | self._campers = {name:0.0 for name in names} 9 | 10 | def total(self): 11 | return sum(self._campers.values()) 12 | 13 | def people(self): 14 | return sorted(self._campers) 15 | 16 | def contribute(self, name, amount): 17 | if name not in self._campers: 18 | raise LookupError("Person not in budget") 19 | self._campers[name] += amount 20 | 21 | def individual_share(self): 22 | return self.total() / len(self._campers) 23 | 24 | def balances(self): 25 | share = self.individual_share() 26 | result = [] 27 | for name in self.people(): 28 | paid = self._campers[name] 29 | result.append((paid - share, name, paid)) 30 | return result 31 | 32 | def report(self): 33 | """report displays names and amounts due or owed""" 34 | heading_tpl = 'Total: $ {:.2f}; individual share: $ {:.2f}' 35 | print(heading_tpl.format(self.total(), self.individual_share())) 36 | print("-"* 42) 37 | name_len = max(len(name) for name in self._campers) 38 | for balance, name, paid in sorted(self.balances()): 39 | print(f"{name:>{name_len}} paid ${paid:6.2f}, balance: $ {balance:6.2f}") 40 | -------------------------------------------------------------------------------- /examples/camping/two_classes/README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Camping Budget Example 3 | ====================== 4 | 5 | Class ``camping.Camper`` represents a contributor to the budget of a camping trip. 6 | 7 | >>> from camping import Camper 8 | >>> a = Camper('Anna') 9 | >>> a.pay(33) 10 | >>> a.display() 11 | 'Anna paid $ 33.00' 12 | 13 | A camper can be created with an initial balance: 14 | 15 | >>> c = Camper('Charlie', 9) 16 | 17 | The ``.display()`` method right-justifies the names taking into account the 18 | longest name so far, so that multiple calls show aligned columns: 19 | 20 | >>> for camper in [a, c]: 21 | ... print(camper.display()) 22 | Anna paid $ 33.00 23 | Charlie paid $ 9.00 24 | 25 | 26 | Class ``camping.Budget`` represents the budget for a camping trip 27 | in which campers who pitched in more than average need to be 28 | reimbursed by the others. 29 | 30 | >>> from camping import Budget 31 | >>> b = Budget('Debbie', 'Ann', 'Bob', 'Charlie') 32 | >>> b.total() 33 | 0.0 34 | >>> b.people() 35 | ['Ann', 'Bob', 'Charlie', 'Debbie'] 36 | >>> b.contribute("Bob", 50.00) 37 | >>> b.contribute("Debbie", 40.00) 38 | >>> b.contribute("Ann", 10.00) 39 | >>> b.total() 40 | 100.0 41 | 42 | The ``report`` method lists who should receive or pay, and the 43 | respective amounts. 44 | 45 | >>> b.report() 46 | Total: $ 100.00; individual share: $ 25.00 47 | ------------------------------------------ 48 | Charlie paid $ 0.00, balance: $ -25.00 49 | Ann paid $ 10.00, balance: $ -15.00 50 | Debbie paid $ 40.00, balance: $ 15.00 51 | Bob paid $ 50.00, balance: $ 25.00 52 | 53 | 54 | 55 | ------------- 56 | Running tests 57 | ------------- 58 | 59 | To run these doctests on **bash** use this command line:: 60 | 61 | $ python3 -m doctest README.rst 62 | 63 | 64 | -------- 65 | Exercise 66 | -------- 67 | 68 | .. tip:: To practice TDD with doctests, this is a good option to run the tests:: 69 | 70 | $ python3 -m doctest README.rst -f 71 | 72 | 73 | 1. Allow adding contributors later 74 | ---------------------------------- 75 | 76 | As implemented, ``camping.Budget`` does not allow adding contributor names after the budget is created. 77 | Implement a method to allow adding a contributor with an optional contribution. 78 | 79 | An alternative to such a method would be to change the ``contribute`` method, 80 | removing the code that tests whether the contributor's name is found in ``self._campers``. 81 | This would be simpler, but is there a drawback to this approach? Discuss. 82 | -------------------------------------------------------------------------------- /examples/camping/two_classes/camping.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | class Camper: 4 | 5 | max_name_len = 0 6 | template = '{name:>{name_len}} paid ${paid:7.2f}' 7 | 8 | def __init__(self, name, paid=0.0): 9 | self.name = name 10 | self.paid = float(paid) 11 | if len(name) > Camper.max_name_len: 12 | Camper.max_name_len = len(name) 13 | 14 | def pay(self, amount): 15 | self.paid += float(amount) 16 | 17 | def display(self): 18 | return Camper.template.format( 19 | name = self.name, 20 | name_len = self.max_name_len, 21 | paid = self.paid, 22 | ) 23 | 24 | class Budget: 25 | """ 26 | Class ``camping.Budget`` represents the budget for a camping trip. 27 | """ 28 | 29 | def __init__(self, *names): 30 | self._campers = {name: Camper(name) for name in names} 31 | 32 | def total(self): 33 | return sum(c.paid for c in self._campers.values()) 34 | 35 | def people(self): 36 | return sorted(self._campers) 37 | 38 | def contribute(self, name, amount): 39 | if name not in self._campers: 40 | raise LookupError("Name not in budget") 41 | self._campers[name].pay(amount) 42 | 43 | def individual_share(self): 44 | return self.total() / len(self._campers) 45 | 46 | def report(self): 47 | """report displays names and amounts due or owed""" 48 | share = self.individual_share() 49 | heading_tpl = 'Total: $ {:.2f}; individual share: $ {:.2f}' 50 | print(heading_tpl.format(self.total(), share)) 51 | print("-"* 42) 52 | sorted_campers = sorted(self._campers.values(), key=operator.attrgetter('paid')) 53 | for camper in sorted_campers: 54 | balance = f'balance: $ {camper.paid - share:7.2f}' 55 | print(camper.display(), balance, sep=', ') 56 | -------------------------------------------------------------------------------- /examples/coordinate.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class Coordinate: 5 | lat: float 6 | long: float 7 | 8 | def __str__(self): 9 | ns = 'NS'[self.lat < 0] 10 | we = 'EW'[self.long < 0] 11 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}' 12 | 13 | 14 | sp = Coordinate(-23, 'bla') 15 | 16 | print(sp) 17 | 18 | -------------------------------------------------------------------------------- /examples/dc/resource.py: -------------------------------------------------------------------------------- 1 | """ 2 | Media resource description class with subset of the Dubin Core fields. 3 | 4 | Default field values: 5 | 6 | >>> r = Resource() 7 | >>> r 8 | Resource( 9 | identifier = '0000000000000', 10 | title = '', 11 | creators = [], 12 | date = '', 13 | type = '', 14 | description = '', 15 | language = '', 16 | subjects = [], 17 | ) 18 | 19 | A complete resource record: 20 | 21 | >>> description = 'A hands-on guide to idiomatic Python code.' 22 | >>> book = Resource('9781491946008', 'Fluent Python', 23 | ... ['Luciano Ramalho'], '2015-08-20', 'book', description, 24 | ... 'EN', ['computer programming', 'Python']) 25 | >>> book 26 | Resource( 27 | identifier = '9781491946008', 28 | title = 'Fluent Python', 29 | creators = ['Luciano Ramalho'], 30 | date = '2015-08-20', 31 | type = 'book', 32 | description = 'A hands-on guide to idiomatic Python code.', 33 | language = 'EN', 34 | subjects = ['computer programming', 'Python'], 35 | ) 36 | 37 | """ 38 | 39 | from dataclasses import dataclass, field, fields 40 | from typing import List 41 | 42 | @dataclass 43 | class Resource: 44 | """Media resource description.""" 45 | identifier: str = "0" * 13 46 | title: str = "" 47 | creators: List[str] = field(default_factory=list) 48 | date: str = "" 49 | type: str = "" 50 | description: str = "" 51 | language: str = "" 52 | subjects: List[str] = field(default_factory=list) 53 | 54 | 55 | def __repr__(self): 56 | cls = self.__class__ 57 | cls_name = cls.__name__ 58 | res = [f'{cls_name}('] 59 | for field in fields(cls): 60 | value = getattr(self, field.name) 61 | res.append(f' {field.name} = {value!r},') 62 | res.append(f')') 63 | return '\n'.join(res) 64 | -------------------------------------------------------------------------------- /examples/finstory/README.rst: -------------------------------------------------------------------------------- 1 | Financial History Example 2 | ========================= 3 | 4 | ``FinancialHistory`` instances keep track of a person's expenses and income. 5 | 6 | .. note:: This example is adapted from *Smalltalk-80: the language*, 7 | by Adele Goldberg and Dave Robson (Addison-Wesley, 1989). 8 | 9 | The interface of ``FinancialHistory`` consists of: 10 | 11 | ``__init__(amount)`` 12 | Begin a financial history with an amount given (default: 0). 13 | 14 | ``__repr__()`` 15 | Return string representation of the instance, for debugging. 16 | 17 | ``receive(amount, source)`` 18 | Receive an amount from the named source. 19 | 20 | ``spend(amount, reason)`` 21 | Spend an amount for the named reason. 22 | 23 | ``balance()`` 24 | Return total amount currenly on hand. 25 | 26 | ``received_from(source)`` 27 | Return total amount received from the given source. 28 | 29 | ``spent_for(reason)`` 30 | Return total amount spent for the given reason. 31 | 32 | 33 | Demonstration 34 | ------------- 35 | 36 | Create ``FinancialHistory`` with $ 100:: 37 | 38 | >>> from finstory import FinancialHistory 39 | >>> h = FinancialHistory(100) 40 | >>> h 41 | 42 | 43 | Spend some money:: 44 | 45 | >>> h.spend(39.95, 'meal') 46 | >>> h 47 | 48 | 49 | Decimals can be formatted like floats:: 50 | 51 | >>> print(f'${h.balance:0.2f}') 52 | $60.05 53 | 54 | Get more money:: 55 | 56 | >>> h.receive(1000.01, "Molly's game") 57 | >>> h.receive(10.00, 'found on street') 58 | >>> h 59 | 60 | 61 | Spend more money:: 62 | 63 | >>> h.spend(55.36, 'meal') 64 | >>> h.spend(26.65, 'meal') 65 | >>> h.spend(300, 'concert') 66 | >>> h 67 | 68 | 69 | Check amount spent on meals:: 70 | 71 | >>> h.spent_for('meal') 72 | Decimal('121.96') 73 | 74 | Check amount spent on travel (zero): 75 | 76 | >>> h.spent_for('travel') 77 | Decimal('0') 78 | -------------------------------------------------------------------------------- /examples/finstory/deducstory.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import decimal 3 | 4 | from finstory import FinancialHistory, new_decimal 5 | 6 | class DeductibleHistory(FinancialHistory): 7 | 8 | def __init__(self, initial_balance=0.0): 9 | super().__init__(initial_balance) 10 | self._deductions = decimal.Decimal(0) 11 | 12 | def spend(self, amount, reason, deducting=0.0): 13 | """Record expense with partial deduction""" 14 | super().spend(amount, reason) 15 | if deducting: 16 | self._deductions += new_decimal(deducting) 17 | 18 | def spend_deductible(self, amount, reason): 19 | """Record expense with full deduction""" 20 | self.spend(amount, reason, amount) 21 | 22 | @property 23 | def deductions(self): 24 | return self._deductions 25 | -------------------------------------------------------------------------------- /examples/finstory/deducstory.rst: -------------------------------------------------------------------------------- 1 | Financial History with Deductible Expenses Example 2 | ================================================== 3 | 4 | ``DeductibleHistory`` instances keep track of a person's expenses and income, 5 | including expenses that are deductible 6 | 7 | .. note:: This example is adapted from *Smalltalk-80: the language*, 8 | by Adele Goldberg and Dave Robson (Addison-Wesley, 1989). 9 | 10 | Class ``DeductibleHistory`` inherits methods and properties from ``FinancialHistory``, plus: 11 | 12 | ``spend(amount, reason, deducting=0.0)`` 13 | Spend an amount for the named reason, with an optional partial deduction. 14 | 15 | ``spend_deductible(amount, reason)`` 16 | Spend an amount for the named reason, with full deduction. 17 | 18 | The total amount of deductions is available in the ``deductions`` read-only property. 19 | 20 | 21 | 22 | Demonstration 23 | ------------- 24 | 25 | Create ``DeductibleHistory`` with $ 100. 26 | 27 | >>> from deducstory import DeductibleHistory 28 | >>> h = DeductibleHistory(1000) 29 | >>> h 30 | 31 | 32 | Spend some money on a deductible course:: 33 | 34 | >>> h.spend(600, 'course', 150) 35 | >>> h 36 | 37 | 38 | Make a donation:: 39 | 40 | >>> h.spend_deductible(250, 'charity') 41 | >>> h 42 | 43 | 44 | Check amount spent on "charity":: 45 | 46 | >>> h.spent_for('charity') 47 | Decimal('250') 48 | 49 | Check balance:: 50 | 51 | >>> h.balance 52 | Decimal('150') 53 | 54 | Get total deductions:: 55 | 56 | >>> h.deductions 57 | Decimal('400') 58 | -------------------------------------------------------------------------------- /examples/finstory/deducstory2.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import decimal 3 | 4 | from finstory import FinancialHistory, new_decimal 5 | 6 | class DeductibleHistory: 7 | 8 | def __init__(self, initial_balance=0.0): 9 | self._history = FinancialHistory(initial_balance) 10 | self._deductions = decimal.Decimal(0) 11 | 12 | def __repr__(self): 13 | name = self.__class__.__name__ 14 | return f'<{name} balance: {self.balance:.2f}>' 15 | 16 | def spend(self, amount, reason, deducting=0.0): 17 | """Record expense with partial deduction""" 18 | self._history.spend(amount, reason) 19 | if deducting: 20 | self._deductions += new_decimal(deducting) 21 | 22 | def spend_deductible(self, amount, reason): 23 | """Record expense with full deduction""" 24 | self._history.spend(amount, reason) 25 | self.spend(amount, reason, amount) 26 | 27 | @property 28 | def deductions(self): 29 | return self._deductions 30 | 31 | def __getattr__(self, name): 32 | return getattr(self._history, name) 33 | -------------------------------------------------------------------------------- /examples/finstory/finstory.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import decimal 3 | 4 | decimal.setcontext(decimal.BasicContext) 5 | 6 | def new_decimal(value): 7 | """Builds a Decimal using the cleaner float `repr`""" 8 | if isinstance(value, float): 9 | value = repr(value) 10 | return decimal.Decimal(value) 11 | 12 | 13 | class FinancialHistory: 14 | 15 | def __init__(self, initial_balance=0.0): 16 | self._balance = new_decimal(initial_balance) 17 | self._incomes = collections.defaultdict(decimal.Decimal) 18 | self._expenses = collections.defaultdict(decimal.Decimal) 19 | 20 | def __repr__(self): 21 | name = self.__class__.__name__ 22 | return f'<{name} balance: {self._balance:.2f}>' 23 | 24 | @property 25 | def balance(self): 26 | return self._balance 27 | 28 | 29 | def receive(self, amount, source): 30 | amount = new_decimal(amount) 31 | self._incomes[source] += amount 32 | self._balance += amount 33 | 34 | def spend(self, amount, reason): 35 | amount = new_decimal(amount) 36 | self._expenses[reason] += amount 37 | self._balance -= amount 38 | 39 | def received_from(self, source): 40 | return self._incomes[source] 41 | 42 | def spent_for(self, reason): 43 | return self._expenses[reason] 44 | -------------------------------------------------------------------------------- /examples/tombola/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 11 - "Interfaces, protocols and ABCs" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /examples/tombola/bingo.py: -------------------------------------------------------------------------------- 1 | # BEGIN TOMBOLA_BINGO 2 | 3 | import random 4 | 5 | from tombola import Tombola 6 | 7 | 8 | class BingoCage(Tombola): # <1> 9 | 10 | def __init__(self, items): 11 | self._randomizer = random.SystemRandom() # <2> 12 | self._items = [] 13 | self.load(items) # <3> 14 | 15 | def load(self, items): 16 | self._items.extend(items) 17 | self._randomizer.shuffle(self._items) # <4> 18 | 19 | def pick(self): # <5> 20 | try: 21 | return self._items.pop() 22 | except IndexError: 23 | raise LookupError('pick from empty BingoCage') 24 | 25 | def __call__(self): # <7> 26 | self.pick() 27 | 28 | # END TOMBOLA_BINGO 29 | -------------------------------------------------------------------------------- /examples/tombola/drum.py: -------------------------------------------------------------------------------- 1 | from random import shuffle 2 | 3 | from tombola import Tombola 4 | 5 | 6 | class TumblingDrum(Tombola): 7 | 8 | def __init__(self, iterable): 9 | self._balls = [] 10 | self.load(iterable) 11 | 12 | def load(self, iterable): 13 | self._balls.extend(iterable) 14 | shuffle(self._balls) 15 | 16 | def pick(self): 17 | return self._balls.pop() 18 | -------------------------------------------------------------------------------- /examples/tombola/lotto.py: -------------------------------------------------------------------------------- 1 | # BEGIN LOTTERY_BLOWER 2 | 3 | import random 4 | 5 | from tombola import Tombola 6 | 7 | 8 | class LotteryBlower(Tombola): 9 | 10 | def __init__(self, iterable): 11 | self._balls = list(iterable) # <1> 12 | 13 | def load(self, iterable): 14 | self._balls.extend(iterable) 15 | 16 | def pick(self): 17 | try: 18 | position = random.randrange(len(self._balls)) # <2> 19 | except ValueError: 20 | raise LookupError('pick from empty BingoCage') 21 | return self._balls.pop(position) # <3> 22 | 23 | def loaded(self): # <4> 24 | return bool(self._balls) 25 | 26 | def inspect(self): # <5> 27 | return tuple(sorted(self._balls)) 28 | 29 | 30 | # END LOTTERY_BLOWER 31 | -------------------------------------------------------------------------------- /examples/tombola/tombola.py: -------------------------------------------------------------------------------- 1 | # BEGIN TOMBOLA_ABC 2 | 3 | import abc 4 | 5 | class Tombola(abc.ABC): # <1> 6 | 7 | @abc.abstractmethod 8 | def load(self, iterable): # <2> 9 | """Add items from an iterable.""" 10 | 11 | @abc.abstractmethod 12 | def pick(self): # <3> 13 | """Remove item at random, returning it. 14 | 15 | This method should raise `LookupError` when the instance is empty. 16 | """ 17 | 18 | def loaded(self): # <4> 19 | """Return `True` if there's at least 1 item, `False` otherwise.""" 20 | return bool(self.inspect()) # <5> 21 | 22 | 23 | def inspect(self): 24 | """Return a sorted tuple with the items currently inside.""" 25 | items = [] 26 | while True: # <6> 27 | try: 28 | items.append(self.pick()) 29 | except LookupError: 30 | break 31 | self.load(items) # <7> 32 | return tuple(sorted(items)) 33 | 34 | 35 | # END TOMBOLA_ABC 36 | -------------------------------------------------------------------------------- /examples/tombola/tombola_runner.py: -------------------------------------------------------------------------------- 1 | # BEGIN TOMBOLA_RUNNER 2 | import doctest 3 | 4 | from tombola import Tombola 5 | 6 | # modules to test 7 | import bingo, lotto, tombolist, drum # <1> 8 | 9 | TEST_FILE = 'tombola_tests.rst' 10 | TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}' 11 | 12 | 13 | def main(argv): 14 | verbose = '-v' in argv 15 | real_subclasses = Tombola.__subclasses__() # <2> 16 | virtual_subclasses = list(Tombola._abc_registry) # <3> 17 | 18 | for cls in real_subclasses + virtual_subclasses: # <4> 19 | test(cls, verbose) 20 | 21 | 22 | def test(cls, verbose=False): 23 | 24 | res = doctest.testfile( 25 | TEST_FILE, 26 | globs={'ConcreteTombola': cls}, # <5> 27 | verbose=verbose, 28 | optionflags=doctest.REPORT_ONLY_FIRST_FAILURE) 29 | tag = 'FAIL' if res.failed else 'OK' 30 | print(TEST_MSG.format(cls.__name__, res, tag)) # <6> 31 | 32 | 33 | if __name__ == '__main__': 34 | import sys 35 | main(sys.argv) 36 | # END TOMBOLA_RUNNER 37 | -------------------------------------------------------------------------------- /examples/tombola/tombola_subhook.py: -------------------------------------------------------------------------------- 1 | """ 2 | Variation of ``tombola.Tombola`` implementing ``__subclasshook__``. 3 | 4 | Tests with simple classes:: 5 | 6 | >>> Tombola.__subclasshook__(object) 7 | NotImplemented 8 | >>> class Complete: 9 | ... def __init__(): pass 10 | ... def load(): pass 11 | ... def pick(): pass 12 | ... def loaded(): pass 13 | ... 14 | >>> Tombola.__subclasshook__(Complete) 15 | True 16 | >>> issubclass(Complete, Tombola) 17 | True 18 | 19 | """ 20 | 21 | 22 | from abc import ABC, abstractmethod 23 | from inspect import getmembers, isfunction 24 | 25 | 26 | class Tombola(ABC): # <1> 27 | 28 | @abstractmethod 29 | def __init__(self, iterable): # <2> 30 | """New instance is loaded from an iterable.""" 31 | 32 | @abstractmethod 33 | def load(self, iterable): 34 | """Add items from an iterable.""" 35 | 36 | @abstractmethod 37 | def pick(self): # <3> 38 | """Remove item at random, returning it. 39 | 40 | This method should raise `LookupError` when the instance is empty. 41 | """ 42 | 43 | def loaded(self): # <4> 44 | try: 45 | item = self.pick() 46 | except LookupError: 47 | return False 48 | else: 49 | self.load([item]) # put it back 50 | return True 51 | 52 | @classmethod 53 | def __subclasshook__(cls, other_cls): 54 | if cls is Tombola: 55 | interface_names = function_names(cls) 56 | found_names = set() 57 | for a_cls in other_cls.__mro__: 58 | found_names |= function_names(a_cls) 59 | if found_names >= interface_names: 60 | return True 61 | return NotImplemented 62 | 63 | 64 | def function_names(obj): 65 | return {name for name, _ in getmembers(obj, isfunction)} 66 | -------------------------------------------------------------------------------- /examples/tombola/tombola_tests.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Tombola tests 3 | ============== 4 | 5 | Every concrete subclass of Tombola should pass these tests. 6 | 7 | 8 | Create and load instance from iterable:: 9 | 10 | >>> balls = list(range(3)) 11 | >>> globe = ConcreteTombola(balls) 12 | >>> globe.loaded() 13 | True 14 | >>> globe.inspect() 15 | (0, 1, 2) 16 | 17 | 18 | Pick and collect balls:: 19 | 20 | >>> picks = [] 21 | >>> picks.append(globe.pick()) 22 | >>> picks.append(globe.pick()) 23 | >>> picks.append(globe.pick()) 24 | 25 | 26 | Check state and results:: 27 | 28 | >>> globe.loaded() 29 | False 30 | >>> sorted(picks) == balls 31 | True 32 | 33 | 34 | Reload:: 35 | 36 | >>> globe.load(balls) 37 | >>> globe.loaded() 38 | True 39 | >>> picks = [globe.pick() for i in balls] 40 | >>> globe.loaded() 41 | False 42 | 43 | 44 | Check that `LookupError` (or a subclass) is the exception 45 | thrown when the device is empty:: 46 | 47 | >>> globe = ConcreteTombola([]) 48 | >>> try: 49 | ... globe.pick() 50 | ... except LookupError as exc: 51 | ... print('OK') 52 | OK 53 | 54 | 55 | Load and pick 100 balls to verify that they all come out:: 56 | 57 | >>> balls = list(range(100)) 58 | >>> globe = ConcreteTombola(balls) 59 | >>> picks = [] 60 | >>> while globe.inspect(): 61 | ... picks.append(globe.pick()) 62 | >>> len(picks) == len(balls) 63 | True 64 | >>> set(picks) == set(balls) 65 | True 66 | 67 | 68 | Check that the order has changed and is not simply reversed:: 69 | 70 | >>> picks != balls 71 | True 72 | >>> picks[::-1] != balls 73 | True 74 | 75 | Note: the previous 2 tests have a *very* small chance of failing 76 | even if the implementation is OK. The probability of the 100 77 | balls coming out, by chance, in the order they were inspect is 78 | 1/100!, or approximately 1.07e-158. It's much easier to win the 79 | Lotto or to become a billionaire working as a programmer. 80 | 81 | THE END 82 | 83 | -------------------------------------------------------------------------------- /examples/tombola/tombolist.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | 3 | from tombola import Tombola 4 | 5 | @Tombola.register # <1> 6 | class TomboList(list): # <2> 7 | 8 | def pick(self): 9 | if self: # <3> 10 | position = randrange(len(self)) 11 | return self.pop(position) # <4> 12 | else: 13 | raise LookupError('pop from empty TomboList') 14 | 15 | load = list.extend # <5> 16 | 17 | def loaded(self): 18 | return bool(self) # <6> 19 | 20 | def inspect(self): 21 | return tuple(sorted(self)) 22 | 23 | # Tombola.register(TomboList) # <7> 24 | -------------------------------------------------------------------------------- /experiments/02-classes.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "class Pyramid:\n", 10 | " \"\"\"A semigraphic pyramid\"\"\"\n", 11 | " \n", 12 | " block_shape = 'X' #'\\N{FULL BLOCK}'\n", 13 | " \n", 14 | " def __init__(self, height):\n", 15 | " self.height = height\n", 16 | " \n", 17 | " def __repr__(self):\n", 18 | " return f'Pyramid(height={self._height})'\n", 19 | " \n", 20 | " @property\n", 21 | " def height(self):\n", 22 | " return self._height\n", 23 | " \n", 24 | " @height.setter\n", 25 | " def height(self, value):\n", 26 | " h = int(value)\n", 27 | " if h < 1:\n", 28 | " raise ValueError('height must be an integer >= 1')\n", 29 | " self._height = h \n", 30 | " \n", 31 | " @property\n", 32 | " def width(self):\n", 33 | " return self._height * 2 - 1\n", 34 | " \n", 35 | " def levels(self):\n", 36 | " for i in range(self._height):\n", 37 | " level = Pyramid.block_shape * (2 * i + 1) \n", 38 | " yield level.center(self.width)\n", 39 | "\n", 40 | " def __str__(self):\n", 41 | " return '\\n'.join(self.levels())\n", 42 | " \n", 43 | " def draw(self):\n", 44 | " print(self)\n", 45 | " \n", 46 | " def __eq__(self, other):\n", 47 | " return type(self) is type(other) and self._height == other._height" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 2, 53 | "metadata": {}, 54 | "outputs": [ 55 | { 56 | "data": { 57 | "text/plain": [ 58 | "mappingproxy({'__module__': '__main__',\n", 59 | " '__doc__': 'A semigraphic pyramid',\n", 60 | " 'block_shape': 'X',\n", 61 | " '__init__': ,\n", 62 | " '__repr__': ,\n", 63 | " 'height': ,\n", 64 | " 'width': ,\n", 65 | " 'levels': ,\n", 66 | " '__str__': ,\n", 67 | " 'draw': ,\n", 68 | " '__eq__': ,\n", 69 | " '__dict__': ,\n", 70 | " '__weakref__': ,\n", 71 | " '__hash__': None})" 72 | ] 73 | }, 74 | "execution_count": 2, 75 | "metadata": {}, 76 | "output_type": "execute_result" 77 | } 78 | ], 79 | "source": [ 80 | "Pyramid.__dict__" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 3, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "p = Pyramid(4)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 4, 95 | "metadata": { 96 | "scrolled": true 97 | }, 98 | "outputs": [ 99 | { 100 | "name": "stdout", 101 | "output_type": "stream", 102 | "text": [ 103 | " X \n", 104 | " XXX \n", 105 | " XXXXX \n", 106 | "XXXXXXX\n" 107 | ] 108 | } 109 | ], 110 | "source": [ 111 | "print(p)" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 5, 117 | "metadata": {}, 118 | "outputs": [ 119 | { 120 | "data": { 121 | "text/plain": [ 122 | "4" 123 | ] 124 | }, 125 | "execution_count": 5, 126 | "metadata": {}, 127 | "output_type": "execute_result" 128 | } 129 | ], 130 | "source": [ 131 | "p.height" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 6, 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "name": "stdout", 141 | "output_type": "stream", 142 | "text": [ 143 | " X \n", 144 | " XXX \n", 145 | " XXXXX \n", 146 | " XXXXXXX \n", 147 | " XXXXXXXXX \n", 148 | " XXXXXXXXXXX \n", 149 | "XXXXXXXXXXXXX\n" 150 | ] 151 | } 152 | ], 153 | "source": [ 154 | "p.height = 7\n", 155 | "print(p)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 7, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "p.height = 3" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 8, 170 | "metadata": {}, 171 | "outputs": [ 172 | { 173 | "data": { 174 | "text/plain": [ 175 | "5" 176 | ] 177 | }, 178 | "execution_count": 8, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "p.width" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": 9, 190 | "metadata": {}, 191 | "outputs": [ 192 | { 193 | "name": "stdout", 194 | "output_type": "stream", 195 | "text": [ 196 | " X \n", 197 | " XXX \n", 198 | "XXXXX\n" 199 | ] 200 | } 201 | ], 202 | "source": [ 203 | "print(p)" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 10, 209 | "metadata": {}, 210 | "outputs": [ 211 | { 212 | "name": "stdout", 213 | "output_type": "stream", 214 | "text": [ 215 | "height must be an integer >= 1\n" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "try:\n", 221 | " p.height = -2\n", 222 | "except ValueError as e:\n", 223 | " print(e)" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 11, 229 | "metadata": {}, 230 | "outputs": [ 231 | { 232 | "name": "stdout", 233 | "output_type": "stream", 234 | "text": [ 235 | "int() argument must be a string, a bytes-like object or a number, not 'NoneType'\n" 236 | ] 237 | } 238 | ], 239 | "source": [ 240 | "try:\n", 241 | " p.height = None\n", 242 | "except TypeError as e:\n", 243 | " print(e)" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": 12, 249 | "metadata": {}, 250 | "outputs": [ 251 | { 252 | "name": "stdout", 253 | "output_type": "stream", 254 | "text": [ 255 | " X \n", 256 | " XXX \n", 257 | "XXXXX\n" 258 | ] 259 | } 260 | ], 261 | "source": [ 262 | "print(p)" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "## Named tuple" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 13, 275 | "metadata": {}, 276 | "outputs": [], 277 | "source": [ 278 | "sp = (23.0, 46.0) # find lat > long" 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": 14, 284 | "metadata": {}, 285 | "outputs": [], 286 | "source": [ 287 | "lat, long = sp" 288 | ] 289 | }, 290 | { 291 | "cell_type": "code", 292 | "execution_count": 15, 293 | "metadata": {}, 294 | "outputs": [ 295 | { 296 | "data": { 297 | "text/plain": [ 298 | "23.0" 299 | ] 300 | }, 301 | "execution_count": 15, 302 | "metadata": {}, 303 | "output_type": "execute_result" 304 | } 305 | ], 306 | "source": [ 307 | "lat" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 16, 313 | "metadata": {}, 314 | "outputs": [ 315 | { 316 | "data": { 317 | "text/plain": [ 318 | "46.0" 319 | ] 320 | }, 321 | "execution_count": 16, 322 | "metadata": {}, 323 | "output_type": "execute_result" 324 | } 325 | ], 326 | "source": [ 327 | "long" 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": 17, 333 | "metadata": {}, 334 | "outputs": [], 335 | "source": [ 336 | "from collections import namedtuple\n", 337 | "\n", 338 | "Coord = namedtuple('Coord', ['lat', 'long'])" 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": 18, 344 | "metadata": {}, 345 | "outputs": [], 346 | "source": [ 347 | "sp = Coord(23.0, 46.0)" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 19, 353 | "metadata": {}, 354 | "outputs": [ 355 | { 356 | "data": { 357 | "text/plain": [ 358 | "Coord(lat=23.0, long=46.0)" 359 | ] 360 | }, 361 | "execution_count": 19, 362 | "metadata": {}, 363 | "output_type": "execute_result" 364 | } 365 | ], 366 | "source": [ 367 | "sp" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 20, 373 | "metadata": {}, 374 | "outputs": [ 375 | { 376 | "data": { 377 | "text/plain": [ 378 | "23.0" 379 | ] 380 | }, 381 | "execution_count": 20, 382 | "metadata": {}, 383 | "output_type": "execute_result" 384 | } 385 | ], 386 | "source": [ 387 | "sp.lat" 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": 21, 393 | "metadata": {}, 394 | "outputs": [ 395 | { 396 | "data": { 397 | "text/plain": [ 398 | "23.0" 399 | ] 400 | }, 401 | "execution_count": 21, 402 | "metadata": {}, 403 | "output_type": "execute_result" 404 | } 405 | ], 406 | "source": [ 407 | "sp[0]" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 22, 413 | "metadata": {}, 414 | "outputs": [], 415 | "source": [ 416 | "lat, long = sp" 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": 23, 422 | "metadata": {}, 423 | "outputs": [ 424 | { 425 | "data": { 426 | "text/plain": [ 427 | "23.0" 428 | ] 429 | }, 430 | "execution_count": 23, 431 | "metadata": {}, 432 | "output_type": "execute_result" 433 | } 434 | ], 435 | "source": [ 436 | "lat" 437 | ] 438 | }, 439 | { 440 | "cell_type": "code", 441 | "execution_count": 24, 442 | "metadata": {}, 443 | "outputs": [ 444 | { 445 | "name": "stdout", 446 | "output_type": "stream", 447 | "text": [ 448 | "can't set attribute\n" 449 | ] 450 | } 451 | ], 452 | "source": [ 453 | "try:\n", 454 | " sp.lat = 4\n", 455 | "except AttributeError as e:\n", 456 | " print(e)" 457 | ] 458 | }, 459 | { 460 | "cell_type": "code", 461 | "execution_count": 25, 462 | "metadata": {}, 463 | "outputs": [], 464 | "source": [ 465 | "sp = dict(lat=23, long=46)" 466 | ] 467 | }, 468 | { 469 | "cell_type": "code", 470 | "execution_count": 26, 471 | "metadata": {}, 472 | "outputs": [ 473 | { 474 | "data": { 475 | "text/plain": [ 476 | "{'lat': 23, 'long': 46}" 477 | ] 478 | }, 479 | "execution_count": 26, 480 | "metadata": {}, 481 | "output_type": "execute_result" 482 | } 483 | ], 484 | "source": [ 485 | "sp" 486 | ] 487 | }, 488 | { 489 | "cell_type": "markdown", 490 | "metadata": {}, 491 | "source": [ 492 | "## Data classes" 493 | ] 494 | }, 495 | { 496 | "cell_type": "code", 497 | "execution_count": 27, 498 | "metadata": {}, 499 | "outputs": [], 500 | "source": [ 501 | "from dataclasses import dataclass\n", 502 | "\n", 503 | "@dataclass\n", 504 | "class Coordinate:\n", 505 | " lat: float\n", 506 | " long: float" 507 | ] 508 | }, 509 | { 510 | "cell_type": "code", 511 | "execution_count": 28, 512 | "metadata": {}, 513 | "outputs": [ 514 | { 515 | "data": { 516 | "text/plain": [ 517 | "Coordinate(lat=23, long=46)" 518 | ] 519 | }, 520 | "execution_count": 28, 521 | "metadata": {}, 522 | "output_type": "execute_result" 523 | } 524 | ], 525 | "source": [ 526 | "sp = Coordinate(23, 46)\n", 527 | "sp" 528 | ] 529 | }, 530 | { 531 | "cell_type": "code", 532 | "execution_count": 29, 533 | "metadata": {}, 534 | "outputs": [ 535 | { 536 | "data": { 537 | "text/plain": [ 538 | "Coordinate(lat='x', long='y')" 539 | ] 540 | }, 541 | "execution_count": 29, 542 | "metadata": {}, 543 | "output_type": "execute_result" 544 | } 545 | ], 546 | "source": [ 547 | "xy = Coordinate('x', 'y')\n", 548 | "xy" 549 | ] 550 | }, 551 | { 552 | "cell_type": "code", 553 | "execution_count": null, 554 | "metadata": {}, 555 | "outputs": [], 556 | "source": [] 557 | } 558 | ], 559 | "metadata": { 560 | "kernelspec": { 561 | "display_name": "Python 3", 562 | "language": "python", 563 | "name": "python3" 564 | }, 565 | "language_info": { 566 | "codemirror_mode": { 567 | "name": "ipython", 568 | "version": 3 569 | }, 570 | "file_extension": ".py", 571 | "mimetype": "text/x-python", 572 | "name": "python", 573 | "nbconvert_exporter": "python", 574 | "pygments_lexer": "ipython3", 575 | "version": "3.7.3" 576 | } 577 | }, 578 | "nbformat": 4, 579 | "nbformat_minor": 2 580 | } 581 | -------------------------------------------------------------------------------- /experiments/03-subclassing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Subclassing" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "class UpperDict0(dict):\n", 17 | "\n", 18 | " def __setitem__(self, key, value):\n", 19 | " ks = str(key).upper()\n", 20 | " vs = str(value).upper()\n", 21 | " super().__setitem__(ks, vs)" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 2, 27 | "metadata": {}, 28 | "outputs": [ 29 | { 30 | "data": { 31 | "text/plain": [ 32 | "{'one': 'um'}" 33 | ] 34 | }, 35 | "execution_count": 2, 36 | "metadata": {}, 37 | "output_type": "execute_result" 38 | } 39 | ], 40 | "source": [ 41 | "ud0 = UpperDict0(one='um')\n", 42 | "ud0" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 3, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "data": { 52 | "text/plain": [ 53 | "{'one': 'um', 'TWO': 'DOIS'}" 54 | ] 55 | }, 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "output_type": "execute_result" 59 | } 60 | ], 61 | "source": [ 62 | "ud0['two'] = 'dois'\n", 63 | "ud0" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 4, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "class UpperDict1(dict):\n", 73 | " \n", 74 | " def __getitem__(self, key):\n", 75 | " ks = str(key).upper()\n", 76 | " return super().__getitem__(ks)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 5, 82 | "metadata": {}, 83 | "outputs": [ 84 | { 85 | "data": { 86 | "text/plain": [ 87 | "{'ONE': 'um'}" 88 | ] 89 | }, 90 | "execution_count": 5, 91 | "metadata": {}, 92 | "output_type": "execute_result" 93 | } 94 | ], 95 | "source": [ 96 | "ud1 = UpperDict1(ONE='um')\n", 97 | "ud1" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 6, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "data": { 107 | "text/plain": [ 108 | "'um'" 109 | ] 110 | }, 111 | "execution_count": 6, 112 | "metadata": {}, 113 | "output_type": "execute_result" 114 | } 115 | ], 116 | "source": [ 117 | "ud1['one']" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 7, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "import collections\n", 127 | "\n", 128 | "class UpperDict2(collections.UserDict):\n", 129 | " \n", 130 | " def __setitem__(self, key, value):\n", 131 | " ks = str(key).upper()\n", 132 | " vs = str(value).upper()\n", 133 | " super().__setitem__(ks, vs)\n", 134 | " \n", 135 | " def __getitem__(self, key):\n", 136 | " ks = str(key).upper()\n", 137 | " return super().__getitem__(ks)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 8, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "ud2 = UpperDict2(one='um')" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": 9, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "ud2['two'] = 'dois'" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 10, 161 | "metadata": {}, 162 | "outputs": [ 163 | { 164 | "data": { 165 | "text/plain": [ 166 | "{'ONE': 'UM', 'TWO': 'DOIS'}" 167 | ] 168 | }, 169 | "execution_count": 10, 170 | "metadata": {}, 171 | "output_type": "execute_result" 172 | } 173 | ], 174 | "source": [ 175 | "ud2" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "metadata": {}, 182 | "outputs": [], 183 | "source": [] 184 | } 185 | ], 186 | "metadata": { 187 | "kernelspec": { 188 | "display_name": "Python 3", 189 | "language": "python", 190 | "name": "python3" 191 | }, 192 | "language_info": { 193 | "codemirror_mode": { 194 | "name": "ipython", 195 | "version": 3 196 | }, 197 | "file_extension": ".py", 198 | "mimetype": "text/x-python", 199 | "name": "python", 200 | "nbconvert_exporter": "python", 201 | "pygments_lexer": "ipython3", 202 | "version": "3.7.3" 203 | } 204 | }, 205 | "nbformat": 4, 206 | "nbformat_minor": 2 207 | } 208 | -------------------------------------------------------------------------------- /experiments/notebook-hacks.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import json\n", 10 | "with open('01-objects.ipynb') as fp:\n", 11 | " nb = json.load(fp)\n", 12 | "\n", 13 | "assert nb['nbformat'], nb['nbformat_minor'] == (4, 2)" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 2, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "cells = nb['cells']" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 3, 28 | "metadata": {}, 29 | "outputs": [ 30 | { 31 | "data": { 32 | "text/plain": [ 33 | "107" 34 | ] 35 | }, 36 | "execution_count": 3, 37 | "metadata": {}, 38 | "output_type": "execute_result" 39 | } 40 | ], 41 | "source": [ 42 | "len(cells)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "name": "stdout", 52 | "output_type": "stream", 53 | "text": [ 54 | "* [Definitions](#Definitions)\n", 55 | "* [The attributes of an object](#The-attributes-of-an-object)\n", 56 | "* [The class of an object](#The-class-of-an-object)\n", 57 | " * [A user-defined class](#A-user-defined-class)\n", 58 | " * [Exercises](#Exercises)\n", 59 | " * [Exercises](#Exercises)\n", 60 | "* [The identity of an object](#The-identity-of-an-object)\n", 61 | "* [Containers](#Containers)\n", 62 | "* [Flat sequences](#Flat-sequences)\n" 63 | ] 64 | } 65 | ], 66 | "source": [ 67 | "for cell in cells:\n", 68 | " if cell['cell_type'] != 'markdown':\n", 69 | " continue\n", 70 | " for line in cell['source']:\n", 71 | " if line.startswith('##'):\n", 72 | " mark, _, title = line.strip().partition(' ')\n", 73 | " link = title.replace(' ', '-')\n", 74 | " indent = ' ' * (len(mark)-2) * 2\n", 75 | " print(f'{indent}* [{title}](#{link})')" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [] 84 | } 85 | ], 86 | "metadata": { 87 | "kernelspec": { 88 | "display_name": "Python 3", 89 | "language": "python", 90 | "name": "python3" 91 | }, 92 | "language_info": { 93 | "codemirror_mode": { 94 | "name": "ipython", 95 | "version": 3 96 | }, 97 | "file_extension": ".py", 98 | "mimetype": "text/x-python", 99 | "name": "python", 100 | "nbconvert_exporter": "python", 101 | "pygments_lexer": "ipython3", 102 | "version": "3.7.3" 103 | } 104 | }, 105 | "nbformat": 4, 106 | "nbformat_minor": 2 107 | } 108 | -------------------------------------------------------------------------------- /geohash.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | """ 3 | Copyright (C) 2009 Hiroaki Kawai 4 | """ 5 | try: 6 | import _geohash 7 | except ImportError: 8 | _geohash = None 9 | 10 | __version__ = "0.8.5" 11 | __all__ = ['encode','decode','decode_exactly','bbox', 'neighbors', 'expand'] 12 | 13 | _base32 = '0123456789bcdefghjkmnpqrstuvwxyz' 14 | _base32_map = {} 15 | for i in range(len(_base32)): 16 | _base32_map[_base32[i]] = i 17 | del i 18 | 19 | LONG_ZERO = 0 20 | import sys 21 | if sys.version_info[0] < 3: 22 | LONG_ZERO = long(0) 23 | 24 | def _float_hex_to_int(f): 25 | if f<-1.0 or f>=1.0: 26 | return None 27 | 28 | if f==0.0: 29 | return 1,1 30 | 31 | h = f.hex() 32 | x = h.find("0x1.") 33 | assert(x>=0) 34 | p = h.find("p") 35 | assert(p>0) 36 | 37 | half_len = len(h[x+4:p])*4-int(h[p+1:]) 38 | if x==0: 39 | r = (1<= half: 52 | i = i-half 53 | return float.fromhex(("0x0.%0"+str(s)+"xp1") % (i<<(s*4-l),)) 54 | else: 55 | i = half-i 56 | return float.fromhex(("-0x0.%0"+str(s)+"xp1") % (i<<(s*4-l),)) 57 | 58 | def _encode_i2c(lat,lon,lat_length,lon_length): 59 | precision = int((lat_length+lon_length)/5) 60 | if lat_length < lon_length: 61 | a = lon 62 | b = lat 63 | else: 64 | a = lat 65 | b = lon 66 | 67 | boost = (0,1,4,5,16,17,20,21) 68 | ret = '' 69 | for i in range(precision): 70 | ret+=_base32[(boost[a&7]+(boost[b&3]<<1))&0x1F] 71 | t = a>>3 72 | a = b>>2 73 | b = t 74 | 75 | return ret[::-1] 76 | 77 | def encode(latitude, longitude, precision=12): 78 | if latitude >= 90.0 or latitude < -90.0: 79 | raise Exception("invalid latitude.") 80 | while longitude < -180.0: 81 | longitude += 360.0 82 | while longitude >= 180.0: 83 | longitude -= 360.0 84 | 85 | if _geohash: 86 | basecode=_geohash.encode(latitude,longitude) 87 | if len(basecode)>precision: 88 | return basecode[0:precision] 89 | return basecode+'0'*(precision-len(basecode)) 90 | 91 | xprecision=precision+1 92 | lat_length = lon_length = int(xprecision*5/2) 93 | if xprecision%2==1: 94 | lon_length+=1 95 | 96 | if hasattr(float, "fromhex"): 97 | a = _float_hex_to_int(latitude/90.0) 98 | o = _float_hex_to_int(longitude/180.0) 99 | if a[1] > lat_length: 100 | ai = a[0]>>(a[1]-lat_length) 101 | else: 102 | ai = a[0]<<(lat_length-a[1]) 103 | 104 | if o[1] > lon_length: 105 | oi = o[0]>>(o[1]-lon_length) 106 | else: 107 | oi = o[0]<<(lon_length-o[1]) 108 | 109 | return _encode_i2c(ai, oi, lat_length, lon_length)[:precision] 110 | 111 | lat = latitude/180.0 112 | lon = longitude/360.0 113 | 114 | if lat>0: 115 | lat = int((1<0: 120 | lon = int((1<>2)&4 138 | lat += (t>>2)&2 139 | lon += (t>>1)&2 140 | lat += (t>>1)&1 141 | lon += t&1 142 | lon_length+=3 143 | lat_length+=2 144 | else: 145 | lon = lon<<2 146 | lat = lat<<3 147 | lat += (t>>2)&4 148 | lon += (t>>2)&2 149 | lat += (t>>1)&2 150 | lon += (t>>1)&1 151 | lat += t&1 152 | lon_length+=2 153 | lat_length+=3 154 | 155 | bit_length+=5 156 | 157 | return (lat,lon,lat_length,lon_length) 158 | 159 | def decode(hashcode, delta=False): 160 | ''' 161 | decode a hashcode and get center coordinate, and distance between center and outer border 162 | ''' 163 | if _geohash: 164 | (lat,lon,lat_bits,lon_bits) = _geohash.decode(hashcode) 165 | latitude_delta = 90.0/(1<> lat_length: 252 | for tlon in (lon-1, lon, lon+1): 253 | ret.append(_encode_i2c(tlat,tlon,lat_length,lon_length)) 254 | 255 | tlat = lat-1 256 | if tlat >= 0: 257 | for tlon in (lon-1, lon, lon+1): 258 | ret.append(_encode_i2c(tlat,tlon,lat_length,lon_length)) 259 | 260 | return ret 261 | 262 | def expand(hashcode): 263 | ret = neighbors(hashcode) 264 | ret.append(hashcode) 265 | return ret 266 | 267 | def _uint64_interleave(lat32, lon32): 268 | intr = 0 269 | boost = (0,1,4,5,16,17,20,21,64,65,68,69,80,81,84,85) 270 | for i in range(8): 271 | intr = (intr<<8) + (boost[(lon32>>(28-i*4))%16]<<1) + boost[(lat32>>(28-i*4))%16] 272 | 273 | return intr 274 | 275 | def _uint64_deinterleave(ui64): 276 | lat = lon = 0 277 | boost = ((0,0),(0,1),(1,0),(1,1),(0,2),(0,3),(1,2),(1,3), 278 | (2,0),(2,1),(3,0),(3,1),(2,2),(2,3),(3,2),(3,3)) 279 | for i in range(16): 280 | p = boost[(ui64>>(60-i*4))%16] 281 | lon = (lon<<2) + p[0] 282 | lat = (lat<<2) + p[1] 283 | 284 | return (lat, lon) 285 | 286 | def encode_uint64(latitude, longitude): 287 | if latitude >= 90.0 or latitude < -90.0: 288 | raise ValueError("Latitude must be in the range of (-90.0, 90.0)") 289 | while longitude < -180.0: 290 | longitude += 360.0 291 | while longitude >= 180.0: 292 | longitude -= 360.0 293 | 294 | if _geohash: 295 | ui128 = _geohash.encode_int(latitude,longitude) 296 | if _geohash.intunit == 64: 297 | return ui128[0] 298 | elif _geohash.intunit == 32: 299 | return (ui128[0]<<32) + ui128[1] 300 | elif _geohash.intunit == 16: 301 | return (ui128[0]<<48) + (ui128[1]<<32) + (ui128[2]<<16) + ui128[3] 302 | 303 | lat = int(((latitude + 90.0)/180.0)*(1<<32)) 304 | lon = int(((longitude+180.0)/360.0)*(1<<32)) 305 | return _uint64_interleave(lat, lon) 306 | 307 | def decode_uint64(ui64): 308 | if _geohash: 309 | latlon = _geohash.decode_int(ui64 % 0xFFFFFFFFFFFFFFFF, LONG_ZERO) 310 | if latlon: 311 | return latlon 312 | 313 | lat,lon = _uint64_deinterleave(ui64) 314 | return (180.0*lat/(1<<32) - 90.0, 360.0*lon/(1<<32) - 180.0) 315 | 316 | def expand_uint64(ui64, precision=50): 317 | ui64 = ui64 & (0xFFFFFFFFFFFFFFFF << (64-precision)) 318 | lat,lon = _uint64_deinterleave(ui64) 319 | lat_grid = 1<<(32-int(precision/2)) 320 | lon_grid = lat_grid>>(precision%2) 321 | 322 | if precision<=2: # expand becomes to the whole range 323 | return [] 324 | 325 | ranges = [] 326 | if lat & lat_grid: 327 | if lon & lon_grid: 328 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 329 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 330 | if precision%2==0: 331 | # lat,lon = (1, 1) and even precision 332 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 333 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 334 | 335 | if lat + lat_grid < 0xFFFFFFFF: 336 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 337 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 338 | ui64 = _uint64_interleave(lat+lat_grid, lon) 339 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 340 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 341 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 342 | else: 343 | # lat,lon = (1, 1) and odd precision 344 | if lat + lat_grid < 0xFFFFFFFF: 345 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 346 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 347 | 348 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 349 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 350 | 351 | ui64 = _uint64_interleave(lat, lon+lon_grid) 352 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 353 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 354 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 355 | else: 356 | ui64 = _uint64_interleave(lat-lat_grid, lon) 357 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 358 | if precision%2==0: 359 | # lat,lon = (1, 0) and odd precision 360 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 361 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 362 | 363 | if lat + lat_grid < 0xFFFFFFFF: 364 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 365 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 366 | ui64 = _uint64_interleave(lat+lat_grid, lon) 367 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 368 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 369 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 370 | else: 371 | # lat,lon = (1, 0) and odd precision 372 | if lat + lat_grid < 0xFFFFFFFF: 373 | ui64 = _uint64_interleave(lat+lat_grid, lon) 374 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 375 | 376 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 377 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 378 | ui64 = _uint64_interleave(lat, lon-lon_grid) 379 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 380 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 381 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 382 | else: 383 | if lon & lon_grid: 384 | ui64 = _uint64_interleave(lat, lon-lon_grid) 385 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 386 | if precision%2==0: 387 | # lat,lon = (0, 1) and even precision 388 | ui64 = _uint64_interleave(lat, lon+lon_grid) 389 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 390 | 391 | if lat > 0: 392 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 393 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 394 | ui64 = _uint64_interleave(lat-lat_grid, lon) 395 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 396 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 397 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 398 | else: 399 | # lat,lon = (0, 1) and odd precision 400 | if lat > 0: 401 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 402 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 403 | 404 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 405 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 406 | ui64 = _uint64_interleave(lat, lon+lon_grid) 407 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 408 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 409 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 410 | else: 411 | ui64 = _uint64_interleave(lat, lon) 412 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 413 | if precision%2==0: 414 | # lat,lon = (0, 0) and even precision 415 | ui64 = _uint64_interleave(lat, lon-lon_grid) 416 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 417 | 418 | if lat > 0: 419 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 420 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 421 | ui64 = _uint64_interleave(lat-lat_grid, lon) 422 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 423 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 424 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 425 | else: 426 | # lat,lon = (0, 0) and odd precision 427 | if lat > 0: 428 | ui64 = _uint64_interleave(lat-lat_grid, lon) 429 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 430 | 431 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 432 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 433 | ui64 = _uint64_interleave(lat, lon-lon_grid) 434 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 435 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 436 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 437 | 438 | ranges.sort() 439 | 440 | # merge the conditions 441 | shrink = [] 442 | prev = None 443 | for i in ranges: 444 | if prev: 445 | if prev[1] != i[0]: 446 | shrink.append(prev) 447 | prev = i 448 | else: 449 | prev = (prev[0], i[1]) 450 | else: 451 | prev = i 452 | 453 | shrink.append(prev) 454 | 455 | ranges = [] 456 | for i in shrink: 457 | a,b=i 458 | if a == 0: 459 | a = None # we can remove the condition because it is the lowest value 460 | if b == 0x10000000000000000: 461 | b = None # we can remove the condition because it is the highest value 462 | 463 | ranges.append((a,b)) 464 | 465 | return ranges 466 | -------------------------------------------------------------------------------- /img/array-of-floats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/array-of-floats.png -------------------------------------------------------------------------------- /img/camping-UML.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/camping-UML.graffle -------------------------------------------------------------------------------- /img/camping-UML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/camping-UML.png -------------------------------------------------------------------------------- /img/concepts-bicycleObject.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/concepts-bicycleObject.gif -------------------------------------------------------------------------------- /img/concepts-object.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/concepts-object.gif -------------------------------------------------------------------------------- /img/dog_st_bernard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/dog_st_bernard.jpg -------------------------------------------------------------------------------- /img/finhist-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/finhist-browser.png -------------------------------------------------------------------------------- /img/finstory-UML.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/finstory-UML.graffle -------------------------------------------------------------------------------- /img/finstory-UML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/finstory-UML.png -------------------------------------------------------------------------------- /img/finstory-spec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/finstory-spec.png -------------------------------------------------------------------------------- /img/intro-oop-budd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/intro-oop-budd.jpg -------------------------------------------------------------------------------- /img/list-of-floats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/list-of-floats.png -------------------------------------------------------------------------------- /img/pyob-mindmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/pyob-mindmap.png -------------------------------------------------------------------------------- /img/safety-switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/safety-switch.jpg -------------------------------------------------------------------------------- /img/thoughtworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/thoughtworks.png -------------------------------------------------------------------------------- /img/title-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/img/title-card.png -------------------------------------------------------------------------------- /labs/1/README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Lab 1: enhancing ``Coordinate`` 3 | =============================== 4 | 5 | Your goal is to enhance the ``Coordinate`` class, adding the following features: 6 | 7 | * Create an ``__init__`` accepting a latitude and a longitude. 8 | * Create ``reference_system`` class attribute. 9 | * Create a ``geohash`` method using the ``encode`` function from the ``geohash.py`` module. 10 | 11 | General instructions 12 | ==================== 13 | 14 | Edit the ``coordinate.py`` module in this directory. 15 | 16 | Use the examples in this ``README.rst`` as **doctests** to guide your work. 17 | 18 | To run tests, use ``doctest`` with the ``-f`` option (for "fail fast", to stop at first failure):: 19 | 20 | $ python3 -m doctest README.rst -f 21 | 22 | 23 | Step 1 24 | ====== 25 | 26 | Implement ``__init__`` taking latitude and longitude, both optional with ``0.0`` as default. 27 | 28 | Example:: 29 | 30 | >>> from coordinate import Coordinate 31 | >>> gulf_of_guinea = Coordinate() 32 | >>> gulf_of_guinea 33 | Coordinate(0.0, 0.0) 34 | >>> greenwich = Coordinate(51.5) 35 | >>> greenwich 36 | Coordinate(51.5, 0.0) 37 | >>> london = Coordinate(51.5, -0.1) 38 | >>> print(london) 39 | 51.5°N, 0.1°W 40 | 41 | 42 | Step 2 43 | ====== 44 | 45 | Create a class attribute named ``reference_system`` with value ``'WGS84'``. 46 | 47 | Example:: 48 | 49 | >>> Coordinate.reference_system 50 | 'WGS84' 51 | >>> cleveland = Coordinate(41.5, -81.7) 52 | >>> cleveland.reference_system 53 | 'WGS84' 54 | 55 | 56 | Step 3 57 | ====== 58 | 59 | Use the ``encode`` function of the ``geohash.py`` module 60 | to create a ``geohash`` method in the ``Coordinate`` class. 61 | 62 | Here is how to use the ``encode`` function:: 63 | 64 | >>> import geohash 65 | >>> geohash.encode(41.40, -81.85) # CLE airport 66 | 'dpmg92wskz27' 67 | 68 | After you implement the ``geohash`` method, this should work:: 69 | 70 | >>> cleveland = Coordinate(41.5, -81.7) 71 | >>> cleveland.geohash() 72 | 'dpmuhfggh08w' 73 | -------------------------------------------------------------------------------- /labs/1/coordinate.py: -------------------------------------------------------------------------------- 1 | class Coordinate: 2 | '''Coordinate on Earth''' 3 | 4 | def __repr__(self): 5 | return f'Coordinate({self.lat}, {self.long})' 6 | 7 | def __str__(self): 8 | ns = 'NS'[self.lat < 0] 9 | we = 'EW'[self.long < 0] 10 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}' -------------------------------------------------------------------------------- /labs/1/geohash.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | """ 3 | Copyright (C) 2009 Hiroaki Kawai 4 | """ 5 | try: 6 | import _geohash 7 | except ImportError: 8 | _geohash = None 9 | 10 | __version__ = "0.8.5" 11 | __all__ = ['encode','decode','decode_exactly','bbox', 'neighbors', 'expand'] 12 | 13 | _base32 = '0123456789bcdefghjkmnpqrstuvwxyz' 14 | _base32_map = {} 15 | for i in range(len(_base32)): 16 | _base32_map[_base32[i]] = i 17 | del i 18 | 19 | LONG_ZERO = 0 20 | import sys 21 | if sys.version_info[0] < 3: 22 | LONG_ZERO = long(0) 23 | 24 | def _float_hex_to_int(f): 25 | if f<-1.0 or f>=1.0: 26 | return None 27 | 28 | if f==0.0: 29 | return 1,1 30 | 31 | h = f.hex() 32 | x = h.find("0x1.") 33 | assert(x>=0) 34 | p = h.find("p") 35 | assert(p>0) 36 | 37 | half_len = len(h[x+4:p])*4-int(h[p+1:]) 38 | if x==0: 39 | r = (1<= half: 52 | i = i-half 53 | return float.fromhex(("0x0.%0"+str(s)+"xp1") % (i<<(s*4-l),)) 54 | else: 55 | i = half-i 56 | return float.fromhex(("-0x0.%0"+str(s)+"xp1") % (i<<(s*4-l),)) 57 | 58 | def _encode_i2c(lat,lon,lat_length,lon_length): 59 | precision = int((lat_length+lon_length)/5) 60 | if lat_length < lon_length: 61 | a = lon 62 | b = lat 63 | else: 64 | a = lat 65 | b = lon 66 | 67 | boost = (0,1,4,5,16,17,20,21) 68 | ret = '' 69 | for i in range(precision): 70 | ret+=_base32[(boost[a&7]+(boost[b&3]<<1))&0x1F] 71 | t = a>>3 72 | a = b>>2 73 | b = t 74 | 75 | return ret[::-1] 76 | 77 | def encode(latitude, longitude, precision=12): 78 | if latitude >= 90.0 or latitude < -90.0: 79 | raise Exception("invalid latitude.") 80 | while longitude < -180.0: 81 | longitude += 360.0 82 | while longitude >= 180.0: 83 | longitude -= 360.0 84 | 85 | if _geohash: 86 | basecode=_geohash.encode(latitude,longitude) 87 | if len(basecode)>precision: 88 | return basecode[0:precision] 89 | return basecode+'0'*(precision-len(basecode)) 90 | 91 | xprecision=precision+1 92 | lat_length = lon_length = int(xprecision*5/2) 93 | if xprecision%2==1: 94 | lon_length+=1 95 | 96 | if hasattr(float, "fromhex"): 97 | a = _float_hex_to_int(latitude/90.0) 98 | o = _float_hex_to_int(longitude/180.0) 99 | if a[1] > lat_length: 100 | ai = a[0]>>(a[1]-lat_length) 101 | else: 102 | ai = a[0]<<(lat_length-a[1]) 103 | 104 | if o[1] > lon_length: 105 | oi = o[0]>>(o[1]-lon_length) 106 | else: 107 | oi = o[0]<<(lon_length-o[1]) 108 | 109 | return _encode_i2c(ai, oi, lat_length, lon_length)[:precision] 110 | 111 | lat = latitude/180.0 112 | lon = longitude/360.0 113 | 114 | if lat>0: 115 | lat = int((1<0: 120 | lon = int((1<>2)&4 138 | lat += (t>>2)&2 139 | lon += (t>>1)&2 140 | lat += (t>>1)&1 141 | lon += t&1 142 | lon_length+=3 143 | lat_length+=2 144 | else: 145 | lon = lon<<2 146 | lat = lat<<3 147 | lat += (t>>2)&4 148 | lon += (t>>2)&2 149 | lat += (t>>1)&2 150 | lon += (t>>1)&1 151 | lat += t&1 152 | lon_length+=2 153 | lat_length+=3 154 | 155 | bit_length+=5 156 | 157 | return (lat,lon,lat_length,lon_length) 158 | 159 | def decode(hashcode, delta=False): 160 | ''' 161 | decode a hashcode and get center coordinate, and distance between center and outer border 162 | ''' 163 | if _geohash: 164 | (lat,lon,lat_bits,lon_bits) = _geohash.decode(hashcode) 165 | latitude_delta = 90.0/(1<> lat_length: 252 | for tlon in (lon-1, lon, lon+1): 253 | ret.append(_encode_i2c(tlat,tlon,lat_length,lon_length)) 254 | 255 | tlat = lat-1 256 | if tlat >= 0: 257 | for tlon in (lon-1, lon, lon+1): 258 | ret.append(_encode_i2c(tlat,tlon,lat_length,lon_length)) 259 | 260 | return ret 261 | 262 | def expand(hashcode): 263 | ret = neighbors(hashcode) 264 | ret.append(hashcode) 265 | return ret 266 | 267 | def _uint64_interleave(lat32, lon32): 268 | intr = 0 269 | boost = (0,1,4,5,16,17,20,21,64,65,68,69,80,81,84,85) 270 | for i in range(8): 271 | intr = (intr<<8) + (boost[(lon32>>(28-i*4))%16]<<1) + boost[(lat32>>(28-i*4))%16] 272 | 273 | return intr 274 | 275 | def _uint64_deinterleave(ui64): 276 | lat = lon = 0 277 | boost = ((0,0),(0,1),(1,0),(1,1),(0,2),(0,3),(1,2),(1,3), 278 | (2,0),(2,1),(3,0),(3,1),(2,2),(2,3),(3,2),(3,3)) 279 | for i in range(16): 280 | p = boost[(ui64>>(60-i*4))%16] 281 | lon = (lon<<2) + p[0] 282 | lat = (lat<<2) + p[1] 283 | 284 | return (lat, lon) 285 | 286 | def encode_uint64(latitude, longitude): 287 | if latitude >= 90.0 or latitude < -90.0: 288 | raise ValueError("Latitude must be in the range of (-90.0, 90.0)") 289 | while longitude < -180.0: 290 | longitude += 360.0 291 | while longitude >= 180.0: 292 | longitude -= 360.0 293 | 294 | if _geohash: 295 | ui128 = _geohash.encode_int(latitude,longitude) 296 | if _geohash.intunit == 64: 297 | return ui128[0] 298 | elif _geohash.intunit == 32: 299 | return (ui128[0]<<32) + ui128[1] 300 | elif _geohash.intunit == 16: 301 | return (ui128[0]<<48) + (ui128[1]<<32) + (ui128[2]<<16) + ui128[3] 302 | 303 | lat = int(((latitude + 90.0)/180.0)*(1<<32)) 304 | lon = int(((longitude+180.0)/360.0)*(1<<32)) 305 | return _uint64_interleave(lat, lon) 306 | 307 | def decode_uint64(ui64): 308 | if _geohash: 309 | latlon = _geohash.decode_int(ui64 % 0xFFFFFFFFFFFFFFFF, LONG_ZERO) 310 | if latlon: 311 | return latlon 312 | 313 | lat,lon = _uint64_deinterleave(ui64) 314 | return (180.0*lat/(1<<32) - 90.0, 360.0*lon/(1<<32) - 180.0) 315 | 316 | def expand_uint64(ui64, precision=50): 317 | ui64 = ui64 & (0xFFFFFFFFFFFFFFFF << (64-precision)) 318 | lat,lon = _uint64_deinterleave(ui64) 319 | lat_grid = 1<<(32-int(precision/2)) 320 | lon_grid = lat_grid>>(precision%2) 321 | 322 | if precision<=2: # expand becomes to the whole range 323 | return [] 324 | 325 | ranges = [] 326 | if lat & lat_grid: 327 | if lon & lon_grid: 328 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 329 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 330 | if precision%2==0: 331 | # lat,lon = (1, 1) and even precision 332 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 333 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 334 | 335 | if lat + lat_grid < 0xFFFFFFFF: 336 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 337 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 338 | ui64 = _uint64_interleave(lat+lat_grid, lon) 339 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 340 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 341 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 342 | else: 343 | # lat,lon = (1, 1) and odd precision 344 | if lat + lat_grid < 0xFFFFFFFF: 345 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 346 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 347 | 348 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 349 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 350 | 351 | ui64 = _uint64_interleave(lat, lon+lon_grid) 352 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 353 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 354 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 355 | else: 356 | ui64 = _uint64_interleave(lat-lat_grid, lon) 357 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 358 | if precision%2==0: 359 | # lat,lon = (1, 0) and odd precision 360 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 361 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 362 | 363 | if lat + lat_grid < 0xFFFFFFFF: 364 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 365 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 366 | ui64 = _uint64_interleave(lat+lat_grid, lon) 367 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 368 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 369 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 370 | else: 371 | # lat,lon = (1, 0) and odd precision 372 | if lat + lat_grid < 0xFFFFFFFF: 373 | ui64 = _uint64_interleave(lat+lat_grid, lon) 374 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 375 | 376 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 377 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 378 | ui64 = _uint64_interleave(lat, lon-lon_grid) 379 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 380 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 381 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 382 | else: 383 | if lon & lon_grid: 384 | ui64 = _uint64_interleave(lat, lon-lon_grid) 385 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 386 | if precision%2==0: 387 | # lat,lon = (0, 1) and even precision 388 | ui64 = _uint64_interleave(lat, lon+lon_grid) 389 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 390 | 391 | if lat > 0: 392 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 393 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 394 | ui64 = _uint64_interleave(lat-lat_grid, lon) 395 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 396 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 397 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 398 | else: 399 | # lat,lon = (0, 1) and odd precision 400 | if lat > 0: 401 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 402 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 403 | 404 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 405 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 406 | ui64 = _uint64_interleave(lat, lon+lon_grid) 407 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 408 | ui64 = _uint64_interleave(lat+lat_grid, lon+lon_grid) 409 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 410 | else: 411 | ui64 = _uint64_interleave(lat, lon) 412 | ranges.append((ui64, ui64 + (1<<(64-precision+2)))) 413 | if precision%2==0: 414 | # lat,lon = (0, 0) and even precision 415 | ui64 = _uint64_interleave(lat, lon-lon_grid) 416 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 417 | 418 | if lat > 0: 419 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 420 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 421 | ui64 = _uint64_interleave(lat-lat_grid, lon) 422 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 423 | ui64 = _uint64_interleave(lat-lat_grid, lon+lon_grid) 424 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 425 | else: 426 | # lat,lon = (0, 0) and odd precision 427 | if lat > 0: 428 | ui64 = _uint64_interleave(lat-lat_grid, lon) 429 | ranges.append((ui64, ui64 + (1<<(64-precision+1)))) 430 | 431 | ui64 = _uint64_interleave(lat-lat_grid, lon-lon_grid) 432 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 433 | ui64 = _uint64_interleave(lat, lon-lon_grid) 434 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 435 | ui64 = _uint64_interleave(lat+lat_grid, lon-lon_grid) 436 | ranges.append((ui64, ui64 + (1<<(64-precision)))) 437 | 438 | ranges.sort() 439 | 440 | # merge the conditions 441 | shrink = [] 442 | prev = None 443 | for i in ranges: 444 | if prev: 445 | if prev[1] != i[0]: 446 | shrink.append(prev) 447 | prev = i 448 | else: 449 | prev = (prev[0], i[1]) 450 | else: 451 | prev = i 452 | 453 | shrink.append(prev) 454 | 455 | ranges = [] 456 | for i in shrink: 457 | a,b=i 458 | if a == 0: 459 | a = None # we can remove the condition because it is the lowest value 460 | if b == 0x10000000000000000: 461 | b = None # we can remove the condition because it is the highest value 462 | 463 | ranges.append((a,b)) 464 | 465 | return ranges 466 | -------------------------------------------------------------------------------- /labs/1/solution/coordinate.py: -------------------------------------------------------------------------------- 1 | import geohash 2 | 3 | class Coordinate: 4 | '''Coordinate on Earth''' 5 | 6 | reference_system = 'WGS84' 7 | 8 | def __init__(self, lat=0.0, long=0.0): 9 | self.lat = lat 10 | self.long = long 11 | 12 | def __repr__(self): 13 | return f'Coordinate({self.lat}, {self.long})' 14 | 15 | def __str__(self): 16 | ns = 'NS'[self.lat < 0] 17 | we = 'EW'[self.long < 0] 18 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}' 19 | 20 | def geohash(self): 21 | return geohash.encode(self.lat, self.long) -------------------------------------------------------------------------------- /labs/2/README.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Lab 2: enhancing ``Budget`` 3 | =========================== 4 | 5 | Your goal is to enhance the ``camping.Budget`` class. 6 | 7 | As implemented, ``camping.Budget`` does not allow adding contributor names after the budget is created. 8 | Implement an ``.include(name)`` method to allow adding a contributor with an optional contribution. 9 | 10 | General instructions 11 | ==================== 12 | 13 | Use the examples in this ``README.rst`` as **doctests** to guide your work. 14 | 15 | To run tests, use ``doctest`` with the ``-f`` option (for "fail fast", to stop at first failure):: 16 | 17 | $ python3 -m doctest README.rst -f 18 | 19 | 20 | Step 1 21 | ====== 22 | 23 | Implement ``include`` taking a name and an optional contribution (default: 0.0). 24 | 25 | Example:: 26 | 27 | >>> from camping import Budget 28 | >>> b = Budget('Debbie', 'Ann', 'Charlie') 29 | >>> b.total() 30 | 0.0 31 | >>> b.people() 32 | ['Ann', 'Charlie', 'Debbie'] 33 | >>> b.contribute("Debbie", 40.00) 34 | >>> b.contribute("Ann", 10.00) 35 | >>> b.include("Bob", 15) 36 | >>> b.people() 37 | ['Ann', 'Bob', 'Charlie', 'Debbie'] 38 | >>> b.contribute("Bob", 20) 39 | >>> b.total() 40 | 85.0 41 | 42 | Step 2 (bonus) 43 | ============== 44 | 45 | An alternative to such a method would be to change the contribute method, 46 | removing the code that tests whether the contributor's name is found in ``self._campers``. 47 | This would be simpler, but is there a drawback to this approach? 48 | Discuss with tutorial participants near you. 49 | -------------------------------------------------------------------------------- /labs/2/camping.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | class Camper: 4 | 5 | max_name_len = 0 6 | template = '{name:>{name_len}} paid ${paid:7.2f}' 7 | 8 | def __init__(self, name, paid=0.0): 9 | self.name = name 10 | self.paid = float(paid) 11 | if len(name) > Camper.max_name_len: 12 | Camper.max_name_len = len(name) 13 | 14 | def pay(self, amount): 15 | self.paid += float(amount) 16 | 17 | def display(self): 18 | return Camper.template.format( 19 | name = self.name, 20 | name_len = self.max_name_len, 21 | paid = self.paid, 22 | ) 23 | 24 | class Budget: 25 | """ 26 | Class ``camping.Budget`` represents the budget for a camping trip. 27 | """ 28 | 29 | def __init__(self, *names): 30 | self._campers = {name: Camper(name) for name in names} 31 | 32 | def total(self): 33 | return sum(c.paid for c in self._campers.values()) 34 | 35 | def people(self): 36 | return sorted(self._campers) 37 | 38 | def contribute(self, name, amount): 39 | if name not in self._campers: 40 | raise LookupError("Name not in budget") 41 | self._campers[name].pay(amount) 42 | 43 | def individual_share(self): 44 | return self.total() / len(self._campers) 45 | 46 | def report(self): 47 | """report displays names and amounts due or owed""" 48 | share = self.individual_share() 49 | heading_tpl = 'Total: $ {:.2f}; individual share: $ {:.2f}' 50 | print(heading_tpl.format(self.total(), share)) 51 | print("-"* 42) 52 | sorted_campers = sorted(self._campers.values(), key=operator.attrgetter('paid')) 53 | for camper in sorted_campers: 54 | balance = f'balance: $ {camper.paid - share:7.2f}' 55 | print(camper.display(), balance, sep=', ') 56 | -------------------------------------------------------------------------------- /references.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | ## Inheritance 4 | 5 | "I'd leave out classes" -- James Gosling 6 | 7 | https://www.javaworld.com/article/2073649/why-extends-is-evil.html 8 | 9 | -------------------------------------------------------------------------------- /slides/pythonic-objects.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/slides/pythonic-objects.key -------------------------------------------------------------------------------- /slides/pythonic-objects.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/pyob2019/8881d18b2ac5716f059b7d4eaa93191b13afc965/slides/pythonic-objects.pdf --------------------------------------------------------------------------------