\" % (\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 |
6 | Chapter 6
7 | A Case Study : Eight Queens
8 |
9 | Outline
10 |
11 | - Statement of the Problem
12 |
- OOP Approach
13 |
- Observations
14 |
- Pointers
15 |
- CRC Card for Queen
16 |
- CRC Card for Queen - Backside
17 |
- Initialization
18 |
- Finding First Solution
19 |
- Advancing to Next Position
20 |
- Printing Solution
21 |
- Can Attack
22 |
- The Last Queen
23 |
- 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 |
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 |
115 |
116 |
Intro OOP, Chapter 6, Slide 4
117 |
118 |
119 |
120 |
121 |
122 | CRC Card for Queen
123 |
124 | Intro OOP, Chapter 6, Slide 5
125 |
126 |
127 |
128 |
129 |
130 | CRC Card for Queen - Backside
131 |
132 |
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
--------------------------------------------------------------------------------