ch11-24: clean up by @eumiro & sync with Atlas
This commit is contained in:
2
11-pythonic-obj/private/.gitignore
vendored
Normal file
2
11-pythonic-obj/private/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.class
|
||||||
|
.jython_cache/
|
||||||
4
13-protocol-abc/README.rst
Normal file
4
13-protocol-abc/README.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sample code for Chapter 11 - "Interfaces, protocols and ABCs"
|
||||||
|
|
||||||
|
From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015)
|
||||||
|
http://shop.oreilly.com/product/0636920032519.do
|
||||||
28
13-protocol-abc/bingo.py
Normal file
28
13-protocol-abc/bingo.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# tag::TOMBOLA_BINGO[]
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from tombola import Tombola
|
||||||
|
|
||||||
|
|
||||||
|
class BingoCage(Tombola): # <1>
|
||||||
|
|
||||||
|
def __init__(self, items):
|
||||||
|
self._randomizer = random.SystemRandom() # <2>
|
||||||
|
self._items = []
|
||||||
|
self.load(items) # <3>
|
||||||
|
|
||||||
|
def load(self, items):
|
||||||
|
self._items.extend(items)
|
||||||
|
self._randomizer.shuffle(self._items) # <4>
|
||||||
|
|
||||||
|
def pick(self): # <5>
|
||||||
|
try:
|
||||||
|
return self._items.pop()
|
||||||
|
except IndexError:
|
||||||
|
raise LookupError('pick from empty BingoCage')
|
||||||
|
|
||||||
|
def __call__(self): # <6>
|
||||||
|
self.pick()
|
||||||
|
|
||||||
|
# end::TOMBOLA_BINGO[]
|
||||||
2
13-protocol-abc/double/double_object.py
Normal file
2
13-protocol-abc/double/double_object.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def double(x: object) -> object:
|
||||||
|
return x * 2
|
||||||
11
13-protocol-abc/double/double_protocol.py
Normal file
11
13-protocol-abc/double/double_protocol.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from typing import TypeVar, Protocol
|
||||||
|
|
||||||
|
T = TypeVar('T') # <1>
|
||||||
|
|
||||||
|
class Repeatable(Protocol):
|
||||||
|
def __mul__(self: T, repeat_count: int) -> T: ... # <2>
|
||||||
|
|
||||||
|
RT = TypeVar('RT', bound=Repeatable) # <3>
|
||||||
|
|
||||||
|
def double(x: RT) -> RT: # <4>
|
||||||
|
return x * 2
|
||||||
6
13-protocol-abc/double/double_sequence.py
Normal file
6
13-protocol-abc/double/double_sequence.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from collections import abc
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
def double(x: abc.Sequence) -> Any:
|
||||||
|
return x * 2
|
||||||
|
|
||||||
56
13-protocol-abc/double/double_test.py
Normal file
56
13-protocol-abc/double/double_test.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import pytest
|
||||||
|
from double_protocol import double
|
||||||
|
|
||||||
|
def test_double_int() -> None:
|
||||||
|
given = 2
|
||||||
|
result = double(given)
|
||||||
|
assert result == given * 2
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(given)
|
||||||
|
reveal_type(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_double_str() -> None:
|
||||||
|
given = 'A'
|
||||||
|
result = double(given)
|
||||||
|
assert result == given * 2
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(given)
|
||||||
|
reveal_type(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_double_fraction() -> None:
|
||||||
|
from fractions import Fraction
|
||||||
|
given = Fraction(2, 5)
|
||||||
|
result = double(given)
|
||||||
|
assert result == given * 2
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(given)
|
||||||
|
reveal_type(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_double_array() -> None:
|
||||||
|
from array import array
|
||||||
|
given = array('d', [1.0, 2.0, 3.14])
|
||||||
|
result = double(given)
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(given)
|
||||||
|
reveal_type(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_double_nparray() -> None:
|
||||||
|
import numpy as np # type: ignore
|
||||||
|
given = np.array([[1, 2], [3, 4]])
|
||||||
|
result = double(given)
|
||||||
|
comparison = result == given * 2
|
||||||
|
assert comparison.all()
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(given)
|
||||||
|
reveal_type(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_double_none() -> None:
|
||||||
|
given = None
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
result = double(given)
|
||||||
17
13-protocol-abc/drum.py
Normal file
17
13-protocol-abc/drum.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from random import shuffle
|
||||||
|
|
||||||
|
from tombola import Tombola
|
||||||
|
|
||||||
|
|
||||||
|
class TumblingDrum(Tombola):
|
||||||
|
|
||||||
|
def __init__(self, iterable):
|
||||||
|
self._balls = []
|
||||||
|
self.load(iterable)
|
||||||
|
|
||||||
|
def load(self, iterable):
|
||||||
|
self._balls.extend(iterable)
|
||||||
|
shuffle(self._balls)
|
||||||
|
|
||||||
|
def pick(self):
|
||||||
|
return self._balls.pop()
|
||||||
26
13-protocol-abc/frenchdeck2.py
Normal file
26
13-protocol-abc/frenchdeck2.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
Card = collections.namedtuple('Card', ['rank', 'suit'])
|
||||||
|
|
||||||
|
class FrenchDeck2(collections.MutableSequence):
|
||||||
|
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
|
||||||
|
suits = 'spades diamonds clubs hearts'.split()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._cards = [Card(rank, suit) for suit in self.suits
|
||||||
|
for rank in self.ranks]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._cards)
|
||||||
|
|
||||||
|
def __getitem__(self, position):
|
||||||
|
return self._cards[position]
|
||||||
|
|
||||||
|
def __setitem__(self, position, value): # <1>
|
||||||
|
self._cards[position] = value
|
||||||
|
|
||||||
|
def __delitem__(self, position): # <2>
|
||||||
|
del self._cards[position]
|
||||||
|
|
||||||
|
def insert(self, position, value): # <3>
|
||||||
|
self._cards.insert(position, value)
|
||||||
30
13-protocol-abc/lotto.py
Normal file
30
13-protocol-abc/lotto.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# tag::LOTTERY_BLOWER[]
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from tombola import Tombola
|
||||||
|
|
||||||
|
|
||||||
|
class LotteryBlower(Tombola):
|
||||||
|
|
||||||
|
def __init__(self, iterable):
|
||||||
|
self._balls = list(iterable) # <1>
|
||||||
|
|
||||||
|
def load(self, iterable):
|
||||||
|
self._balls.extend(iterable)
|
||||||
|
|
||||||
|
def pick(self):
|
||||||
|
try:
|
||||||
|
position = random.randrange(len(self._balls)) # <2>
|
||||||
|
except ValueError:
|
||||||
|
raise LookupError('pick from empty BingoCage')
|
||||||
|
return self._balls.pop(position) # <3>
|
||||||
|
|
||||||
|
def loaded(self): # <4>
|
||||||
|
return bool(self._balls)
|
||||||
|
|
||||||
|
def inspect(self): # <5>
|
||||||
|
return tuple(sorted(self._balls))
|
||||||
|
|
||||||
|
|
||||||
|
# end::LOTTERY_BLOWER[]
|
||||||
35
13-protocol-abc/tombola.py
Normal file
35
13-protocol-abc/tombola.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# tag::TOMBOLA_ABC[]
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
class Tombola(abc.ABC): # <1>
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def load(self, iterable): # <2>
|
||||||
|
"""Add items from an iterable."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def pick(self): # <3>
|
||||||
|
"""Remove item at random, returning it.
|
||||||
|
|
||||||
|
This method should raise `LookupError` when the instance is empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def loaded(self): # <4>
|
||||||
|
"""Return `True` if there's at least 1 item, `False` otherwise."""
|
||||||
|
return bool(self.inspect()) # <5>
|
||||||
|
|
||||||
|
|
||||||
|
def inspect(self):
|
||||||
|
"""Return a sorted tuple with the items currently inside."""
|
||||||
|
items = []
|
||||||
|
while True: # <6>
|
||||||
|
try:
|
||||||
|
items.append(self.pick())
|
||||||
|
except LookupError:
|
||||||
|
break
|
||||||
|
self.load(items) # <7>
|
||||||
|
return tuple(sorted(items))
|
||||||
|
|
||||||
|
|
||||||
|
# end::TOMBOLA_ABC[]
|
||||||
36
13-protocol-abc/tombola_runner.py
Normal file
36
13-protocol-abc/tombola_runner.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# tag::TOMBOLA_RUNNER[]
|
||||||
|
import doctest
|
||||||
|
|
||||||
|
from tombola import Tombola
|
||||||
|
|
||||||
|
# modules to test
|
||||||
|
import bingo, lotto, tombolist, drum # <1>
|
||||||
|
|
||||||
|
TEST_FILE = 'tombola_tests.rst'
|
||||||
|
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}'
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
verbose = '-v' in argv
|
||||||
|
real_subclasses = Tombola.__subclasses__() # <2>
|
||||||
|
virtual_subclasses = list(Tombola._abc_registry) # <3>
|
||||||
|
|
||||||
|
for cls in real_subclasses + virtual_subclasses: # <4>
|
||||||
|
test(cls, verbose)
|
||||||
|
|
||||||
|
|
||||||
|
def test(cls, verbose=False):
|
||||||
|
|
||||||
|
res = doctest.testfile(
|
||||||
|
TEST_FILE,
|
||||||
|
globs={'ConcreteTombola': cls}, # <5>
|
||||||
|
verbose=verbose,
|
||||||
|
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
|
||||||
|
tag = 'FAIL' if res.failed else 'OK'
|
||||||
|
print(TEST_MSG.format(cls.__name__, res, tag)) # <6>
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
|
# end::TOMBOLA_RUNNER[]
|
||||||
64
13-protocol-abc/tombola_subhook.py
Normal file
64
13-protocol-abc/tombola_subhook.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Variation of ``tombola.Tombola`` implementing ``__subclasshook__``.
|
||||||
|
|
||||||
|
Tests with simple classes::
|
||||||
|
|
||||||
|
>>> Tombola.__subclasshook__(object)
|
||||||
|
NotImplemented
|
||||||
|
>>> class Complete:
|
||||||
|
... def __init__(): pass
|
||||||
|
... def load(): pass
|
||||||
|
... def pick(): pass
|
||||||
|
... def loaded(): pass
|
||||||
|
...
|
||||||
|
>>> Tombola.__subclasshook__(Complete)
|
||||||
|
True
|
||||||
|
>>> issubclass(Complete, Tombola)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from inspect import getmembers, isfunction
|
||||||
|
|
||||||
|
|
||||||
|
class Tombola(ABC): # <1>
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self, iterable): # <2>
|
||||||
|
"""New instance is loaded from an iterable."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load(self, iterable):
|
||||||
|
"""Add items from an iterable."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def pick(self): # <3>
|
||||||
|
"""Remove item at random, returning it.
|
||||||
|
|
||||||
|
This method should raise `LookupError` when the instance is empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def loaded(self): # <4>
|
||||||
|
try:
|
||||||
|
item = self.pick()
|
||||||
|
except LookupError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.load([item]) # put it back
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __subclasshook__(cls, other_cls):
|
||||||
|
if cls is Tombola:
|
||||||
|
interface_names = function_names(cls)
|
||||||
|
found_names = set()
|
||||||
|
for a_cls in other_cls.__mro__:
|
||||||
|
found_names |= function_names(a_cls)
|
||||||
|
if found_names >= interface_names:
|
||||||
|
return True
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
def function_names(obj):
|
||||||
|
return {name for name, _ in getmembers(obj, isfunction)}
|
||||||
82
13-protocol-abc/tombola_tests.rst
Normal file
82
13-protocol-abc/tombola_tests.rst
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
==============
|
||||||
|
Tombola tests
|
||||||
|
==============
|
||||||
|
|
||||||
|
Every concrete subclass of Tombola should pass these tests.
|
||||||
|
|
||||||
|
|
||||||
|
Create and load instance from iterable::
|
||||||
|
|
||||||
|
>>> balls = list(range(3))
|
||||||
|
>>> globe = ConcreteTombola(balls)
|
||||||
|
>>> globe.loaded()
|
||||||
|
True
|
||||||
|
>>> globe.inspect()
|
||||||
|
(0, 1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
Pick and collect balls::
|
||||||
|
|
||||||
|
>>> picks = []
|
||||||
|
>>> picks.append(globe.pick())
|
||||||
|
>>> picks.append(globe.pick())
|
||||||
|
>>> picks.append(globe.pick())
|
||||||
|
|
||||||
|
|
||||||
|
Check state and results::
|
||||||
|
|
||||||
|
>>> globe.loaded()
|
||||||
|
False
|
||||||
|
>>> sorted(picks) == balls
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Reload::
|
||||||
|
|
||||||
|
>>> globe.load(balls)
|
||||||
|
>>> globe.loaded()
|
||||||
|
True
|
||||||
|
>>> picks = [globe.pick() for i in balls]
|
||||||
|
>>> globe.loaded()
|
||||||
|
False
|
||||||
|
|
||||||
|
|
||||||
|
Check that `LookupError` (or a subclass) is the exception
|
||||||
|
thrown when the device is empty::
|
||||||
|
|
||||||
|
>>> globe = ConcreteTombola([])
|
||||||
|
>>> try:
|
||||||
|
... globe.pick()
|
||||||
|
... except LookupError as exc:
|
||||||
|
... print('OK')
|
||||||
|
OK
|
||||||
|
|
||||||
|
|
||||||
|
Load and pick 100 balls to verify that they all come out::
|
||||||
|
|
||||||
|
>>> balls = list(range(100))
|
||||||
|
>>> globe = ConcreteTombola(balls)
|
||||||
|
>>> picks = []
|
||||||
|
>>> while globe.inspect():
|
||||||
|
... picks.append(globe.pick())
|
||||||
|
>>> len(picks) == len(balls)
|
||||||
|
True
|
||||||
|
>>> set(picks) == set(balls)
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Check that the order has changed and is not simply reversed::
|
||||||
|
|
||||||
|
>>> picks != balls
|
||||||
|
True
|
||||||
|
>>> picks[::-1] != balls
|
||||||
|
True
|
||||||
|
|
||||||
|
Note: the previous 2 tests have a *very* small chance of failing
|
||||||
|
even if the implementation is OK. The probability of the 100
|
||||||
|
balls coming out, by chance, in the order they were inspect is
|
||||||
|
1/100!, or approximately 1.07e-158. It's much easier to win the
|
||||||
|
Lotto or to become a billionaire working as a programmer.
|
||||||
|
|
||||||
|
THE END
|
||||||
|
|
||||||
23
13-protocol-abc/tombolist.py
Normal file
23
13-protocol-abc/tombolist.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from random import randrange
|
||||||
|
|
||||||
|
from tombola import Tombola
|
||||||
|
|
||||||
|
@Tombola.register # <1>
|
||||||
|
class TomboList(list): # <2>
|
||||||
|
|
||||||
|
def pick(self):
|
||||||
|
if self: # <3>
|
||||||
|
position = randrange(len(self))
|
||||||
|
return self.pop(position) # <4>
|
||||||
|
else:
|
||||||
|
raise LookupError('pop from empty TomboList')
|
||||||
|
|
||||||
|
load = list.extend # <5>
|
||||||
|
|
||||||
|
def loaded(self):
|
||||||
|
return bool(self) # <6>
|
||||||
|
|
||||||
|
def inspect(self):
|
||||||
|
return tuple(sorted(self))
|
||||||
|
|
||||||
|
# Tombola.register(TomboList) # <7>
|
||||||
5
13-protocol-abc/typing/randompick.py
Normal file
5
13-protocol-abc/typing/randompick.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from typing import Protocol, runtime_checkable, Any
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class RandomPicker(Protocol):
|
||||||
|
def pick(self) -> Any: ...
|
||||||
25
13-protocol-abc/typing/randompick_test.py
Normal file
25
13-protocol-abc/typing/randompick_test.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import random
|
||||||
|
from typing import Any, Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
|
from randompick import RandomPicker # <1>
|
||||||
|
|
||||||
|
class SimplePicker(): # <2>
|
||||||
|
def __init__(self, items: Iterable) -> None:
|
||||||
|
self._items = list(items)
|
||||||
|
random.shuffle(self._items)
|
||||||
|
|
||||||
|
def pick(self) -> Any: # <3>
|
||||||
|
return self._items.pop()
|
||||||
|
|
||||||
|
def test_isinstance() -> None: # <4>
|
||||||
|
popper = SimplePicker([1])
|
||||||
|
assert isinstance(popper, RandomPicker)
|
||||||
|
|
||||||
|
def test_item_type() -> None: # <5>
|
||||||
|
items = [1, 2]
|
||||||
|
popper = SimplePicker(items)
|
||||||
|
item = popper.pick()
|
||||||
|
assert item in items
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(item) # <6>
|
||||||
|
assert isinstance(item, int)
|
||||||
6
13-protocol-abc/typing/randompickload.py
Normal file
6
13-protocol-abc/typing/randompickload.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from typing import Protocol, runtime_checkable, Any, Iterable
|
||||||
|
from randompick import RandomPicker
|
||||||
|
|
||||||
|
@runtime_checkable # <1>
|
||||||
|
class LoadableRandomPicker(RandomPicker, Protocol): # <2>
|
||||||
|
def load(self, Iterable) -> None: ... # <3>
|
||||||
32
13-protocol-abc/typing/randompickload_test.py
Normal file
32
13-protocol-abc/typing/randompickload_test.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import random
|
||||||
|
from typing import Any, Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
|
from randompickload import LoadableRandomPicker
|
||||||
|
|
||||||
|
class SimplePicker():
|
||||||
|
def __init__(self, items: Iterable) -> None:
|
||||||
|
self._items = list(items)
|
||||||
|
random.shuffle(self._items)
|
||||||
|
|
||||||
|
def pick(self) -> Any:
|
||||||
|
return self._items.pop()
|
||||||
|
|
||||||
|
class LoadablePicker(): # <1>
|
||||||
|
def __init__(self, items: Iterable) -> None:
|
||||||
|
self.load(items)
|
||||||
|
|
||||||
|
def pick(self) -> Any: # <2>
|
||||||
|
return self._items.pop()
|
||||||
|
|
||||||
|
def load(self, items: Iterable) -> Any: # <3>
|
||||||
|
self._items = list(items)
|
||||||
|
random.shuffle(self._items)
|
||||||
|
|
||||||
|
def test_isinstance() -> None: # <4>
|
||||||
|
popper = LoadablePicker([1])
|
||||||
|
assert isinstance(popper, LoadableRandomPicker)
|
||||||
|
|
||||||
|
def test_isinstance_not() -> None: # <5>
|
||||||
|
popper = SimplePicker([1])
|
||||||
|
assert not isinstance(popper, LoadableRandomPicker)
|
||||||
|
|
||||||
172
13-protocol-abc/typing/vector2d_v4.py
Normal file
172
13-protocol-abc/typing/vector2d_v4.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
A two-dimensional vector class
|
||||||
|
|
||||||
|
>>> v1 = Vector2d(3, 4)
|
||||||
|
>>> print(v1.x, v1.y)
|
||||||
|
3.0 4.0
|
||||||
|
>>> x, y = v1
|
||||||
|
>>> x, y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1
|
||||||
|
Vector2d(3.0, 4.0)
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> octets = bytes(v1)
|
||||||
|
>>> octets
|
||||||
|
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> bool(v1), bool(Vector2d(0, 0))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.frombytes()`` class method:
|
||||||
|
|
||||||
|
>>> v1_clone = Vector2d.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector2d(3.0, 4.0)
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates:
|
||||||
|
|
||||||
|
>>> format(v1)
|
||||||
|
'(3.0, 4.0)'
|
||||||
|
>>> format(v1, '.2f')
|
||||||
|
'(3.00, 4.00)'
|
||||||
|
>>> format(v1, '.3e')
|
||||||
|
'(3.000e+00, 4.000e+00)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of the ``angle`` method::
|
||||||
|
|
||||||
|
>>> Vector2d(0, 0).angle()
|
||||||
|
0.0
|
||||||
|
>>> Vector2d(1, 0).angle()
|
||||||
|
0.0
|
||||||
|
>>> epsilon = 10**-8
|
||||||
|
>>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
|
||||||
|
True
|
||||||
|
>>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with polar coordinates:
|
||||||
|
|
||||||
|
>>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS
|
||||||
|
'<1.414213..., 0.785398...>'
|
||||||
|
>>> format(Vector2d(1, 1), '.3ep')
|
||||||
|
'<1.414e+00, 7.854e-01>'
|
||||||
|
>>> format(Vector2d(1, 1), '0.5fp')
|
||||||
|
'<1.41421, 0.78540>'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``x`` and ``y`` read-only properties:
|
||||||
|
|
||||||
|
>>> v1.x, v1.y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1.x = 123
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: can't set attribute
|
||||||
|
|
||||||
|
|
||||||
|
Tests of hashing:
|
||||||
|
|
||||||
|
>>> v1 = Vector2d(3, 4)
|
||||||
|
>>> v2 = Vector2d(3.1, 4.2)
|
||||||
|
>>> hash(v1), hash(v2)
|
||||||
|
(7, 384307168202284039)
|
||||||
|
>>> len(set([v1, v2]))
|
||||||
|
2
|
||||||
|
|
||||||
|
Converting to/from a ``complex``:
|
||||||
|
# tag::VECTOR2D_V4_DEMO[]
|
||||||
|
>>> from typing import SupportsComplex
|
||||||
|
>>> v3 = Vector2d(1.5, 2.5)
|
||||||
|
>>> isinstance(v3, SupportsComplex) # <1>
|
||||||
|
True
|
||||||
|
>>> complex(v3) # <2>
|
||||||
|
(1.5+2.5j)
|
||||||
|
>>> Vector2d.fromcomplex(4+5j) # <3>
|
||||||
|
Vector2d(4.0, 5.0)
|
||||||
|
|
||||||
|
# end::VECTOR2D_V4_DEMO[]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import math
|
||||||
|
|
||||||
|
class Vector2d:
|
||||||
|
typecode = 'd'
|
||||||
|
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.__x = float(x)
|
||||||
|
self.__y = float(y)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x(self):
|
||||||
|
return self.__x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self):
|
||||||
|
return self.__y
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (i for i in (self.x, self.y))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
class_name = type(self).__name__
|
||||||
|
return '{}({!r}, {!r})'.format(class_name, *self)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(tuple(self))
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (bytes([ord(self.typecode)]) +
|
||||||
|
bytes(array(self.typecode, self)))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return tuple(self) == tuple(other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.x) ^ hash(self.y)
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return math.hypot(self.x, self.y)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(abs(self))
|
||||||
|
|
||||||
|
def angle(self):
|
||||||
|
return math.atan2(self.y, self.x)
|
||||||
|
|
||||||
|
def __format__(self, fmt_spec=''):
|
||||||
|
if fmt_spec.endswith('p'):
|
||||||
|
fmt_spec = fmt_spec[:-1]
|
||||||
|
coords = (abs(self), self.angle())
|
||||||
|
outer_fmt = '<{}, {}>'
|
||||||
|
else:
|
||||||
|
coords = self
|
||||||
|
outer_fmt = '({}, {})'
|
||||||
|
components = (format(c, fmt_spec) for c in coords)
|
||||||
|
return outer_fmt.format(*components)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, octets):
|
||||||
|
typecode = chr(octets[0])
|
||||||
|
memv = memoryview(octets[1:]).cast(typecode)
|
||||||
|
return cls(*memv)
|
||||||
|
|
||||||
|
# tag::VECTOR2D_V4_COMPLEX[]
|
||||||
|
def __complex__(self):
|
||||||
|
return complex(self.x, self.y)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromcomplex(cls, datum):
|
||||||
|
return Vector2d(datum.real, datum.imag) # <1>
|
||||||
|
# end::VECTOR2D_V4_COMPLEX[]
|
||||||
56
13-protocol-abc/typing/vector2d_v4_test.py
Normal file
56
13-protocol-abc/typing/vector2d_v4_test.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from typing import SupportsComplex, SupportsAbs, Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import math
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vector2d_v4 import Vector2d
|
||||||
|
|
||||||
|
def test_SupportsComplex_subclass() -> None:
|
||||||
|
assert issubclass(Vector2d, SupportsComplex)
|
||||||
|
|
||||||
|
def test_SupportsComplex_isinstance() -> None:
|
||||||
|
v = Vector2d(3, 4)
|
||||||
|
assert isinstance(v, SupportsComplex)
|
||||||
|
c = complex(v)
|
||||||
|
assert c == 3 + 4j
|
||||||
|
|
||||||
|
def test_SupportsAbs_subclass() -> None:
|
||||||
|
assert issubclass(Vector2d, SupportsAbs)
|
||||||
|
|
||||||
|
def test_SupportsAbs_isinstance() -> None:
|
||||||
|
v = Vector2d(3, 4)
|
||||||
|
assert isinstance(v, SupportsAbs)
|
||||||
|
r = abs(v)
|
||||||
|
assert r == 5.0
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(r) # Revealed type is 'Any'
|
||||||
|
|
||||||
|
def magnitude(v: SupportsAbs) -> float:
|
||||||
|
return abs(v)
|
||||||
|
|
||||||
|
def test_SupportsAbs_Vector2d_argument() -> None:
|
||||||
|
assert magnitude(Vector2d(3, 4)) == 5.0
|
||||||
|
|
||||||
|
def test_SupportsAbs_object_argument() -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
magnitude(object())
|
||||||
|
# mypy error:
|
||||||
|
# Argument 1 to "magnitude" has incompatible type "object"; expected "SupportsAbs[Any]"
|
||||||
|
|
||||||
|
def polar(datum: SupportsComplex) -> Tuple[float, float]:
|
||||||
|
c = complex(datum)
|
||||||
|
return abs(c), math.atan2(c.imag, c.real)
|
||||||
|
|
||||||
|
def test_SupportsComplex_Vector2d_argument() -> None:
|
||||||
|
assert polar(Vector2d(2, 0)) == (2, 0)
|
||||||
|
expected = (2, math.pi / 2)
|
||||||
|
result = polar(Vector2d(0, 2))
|
||||||
|
assert math.isclose(result[0], expected[0])
|
||||||
|
assert math.isclose(result[1], expected[1])
|
||||||
|
|
||||||
|
def test_SupportsComplex_complex_argument() -> None:
|
||||||
|
assert polar(complex(2, 0)) == (2, 0)
|
||||||
|
expected = (2, math.pi / 2)
|
||||||
|
result = polar(complex(0, 2))
|
||||||
|
assert math.isclose(result[0], expected[0])
|
||||||
|
assert math.isclose(result[1], expected[1])
|
||||||
174
13-protocol-abc/typing/vector2d_v5.py
Normal file
174
13-protocol-abc/typing/vector2d_v5.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
"""
|
||||||
|
A two-dimensional vector class
|
||||||
|
|
||||||
|
>>> v1 = Vector2d(3, 4)
|
||||||
|
>>> print(v1.x, v1.y)
|
||||||
|
3.0 4.0
|
||||||
|
>>> x, y = v1
|
||||||
|
>>> x, y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1
|
||||||
|
Vector2d(3.0, 4.0)
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> octets = bytes(v1)
|
||||||
|
>>> octets
|
||||||
|
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> bool(v1), bool(Vector2d(0, 0))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.frombytes()`` class method:
|
||||||
|
|
||||||
|
>>> v1_clone = Vector2d.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector2d(3.0, 4.0)
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates:
|
||||||
|
|
||||||
|
>>> format(v1)
|
||||||
|
'(3.0, 4.0)'
|
||||||
|
>>> format(v1, '.2f')
|
||||||
|
'(3.00, 4.00)'
|
||||||
|
>>> format(v1, '.3e')
|
||||||
|
'(3.000e+00, 4.000e+00)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of the ``angle`` method::
|
||||||
|
|
||||||
|
>>> Vector2d(0, 0).angle()
|
||||||
|
0.0
|
||||||
|
>>> Vector2d(1, 0).angle()
|
||||||
|
0.0
|
||||||
|
>>> epsilon = 10**-8
|
||||||
|
>>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
|
||||||
|
True
|
||||||
|
>>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with polar coordinates:
|
||||||
|
|
||||||
|
>>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS
|
||||||
|
'<1.414213..., 0.785398...>'
|
||||||
|
>>> format(Vector2d(1, 1), '.3ep')
|
||||||
|
'<1.414e+00, 7.854e-01>'
|
||||||
|
>>> format(Vector2d(1, 1), '0.5fp')
|
||||||
|
'<1.41421, 0.78540>'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``x`` and ``y`` read-only properties:
|
||||||
|
|
||||||
|
>>> v1.x, v1.y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1.x = 123
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: can't set attribute
|
||||||
|
|
||||||
|
|
||||||
|
Tests of hashing:
|
||||||
|
|
||||||
|
>>> v1 = Vector2d(3, 4)
|
||||||
|
>>> v2 = Vector2d(3.1, 4.2)
|
||||||
|
>>> hash(v1), hash(v2)
|
||||||
|
(7, 384307168202284039)
|
||||||
|
>>> len(set([v1, v2]))
|
||||||
|
2
|
||||||
|
|
||||||
|
Converting to/from a ``complex``:
|
||||||
|
|
||||||
|
>>> from typing import SupportsComplex
|
||||||
|
>>> v3 = Vector2d(1.5, 2.5)
|
||||||
|
>>> isinstance(v3, SupportsComplex) # <1>
|
||||||
|
True
|
||||||
|
>>> complex(v3) # <2>
|
||||||
|
(1.5+2.5j)
|
||||||
|
>>> Vector2d.fromcomplex(4+5j) # <3>
|
||||||
|
Vector2d(4.0, 5.0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import math
|
||||||
|
from typing import SupportsComplex, Iterator
|
||||||
|
|
||||||
|
class Vector2d:
|
||||||
|
typecode = 'd'
|
||||||
|
|
||||||
|
def __init__(self, x, y) -> None:
|
||||||
|
self.__x = float(x)
|
||||||
|
self.__y = float(y)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x(self) -> float:
|
||||||
|
return self.__x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self) -> float:
|
||||||
|
return self.__y
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[float]:
|
||||||
|
return (i for i in (self.x, self.y))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
class_name = type(self).__name__
|
||||||
|
return '{}({!r}, {!r})'.format(class_name, *self)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return str(tuple(self))
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return (bytes([ord(self.typecode)]) +
|
||||||
|
bytes(array(self.typecode, self)))
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
return tuple(self) == tuple(other)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.x) ^ hash(self.y)
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return bool(abs(self))
|
||||||
|
|
||||||
|
def angle(self) -> float:
|
||||||
|
return math.atan2(self.y, self.x)
|
||||||
|
|
||||||
|
def __format__(self, fmt_spec='') -> str:
|
||||||
|
if fmt_spec.endswith('p'):
|
||||||
|
fmt_spec = fmt_spec[:-1]
|
||||||
|
coords = (abs(self), self.angle())
|
||||||
|
outer_fmt = '<{}, {}>'
|
||||||
|
else:
|
||||||
|
coords = self
|
||||||
|
outer_fmt = '({}, {})'
|
||||||
|
components = (format(c, fmt_spec) for c in coords)
|
||||||
|
return outer_fmt.format(*components)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, octets) -> Vector2d:
|
||||||
|
typecode = chr(octets[0])
|
||||||
|
memv = memoryview(octets[1:]).cast(typecode)
|
||||||
|
return cls(*memv)
|
||||||
|
|
||||||
|
# tag::VECTOR2D_V5_COMPLEX[]
|
||||||
|
def __abs__(self) -> float: # <1>
|
||||||
|
return math.hypot(self.x, self.y)
|
||||||
|
|
||||||
|
def __complex__(self) -> complex: # <2>
|
||||||
|
return complex(self.x, self.y)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # <3>
|
||||||
|
c = complex(datum) # <4>
|
||||||
|
return Vector2d(c.real, c.imag)
|
||||||
|
# end::VECTOR2D_V5_COMPLEX[]
|
||||||
35
13-protocol-abc/typing/vector2d_v5_test.py
Normal file
35
13-protocol-abc/typing/vector2d_v5_test.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from vector2d_v5 import Vector2d
|
||||||
|
from typing import SupportsComplex, SupportsAbs, TYPE_CHECKING
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_SupportsComplex_subclass() -> None:
|
||||||
|
assert issubclass(Vector2d, SupportsComplex)
|
||||||
|
|
||||||
|
def test_SupportsComplex_isinstance() -> None:
|
||||||
|
v = Vector2d(3, 4)
|
||||||
|
assert isinstance(v, SupportsComplex)
|
||||||
|
c = complex(v)
|
||||||
|
assert c == 3 + 4j
|
||||||
|
|
||||||
|
def test_SupportsAbs_subclass() -> None:
|
||||||
|
assert issubclass(Vector2d, SupportsAbs)
|
||||||
|
|
||||||
|
def test_SupportsAbs_isinstance() -> None:
|
||||||
|
v = Vector2d(3, 4)
|
||||||
|
assert isinstance(v, SupportsAbs)
|
||||||
|
r = abs(v)
|
||||||
|
assert r == 5.0
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(r) # Revealed type is 'builtins.float*'
|
||||||
|
|
||||||
|
def magnitude(v: SupportsAbs) -> float:
|
||||||
|
return abs(v)
|
||||||
|
|
||||||
|
def test_SupportsAbs_Vector2d_argument() -> None:
|
||||||
|
assert 5.0 == magnitude(Vector2d(3, 4))
|
||||||
|
|
||||||
|
def test_SupportsAbs_object_argument() -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
assert 5.0 == magnitude(object())
|
||||||
4
14-inheritance/README.rst
Normal file
4
14-inheritance/README.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sample code for Chapter 14 - "Inheritance: for good or for worse"
|
||||||
|
|
||||||
|
From the book "Fluent Python, Second Edition" by Luciano Ramalho (O'Reilly, 2021)
|
||||||
|
https://learning.oreilly.com/library/view/fluent-python-2nd/9781492056348/
|
||||||
27
14-inheritance/diamond.py
Normal file
27
14-inheritance/diamond.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
class A:
|
||||||
|
def ping(self):
|
||||||
|
print('ping:', self)
|
||||||
|
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def pong(self):
|
||||||
|
print('pong:', self)
|
||||||
|
|
||||||
|
|
||||||
|
class C(A):
|
||||||
|
def pong(self):
|
||||||
|
print('PONG:', self)
|
||||||
|
|
||||||
|
|
||||||
|
class D(B, C):
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
super().ping()
|
||||||
|
print('post-ping:', self)
|
||||||
|
|
||||||
|
def pingpong(self):
|
||||||
|
self.ping()
|
||||||
|
super().ping()
|
||||||
|
self.pong()
|
||||||
|
super().pong()
|
||||||
|
C.pong(self)
|
||||||
4
16-op-overloading/README.rst
Normal file
4
16-op-overloading/README.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sample code for Chapter 13 - "Operator overloading: doing it right"
|
||||||
|
|
||||||
|
From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015)
|
||||||
|
http://shop.oreilly.com/product/0636920032519.do
|
||||||
28
16-op-overloading/bingo.py
Normal file
28
16-op-overloading/bingo.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# BEGIN TOMBOLA_BINGO
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from tombola import Tombola
|
||||||
|
|
||||||
|
|
||||||
|
class BingoCage(Tombola): # <1>
|
||||||
|
|
||||||
|
def __init__(self, items):
|
||||||
|
self._randomizer = random.SystemRandom() # <2>
|
||||||
|
self._items = []
|
||||||
|
self.load(items) # <3>
|
||||||
|
|
||||||
|
def load(self, items):
|
||||||
|
self._items.extend(items)
|
||||||
|
self._randomizer.shuffle(self._items) # <4>
|
||||||
|
|
||||||
|
def pick(self): # <5>
|
||||||
|
try:
|
||||||
|
return self._items.pop()
|
||||||
|
except IndexError:
|
||||||
|
raise LookupError('pick from empty BingoCage')
|
||||||
|
|
||||||
|
def __call__(self): # <7>
|
||||||
|
self.pick()
|
||||||
|
|
||||||
|
# END TOMBOLA_BINGO
|
||||||
86
16-op-overloading/bingoaddable.py
Normal file
86
16-op-overloading/bingoaddable.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
======================
|
||||||
|
AddableBingoCage tests
|
||||||
|
======================
|
||||||
|
|
||||||
|
|
||||||
|
Tests for __add__:
|
||||||
|
|
||||||
|
# tag::ADDABLE_BINGO_ADD_DEMO[]
|
||||||
|
|
||||||
|
>>> vowels = 'AEIOU'
|
||||||
|
>>> globe = AddableBingoCage(vowels) # <1>
|
||||||
|
>>> globe.inspect()
|
||||||
|
('A', 'E', 'I', 'O', 'U')
|
||||||
|
>>> globe.pick() in vowels # <2>
|
||||||
|
True
|
||||||
|
>>> len(globe.inspect()) # <3>
|
||||||
|
4
|
||||||
|
>>> globe2 = AddableBingoCage('XYZ') # <4>
|
||||||
|
>>> globe3 = globe + globe2
|
||||||
|
>>> len(globe3.inspect()) # <5>
|
||||||
|
7
|
||||||
|
>>> void = globe + [10, 20] # <6>
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'AddableBingoCage' and 'list'
|
||||||
|
|
||||||
|
|
||||||
|
# end::ADDABLE_BINGO_ADD_DEMO[]
|
||||||
|
|
||||||
|
Tests for __iadd__:
|
||||||
|
|
||||||
|
# tag::ADDABLE_BINGO_IADD_DEMO[]
|
||||||
|
|
||||||
|
>>> globe_orig = globe # <1>
|
||||||
|
>>> len(globe.inspect()) # <2>
|
||||||
|
4
|
||||||
|
>>> globe += globe2 # <3>
|
||||||
|
>>> len(globe.inspect())
|
||||||
|
7
|
||||||
|
>>> globe += ['M', 'N'] # <4>
|
||||||
|
>>> len(globe.inspect())
|
||||||
|
9
|
||||||
|
>>> globe is globe_orig # <5>
|
||||||
|
True
|
||||||
|
>>> globe += 1 # <6>
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: right operand in += must be 'AddableBingoCage' or an iterable
|
||||||
|
|
||||||
|
# end::ADDABLE_BINGO_IADD_DEMO[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::ADDABLE_BINGO[]
|
||||||
|
import itertools # <1>
|
||||||
|
|
||||||
|
from tombola import Tombola
|
||||||
|
from bingo import BingoCage
|
||||||
|
|
||||||
|
|
||||||
|
class AddableBingoCage(BingoCage): # <2>
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if isinstance(other, Tombola): # <3>
|
||||||
|
return AddableBingoCage(self.inspect() + other.inspect())
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __iadd__(self, other):
|
||||||
|
if isinstance(other, Tombola):
|
||||||
|
other_iterable = other.inspect() # <4>
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
other_iterable = iter(other) # <5>
|
||||||
|
except TypeError: # <6>
|
||||||
|
self_cls = type(self).__name__
|
||||||
|
msg = "right operand in += must be {!r} or an iterable"
|
||||||
|
raise TypeError(msg.format(self_cls))
|
||||||
|
self.load(other_iterable) # <7>
|
||||||
|
return self # <8>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# end::ADDABLE_BINGO[]
|
||||||
35
16-op-overloading/tombola.py
Normal file
35
16-op-overloading/tombola.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# BEGIN TOMBOLA_ABC
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
class Tombola(abc.ABC): # <1>
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def load(self, iterable): # <2>
|
||||||
|
"""Add items from an iterable."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def pick(self): # <3>
|
||||||
|
"""Remove item at random, returning it.
|
||||||
|
|
||||||
|
This method should raise `LookupError` when the instance is empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def loaded(self): # <4>
|
||||||
|
"""Return `True` if there's at least 1 item, `False` otherwise."""
|
||||||
|
return bool(self.inspect()) # <5>
|
||||||
|
|
||||||
|
|
||||||
|
def inspect(self):
|
||||||
|
"""Return a sorted tuple with the items currently inside."""
|
||||||
|
items = []
|
||||||
|
while True: # <6>
|
||||||
|
try:
|
||||||
|
items.append(self.pick())
|
||||||
|
except LookupError:
|
||||||
|
break
|
||||||
|
self.load(items) # <7>
|
||||||
|
return tuple(sorted(items))
|
||||||
|
|
||||||
|
|
||||||
|
# END TOMBOLA_ABC
|
||||||
35
16-op-overloading/unary_plus_decimal.py
Normal file
35
16-op-overloading/unary_plus_decimal.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
# tag::UNARY_PLUS_DECIMAL[]
|
||||||
|
|
||||||
|
>>> import decimal
|
||||||
|
>>> ctx = decimal.getcontext() # <1>
|
||||||
|
>>> ctx.prec = 40 # <2>
|
||||||
|
>>> one_third = decimal.Decimal('1') / decimal.Decimal('3') # <3>
|
||||||
|
>>> one_third # <4>
|
||||||
|
Decimal('0.3333333333333333333333333333333333333333')
|
||||||
|
>>> one_third == +one_third # <5>
|
||||||
|
True
|
||||||
|
>>> ctx.prec = 28 # <6>
|
||||||
|
>>> one_third == +one_third # <7>
|
||||||
|
False
|
||||||
|
>>> +one_third # <8>
|
||||||
|
Decimal('0.3333333333333333333333333333')
|
||||||
|
|
||||||
|
# end::UNARY_PLUS_DECIMAL[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import decimal
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
with decimal.localcontext() as ctx:
|
||||||
|
ctx.prec = 40
|
||||||
|
print('precision:', ctx.prec)
|
||||||
|
one_third = decimal.Decimal('1') / decimal.Decimal('3')
|
||||||
|
print(' one_third:', one_third)
|
||||||
|
print(' +one_third:', +one_third)
|
||||||
|
|
||||||
|
print('precision:', decimal.getcontext().prec)
|
||||||
|
print(' one_third:', one_third)
|
||||||
|
print(' +one_third:', +one_third)
|
||||||
151
16-op-overloading/vector2d_v3.py
Normal file
151
16-op-overloading/vector2d_v3.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
A 2-dimensional vector class
|
||||||
|
|
||||||
|
>>> v1 = Vector2d(3, 4)
|
||||||
|
>>> print(v1.x, v1.y)
|
||||||
|
3.0 4.0
|
||||||
|
>>> x, y = v1
|
||||||
|
>>> x, y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1
|
||||||
|
Vector2d(3.0, 4.0)
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> octets = bytes(v1)
|
||||||
|
>>> octets
|
||||||
|
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> bool(v1), bool(Vector2d(0, 0))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.frombytes()`` class method:
|
||||||
|
|
||||||
|
>>> v1_clone = Vector2d.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector2d(3.0, 4.0)
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates:
|
||||||
|
|
||||||
|
>>> format(v1)
|
||||||
|
'(3.0, 4.0)'
|
||||||
|
>>> format(v1, '.2f')
|
||||||
|
'(3.00, 4.00)'
|
||||||
|
>>> format(v1, '.3e')
|
||||||
|
'(3.000e+00, 4.000e+00)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of the ``angle`` method::
|
||||||
|
|
||||||
|
>>> Vector2d(0, 0).angle()
|
||||||
|
0.0
|
||||||
|
>>> Vector2d(1, 0).angle()
|
||||||
|
0.0
|
||||||
|
>>> epsilon = 10**-8
|
||||||
|
>>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
|
||||||
|
True
|
||||||
|
>>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with polar coordinates:
|
||||||
|
|
||||||
|
>>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS
|
||||||
|
'<1.414213..., 0.785398...>'
|
||||||
|
>>> format(Vector2d(1, 1), '.3ep')
|
||||||
|
'<1.414e+00, 7.854e-01>'
|
||||||
|
>>> format(Vector2d(1, 1), '0.5fp')
|
||||||
|
'<1.41421, 0.78540>'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of `x` and `y` read-only properties:
|
||||||
|
|
||||||
|
>>> v1.x, v1.y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1.x = 123
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: can't set attribute
|
||||||
|
|
||||||
|
|
||||||
|
Tests of hashing:
|
||||||
|
|
||||||
|
>>> v1 = Vector2d(3, 4)
|
||||||
|
>>> v2 = Vector2d(3.1, 4.2)
|
||||||
|
>>> hash(v1), hash(v2)
|
||||||
|
(7, 384307168202284039)
|
||||||
|
>>> len(set([v1, v2]))
|
||||||
|
2
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import math
|
||||||
|
|
||||||
|
class Vector2d:
|
||||||
|
typecode = 'd'
|
||||||
|
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.__x = float(x)
|
||||||
|
self.__y = float(y)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x(self):
|
||||||
|
return self.__x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self):
|
||||||
|
return self.__y
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (i for i in (self.x, self.y))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
class_name = type(self).__name__
|
||||||
|
return '{}({!r}, {!r})'.format(class_name, *self)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(tuple(self))
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (bytes([ord(self.typecode)]) +
|
||||||
|
bytes(array(self.typecode, self)))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return tuple(self) == tuple(other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.x) ^ hash(self.y)
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return math.hypot(self.x, self.y)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(abs(self))
|
||||||
|
|
||||||
|
def angle(self):
|
||||||
|
return math.atan2(self.y, self.x)
|
||||||
|
|
||||||
|
def __format__(self, fmt_spec=''):
|
||||||
|
if fmt_spec.endswith('p'):
|
||||||
|
fmt_spec = fmt_spec[:-1]
|
||||||
|
coords = (abs(self), self.angle())
|
||||||
|
outer_fmt = '<{}, {}>'
|
||||||
|
else:
|
||||||
|
coords = self
|
||||||
|
outer_fmt = '({}, {})'
|
||||||
|
components = (format(c, fmt_spec) for c in coords)
|
||||||
|
return outer_fmt.format(*components)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, octets):
|
||||||
|
typecode = chr(octets[0])
|
||||||
|
memv = memoryview(octets[1:]).cast(typecode)
|
||||||
|
return cls(*memv)
|
||||||
431
16-op-overloading/vector_py3_5.py
Normal file
431
16-op-overloading/vector_py3_5.py
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
"""
|
||||||
|
A multi-dimensional ``Vector`` class, take 9: operator ``@``
|
||||||
|
|
||||||
|
WARNING: This example requires Python 3.5 or later.
|
||||||
|
|
||||||
|
A ``Vector`` is built from an iterable of numbers::
|
||||||
|
|
||||||
|
>>> Vector([3.1, 4.2])
|
||||||
|
Vector([3.1, 4.2])
|
||||||
|
>>> Vector((3, 4, 5))
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> Vector(range(10))
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 2-dimensions (same results as ``vector2d_v1.py``)::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> x, y = v1
|
||||||
|
>>> x, y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> octets = bytes(v1)
|
||||||
|
>>> octets
|
||||||
|
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> bool(v1), bool(Vector([0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.frombytes()`` class method:
|
||||||
|
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 3-dimensions::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> x, y, z = v1
|
||||||
|
>>> x, y, z
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> abs(v1) # doctest:+ELLIPSIS
|
||||||
|
7.071067811...
|
||||||
|
>>> bool(v1), bool(Vector([0, 0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Tests with many dimensions::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
>>> abs(v7) # doctest:+ELLIPSIS
|
||||||
|
9.53939201...
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.__bytes__`` and ``.frombytes()`` methods::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of sequence behavior::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> len(v1)
|
||||||
|
3
|
||||||
|
>>> v1[0], v1[len(v1)-1], v1[-1]
|
||||||
|
(3.0, 5.0, 5.0)
|
||||||
|
|
||||||
|
|
||||||
|
Test of slicing::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7[-1]
|
||||||
|
6.0
|
||||||
|
>>> v7[1:4]
|
||||||
|
Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> v7[-1:]
|
||||||
|
Vector([6.0])
|
||||||
|
>>> v7[1,2]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: Vector indices must be integers
|
||||||
|
|
||||||
|
|
||||||
|
Tests of dynamic attribute access::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(10))
|
||||||
|
>>> v7.x
|
||||||
|
0.0
|
||||||
|
>>> v7.y, v7.z, v7.t
|
||||||
|
(1.0, 2.0, 3.0)
|
||||||
|
|
||||||
|
Dynamic attribute lookup failures::
|
||||||
|
|
||||||
|
>>> v7.k
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'k'
|
||||||
|
>>> v3 = Vector(range(3))
|
||||||
|
>>> v3.t
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 't'
|
||||||
|
>>> v3.spam
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'spam'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of hashing::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> v2 = Vector([3.1, 4.2])
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> v6 = Vector(range(6))
|
||||||
|
>>> hash(v1), hash(v3), hash(v6)
|
||||||
|
(7, 2, 1)
|
||||||
|
|
||||||
|
|
||||||
|
Most hash codes of non-integers vary from a 32-bit to 64-bit Python build::
|
||||||
|
|
||||||
|
>>> import sys
|
||||||
|
>>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 2D::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> format(v1)
|
||||||
|
'(3.0, 4.0)'
|
||||||
|
>>> format(v1, '.2f')
|
||||||
|
'(3.00, 4.00)'
|
||||||
|
>>> format(v1, '.3e')
|
||||||
|
'(3.000e+00, 4.000e+00)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 3D and 7D::
|
||||||
|
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> format(v3)
|
||||||
|
'(3.0, 4.0, 5.0)'
|
||||||
|
>>> format(Vector(range(7)))
|
||||||
|
'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::
|
||||||
|
|
||||||
|
>>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.414213..., 0.785398...>'
|
||||||
|
>>> format(Vector([1, 1]), '.3eh')
|
||||||
|
'<1.414e+00, 7.854e-01>'
|
||||||
|
>>> format(Vector([1, 1]), '0.5fh')
|
||||||
|
'<1.41421, 0.78540>'
|
||||||
|
>>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.73205..., 0.95531..., 0.78539...>'
|
||||||
|
>>> format(Vector([2, 2, 2]), '.3eh')
|
||||||
|
'<3.464e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 0, 0]), '0.5fh')
|
||||||
|
'<0.00000, 0.00000, 0.00000>'
|
||||||
|
>>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<2.0, 2.09439..., 2.18627..., 3.92699...>'
|
||||||
|
>>> format(Vector([2, 2, 2, 2]), '.3eh')
|
||||||
|
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
|
||||||
|
'<1.00000, 1.57080, 0.00000, 0.00000>'
|
||||||
|
|
||||||
|
|
||||||
|
Basic tests of operator ``+``::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v2 = Vector([6, 7, 8])
|
||||||
|
>>> v1 + v2
|
||||||
|
Vector([9.0, 11.0, 13.0])
|
||||||
|
>>> v1 + v2 == Vector([3+6, 4+7, 5+8])
|
||||||
|
True
|
||||||
|
>>> v3 = Vector([1, 2])
|
||||||
|
>>> v1 + v3 # short vectors are filled with 0.0 on addition
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types::
|
||||||
|
|
||||||
|
>>> v1 + (10, 20, 30)
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v1 + v2d
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types, swapped operands::
|
||||||
|
|
||||||
|
>>> (10, 20, 30) + v1
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v2d + v1
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with an unsuitable operand:
|
||||||
|
|
||||||
|
>>> v1 + 1
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'int'
|
||||||
|
>>> v1 + 'ABC'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'str'
|
||||||
|
|
||||||
|
|
||||||
|
Basic tests of operator ``*``::
|
||||||
|
|
||||||
|
>>> v1 = Vector([1, 2, 3])
|
||||||
|
>>> v1 * 10
|
||||||
|
Vector([10.0, 20.0, 30.0])
|
||||||
|
>>> 10 * v1
|
||||||
|
Vector([10.0, 20.0, 30.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``*`` with unusual but valid operands::
|
||||||
|
|
||||||
|
>>> v1 * True
|
||||||
|
Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> from fractions import Fraction
|
||||||
|
>>> v1 * Fraction(1, 3) # doctest:+ELLIPSIS
|
||||||
|
Vector([0.3333..., 0.6666..., 1.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``*`` with unsuitable operands::
|
||||||
|
|
||||||
|
>>> v1 * (1, 2)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: can't multiply sequence by non-int of type 'Vector'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of operator `==`::
|
||||||
|
|
||||||
|
>>> va = Vector(range(1, 4))
|
||||||
|
>>> vb = Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> va == vb
|
||||||
|
True
|
||||||
|
>>> vc = Vector([1, 2])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> vc == v2d
|
||||||
|
True
|
||||||
|
>>> va == (1, 2, 3)
|
||||||
|
False
|
||||||
|
|
||||||
|
|
||||||
|
Tests of operator `!=`::
|
||||||
|
|
||||||
|
>>> va != vb
|
||||||
|
False
|
||||||
|
>>> vc != v2d
|
||||||
|
False
|
||||||
|
>>> va != (1, 2, 3)
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests for operator `@` (Python >= 3.5), computing the dot product::
|
||||||
|
|
||||||
|
>>> va = Vector([1, 2, 3])
|
||||||
|
>>> vz = Vector([5, 6, 7])
|
||||||
|
>>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7
|
||||||
|
True
|
||||||
|
>>> [10, 20, 30] @ vz
|
||||||
|
380.0
|
||||||
|
>>> va @ 3
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for @: 'Vector' and 'int'
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import reprlib
|
||||||
|
import math
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
import itertools
|
||||||
|
import numbers
|
||||||
|
|
||||||
|
|
||||||
|
class Vector:
|
||||||
|
typecode = 'd'
|
||||||
|
|
||||||
|
def __init__(self, components):
|
||||||
|
self._components = array(self.typecode, components)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._components)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
components = reprlib.repr(self._components)
|
||||||
|
components = components[components.find('['):-1]
|
||||||
|
return 'Vector({})'.format(components)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(tuple(self))
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (bytes([ord(self.typecode)]) +
|
||||||
|
bytes(self._components))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Vector):
|
||||||
|
return (len(self) == len(other) and
|
||||||
|
all(a == b for a, b in zip(self, other)))
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
hashes = (hash(x) for x in self)
|
||||||
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return math.sqrt(sum(x * x for x in self))
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(abs(self))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._components)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
cls = type(self)
|
||||||
|
if isinstance(index, slice):
|
||||||
|
return cls(self._components[index])
|
||||||
|
elif isinstance(index, int):
|
||||||
|
return self._components[index]
|
||||||
|
else:
|
||||||
|
msg = '{.__name__} indices must be integers'
|
||||||
|
raise TypeError(msg.format(cls))
|
||||||
|
|
||||||
|
shortcut_names = 'xyzt'
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
cls = type(self)
|
||||||
|
if len(name) == 1:
|
||||||
|
pos = cls.shortcut_names.find(name)
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
|
msg = '{.__name__!r} object has no attribute {!r}'
|
||||||
|
raise AttributeError(msg.format(cls, name))
|
||||||
|
|
||||||
|
def angle(self, n):
|
||||||
|
r = math.sqrt(sum(x * x for x in self[n:]))
|
||||||
|
a = math.atan2(r, self[n-1])
|
||||||
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
|
return math.pi * 2 - a
|
||||||
|
else:
|
||||||
|
return a
|
||||||
|
|
||||||
|
def angles(self):
|
||||||
|
return (self.angle(n) for n in range(1, len(self)))
|
||||||
|
|
||||||
|
def __format__(self, fmt_spec=''):
|
||||||
|
if fmt_spec.endswith('h'): # hyperspherical coordinates
|
||||||
|
fmt_spec = fmt_spec[:-1]
|
||||||
|
coords = itertools.chain([abs(self)],
|
||||||
|
self.angles())
|
||||||
|
outer_fmt = '<{}>'
|
||||||
|
else:
|
||||||
|
coords = self
|
||||||
|
outer_fmt = '({})'
|
||||||
|
components = (format(c, fmt_spec) for c in coords)
|
||||||
|
return outer_fmt.format(', '.join(components))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, octets):
|
||||||
|
typecode = chr(octets[0])
|
||||||
|
memv = memoryview(octets[1:]).cast(typecode)
|
||||||
|
return cls(memv)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
try:
|
||||||
|
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
|
||||||
|
return Vector(a + b for a, b in pairs)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self + other
|
||||||
|
|
||||||
|
def __mul__(self, scalar):
|
||||||
|
if isinstance(scalar, numbers.Real):
|
||||||
|
return Vector(n * scalar for n in self)
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __rmul__(self, scalar):
|
||||||
|
return self * scalar
|
||||||
|
|
||||||
|
def __matmul__(self, other):
|
||||||
|
try:
|
||||||
|
return sum(a * b for a, b in zip(self, other))
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __rmatmul__(self, other):
|
||||||
|
return self @ other # this only works in Python 3.5
|
||||||
358
16-op-overloading/vector_v6.py
Normal file
358
16-op-overloading/vector_v6.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
"""
|
||||||
|
A multi-dimensional ``Vector`` class, take 6: operator ``+``
|
||||||
|
|
||||||
|
A ``Vector`` is built from an iterable of numbers::
|
||||||
|
|
||||||
|
>>> Vector([3.1, 4.2])
|
||||||
|
Vector([3.1, 4.2])
|
||||||
|
>>> Vector((3, 4, 5))
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> Vector(range(10))
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 2-dimensions (same results as ``vector2d_v1.py``)::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> x, y = v1
|
||||||
|
>>> x, y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> octets = bytes(v1)
|
||||||
|
>>> octets
|
||||||
|
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> bool(v1), bool(Vector([0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.frombytes()`` class method:
|
||||||
|
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 3-dimensions::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> x, y, z = v1
|
||||||
|
>>> x, y, z
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> abs(v1) # doctest:+ELLIPSIS
|
||||||
|
7.071067811...
|
||||||
|
>>> bool(v1), bool(Vector([0, 0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Tests with many dimensions::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
>>> abs(v7) # doctest:+ELLIPSIS
|
||||||
|
9.53939201...
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.__bytes__`` and ``.frombytes()`` methods::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of sequence behavior::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> len(v1)
|
||||||
|
3
|
||||||
|
>>> v1[0], v1[len(v1)-1], v1[-1]
|
||||||
|
(3.0, 5.0, 5.0)
|
||||||
|
|
||||||
|
|
||||||
|
Test of slicing::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7[-1]
|
||||||
|
6.0
|
||||||
|
>>> v7[1:4]
|
||||||
|
Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> v7[-1:]
|
||||||
|
Vector([6.0])
|
||||||
|
>>> v7[1,2]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'tuple' object cannot be interpreted as an integer
|
||||||
|
|
||||||
|
|
||||||
|
Tests of dynamic attribute access::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(10))
|
||||||
|
>>> v7.x
|
||||||
|
0.0
|
||||||
|
>>> v7.y, v7.z, v7.t
|
||||||
|
(1.0, 2.0, 3.0)
|
||||||
|
|
||||||
|
Dynamic attribute lookup failures::
|
||||||
|
|
||||||
|
>>> v7.k
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'k'
|
||||||
|
>>> v3 = Vector(range(3))
|
||||||
|
>>> v3.t
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 't'
|
||||||
|
>>> v3.spam
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'spam'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of hashing::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> v2 = Vector([3.1, 4.2])
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> v6 = Vector(range(6))
|
||||||
|
>>> hash(v1), hash(v3), hash(v6)
|
||||||
|
(7, 2, 1)
|
||||||
|
|
||||||
|
|
||||||
|
Most hash codes of non-integers vary from a 32-bit to 64-bit Python build::
|
||||||
|
|
||||||
|
>>> import sys
|
||||||
|
>>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 2D::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> format(v1)
|
||||||
|
'(3.0, 4.0)'
|
||||||
|
>>> format(v1, '.2f')
|
||||||
|
'(3.00, 4.00)'
|
||||||
|
>>> format(v1, '.3e')
|
||||||
|
'(3.000e+00, 4.000e+00)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 3D and 7D::
|
||||||
|
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> format(v3)
|
||||||
|
'(3.0, 4.0, 5.0)'
|
||||||
|
>>> format(Vector(range(7)))
|
||||||
|
'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::
|
||||||
|
|
||||||
|
>>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.414213..., 0.785398...>'
|
||||||
|
>>> format(Vector([1, 1]), '.3eh')
|
||||||
|
'<1.414e+00, 7.854e-01>'
|
||||||
|
>>> format(Vector([1, 1]), '0.5fh')
|
||||||
|
'<1.41421, 0.78540>'
|
||||||
|
>>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.73205..., 0.95531..., 0.78539...>'
|
||||||
|
>>> format(Vector([2, 2, 2]), '.3eh')
|
||||||
|
'<3.464e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 0, 0]), '0.5fh')
|
||||||
|
'<0.00000, 0.00000, 0.00000>'
|
||||||
|
>>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<2.0, 2.09439..., 2.18627..., 3.92699...>'
|
||||||
|
>>> format(Vector([2, 2, 2, 2]), '.3eh')
|
||||||
|
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
|
||||||
|
'<1.00000, 1.57080, 0.00000, 0.00000>'
|
||||||
|
|
||||||
|
|
||||||
|
Unary operator tests::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> -v1
|
||||||
|
Vector([-3.0, -4.0])
|
||||||
|
>>> +v1
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
|
||||||
|
|
||||||
|
Basic tests of operator ``+``::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v2 = Vector([6, 7, 8])
|
||||||
|
>>> v1 + v2
|
||||||
|
Vector([9.0, 11.0, 13.0])
|
||||||
|
>>> v1 + v2 == Vector([3+6, 4+7, 5+8])
|
||||||
|
True
|
||||||
|
>>> v3 = Vector([1, 2])
|
||||||
|
>>> v1 + v3 # short vectors are filled with 0.0 on addition
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types::
|
||||||
|
|
||||||
|
>>> v1 + (10, 20, 30)
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v1 + v2d
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types, swapped operands::
|
||||||
|
|
||||||
|
>>> (10, 20, 30) + v1
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v2d + v1
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with an unsuitable operand:
|
||||||
|
|
||||||
|
>>> v1 + 1
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'int'
|
||||||
|
>>> v1 + 'ABC'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'str'
|
||||||
|
"""
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import reprlib
|
||||||
|
import math
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
class Vector:
|
||||||
|
typecode = 'd'
|
||||||
|
|
||||||
|
def __init__(self, components):
|
||||||
|
self._components = array(self.typecode, components)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._components)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
components = reprlib.repr(self._components)
|
||||||
|
components = components[components.find('['):-1]
|
||||||
|
return 'Vector({})'.format(components)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(tuple(self))
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (bytes([ord(self.typecode)]) +
|
||||||
|
bytes(self._components))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (len(self) == len(other) and
|
||||||
|
all(a == b for a, b in zip(self, other)))
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
hashes = (hash(x) for x in self)
|
||||||
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
|
# tag::VECTOR_V6_UNARY[]
|
||||||
|
def __abs__(self):
|
||||||
|
return math.sqrt(sum(x * x for x in self))
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return Vector(-x for x in self) # <1>
|
||||||
|
|
||||||
|
def __pos__(self):
|
||||||
|
return Vector(self) # <2>
|
||||||
|
# end::VECTOR_V6_UNARY[]
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(abs(self))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._components)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, slice):
|
||||||
|
cls = type(self)
|
||||||
|
return cls(self._components[key])
|
||||||
|
index = operator.index(key)
|
||||||
|
return self._components[index]
|
||||||
|
|
||||||
|
shortcut_names = 'xyzt'
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
cls = type(self)
|
||||||
|
if len(name) == 1:
|
||||||
|
pos = cls.shortcut_names.find(name)
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
|
msg = '{.__name__!r} object has no attribute {!r}'
|
||||||
|
raise AttributeError(msg.format(cls, name))
|
||||||
|
|
||||||
|
def angle(self, n):
|
||||||
|
r = math.sqrt(sum(x * x for x in self[n:]))
|
||||||
|
a = math.atan2(r, self[n-1])
|
||||||
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
|
return math.pi * 2 - a
|
||||||
|
else:
|
||||||
|
return a
|
||||||
|
|
||||||
|
def angles(self):
|
||||||
|
return (self.angle(n) for n in range(1, len(self)))
|
||||||
|
|
||||||
|
def __format__(self, fmt_spec=''):
|
||||||
|
if fmt_spec.endswith('h'): # hyperspherical coordinates
|
||||||
|
fmt_spec = fmt_spec[:-1]
|
||||||
|
coords = itertools.chain([abs(self)],
|
||||||
|
self.angles())
|
||||||
|
outer_fmt = '<{}>'
|
||||||
|
else:
|
||||||
|
coords = self
|
||||||
|
outer_fmt = '({})'
|
||||||
|
components = (format(c, fmt_spec) for c in coords)
|
||||||
|
return outer_fmt.format(', '.join(components))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, octets):
|
||||||
|
typecode = chr(octets[0])
|
||||||
|
memv = memoryview(octets[1:]).cast(typecode)
|
||||||
|
return cls(memv)
|
||||||
|
|
||||||
|
# tag::VECTOR_V6_ADD[]
|
||||||
|
def __add__(self, other):
|
||||||
|
try:
|
||||||
|
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
|
||||||
|
return Vector(a + b for a, b in pairs)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self + other
|
||||||
|
# end::VECTOR_V6_ADD[]
|
||||||
429
16-op-overloading/vector_v7.py
Normal file
429
16-op-overloading/vector_v7.py
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
"""
|
||||||
|
A multi-dimensional ``Vector`` class, take 7: operator ``*``
|
||||||
|
|
||||||
|
A ``Vector`` is built from an iterable of numbers::
|
||||||
|
|
||||||
|
>>> Vector([3.1, 4.2])
|
||||||
|
Vector([3.1, 4.2])
|
||||||
|
>>> Vector((3, 4, 5))
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> Vector(range(10))
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 2-dimensions (same results as ``vector2d_v1.py``)::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> x, y = v1
|
||||||
|
>>> x, y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> octets = bytes(v1)
|
||||||
|
>>> octets
|
||||||
|
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> bool(v1), bool(Vector([0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.frombytes()`` class method:
|
||||||
|
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 3-dimensions::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> x, y, z = v1
|
||||||
|
>>> x, y, z
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> abs(v1) # doctest:+ELLIPSIS
|
||||||
|
7.071067811...
|
||||||
|
>>> bool(v1), bool(Vector([0, 0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Tests with many dimensions::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
>>> abs(v7) # doctest:+ELLIPSIS
|
||||||
|
9.53939201...
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.__bytes__`` and ``.frombytes()`` methods::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of sequence behavior::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> len(v1)
|
||||||
|
3
|
||||||
|
>>> v1[0], v1[len(v1)-1], v1[-1]
|
||||||
|
(3.0, 5.0, 5.0)
|
||||||
|
|
||||||
|
|
||||||
|
Test of slicing::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7[-1]
|
||||||
|
6.0
|
||||||
|
>>> v7[1:4]
|
||||||
|
Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> v7[-1:]
|
||||||
|
Vector([6.0])
|
||||||
|
>>> v7[1,2]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'tuple' object cannot be interpreted as an integer
|
||||||
|
|
||||||
|
|
||||||
|
Tests of dynamic attribute access::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(10))
|
||||||
|
>>> v7.x
|
||||||
|
0.0
|
||||||
|
>>> v7.y, v7.z, v7.t
|
||||||
|
(1.0, 2.0, 3.0)
|
||||||
|
|
||||||
|
Dynamic attribute lookup failures::
|
||||||
|
|
||||||
|
>>> v7.k
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'k'
|
||||||
|
>>> v3 = Vector(range(3))
|
||||||
|
>>> v3.t
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 't'
|
||||||
|
>>> v3.spam
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'spam'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of hashing::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> v2 = Vector([3.1, 4.2])
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> v6 = Vector(range(6))
|
||||||
|
>>> hash(v1), hash(v3), hash(v6)
|
||||||
|
(7, 2, 1)
|
||||||
|
|
||||||
|
|
||||||
|
Most hash codes of non-integers vary from a 32-bit to 64-bit Python build::
|
||||||
|
|
||||||
|
>>> import sys
|
||||||
|
>>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 2D::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> format(v1)
|
||||||
|
'(3.0, 4.0)'
|
||||||
|
>>> format(v1, '.2f')
|
||||||
|
'(3.00, 4.00)'
|
||||||
|
>>> format(v1, '.3e')
|
||||||
|
'(3.000e+00, 4.000e+00)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 3D and 7D::
|
||||||
|
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> format(v3)
|
||||||
|
'(3.0, 4.0, 5.0)'
|
||||||
|
>>> format(Vector(range(7)))
|
||||||
|
'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::
|
||||||
|
|
||||||
|
>>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.414213..., 0.785398...>'
|
||||||
|
>>> format(Vector([1, 1]), '.3eh')
|
||||||
|
'<1.414e+00, 7.854e-01>'
|
||||||
|
>>> format(Vector([1, 1]), '0.5fh')
|
||||||
|
'<1.41421, 0.78540>'
|
||||||
|
>>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.73205..., 0.95531..., 0.78539...>'
|
||||||
|
>>> format(Vector([2, 2, 2]), '.3eh')
|
||||||
|
'<3.464e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 0, 0]), '0.5fh')
|
||||||
|
'<0.00000, 0.00000, 0.00000>'
|
||||||
|
>>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<2.0, 2.09439..., 2.18627..., 3.92699...>'
|
||||||
|
>>> format(Vector([2, 2, 2, 2]), '.3eh')
|
||||||
|
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
|
||||||
|
'<1.00000, 1.57080, 0.00000, 0.00000>'
|
||||||
|
|
||||||
|
|
||||||
|
Unary operator tests::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> -v1
|
||||||
|
Vector([-3.0, -4.0])
|
||||||
|
>>> +v1
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
|
||||||
|
|
||||||
|
Basic tests of operator ``+``::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v2 = Vector([6, 7, 8])
|
||||||
|
>>> v1 + v2
|
||||||
|
Vector([9.0, 11.0, 13.0])
|
||||||
|
>>> v1 + v2 == Vector([3+6, 4+7, 5+8])
|
||||||
|
True
|
||||||
|
>>> v3 = Vector([1, 2])
|
||||||
|
>>> v1 + v3 # short vectors are filled with 0.0 on addition
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types::
|
||||||
|
|
||||||
|
>>> v1 + (10, 20, 30)
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v1 + v2d
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types, swapped operands::
|
||||||
|
|
||||||
|
>>> (10, 20, 30) + v1
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v2d + v1
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with an unsuitable operand:
|
||||||
|
|
||||||
|
>>> v1 + 1
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'int'
|
||||||
|
>>> v1 + 'ABC'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'str'
|
||||||
|
|
||||||
|
|
||||||
|
Basic tests of operator ``*``::
|
||||||
|
|
||||||
|
>>> v1 = Vector([1, 2, 3])
|
||||||
|
>>> v1 * 10
|
||||||
|
Vector([10.0, 20.0, 30.0])
|
||||||
|
>>> 10 * v1
|
||||||
|
Vector([10.0, 20.0, 30.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``*`` with unusual but valid operands::
|
||||||
|
|
||||||
|
>>> v1 * True
|
||||||
|
Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> from fractions import Fraction
|
||||||
|
>>> v1 * Fraction(1, 3) # doctest:+ELLIPSIS
|
||||||
|
Vector([0.3333..., 0.6666..., 1.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``*`` with unsuitable operands::
|
||||||
|
|
||||||
|
>>> v1 * (1, 2)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: can't multiply sequence by non-int of type 'Vector'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``@``::
|
||||||
|
|
||||||
|
>>> va = Vector([1, 2, 3])
|
||||||
|
>>> vz = Vector([5, 6, 7])
|
||||||
|
>>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7
|
||||||
|
True
|
||||||
|
>>> [10, 20, 30] @ vz
|
||||||
|
380.0
|
||||||
|
>>> va @ 3
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for @: 'Vector' and 'int'
|
||||||
|
|
||||||
|
|
||||||
|
For ``@`` to work, both operands need to have the same length::
|
||||||
|
|
||||||
|
>>> va = Vector([1, 2, 3])
|
||||||
|
>>> vb = Vector([1, 2])
|
||||||
|
>>> va @ vb
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: @ requires vectors of equal length.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import reprlib
|
||||||
|
import math
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
import itertools
|
||||||
|
from collections import abc
|
||||||
|
|
||||||
|
|
||||||
|
class Vector:
|
||||||
|
typecode = 'd'
|
||||||
|
|
||||||
|
def __init__(self, components):
|
||||||
|
self._components = array(self.typecode, components)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._components)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
components = reprlib.repr(self._components)
|
||||||
|
components = components[components.find('['):-1]
|
||||||
|
return 'Vector({})'.format(components)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(tuple(self))
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (bytes([ord(self.typecode)]) +
|
||||||
|
bytes(self._components))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (len(self) == len(other) and
|
||||||
|
all(a == b for a, b in zip(self, other)))
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
hashes = (hash(x) for x in self)
|
||||||
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return math.sqrt(sum(x * x for x in self))
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return Vector(-x for x in self)
|
||||||
|
|
||||||
|
def __pos__(self):
|
||||||
|
return Vector(self)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(abs(self))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._components)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, slice):
|
||||||
|
cls = type(self)
|
||||||
|
return cls(self._components[key])
|
||||||
|
index = operator.index(key)
|
||||||
|
return self._components[index]
|
||||||
|
|
||||||
|
shortcut_names = 'xyzt'
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
cls = type(self)
|
||||||
|
if len(name) == 1:
|
||||||
|
pos = cls.shortcut_names.find(name)
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
|
msg = '{.__name__!r} object has no attribute {!r}'
|
||||||
|
raise AttributeError(msg.format(cls, name))
|
||||||
|
|
||||||
|
def angle(self, n):
|
||||||
|
r = math.sqrt(sum(x * x for x in self[n:]))
|
||||||
|
a = math.atan2(r, self[n-1])
|
||||||
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
|
return math.pi * 2 - a
|
||||||
|
else:
|
||||||
|
return a
|
||||||
|
|
||||||
|
def angles(self):
|
||||||
|
return (self.angle(n) for n in range(1, len(self)))
|
||||||
|
|
||||||
|
def __format__(self, fmt_spec=''):
|
||||||
|
if fmt_spec.endswith('h'): # hyperspherical coordinates
|
||||||
|
fmt_spec = fmt_spec[:-1]
|
||||||
|
coords = itertools.chain([abs(self)],
|
||||||
|
self.angles())
|
||||||
|
outer_fmt = '<{}>'
|
||||||
|
else:
|
||||||
|
coords = self
|
||||||
|
outer_fmt = '({})'
|
||||||
|
components = (format(c, fmt_spec) for c in coords)
|
||||||
|
return outer_fmt.format(', '.join(components))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, octets):
|
||||||
|
typecode = chr(octets[0])
|
||||||
|
memv = memoryview(octets[1:]).cast(typecode)
|
||||||
|
return cls(memv)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
try:
|
||||||
|
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
|
||||||
|
return Vector(a + b for a, b in pairs)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self + other
|
||||||
|
|
||||||
|
def __mul__(self, scalar):
|
||||||
|
try:
|
||||||
|
factor = float(scalar)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
return Vector(n * factor for n in self)
|
||||||
|
|
||||||
|
def __rmul__(self, scalar):
|
||||||
|
return self * scalar
|
||||||
|
|
||||||
|
def __matmul__(self, other):
|
||||||
|
if (isinstance(other, abc.Sized) and
|
||||||
|
isinstance(other, abc.Iterable)):
|
||||||
|
if len(self) == len(other):
|
||||||
|
return sum(a * b for a, b in zip(self, other))
|
||||||
|
else:
|
||||||
|
raise ValueError('@ requires vectors of equal length.')
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __rmatmul__(self, other):
|
||||||
|
return self @ other
|
||||||
421
16-op-overloading/vector_v8.py
Normal file
421
16-op-overloading/vector_v8.py
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
"""
|
||||||
|
A multi-dimensional ``Vector`` class, take 8: operator ``==``
|
||||||
|
|
||||||
|
A ``Vector`` is built from an iterable of numbers::
|
||||||
|
|
||||||
|
>>> Vector([3.1, 4.2])
|
||||||
|
Vector([3.1, 4.2])
|
||||||
|
>>> Vector((3, 4, 5))
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> Vector(range(10))
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 2-dimensions (same results as ``vector2d_v1.py``)::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> x, y = v1
|
||||||
|
>>> x, y
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0)
|
||||||
|
>>> octets = bytes(v1)
|
||||||
|
>>> octets
|
||||||
|
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> bool(v1), bool(Vector([0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.frombytes()`` class method:
|
||||||
|
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests with 3-dimensions::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> x, y, z = v1
|
||||||
|
>>> x, y, z
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> v1
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1_clone = eval(repr(v1))
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
>>> print(v1)
|
||||||
|
(3.0, 4.0, 5.0)
|
||||||
|
>>> abs(v1) # doctest:+ELLIPSIS
|
||||||
|
7.071067811...
|
||||||
|
>>> bool(v1), bool(Vector([0, 0, 0]))
|
||||||
|
(True, False)
|
||||||
|
|
||||||
|
|
||||||
|
Tests with many dimensions::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7
|
||||||
|
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
||||||
|
>>> abs(v7) # doctest:+ELLIPSIS
|
||||||
|
9.53939201...
|
||||||
|
|
||||||
|
|
||||||
|
Test of ``.__bytes__`` and ``.frombytes()`` methods::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v1_clone = Vector.frombytes(bytes(v1))
|
||||||
|
>>> v1_clone
|
||||||
|
Vector([3.0, 4.0, 5.0])
|
||||||
|
>>> v1 == v1_clone
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of sequence behavior::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> len(v1)
|
||||||
|
3
|
||||||
|
>>> v1[0], v1[len(v1)-1], v1[-1]
|
||||||
|
(3.0, 5.0, 5.0)
|
||||||
|
|
||||||
|
|
||||||
|
Test of slicing::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(7))
|
||||||
|
>>> v7[-1]
|
||||||
|
6.0
|
||||||
|
>>> v7[1:4]
|
||||||
|
Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> v7[-1:]
|
||||||
|
Vector([6.0])
|
||||||
|
>>> v7[1,2]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'tuple' object cannot be interpreted as an integer
|
||||||
|
|
||||||
|
|
||||||
|
Tests of dynamic attribute access::
|
||||||
|
|
||||||
|
>>> v7 = Vector(range(10))
|
||||||
|
>>> v7.x
|
||||||
|
0.0
|
||||||
|
>>> v7.y, v7.z, v7.t
|
||||||
|
(1.0, 2.0, 3.0)
|
||||||
|
|
||||||
|
Dynamic attribute lookup failures::
|
||||||
|
|
||||||
|
>>> v7.k
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'k'
|
||||||
|
>>> v3 = Vector(range(3))
|
||||||
|
>>> v3.t
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 't'
|
||||||
|
>>> v3.spam
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Vector' object has no attribute 'spam'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of hashing::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> v2 = Vector([3.1, 4.2])
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> v6 = Vector(range(6))
|
||||||
|
>>> hash(v1), hash(v3), hash(v6)
|
||||||
|
(7, 2, 1)
|
||||||
|
|
||||||
|
|
||||||
|
Most hash codes of non-integers vary from a 32-bit to 64-bit Python build::
|
||||||
|
|
||||||
|
>>> import sys
|
||||||
|
>>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 2D::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> format(v1)
|
||||||
|
'(3.0, 4.0)'
|
||||||
|
>>> format(v1, '.2f')
|
||||||
|
'(3.00, 4.00)'
|
||||||
|
>>> format(v1, '.3e')
|
||||||
|
'(3.000e+00, 4.000e+00)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with Cartesian coordinates in 3D and 7D::
|
||||||
|
|
||||||
|
>>> v3 = Vector([3, 4, 5])
|
||||||
|
>>> format(v3)
|
||||||
|
'(3.0, 4.0, 5.0)'
|
||||||
|
>>> format(Vector(range(7)))
|
||||||
|
'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::
|
||||||
|
|
||||||
|
>>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.414213..., 0.785398...>'
|
||||||
|
>>> format(Vector([1, 1]), '.3eh')
|
||||||
|
'<1.414e+00, 7.854e-01>'
|
||||||
|
>>> format(Vector([1, 1]), '0.5fh')
|
||||||
|
'<1.41421, 0.78540>'
|
||||||
|
>>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<1.73205..., 0.95531..., 0.78539...>'
|
||||||
|
>>> format(Vector([2, 2, 2]), '.3eh')
|
||||||
|
'<3.464e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 0, 0]), '0.5fh')
|
||||||
|
'<0.00000, 0.00000, 0.00000>'
|
||||||
|
>>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS
|
||||||
|
'<2.0, 2.09439..., 2.18627..., 3.92699...>'
|
||||||
|
>>> format(Vector([2, 2, 2, 2]), '.3eh')
|
||||||
|
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
|
||||||
|
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
|
||||||
|
'<1.00000, 1.57080, 0.00000, 0.00000>'
|
||||||
|
|
||||||
|
|
||||||
|
Unary operator tests::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4])
|
||||||
|
>>> abs(v1)
|
||||||
|
5.0
|
||||||
|
>>> -v1
|
||||||
|
Vector([-3.0, -4.0])
|
||||||
|
>>> +v1
|
||||||
|
Vector([3.0, 4.0])
|
||||||
|
|
||||||
|
|
||||||
|
Basic tests of operator ``+``::
|
||||||
|
|
||||||
|
>>> v1 = Vector([3, 4, 5])
|
||||||
|
>>> v2 = Vector([6, 7, 8])
|
||||||
|
>>> v1 + v2
|
||||||
|
Vector([9.0, 11.0, 13.0])
|
||||||
|
>>> v1 + v2 == Vector([3+6, 4+7, 5+8])
|
||||||
|
True
|
||||||
|
>>> v3 = Vector([1, 2])
|
||||||
|
>>> v1 + v3 # short vectors are filled with 0.0 on addition
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types::
|
||||||
|
|
||||||
|
>>> v1 + (10, 20, 30)
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v1 + v2d
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with mixed types, swapped operands::
|
||||||
|
|
||||||
|
>>> (10, 20, 30) + v1
|
||||||
|
Vector([13.0, 24.0, 35.0])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> v2d + v1
|
||||||
|
Vector([4.0, 6.0, 5.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``+`` with an unsuitable operand:
|
||||||
|
|
||||||
|
>>> v1 + 1
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'int'
|
||||||
|
>>> v1 + 'ABC'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: unsupported operand type(s) for +: 'Vector' and 'str'
|
||||||
|
|
||||||
|
|
||||||
|
Basic tests of operator ``*``::
|
||||||
|
|
||||||
|
>>> v1 = Vector([1, 2, 3])
|
||||||
|
>>> v1 * 10
|
||||||
|
Vector([10.0, 20.0, 30.0])
|
||||||
|
>>> 10 * v1
|
||||||
|
Vector([10.0, 20.0, 30.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``*`` with unusual but valid operands::
|
||||||
|
|
||||||
|
>>> v1 * True
|
||||||
|
Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> from fractions import Fraction
|
||||||
|
>>> v1 * Fraction(1, 3) # doctest:+ELLIPSIS
|
||||||
|
Vector([0.3333..., 0.6666..., 1.0])
|
||||||
|
|
||||||
|
|
||||||
|
Tests of ``*`` with unsuitable operands::
|
||||||
|
|
||||||
|
>>> v1 * (1, 2)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: can't multiply sequence by non-int of type 'Vector'
|
||||||
|
|
||||||
|
|
||||||
|
Tests of operator `==`::
|
||||||
|
|
||||||
|
>>> va = Vector(range(1, 4))
|
||||||
|
>>> vb = Vector([1.0, 2.0, 3.0])
|
||||||
|
>>> va == vb
|
||||||
|
True
|
||||||
|
>>> vc = Vector([1, 2])
|
||||||
|
>>> from vector2d_v3 import Vector2d
|
||||||
|
>>> v2d = Vector2d(1, 2)
|
||||||
|
>>> vc == v2d
|
||||||
|
True
|
||||||
|
>>> va == (1, 2, 3)
|
||||||
|
False
|
||||||
|
|
||||||
|
|
||||||
|
Tests of operator `!=`::
|
||||||
|
|
||||||
|
>>> va != vb
|
||||||
|
False
|
||||||
|
>>> vc != v2d
|
||||||
|
False
|
||||||
|
>>> va != (1, 2, 3)
|
||||||
|
True
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import reprlib
|
||||||
|
import math
|
||||||
|
import numbers
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
class Vector:
|
||||||
|
typecode = 'd'
|
||||||
|
|
||||||
|
def __init__(self, components):
|
||||||
|
self._components = array(self.typecode, components)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._components)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
components = reprlib.repr(self._components)
|
||||||
|
components = components[components.find('['):-1]
|
||||||
|
return 'Vector({})'.format(components)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(tuple(self))
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (bytes([ord(self.typecode)]) +
|
||||||
|
bytes(self._components))
|
||||||
|
|
||||||
|
# tag::VECTOR_V8_EQ[]
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Vector): # <1>
|
||||||
|
return (len(self) == len(other) and
|
||||||
|
all(a == b for a, b in zip(self, other)))
|
||||||
|
else:
|
||||||
|
return NotImplemented # <2>
|
||||||
|
# end::VECTOR_V8_EQ[]
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
hashes = (hash(x) for x in self)
|
||||||
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return math.sqrt(sum(x * x for x in self))
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return Vector(-x for x in self)
|
||||||
|
|
||||||
|
def __pos__(self):
|
||||||
|
return Vector(self)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(abs(self))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._components)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, slice):
|
||||||
|
cls = type(self)
|
||||||
|
return cls(self._components[key])
|
||||||
|
index = operator.index(key)
|
||||||
|
return self._components[index]
|
||||||
|
|
||||||
|
shortcut_names = 'xyzt'
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
cls = type(self)
|
||||||
|
if len(name) == 1:
|
||||||
|
pos = cls.shortcut_names.find(name)
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
|
msg = '{.__name__!r} object has no attribute {!r}'
|
||||||
|
raise AttributeError(msg.format(cls, name))
|
||||||
|
|
||||||
|
def angle(self, n):
|
||||||
|
r = math.sqrt(sum(x * x for x in self[n:]))
|
||||||
|
a = math.atan2(r, self[n-1])
|
||||||
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
|
return math.pi * 2 - a
|
||||||
|
else:
|
||||||
|
return a
|
||||||
|
|
||||||
|
def angles(self):
|
||||||
|
return (self.angle(n) for n in range(1, len(self)))
|
||||||
|
|
||||||
|
def __format__(self, fmt_spec=''):
|
||||||
|
if fmt_spec.endswith('h'): # hyperspherical coordinates
|
||||||
|
fmt_spec = fmt_spec[:-1]
|
||||||
|
coords = itertools.chain([abs(self)],
|
||||||
|
self.angles())
|
||||||
|
outer_fmt = '<{}>'
|
||||||
|
else:
|
||||||
|
coords = self
|
||||||
|
outer_fmt = '({})'
|
||||||
|
components = (format(c, fmt_spec) for c in coords)
|
||||||
|
return outer_fmt.format(', '.join(components))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, octets):
|
||||||
|
typecode = chr(octets[0])
|
||||||
|
memv = memoryview(octets[1:]).cast(typecode)
|
||||||
|
return cls(memv)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
try:
|
||||||
|
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
|
||||||
|
return Vector(a + b for a, b in pairs)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self + other
|
||||||
|
|
||||||
|
def __mul__(self, scalar):
|
||||||
|
if isinstance(scalar, numbers.Real):
|
||||||
|
return Vector(n * scalar for n in self)
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __rmul__(self, scalar):
|
||||||
|
return self * scalar
|
||||||
4
17-it-generator/README.rst
Normal file
4
17-it-generator/README.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sample code for Chapter 14 - "Iterables, iterators and generators"
|
||||||
|
|
||||||
|
From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015)
|
||||||
|
http://shop.oreilly.com/product/0636920032519.do
|
||||||
31
17-it-generator/aritprog.rst
Normal file
31
17-it-generator/aritprog.rst
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
===========================================
|
||||||
|
Tests for arithmetic progression generators
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Tests with built-in numeric types::
|
||||||
|
|
||||||
|
>>> ap = aritprog_gen(1, .5, 3)
|
||||||
|
>>> list(ap)
|
||||||
|
[1.0, 1.5, 2.0, 2.5]
|
||||||
|
>>> ap = aritprog_gen(0, 1/3, 1)
|
||||||
|
>>> list(ap)
|
||||||
|
[0.0, 0.3333333333333333, 0.6666666666666666]
|
||||||
|
|
||||||
|
|
||||||
|
Tests with standard library numeric types::
|
||||||
|
|
||||||
|
>>> from fractions import Fraction
|
||||||
|
>>> ap = aritprog_gen(0, Fraction(1, 3), 1)
|
||||||
|
>>> list(ap)
|
||||||
|
[Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)]
|
||||||
|
>>> from decimal import Decimal
|
||||||
|
>>> ap = aritprog_gen(0, Decimal('.1'), .3)
|
||||||
|
>>> list(ap)
|
||||||
|
[Decimal('0'), Decimal('0.1'), Decimal('0.2')]
|
||||||
|
|
||||||
|
|
||||||
|
Test producing an empty series::
|
||||||
|
|
||||||
|
>>> ap = aritprog_gen(0, 1, 0)
|
||||||
|
>>> list(ap)
|
||||||
|
[]
|
||||||
26
17-it-generator/aritprog_float_error.py
Normal file
26
17-it-generator/aritprog_float_error.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Demonstrate difference between Arithmetic Progression calculated
|
||||||
|
as a series of increments accumulating errors versus one addition
|
||||||
|
and one multiplication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fractions import Fraction
|
||||||
|
from aritprog_v0 import ArithmeticProgression as APv0
|
||||||
|
from aritprog_v1 import ArithmeticProgression as APv1
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
ap0 = iter(APv0(1, .1))
|
||||||
|
ap1 = iter(APv1(1, .1))
|
||||||
|
ap_frac = iter(APv1(Fraction(1, 1), Fraction(1, 10)))
|
||||||
|
epsilon = 10**-10
|
||||||
|
iteration = 0
|
||||||
|
delta = next(ap0) - next(ap1)
|
||||||
|
frac = next(ap_frac)
|
||||||
|
while abs(delta) <= epsilon:
|
||||||
|
delta = next(ap0) - next(ap1)
|
||||||
|
frac = next(ap_frac)
|
||||||
|
iteration +=1
|
||||||
|
|
||||||
|
print('iteration: {}\tfraction: {}\tepsilon: {}\tdelta: {}'.
|
||||||
|
format(iteration, frac, epsilon, delta))
|
||||||
37
17-it-generator/aritprog_runner.py
Normal file
37
17-it-generator/aritprog_runner.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import doctest
|
||||||
|
import importlib
|
||||||
|
import glob
|
||||||
|
|
||||||
|
|
||||||
|
TARGET_GLOB = 'aritprog*.py'
|
||||||
|
TEST_FILE = 'aritprog.rst'
|
||||||
|
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}'
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
verbose = '-v' in argv
|
||||||
|
for module_file_name in sorted(glob.glob(TARGET_GLOB)):
|
||||||
|
module_name = module_file_name.replace('.py', '')
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
gen_factory = getattr(module, 'ArithmeticProgression', None)
|
||||||
|
if gen_factory is None:
|
||||||
|
gen_factory = getattr(module, 'aritprog_gen', None)
|
||||||
|
if gen_factory is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
test(gen_factory, verbose)
|
||||||
|
|
||||||
|
|
||||||
|
def test(gen_factory, verbose=False):
|
||||||
|
res = doctest.testfile(
|
||||||
|
TEST_FILE,
|
||||||
|
globs={'aritprog_gen': gen_factory},
|
||||||
|
verbose=verbose,
|
||||||
|
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
|
||||||
|
tag = 'FAIL' if res.failed else 'OK'
|
||||||
|
print(TEST_MSG.format(gen_factory.__module__, res, tag))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
25
17-it-generator/aritprog_v0.py
Normal file
25
17-it-generator/aritprog_v0.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Arithmetic progression class
|
||||||
|
|
||||||
|
>>> ap = ArithmeticProgression(1, .5, 3)
|
||||||
|
>>> list(ap)
|
||||||
|
[1.0, 1.5, 2.0, 2.5]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ArithmeticProgression:
|
||||||
|
|
||||||
|
def __init__(self, begin, step, end=None):
|
||||||
|
self.begin = begin
|
||||||
|
self.step = step
|
||||||
|
self.end = end # None -> "infinite" series
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
result_type = type(self.begin + self.step)
|
||||||
|
result = result_type(self.begin)
|
||||||
|
forever = self.end is None
|
||||||
|
while forever or result < self.end:
|
||||||
|
yield result
|
||||||
|
result += self.step
|
||||||
45
17-it-generator/aritprog_v1.py
Normal file
45
17-it-generator/aritprog_v1.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Arithmetic progression class
|
||||||
|
|
||||||
|
# tag::ARITPROG_CLASS_DEMO[]
|
||||||
|
|
||||||
|
>>> ap = ArithmeticProgression(0, 1, 3)
|
||||||
|
>>> list(ap)
|
||||||
|
[0, 1, 2]
|
||||||
|
>>> ap = ArithmeticProgression(1, .5, 3)
|
||||||
|
>>> list(ap)
|
||||||
|
[1.0, 1.5, 2.0, 2.5]
|
||||||
|
>>> ap = ArithmeticProgression(0, 1/3, 1)
|
||||||
|
>>> list(ap)
|
||||||
|
[0.0, 0.3333333333333333, 0.6666666666666666]
|
||||||
|
>>> from fractions import Fraction
|
||||||
|
>>> ap = ArithmeticProgression(0, Fraction(1, 3), 1)
|
||||||
|
>>> list(ap)
|
||||||
|
[Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)]
|
||||||
|
>>> from decimal import Decimal
|
||||||
|
>>> ap = ArithmeticProgression(0, Decimal('.1'), .3)
|
||||||
|
>>> list(ap)
|
||||||
|
[Decimal('0.0'), Decimal('0.1'), Decimal('0.2')]
|
||||||
|
|
||||||
|
# end::ARITPROG_CLASS_DEMO[]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::ARITPROG_CLASS[]
|
||||||
|
class ArithmeticProgression:
|
||||||
|
|
||||||
|
def __init__(self, begin, step, end=None): # <1>
|
||||||
|
self.begin = begin
|
||||||
|
self.step = step
|
||||||
|
self.end = end # None -> "infinite" series
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
result_type = type(self.begin + self.step) # <2>
|
||||||
|
result = result_type(self.begin) # <3>
|
||||||
|
forever = self.end is None # <4>
|
||||||
|
index = 0
|
||||||
|
while forever or result < self.end: # <5>
|
||||||
|
yield result # <6>
|
||||||
|
index += 1
|
||||||
|
result = self.begin + self.step * index # <7>
|
||||||
|
# end::ARITPROG_CLASS[]
|
||||||
31
17-it-generator/aritprog_v2.py
Normal file
31
17-it-generator/aritprog_v2.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Arithmetic progression generator function::
|
||||||
|
|
||||||
|
>>> ap = aritprog_gen(1, .5, 3)
|
||||||
|
>>> list(ap)
|
||||||
|
[1.0, 1.5, 2.0, 2.5]
|
||||||
|
>>> ap = aritprog_gen(0, 1/3, 1)
|
||||||
|
>>> list(ap)
|
||||||
|
[0.0, 0.3333333333333333, 0.6666666666666666]
|
||||||
|
>>> from fractions import Fraction
|
||||||
|
>>> ap = aritprog_gen(0, Fraction(1, 3), 1)
|
||||||
|
>>> list(ap)
|
||||||
|
[Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)]
|
||||||
|
>>> from decimal import Decimal
|
||||||
|
>>> ap = aritprog_gen(0, Decimal('.1'), .3)
|
||||||
|
>>> list(ap)
|
||||||
|
[Decimal('0.0'), Decimal('0.1'), Decimal('0.2')]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::ARITPROG_GENFUNC[]
|
||||||
|
def aritprog_gen(begin, step, end=None):
|
||||||
|
result = type(begin + step)(begin)
|
||||||
|
forever = end is None
|
||||||
|
index = 0
|
||||||
|
while forever or result < end:
|
||||||
|
yield result
|
||||||
|
index += 1
|
||||||
|
result = begin + step * index
|
||||||
|
# end::ARITPROG_GENFUNC[]
|
||||||
11
17-it-generator/aritprog_v3.py
Normal file
11
17-it-generator/aritprog_v3.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# tag::ARITPROG_ITERTOOLS[]
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
def aritprog_gen(begin, step, end=None):
|
||||||
|
first = type(begin + step)(begin)
|
||||||
|
ap_gen = itertools.count(first, step)
|
||||||
|
if end is not None:
|
||||||
|
ap_gen = itertools.takewhile(lambda n: n < end, ap_gen)
|
||||||
|
return ap_gen
|
||||||
|
# end::ARITPROG_ITERTOOLS[]
|
||||||
26
17-it-generator/columnize_iter.py
Normal file
26
17-it-generator/columnize_iter.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# tag::COLUMNIZE[]
|
||||||
|
from typing import Sequence, Tuple, Iterator
|
||||||
|
|
||||||
|
def columnize(sequence: Sequence[str], num_columns: int = 0) -> Iterator[Tuple[str, ...]]:
|
||||||
|
if num_columns == 0:
|
||||||
|
num_columns = round(len(sequence) ** .5)
|
||||||
|
num_rows, reminder = divmod(len(sequence), num_columns)
|
||||||
|
num_rows += bool(reminder)
|
||||||
|
return (tuple(sequence[i::num_rows]) for i in range(num_rows))
|
||||||
|
# end::COLUMNIZE[]
|
||||||
|
|
||||||
|
|
||||||
|
def demo() -> None:
|
||||||
|
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
|
||||||
|
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
|
||||||
|
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
|
||||||
|
).split()
|
||||||
|
|
||||||
|
for row in columnize(nato, 4):
|
||||||
|
for word in row:
|
||||||
|
print(f'{word:15}', end='')
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
demo()
|
||||||
51
17-it-generator/fibo_by_hand.py
Normal file
51
17-it-generator/fibo_by_hand.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Fibonacci generator implemented "by hand" without generator objects
|
||||||
|
|
||||||
|
>>> from itertools import islice
|
||||||
|
>>> list(islice(Fibonacci(), 15))
|
||||||
|
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::FIBO_BY_HAND[]
|
||||||
|
class Fibonacci:
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return FibonacciGenerator()
|
||||||
|
|
||||||
|
|
||||||
|
class FibonacciGenerator:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.a = 0
|
||||||
|
self.b = 1
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
result = self.a
|
||||||
|
self.a, self.b = self.b, self.a + self.b
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
# end::FIBO_BY_HAND[]
|
||||||
|
|
||||||
|
# for comparison, this is the usual implementation of a Fibonacci
|
||||||
|
# generator in Python:
|
||||||
|
|
||||||
|
|
||||||
|
def fibonacci():
|
||||||
|
a, b = 0, 1
|
||||||
|
while True:
|
||||||
|
yield a
|
||||||
|
a, b = b, a + b
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
for x, y in zip(Fibonacci(), fibonacci()):
|
||||||
|
assert x == y, '%s != %s' % (x, y)
|
||||||
|
print(x)
|
||||||
|
if x > 10**10:
|
||||||
|
break
|
||||||
|
print('etc...')
|
||||||
12
17-it-generator/isis2json/README.rst
Normal file
12
17-it-generator/isis2json/README.rst
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
isis2json.py
|
||||||
|
============
|
||||||
|
|
||||||
|
This directory contains a copy of the ``isis2json.py`` script, with
|
||||||
|
minimal dependencies, just to allow the O'Reilly Atlas toolchain to
|
||||||
|
render the listing of the script in appendix A of the book.
|
||||||
|
|
||||||
|
If you want to use or contribute to this script, please get the full
|
||||||
|
source code with all dependencies from the main ``isis2json``
|
||||||
|
repository:
|
||||||
|
|
||||||
|
https://github.com/fluentpython/isis2json
|
||||||
263
17-it-generator/isis2json/isis2json.py
Executable file
263
17-it-generator/isis2json/isis2json.py
Executable file
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
# isis2json.py: convert ISIS and ISO-2709 files to JSON
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010 BIREME/PAHO/WHO
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
############################
|
||||||
|
# BEGIN ISIS2JSON
|
||||||
|
# this script works with Python or Jython (versions >=2.5 and <3)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from uuid import uuid4
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
if os.name == 'java': # running Jython
|
||||||
|
from com.xhaus.jyson import JysonCodec as json
|
||||||
|
else:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
SKIP_INACTIVE = True
|
||||||
|
DEFAULT_QTY = 2**31
|
||||||
|
ISIS_MFN_KEY = 'mfn'
|
||||||
|
ISIS_ACTIVE_KEY = 'active'
|
||||||
|
SUBFIELD_DELIMITER = '^'
|
||||||
|
INPUT_ENCODING = 'cp1252'
|
||||||
|
|
||||||
|
|
||||||
|
def iter_iso_records(iso_file_name, isis_json_type): # <1>
|
||||||
|
from iso2709 import IsoFile
|
||||||
|
from subfield import expand
|
||||||
|
|
||||||
|
iso = IsoFile(iso_file_name)
|
||||||
|
for record in iso:
|
||||||
|
fields = {}
|
||||||
|
for field in record.directory:
|
||||||
|
field_key = str(int(field.tag)) # remove leading zeroes
|
||||||
|
field_occurrences = fields.setdefault(field_key, [])
|
||||||
|
content = field.value.decode(INPUT_ENCODING, 'replace')
|
||||||
|
if isis_json_type == 1:
|
||||||
|
field_occurrences.append(content)
|
||||||
|
elif isis_json_type == 2:
|
||||||
|
field_occurrences.append(expand(content))
|
||||||
|
elif isis_json_type == 3:
|
||||||
|
field_occurrences.append(dict(expand(content)))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('ISIS-JSON type %s conversion '
|
||||||
|
'not yet implemented for .iso input' % isis_json_type)
|
||||||
|
|
||||||
|
yield fields
|
||||||
|
iso.close()
|
||||||
|
|
||||||
|
|
||||||
|
def iter_mst_records(master_file_name, isis_json_type): # <2>
|
||||||
|
try:
|
||||||
|
from bruma.master import MasterFactory, Record
|
||||||
|
except ImportError:
|
||||||
|
print('IMPORT ERROR: Jython 2.5 and Bruma.jar '
|
||||||
|
'are required to read .mst files')
|
||||||
|
raise SystemExit
|
||||||
|
mst = MasterFactory.getInstance(master_file_name).open()
|
||||||
|
for record in mst:
|
||||||
|
fields = {}
|
||||||
|
if SKIP_INACTIVE:
|
||||||
|
if record.getStatus() != Record.Status.ACTIVE:
|
||||||
|
continue
|
||||||
|
else: # save status only there are non-active records
|
||||||
|
fields[ISIS_ACTIVE_KEY] = (record.getStatus() ==
|
||||||
|
Record.Status.ACTIVE)
|
||||||
|
fields[ISIS_MFN_KEY] = record.getMfn()
|
||||||
|
for field in record.getFields():
|
||||||
|
field_key = str(field.getId())
|
||||||
|
field_occurrences = fields.setdefault(field_key, [])
|
||||||
|
if isis_json_type == 3:
|
||||||
|
content = {}
|
||||||
|
for subfield in field.getSubfields():
|
||||||
|
subfield_key = subfield.getId()
|
||||||
|
if subfield_key == '*':
|
||||||
|
content['_'] = subfield.getContent()
|
||||||
|
else:
|
||||||
|
subfield_occurrences = content.setdefault(subfield_key, [])
|
||||||
|
subfield_occurrences.append(subfield.getContent())
|
||||||
|
field_occurrences.append(content)
|
||||||
|
elif isis_json_type == 1:
|
||||||
|
content = []
|
||||||
|
for subfield in field.getSubfields():
|
||||||
|
subfield_key = subfield.getId()
|
||||||
|
if subfield_key == '*':
|
||||||
|
content.insert(0, subfield.getContent())
|
||||||
|
else:
|
||||||
|
content.append(SUBFIELD_DELIMITER + subfield_key +
|
||||||
|
subfield.getContent())
|
||||||
|
field_occurrences.append(''.join(content))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('ISIS-JSON type %s conversion '
|
||||||
|
'not yet implemented for .mst input' % isis_json_type)
|
||||||
|
yield fields
|
||||||
|
mst.close()
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(input_gen, file_name, output, qty, skip, id_tag, # <3>
|
||||||
|
gen_uuid, mongo, mfn, isis_json_type, prefix,
|
||||||
|
constant):
|
||||||
|
start = skip
|
||||||
|
end = start + qty
|
||||||
|
if id_tag:
|
||||||
|
id_tag = str(id_tag)
|
||||||
|
ids = set()
|
||||||
|
else:
|
||||||
|
id_tag = ''
|
||||||
|
for i, record in enumerate(input_gen):
|
||||||
|
if i >= end:
|
||||||
|
break
|
||||||
|
if not mongo:
|
||||||
|
if i == 0:
|
||||||
|
output.write('[')
|
||||||
|
elif i > start:
|
||||||
|
output.write(',')
|
||||||
|
if start <= i < end:
|
||||||
|
if id_tag:
|
||||||
|
occurrences = record.get(id_tag, None)
|
||||||
|
if occurrences is None:
|
||||||
|
msg = 'id tag #%s not found in record %s'
|
||||||
|
if ISIS_MFN_KEY in record:
|
||||||
|
msg = msg + (' (mfn=%s)' % record[ISIS_MFN_KEY])
|
||||||
|
raise KeyError(msg % (id_tag, i))
|
||||||
|
if len(occurrences) > 1:
|
||||||
|
msg = 'multiple id tags #%s found in record %s'
|
||||||
|
if ISIS_MFN_KEY in record:
|
||||||
|
msg = msg + (' (mfn=%s)' % record[ISIS_MFN_KEY])
|
||||||
|
raise TypeError(msg % (id_tag, i))
|
||||||
|
else: # ok, we have one and only one id field
|
||||||
|
if isis_json_type == 1:
|
||||||
|
id = occurrences[0]
|
||||||
|
elif isis_json_type == 2:
|
||||||
|
id = occurrences[0][0][1]
|
||||||
|
elif isis_json_type == 3:
|
||||||
|
id = occurrences[0]['_']
|
||||||
|
if id in ids:
|
||||||
|
msg = 'duplicate id %s in tag #%s, record %s'
|
||||||
|
if ISIS_MFN_KEY in record:
|
||||||
|
msg = msg + (' (mfn=%s)' % record[ISIS_MFN_KEY])
|
||||||
|
raise TypeError(msg % (id, id_tag, i))
|
||||||
|
record['_id'] = id
|
||||||
|
ids.add(id)
|
||||||
|
elif gen_uuid:
|
||||||
|
record['_id'] = unicode(uuid4())
|
||||||
|
elif mfn:
|
||||||
|
record['_id'] = record[ISIS_MFN_KEY]
|
||||||
|
if prefix:
|
||||||
|
# iterate over a fixed sequence of tags
|
||||||
|
for tag in tuple(record):
|
||||||
|
if str(tag).isdigit():
|
||||||
|
record[prefix+tag] = record[tag]
|
||||||
|
del record[tag] # this is why we iterate over a tuple
|
||||||
|
# with the tags, and not directly on the record dict
|
||||||
|
if constant:
|
||||||
|
constant_key, constant_value = constant.split(':')
|
||||||
|
record[constant_key] = constant_value
|
||||||
|
output.write(json.dumps(record).encode('utf-8'))
|
||||||
|
output.write('\n')
|
||||||
|
if not mongo:
|
||||||
|
output.write(']\n')
|
||||||
|
|
||||||
|
|
||||||
|
def main(): # <4>
|
||||||
|
# create the parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Convert an ISIS .mst or .iso file to a JSON array')
|
||||||
|
|
||||||
|
# add the arguments
|
||||||
|
parser.add_argument(
|
||||||
|
'file_name', metavar='INPUT.(mst|iso)',
|
||||||
|
help='.mst or .iso file to read')
|
||||||
|
parser.add_argument(
|
||||||
|
'-o', '--out', type=argparse.FileType('w'), default=sys.stdout,
|
||||||
|
metavar='OUTPUT.json',
|
||||||
|
help='the file where the JSON output should be written'
|
||||||
|
' (default: write to stdout)')
|
||||||
|
parser.add_argument(
|
||||||
|
'-c', '--couch', action='store_true',
|
||||||
|
help='output array within a "docs" item in a JSON document'
|
||||||
|
' for bulk insert to CouchDB via POST to db/_bulk_docs')
|
||||||
|
parser.add_argument(
|
||||||
|
'-m', '--mongo', action='store_true',
|
||||||
|
help='output individual records as separate JSON dictionaries, one'
|
||||||
|
' per line for bulk insert to MongoDB via mongoimport utility')
|
||||||
|
parser.add_argument(
|
||||||
|
'-t', '--type', type=int, metavar='ISIS_JSON_TYPE', default=1,
|
||||||
|
help='ISIS-JSON type, sets field structure: 1=string, 2=alist,'
|
||||||
|
' 3=dict (default=1)')
|
||||||
|
parser.add_argument(
|
||||||
|
'-q', '--qty', type=int, default=DEFAULT_QTY,
|
||||||
|
help='maximum quantity of records to read (default=ALL)')
|
||||||
|
parser.add_argument(
|
||||||
|
'-s', '--skip', type=int, default=0,
|
||||||
|
help='records to skip from start of .mst (default=0)')
|
||||||
|
parser.add_argument(
|
||||||
|
'-i', '--id', type=int, metavar='TAG_NUMBER', default=0,
|
||||||
|
help='generate an "_id" from the given unique TAG field number'
|
||||||
|
' for each record')
|
||||||
|
parser.add_argument(
|
||||||
|
'-u', '--uuid', action='store_true',
|
||||||
|
help='generate an "_id" with a random UUID for each record')
|
||||||
|
parser.add_argument(
|
||||||
|
'-p', '--prefix', type=str, metavar='PREFIX', default='',
|
||||||
|
help='concatenate prefix to every numeric field tag'
|
||||||
|
' (ex. 99 becomes "v99")')
|
||||||
|
parser.add_argument(
|
||||||
|
'-n', '--mfn', action='store_true',
|
||||||
|
help='generate an "_id" from the MFN of each record'
|
||||||
|
' (available only for .mst input)')
|
||||||
|
parser.add_argument(
|
||||||
|
'-k', '--constant', type=str, metavar='TAG:VALUE', default='',
|
||||||
|
help='Include a constant tag:value in every record (ex. -k type:AS)')
|
||||||
|
|
||||||
|
'''
|
||||||
|
# TODO: implement this to export large quantities of records to CouchDB
|
||||||
|
parser.add_argument(
|
||||||
|
'-r', '--repeat', type=int, default=1,
|
||||||
|
help='repeat operation, saving multiple JSON files'
|
||||||
|
' (default=1, use -r 0 to repeat until end of input)')
|
||||||
|
'''
|
||||||
|
# parse the command line
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.file_name.lower().endswith('.mst'):
|
||||||
|
input_gen_func = iter_mst_records # <5>
|
||||||
|
else:
|
||||||
|
if args.mfn:
|
||||||
|
print('UNSUPORTED: -n/--mfn option only available for .mst input.')
|
||||||
|
raise SystemExit
|
||||||
|
input_gen_func = iter_iso_records # <6>
|
||||||
|
input_gen = input_gen_func(args.file_name, args.type) # <7>
|
||||||
|
if args.couch:
|
||||||
|
args.out.write('{ "docs" : ')
|
||||||
|
write_json(input_gen, args.file_name, args.out, args.qty, # <8>
|
||||||
|
args.skip, args.id, args.uuid, args.mongo, args.mfn,
|
||||||
|
args.type, args.prefix, args.constant)
|
||||||
|
if args.couch:
|
||||||
|
args.out.write('}\n')
|
||||||
|
args.out.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
# END ISIS2JSON
|
||||||
167
17-it-generator/isis2json/iso2709.py
Normal file
167
17-it-generator/isis2json/iso2709.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
# ISO-2709 file reader
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010 BIREME/PAHO/WHO
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from struct import unpack
|
||||||
|
|
||||||
|
CR = '\x0D' # \r
|
||||||
|
LF = '\x0A' # \n
|
||||||
|
IS1 = '\x1F' # ECMA-48 Unit Separator
|
||||||
|
IS2 = '\x1E' # ECMA-48 Record Separator / ISO-2709 field separator
|
||||||
|
IS3 = '\x1D' # ECMA-48 Group Separator / ISO-2709 record separator
|
||||||
|
LABEL_LEN = 24
|
||||||
|
LABEL_FORMAT = '5s c 4s c c 5s 3s c c c c'
|
||||||
|
TAG_LEN = 3
|
||||||
|
DEFAULT_ENCODING = 'ASCII'
|
||||||
|
SUBFIELD_DELIMITER = '^'
|
||||||
|
|
||||||
|
class IsoFile(object):
|
||||||
|
|
||||||
|
def __init__(self, filename, encoding = DEFAULT_ENCODING):
|
||||||
|
self.file = open(filename, 'rb')
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
return IsoRecord(self)
|
||||||
|
|
||||||
|
__next__ = next # Python 3 compatibility
|
||||||
|
|
||||||
|
def read(self, size):
|
||||||
|
''' read and drop all CR and LF characters '''
|
||||||
|
# TODO: this is inneficient but works, patches accepted!
|
||||||
|
# NOTE: our fixtures include files which have no linebreaks,
|
||||||
|
# files with CR-LF linebreaks and files with LF linebreaks
|
||||||
|
chunks = []
|
||||||
|
count = 0
|
||||||
|
while count < size:
|
||||||
|
chunk = self.file.read(size-count)
|
||||||
|
if len(chunk) == 0:
|
||||||
|
break
|
||||||
|
chunk = chunk.replace(CR+LF,'')
|
||||||
|
if CR in chunk:
|
||||||
|
chunk = chunk.replace(CR,'')
|
||||||
|
if LF in chunk:
|
||||||
|
chunk = chunk.replace(LF,'')
|
||||||
|
count += len(chunk)
|
||||||
|
chunks.append(chunk)
|
||||||
|
return ''.join(chunks)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.file.close()
|
||||||
|
|
||||||
|
class IsoRecord(object):
|
||||||
|
label_part_names = ('rec_len rec_status impl_codes indicator_len identifier_len'
|
||||||
|
' base_addr user_defined'
|
||||||
|
# directory map:
|
||||||
|
' fld_len_len start_len impl_len reserved').split()
|
||||||
|
rec_len = 0
|
||||||
|
|
||||||
|
def __init__(self, iso_file=None):
|
||||||
|
self.iso_file = iso_file
|
||||||
|
self.load_label()
|
||||||
|
self.load_directory()
|
||||||
|
self.load_fields()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.rec_len
|
||||||
|
|
||||||
|
def load_label(self):
|
||||||
|
label = self.iso_file.read(LABEL_LEN)
|
||||||
|
if len(label) == 0:
|
||||||
|
raise StopIteration
|
||||||
|
elif len(label) != 24:
|
||||||
|
raise ValueError('Invalid record label: "%s"' % label)
|
||||||
|
parts = unpack(LABEL_FORMAT, label)
|
||||||
|
for name, part in zip(self.label_part_names, parts):
|
||||||
|
if name.endswith('_len') or name.endswith('_addr'):
|
||||||
|
part = int(part)
|
||||||
|
setattr(self, name, part)
|
||||||
|
|
||||||
|
def show_label(self):
|
||||||
|
for name in self.label_part_names:
|
||||||
|
print('%15s : %r' % (name, getattr(self, name)))
|
||||||
|
|
||||||
|
def load_directory(self):
|
||||||
|
fmt_dir = '3s %ss %ss %ss' % (self.fld_len_len, self.start_len, self.impl_len)
|
||||||
|
entry_len = TAG_LEN + self.fld_len_len + self.start_len + self.impl_len
|
||||||
|
self.directory = []
|
||||||
|
while True:
|
||||||
|
char = self.iso_file.read(1)
|
||||||
|
if char.isdigit():
|
||||||
|
entry = char + self.iso_file.read(entry_len-1)
|
||||||
|
entry = Field(* unpack(fmt_dir, entry))
|
||||||
|
self.directory.append(entry)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
def load_fields(self):
|
||||||
|
for field in self.directory:
|
||||||
|
if self.indicator_len > 0:
|
||||||
|
field.indicator = self.iso_file.read(self.indicator_len)
|
||||||
|
# XXX: lilacs30.iso has an identifier_len == 2,
|
||||||
|
# but we need to ignore it to succesfully read the field contents
|
||||||
|
# TODO: find out when to ignore the idenfier_len,
|
||||||
|
# or fix the lilacs30.iso fixture
|
||||||
|
#
|
||||||
|
##if self.identifier_len > 0: #
|
||||||
|
## field.identifier = self.iso_file.read(self.identifier_len)
|
||||||
|
value = self.iso_file.read(len(field))
|
||||||
|
assert len(value) == len(field)
|
||||||
|
field.value = value[:-1] # remove trailing field separator
|
||||||
|
self.iso_file.read(1) # discard record separator
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
for field in self.directory:
|
||||||
|
yield(field)
|
||||||
|
|
||||||
|
__next__ = next # Python 3 compatibility
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
for field in self.directory:
|
||||||
|
print('%3s %r' % (field.tag, field.value))
|
||||||
|
|
||||||
|
class Field(object):
|
||||||
|
|
||||||
|
def __init__(self, tag, len, start, impl):
|
||||||
|
self.tag = tag
|
||||||
|
self.len = int(len)
|
||||||
|
self.start = int(start)
|
||||||
|
self.impl = impl
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
for name in 'tag len start impl'.split():
|
||||||
|
print('%15s : %r' % (name, getattr(self, name)))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.len
|
||||||
|
|
||||||
|
def test():
|
||||||
|
import doctest
|
||||||
|
doctest.testfile('iso2709_test.txt')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
test()
|
||||||
|
|
||||||
142
17-it-generator/isis2json/subfield.py
Normal file
142
17-it-generator/isis2json/subfield.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
# ISIS-DM: the ISIS Data Model API
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010 BIREME/PAHO/WHO
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
MAIN_SUBFIELD_KEY = '_'
|
||||||
|
SUBFIELD_MARKER_RE = re.compile(r'\^([a-z0-9])', re.IGNORECASE)
|
||||||
|
DEFAULT_ENCODING = u'utf-8'
|
||||||
|
|
||||||
|
def expand(content, subkeys=None):
|
||||||
|
''' Parse a field into an association list of keys and subfields
|
||||||
|
|
||||||
|
>>> expand('zero^1one^2two^3three')
|
||||||
|
[('_', 'zero'), ('1', 'one'), ('2', 'two'), ('3', 'three')]
|
||||||
|
|
||||||
|
'''
|
||||||
|
if subkeys is None:
|
||||||
|
regex = SUBFIELD_MARKER_RE
|
||||||
|
elif subkeys == '':
|
||||||
|
return [(MAIN_SUBFIELD_KEY, content)]
|
||||||
|
else:
|
||||||
|
regex = re.compile(r'\^(['+subkeys+'])', re.IGNORECASE)
|
||||||
|
content = content.replace('^^', '^^ ')
|
||||||
|
parts = []
|
||||||
|
start = 0
|
||||||
|
key = MAIN_SUBFIELD_KEY
|
||||||
|
while True:
|
||||||
|
found = regex.search(content, start)
|
||||||
|
if found is None: break
|
||||||
|
parts.append((key, content[start:found.start()].rstrip()))
|
||||||
|
key = found.group(1).lower()
|
||||||
|
start = found.end()
|
||||||
|
parts.append((key, content[start:].rstrip()))
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
class CompositeString(object):
|
||||||
|
''' Represent an Isis field, with subfields, using
|
||||||
|
Python native datastructures
|
||||||
|
|
||||||
|
>>> author = CompositeString('John Tenniel^xillustrator',
|
||||||
|
... subkeys='x')
|
||||||
|
>>> unicode(author)
|
||||||
|
u'John Tenniel^xillustrator'
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, isis_raw, subkeys=None, encoding=DEFAULT_ENCODING):
|
||||||
|
if not isinstance(isis_raw, basestring):
|
||||||
|
raise TypeError('%r value must be unicode or str instance' % isis_raw)
|
||||||
|
|
||||||
|
self.__isis_raw = isis_raw.decode(encoding)
|
||||||
|
self.__expanded = expand(self.__isis_raw, subkeys)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
for subfield in self.__expanded:
|
||||||
|
if subfield[0] == key:
|
||||||
|
return subfield[1]
|
||||||
|
else:
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (subfield[0] for subfield in self.__expanded)
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self.__expanded
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.__isis_raw
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.__isis_raw)
|
||||||
|
|
||||||
|
|
||||||
|
class CompositeField(object):
|
||||||
|
''' Represent an Isis field, with subfields, using
|
||||||
|
Python native datastructures
|
||||||
|
|
||||||
|
>>> author = CompositeField( [('name','Braz, Marcelo'),('role','writer')] )
|
||||||
|
>>> print author['name']
|
||||||
|
Braz, Marcelo
|
||||||
|
>>> print author['role']
|
||||||
|
writer
|
||||||
|
>>> author
|
||||||
|
CompositeField((('name', 'Braz, Marcelo'), ('role', 'writer')))
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, value, subkeys=None):
|
||||||
|
if subkeys is None:
|
||||||
|
subkeys = [item[0] for item in value]
|
||||||
|
try:
|
||||||
|
value_as_dict = dict(value)
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError('%r value must be a key-value structure' % self)
|
||||||
|
|
||||||
|
for key in value_as_dict:
|
||||||
|
if key not in subkeys:
|
||||||
|
raise TypeError('Unexpected keyword %r' % key)
|
||||||
|
|
||||||
|
self.value = tuple([(key, value_as_dict.get(key,None)) for key in subkeys])
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict(self.value)[key]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "CompositeField(%s)" % str(self.items())
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
unicode(self.items())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
str(self.items())
|
||||||
|
|
||||||
|
|
||||||
|
def test():
|
||||||
|
import doctest
|
||||||
|
doctest.testmod()
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
test()
|
||||||
37
17-it-generator/sentence.py
Normal file
37
17-it-generator/sentence.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
Sentence: access words by index
|
||||||
|
|
||||||
|
>>> text = 'To be, or not to be, that is the question'
|
||||||
|
>>> s = Sentence(text)
|
||||||
|
>>> len(s)
|
||||||
|
10
|
||||||
|
>>> s[1], s[5]
|
||||||
|
('be', 'be')
|
||||||
|
>>> s
|
||||||
|
Sentence('To be, or no... the question')
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::SENTENCE_SEQ[]
|
||||||
|
import re
|
||||||
|
import reprlib
|
||||||
|
|
||||||
|
RE_WORD = re.compile(r'\w+')
|
||||||
|
|
||||||
|
|
||||||
|
class Sentence:
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
self.words = RE_WORD.findall(text) # <1>
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self.words[index] # <2>
|
||||||
|
|
||||||
|
def __len__(self): # <3>
|
||||||
|
return len(self.words)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Sentence(%s)' % reprlib.repr(self.text) # <4>
|
||||||
|
|
||||||
|
# end::SENTENCE_SEQ[]
|
||||||
54
17-it-generator/sentence.rst
Normal file
54
17-it-generator/sentence.rst
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
==============================
|
||||||
|
Tests for a ``Sentence`` class
|
||||||
|
==============================
|
||||||
|
|
||||||
|
A ``Sentence`` is built from a ``str`` and allows iteration
|
||||||
|
word-by-word.
|
||||||
|
|
||||||
|
::
|
||||||
|
>>> s = Sentence('The time has come')
|
||||||
|
>>> s
|
||||||
|
Sentence('The time has come')
|
||||||
|
>>> list(s)
|
||||||
|
['The', 'time', 'has', 'come']
|
||||||
|
>>> it = iter(s)
|
||||||
|
>>> next(it)
|
||||||
|
'The'
|
||||||
|
>>> next(it)
|
||||||
|
'time'
|
||||||
|
>>> next(it)
|
||||||
|
'has'
|
||||||
|
>>> next(it)
|
||||||
|
'come'
|
||||||
|
>>> next(it)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
StopIteration
|
||||||
|
|
||||||
|
|
||||||
|
Any punctuation is skipped while iterating::
|
||||||
|
|
||||||
|
>>> s = Sentence('"The time has come," the Walrus said,')
|
||||||
|
>>> s
|
||||||
|
Sentence('"The time ha... Walrus said,')
|
||||||
|
>>> list(s)
|
||||||
|
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
|
||||||
|
|
||||||
|
|
||||||
|
White space including line breaks are also ignored::
|
||||||
|
|
||||||
|
>>> s = Sentence('''"The time has come," the Walrus said,
|
||||||
|
... "To talk of many things:"''')
|
||||||
|
>>> s
|
||||||
|
Sentence('"The time ha...many things:"')
|
||||||
|
>>> list(s)
|
||||||
|
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said', 'To', 'talk', 'of', 'many', 'things']
|
||||||
|
|
||||||
|
|
||||||
|
Accented Latin characters are also recognized as word characters::
|
||||||
|
|
||||||
|
>>> s = Sentence('Agora vou-me. Ou me vão?')
|
||||||
|
>>> s
|
||||||
|
Sentence('Agora vou-me. Ou me vão?')
|
||||||
|
>>> list(s)
|
||||||
|
['Agora', 'vou', 'me', 'Ou', 'me', 'vão']
|
||||||
28
17-it-generator/sentence_gen.py
Normal file
28
17-it-generator/sentence_gen.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Sentence: iterate over words using a generator function
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::SENTENCE_GEN[]
|
||||||
|
import re
|
||||||
|
import reprlib
|
||||||
|
|
||||||
|
RE_WORD = re.compile(r'\w+')
|
||||||
|
|
||||||
|
|
||||||
|
class Sentence:
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
self.words = RE_WORD.findall(text)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Sentence(%s)' % reprlib.repr(self.text)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for word in self.words: # <1>
|
||||||
|
yield word # <2>
|
||||||
|
return # <3>
|
||||||
|
|
||||||
|
# done! <4>
|
||||||
|
|
||||||
|
# end::SENTENCE_GEN[]
|
||||||
24
17-it-generator/sentence_gen2.py
Normal file
24
17-it-generator/sentence_gen2.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""
|
||||||
|
Sentence: iterate over words using a generator function
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::SENTENCE_GEN2[]
|
||||||
|
import re
|
||||||
|
import reprlib
|
||||||
|
|
||||||
|
RE_WORD = re.compile('r\w+')
|
||||||
|
|
||||||
|
|
||||||
|
class Sentence:
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text # <1>
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Sentence(%s)' % reprlib.repr(self.text)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for match in RE_WORD.finditer(self.text): # <2>
|
||||||
|
yield match.group() # <3>
|
||||||
|
|
||||||
|
# end::SENTENCE_GEN2[]
|
||||||
44
17-it-generator/sentence_genexp.py
Normal file
44
17-it-generator/sentence_genexp.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Sentence: iterate over words using a generator expression
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::SENTENCE_GENEXP[]
|
||||||
|
import re
|
||||||
|
import reprlib
|
||||||
|
|
||||||
|
RE_WORD = re.compile(r'\w+')
|
||||||
|
|
||||||
|
|
||||||
|
class Sentence:
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Sentence(%s)' % reprlib.repr(self.text)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (match.group() for match in RE_WORD.finditer(self.text))
|
||||||
|
# end::SENTENCE_GENEXP[]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
try:
|
||||||
|
filename = sys.argv[1]
|
||||||
|
word_number = int(sys.argv[2])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
print('Usage: %s <file-name> <word-number>' % sys.argv[0])
|
||||||
|
sys.exit(1)
|
||||||
|
with open(filename, 'rt', encoding='utf-8') as text_file:
|
||||||
|
s = Sentence(text_file.read())
|
||||||
|
for n, word in enumerate(s, 1):
|
||||||
|
if n == word_number:
|
||||||
|
print(word)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
warnings.warn('last word is #%d, "%s"' % (n, word))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
65
17-it-generator/sentence_iter.py
Normal file
65
17-it-generator/sentence_iter.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Sentence: iterate over words using the Iterator Pattern, take #1
|
||||||
|
|
||||||
|
WARNING: the Iterator Pattern is much simpler in idiomatic Python;
|
||||||
|
see: sentence_gen*.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::SENTENCE_ITER[]
|
||||||
|
import re
|
||||||
|
import reprlib
|
||||||
|
|
||||||
|
RE_WORD = re.compile(r'\w+')
|
||||||
|
|
||||||
|
|
||||||
|
class Sentence:
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
self.words = RE_WORD.findall(text)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Sentence(%s)' % reprlib.repr(self.text)
|
||||||
|
|
||||||
|
def __iter__(self): # <1>
|
||||||
|
return SentenceIterator(self.words) # <2>
|
||||||
|
|
||||||
|
|
||||||
|
class SentenceIterator:
|
||||||
|
|
||||||
|
def __init__(self, words):
|
||||||
|
self.words = words # <3>
|
||||||
|
self.index = 0 # <4>
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
try:
|
||||||
|
word = self.words[self.index] # <5>
|
||||||
|
except IndexError:
|
||||||
|
raise StopIteration() # <6>
|
||||||
|
self.index += 1 # <7>
|
||||||
|
return word # <8>
|
||||||
|
|
||||||
|
def __iter__(self): # <9>
|
||||||
|
return self
|
||||||
|
# end::SENTENCE_ITER[]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
try:
|
||||||
|
filename = sys.argv[1]
|
||||||
|
word_number = int(sys.argv[2])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
print('Usage: %s <file-name> <word-number>' % sys.argv[0])
|
||||||
|
sys.exit(1)
|
||||||
|
with open(filename, 'rt', encoding='utf-8') as text_file:
|
||||||
|
s = Sentence(text_file.read())
|
||||||
|
for n, word in enumerate(s, 1):
|
||||||
|
if n == word_number:
|
||||||
|
print(word)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
warnings.warn('last word is #%d, "%s"' % (n, word))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
37
17-it-generator/sentence_iter2.py
Normal file
37
17-it-generator/sentence_iter2.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
Sentence: iterate over words using the Iterator Pattern, take #2
|
||||||
|
|
||||||
|
WARNING: the Iterator Pattern is much simpler in idiomatic Python;
|
||||||
|
see: sentence_gen*.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import reprlib
|
||||||
|
|
||||||
|
RE_WORD = re.compile(r'\w+')
|
||||||
|
|
||||||
|
|
||||||
|
class Sentence:
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Sentence(%s)' % reprlib.repr(self.text)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
word_iter = RE_WORD.finditer(self.text) # <1>
|
||||||
|
return SentenceIter(word_iter) # <2>
|
||||||
|
|
||||||
|
|
||||||
|
class SentenceIter():
|
||||||
|
|
||||||
|
def __init__(self, word_iter):
|
||||||
|
self.word_iter = word_iter # <3>
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
match = next(self.word_iter) # <4>
|
||||||
|
return match.group() # <5>
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
36
17-it-generator/sentence_runner.py
Normal file
36
17-it-generator/sentence_runner.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import doctest
|
||||||
|
import importlib
|
||||||
|
import glob
|
||||||
|
|
||||||
|
|
||||||
|
TARGET_GLOB = 'sentence*.py'
|
||||||
|
TEST_FILE = 'sentence.rst'
|
||||||
|
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}'
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
verbose = '-v' in argv
|
||||||
|
for module_file_name in sorted(glob.glob(TARGET_GLOB)):
|
||||||
|
module_name = module_file_name.replace('.py', '')
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
try:
|
||||||
|
cls = getattr(module, 'Sentence')
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
test(cls, verbose)
|
||||||
|
|
||||||
|
|
||||||
|
def test(cls, verbose=False):
|
||||||
|
|
||||||
|
res = doctest.testfile(
|
||||||
|
TEST_FILE,
|
||||||
|
globs={'Sentence': cls},
|
||||||
|
verbose=verbose,
|
||||||
|
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
|
||||||
|
tag = 'FAIL' if res.failed else 'OK'
|
||||||
|
print(TEST_MSG.format(cls.__module__, res, tag))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
main(sys.argv)
|
||||||
7
17-it-generator/tree/4steps/tree_step0.py
Normal file
7
17-it-generator/tree/4steps/tree_step0.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for cls_name in tree(BaseException):
|
||||||
|
print(cls_name)
|
||||||
10
17-it-generator/tree/4steps/tree_step1.py
Normal file
10
17-it-generator/tree/4steps/tree_step1.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__, 0
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield sub_cls.__name__, 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for cls_name, level in tree(BaseException):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
12
17-it-generator/tree/4steps/tree_step2.py
Normal file
12
17-it-generator/tree/4steps/tree_step2.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__, 0
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield sub_cls.__name__, 1
|
||||||
|
for sub_sub_cls in sub_cls.__subclasses__():
|
||||||
|
yield sub_sub_cls.__name__, 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for cls_name, level in tree(BaseException):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
10
17-it-generator/tree/4steps/tree_step3.py
Normal file
10
17-it-generator/tree/4steps/tree_step3.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
def tree(cls, level=0):
|
||||||
|
yield cls.__name__, level
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield from tree(sub_cls, level + 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for cls_name, level in tree(BaseException):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
35
17-it-generator/tree/extra/pretty_tree.py
Normal file
35
17-it-generator/tree/extra/pretty_tree.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
SPACES = ' ' * 4
|
||||||
|
HLINE = '\u2500' # ─ BOX DRAWINGS LIGHT HORIZONTAL
|
||||||
|
HLINE2 = HLINE * 2
|
||||||
|
ELBOW = f'\u2514{HLINE2} ' # └ BOX DRAWINGS LIGHT UP AND RIGHT
|
||||||
|
TEE = f'\u251C{HLINE2} ' # ├ BOX DRAWINGS LIGHT VERTICAL AND RIGHT
|
||||||
|
PIPE = f'\u2502 ' # │ BOX DRAWINGS LIGHT VERTICAL
|
||||||
|
|
||||||
|
|
||||||
|
def render_lines(tree_iter):
|
||||||
|
name, _, _ = next(tree_iter)
|
||||||
|
yield name
|
||||||
|
prefix = ''
|
||||||
|
|
||||||
|
for name, level, last in tree_iter:
|
||||||
|
if last:
|
||||||
|
connector = ELBOW
|
||||||
|
else:
|
||||||
|
connector = TEE
|
||||||
|
|
||||||
|
prefix = prefix[:4 * (level-1)]
|
||||||
|
prefix = prefix.replace(TEE, PIPE).replace(ELBOW, SPACES)
|
||||||
|
prefix += connector
|
||||||
|
|
||||||
|
yield prefix + name
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for line in render_lines(tree(cls)):
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
101
17-it-generator/tree/extra/test_pretty_tree.py
Normal file
101
17-it-generator/tree/extra/test_pretty_tree.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from pretty_tree import tree, render_lines
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
result = list(render_lines(tree(BrokenPipeError)))
|
||||||
|
expected = [
|
||||||
|
'BrokenPipeError',
|
||||||
|
]
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_1_leaf():
|
||||||
|
result = list(render_lines(tree(IndentationError)))
|
||||||
|
expected = [
|
||||||
|
'IndentationError',
|
||||||
|
'└── TabError',
|
||||||
|
]
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_3_levels_1_leaf():
|
||||||
|
class X: pass
|
||||||
|
class Y(X): pass
|
||||||
|
class Z(Y): pass
|
||||||
|
result = list(render_lines(tree(X)))
|
||||||
|
expected = [
|
||||||
|
'X',
|
||||||
|
'└── Y',
|
||||||
|
' └── Z',
|
||||||
|
]
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_1_leaf():
|
||||||
|
class Level0: pass
|
||||||
|
class Level1(Level0): pass
|
||||||
|
class Level2(Level1): pass
|
||||||
|
class Level3(Level2): pass
|
||||||
|
|
||||||
|
result = list(render_lines(tree(Level0)))
|
||||||
|
expected = [
|
||||||
|
'Level0',
|
||||||
|
'└── Level1',
|
||||||
|
' └── Level2',
|
||||||
|
' └── Level3',
|
||||||
|
]
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
result = list(render_lines(tree(Branch)))
|
||||||
|
expected = [
|
||||||
|
'Branch',
|
||||||
|
'├── Leaf1',
|
||||||
|
'└── Leaf2',
|
||||||
|
]
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_3_levels_2_leaves():
|
||||||
|
class A: pass
|
||||||
|
class B(A): pass
|
||||||
|
class C(B): pass
|
||||||
|
class D(A): pass
|
||||||
|
class E(D): pass
|
||||||
|
|
||||||
|
result = list(render_lines(tree(A)))
|
||||||
|
expected = [
|
||||||
|
'A',
|
||||||
|
'├── B',
|
||||||
|
'│ └── C',
|
||||||
|
'└── D',
|
||||||
|
' └── E',
|
||||||
|
]
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_4_leaves():
|
||||||
|
class A: pass
|
||||||
|
class B1(A): pass
|
||||||
|
class C1(B1): pass
|
||||||
|
class D1(C1): pass
|
||||||
|
class D2(C1): pass
|
||||||
|
class C2(B1): pass
|
||||||
|
class B2(A): pass
|
||||||
|
expected = [
|
||||||
|
'A',
|
||||||
|
'├── B1',
|
||||||
|
'│ ├── C1',
|
||||||
|
'│ │ ├── D1',
|
||||||
|
'│ │ └── D2',
|
||||||
|
'│ └── C2',
|
||||||
|
'└── B2',
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(render_lines(tree(A)))
|
||||||
|
assert expected == result
|
||||||
90
17-it-generator/tree/extra/test_tree.py
Normal file
90
17-it-generator/tree/extra/test_tree.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = [('One', 0, True)]
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
expected = [
|
||||||
|
('Branch', 0, True),
|
||||||
|
('Leaf1', 1, False),
|
||||||
|
('Leaf2', 1, True),
|
||||||
|
]
|
||||||
|
result = list(tree(Branch))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_3_levels_1_leaf():
|
||||||
|
class X: pass
|
||||||
|
class Y(X): pass
|
||||||
|
class Z(Y): pass
|
||||||
|
expected = [
|
||||||
|
('X', 0, True),
|
||||||
|
('Y', 1, True),
|
||||||
|
('Z', 2, True),
|
||||||
|
]
|
||||||
|
result = list(tree(X))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_1_leaf():
|
||||||
|
class Level0: pass
|
||||||
|
class Level1(Level0): pass
|
||||||
|
class Level2(Level1): pass
|
||||||
|
class Level3(Level2): pass
|
||||||
|
expected = [
|
||||||
|
('Level0', 0, True),
|
||||||
|
('Level1', 1, True),
|
||||||
|
('Level2', 2, True),
|
||||||
|
('Level3', 3, True),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(Level0))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_3_leaves():
|
||||||
|
class A: pass
|
||||||
|
class B1(A): pass
|
||||||
|
class B2(A): pass
|
||||||
|
class C1(B1): pass
|
||||||
|
class C2(B2): pass
|
||||||
|
class D1(C1): pass
|
||||||
|
class D2(C1): pass
|
||||||
|
expected = [
|
||||||
|
('A', 0, True),
|
||||||
|
('B1', 1, False),
|
||||||
|
('C1', 2, True),
|
||||||
|
('D1', 3, False),
|
||||||
|
('D2', 3, True),
|
||||||
|
('B2', 1, True),
|
||||||
|
('C2', 2, True),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(A))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_many_levels_1_leaf():
|
||||||
|
class Root: pass
|
||||||
|
level_count = 100
|
||||||
|
expected = [('Root', 0, True)]
|
||||||
|
parent = Root
|
||||||
|
for level in range(1, level_count):
|
||||||
|
name = f'Sub{level}'
|
||||||
|
cls = type(name, (parent,), {})
|
||||||
|
expected.append((name, level, True))
|
||||||
|
parent = cls
|
||||||
|
|
||||||
|
result = list(tree(Root))
|
||||||
|
assert len(result) == level_count
|
||||||
|
assert result[0] == ('Root', 0, True)
|
||||||
|
assert result[-1] == ('Sub99', 99, True)
|
||||||
|
assert expected == result
|
||||||
17
17-it-generator/tree/extra/tree.py
Normal file
17
17-it-generator/tree/extra/tree.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
def tree(cls, level=0, last_in_level=True):
|
||||||
|
yield cls.__name__, level, last_in_level
|
||||||
|
subclasses = cls.__subclasses__()
|
||||||
|
if subclasses:
|
||||||
|
last = subclasses[-1]
|
||||||
|
for sub_cls in subclasses:
|
||||||
|
yield from tree(sub_cls, level+1, sub_cls is last)
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name, level, _ in tree(cls):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
8
17-it-generator/tree/step0/test_tree.py
Normal file
8
17-it-generator/tree/step0/test_tree.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = ['One']
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
11
17-it-generator/tree/step0/tree.py
Normal file
11
17-it-generator/tree/step0/tree.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name in tree(cls):
|
||||||
|
print(cls_name)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
21
17-it-generator/tree/step1/test_tree.py
Normal file
21
17-it-generator/tree/step1/test_tree.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = [('One', 0)]
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
expected = [
|
||||||
|
('Branch', 0),
|
||||||
|
('Leaf1', 1),
|
||||||
|
('Leaf2', 1),
|
||||||
|
]
|
||||||
|
result = list(tree(Branch))
|
||||||
|
assert expected == result
|
||||||
14
17-it-generator/tree/step1/tree.py
Normal file
14
17-it-generator/tree/step1/tree.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__, 0 # <1>
|
||||||
|
for sub_cls in cls.__subclasses__(): # <2>
|
||||||
|
yield sub_cls.__name__, 1 # <3>
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name, level in tree(cls):
|
||||||
|
indent = ' ' * 4 * level # <4>
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
21
17-it-generator/tree/step2/test_tree.py
Normal file
21
17-it-generator/tree/step2/test_tree.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = [('One', 0)]
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
expected = [
|
||||||
|
('Branch', 0),
|
||||||
|
('Leaf1', 1),
|
||||||
|
('Leaf2', 1),
|
||||||
|
]
|
||||||
|
result = list(tree(Branch))
|
||||||
|
assert expected == result
|
||||||
18
17-it-generator/tree/step2/tree.py
Normal file
18
17-it-generator/tree/step2/tree.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__, 0
|
||||||
|
yield from sub_tree(cls) # <1>
|
||||||
|
|
||||||
|
|
||||||
|
def sub_tree(cls):
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield sub_cls.__name__, 1 # <2>
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name, level in tree(cls):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
34
17-it-generator/tree/step3/test_tree.py
Normal file
34
17-it-generator/tree/step3/test_tree.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = [('One', 0)]
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
expected = [
|
||||||
|
('Branch', 0),
|
||||||
|
('Leaf1', 1),
|
||||||
|
('Leaf2', 1),
|
||||||
|
]
|
||||||
|
result = list(tree(Branch))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_3_levels_1_leaf():
|
||||||
|
class X: pass
|
||||||
|
class Y(X): pass
|
||||||
|
class Z(Y): pass
|
||||||
|
expected = [
|
||||||
|
('X', 0),
|
||||||
|
('Y', 1),
|
||||||
|
('Z', 2),
|
||||||
|
]
|
||||||
|
result = list(tree(X))
|
||||||
|
assert expected == result
|
||||||
20
17-it-generator/tree/step3/tree.py
Normal file
20
17-it-generator/tree/step3/tree.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__, 0
|
||||||
|
yield from sub_tree(cls)
|
||||||
|
|
||||||
|
|
||||||
|
def sub_tree(cls):
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield sub_cls.__name__, 1
|
||||||
|
for sub_sub_cls in sub_cls.__subclasses__():
|
||||||
|
yield sub_sub_cls.__name__, 2
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name, level in tree(cls):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
72
17-it-generator/tree/step4/test_tree.py
Normal file
72
17-it-generator/tree/step4/test_tree.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = [('One', 0)]
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
expected = [
|
||||||
|
('Branch', 0),
|
||||||
|
('Leaf1', 1),
|
||||||
|
('Leaf2', 1),
|
||||||
|
]
|
||||||
|
result = list(tree(Branch))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_3_levels_1_leaf():
|
||||||
|
class X: pass
|
||||||
|
class Y(X): pass
|
||||||
|
class Z(Y): pass
|
||||||
|
expected = [
|
||||||
|
('X', 0),
|
||||||
|
('Y', 1),
|
||||||
|
('Z', 2),
|
||||||
|
]
|
||||||
|
result = list(tree(X))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_1_leaf():
|
||||||
|
class Level0: pass
|
||||||
|
class Level1(Level0): pass
|
||||||
|
class Level2(Level1): pass
|
||||||
|
class Level3(Level2): pass
|
||||||
|
expected = [
|
||||||
|
('Level0', 0),
|
||||||
|
('Level1', 1),
|
||||||
|
('Level2', 2),
|
||||||
|
('Level3', 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(Level0))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_3_leaves():
|
||||||
|
class A: pass
|
||||||
|
class B1(A): pass
|
||||||
|
class C1(B1): pass
|
||||||
|
class D1(C1): pass
|
||||||
|
class B2(A): pass
|
||||||
|
class D2(C1): pass
|
||||||
|
class C2(B2): pass
|
||||||
|
expected = [
|
||||||
|
('A', 0),
|
||||||
|
('B1', 1),
|
||||||
|
('C1', 2),
|
||||||
|
('D1', 3),
|
||||||
|
('D2', 3),
|
||||||
|
('B2', 1),
|
||||||
|
('C2', 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(A))
|
||||||
|
assert expected == result
|
||||||
24
17-it-generator/tree/step4/tree.py
Normal file
24
17-it-generator/tree/step4/tree.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__, 0
|
||||||
|
yield from sub_tree(cls)
|
||||||
|
|
||||||
|
|
||||||
|
# tag::SUB_TREE[]
|
||||||
|
def sub_tree(cls):
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield sub_cls.__name__, 1
|
||||||
|
for sub_sub_cls in sub_cls.__subclasses__():
|
||||||
|
yield sub_sub_cls.__name__, 2
|
||||||
|
for sub_sub_sub_cls in sub_sub_cls.__subclasses__():
|
||||||
|
yield sub_sub_sub_cls.__name__, 3
|
||||||
|
# end::SUB_TREE[]
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name, level in tree(cls):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
90
17-it-generator/tree/step5/test_tree.py
Normal file
90
17-it-generator/tree/step5/test_tree.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = [('One', 0)]
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
expected = [
|
||||||
|
('Branch', 0),
|
||||||
|
('Leaf1', 1),
|
||||||
|
('Leaf2', 1),
|
||||||
|
]
|
||||||
|
result = list(tree(Branch))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_3_levels_1_leaf():
|
||||||
|
class X: pass
|
||||||
|
class Y(X): pass
|
||||||
|
class Z(Y): pass
|
||||||
|
expected = [
|
||||||
|
('X', 0),
|
||||||
|
('Y', 1),
|
||||||
|
('Z', 2),
|
||||||
|
]
|
||||||
|
result = list(tree(X))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_1_leaf():
|
||||||
|
class Level0: pass
|
||||||
|
class Level1(Level0): pass
|
||||||
|
class Level2(Level1): pass
|
||||||
|
class Level3(Level2): pass
|
||||||
|
expected = [
|
||||||
|
('Level0', 0),
|
||||||
|
('Level1', 1),
|
||||||
|
('Level2', 2),
|
||||||
|
('Level3', 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(Level0))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_3_leaves():
|
||||||
|
class A: pass
|
||||||
|
class B1(A): pass
|
||||||
|
class B2(A): pass
|
||||||
|
class C1(B1): pass
|
||||||
|
class C2(B2): pass
|
||||||
|
class D1(C1): pass
|
||||||
|
class D2(C1): pass
|
||||||
|
expected = [
|
||||||
|
('A', 0),
|
||||||
|
('B1', 1),
|
||||||
|
('C1', 2),
|
||||||
|
('D1', 3),
|
||||||
|
('D2', 3),
|
||||||
|
('B2', 1),
|
||||||
|
('C2', 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(A))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_many_levels_1_leaf():
|
||||||
|
class Root: pass
|
||||||
|
level_count = 100
|
||||||
|
expected = [('Root', 0)]
|
||||||
|
parent = Root
|
||||||
|
for level in range(1, level_count):
|
||||||
|
name = f'Sub{level}'
|
||||||
|
cls = type(name, (parent,), {})
|
||||||
|
expected.append((name, level))
|
||||||
|
parent = cls
|
||||||
|
|
||||||
|
result = list(tree(Root))
|
||||||
|
assert len(result) == level_count
|
||||||
|
assert result[0] == ('Root', 0)
|
||||||
|
assert result[-1] == ('Sub99', 99)
|
||||||
|
assert expected == result
|
||||||
19
17-it-generator/tree/step5/tree.py
Normal file
19
17-it-generator/tree/step5/tree.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
def tree(cls):
|
||||||
|
yield cls.__name__, 0
|
||||||
|
yield from sub_tree(cls, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def sub_tree(cls, level):
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield sub_cls.__name__, level
|
||||||
|
yield from sub_tree(sub_cls, level+1)
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name, level in tree(cls):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
90
17-it-generator/tree/step6/test_tree.py
Normal file
90
17-it-generator/tree/step6/test_tree.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from tree import tree
|
||||||
|
|
||||||
|
|
||||||
|
def test_1_level():
|
||||||
|
class One: pass
|
||||||
|
expected = [('One', 0)]
|
||||||
|
result = list(tree(One))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_2_levels_2_leaves():
|
||||||
|
class Branch: pass
|
||||||
|
class Leaf1(Branch): pass
|
||||||
|
class Leaf2(Branch): pass
|
||||||
|
expected = [
|
||||||
|
('Branch', 0),
|
||||||
|
('Leaf1', 1),
|
||||||
|
('Leaf2', 1),
|
||||||
|
]
|
||||||
|
result = list(tree(Branch))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_3_levels_1_leaf():
|
||||||
|
class X: pass
|
||||||
|
class Y(X): pass
|
||||||
|
class Z(Y): pass
|
||||||
|
expected = [
|
||||||
|
('X', 0),
|
||||||
|
('Y', 1),
|
||||||
|
('Z', 2),
|
||||||
|
]
|
||||||
|
result = list(tree(X))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_1_leaf():
|
||||||
|
class Level0: pass
|
||||||
|
class Level1(Level0): pass
|
||||||
|
class Level2(Level1): pass
|
||||||
|
class Level3(Level2): pass
|
||||||
|
expected = [
|
||||||
|
('Level0', 0),
|
||||||
|
('Level1', 1),
|
||||||
|
('Level2', 2),
|
||||||
|
('Level3', 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(Level0))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_4_levels_3_leaves():
|
||||||
|
class A: pass
|
||||||
|
class B1(A): pass
|
||||||
|
class B2(A): pass
|
||||||
|
class C1(B1): pass
|
||||||
|
class C2(B2): pass
|
||||||
|
class D1(C1): pass
|
||||||
|
class D2(C1): pass
|
||||||
|
expected = [
|
||||||
|
('A', 0),
|
||||||
|
('B1', 1),
|
||||||
|
('C1', 2),
|
||||||
|
('D1', 3),
|
||||||
|
('D2', 3),
|
||||||
|
('B2', 1),
|
||||||
|
('C2', 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = list(tree(A))
|
||||||
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_many_levels_1_leaf():
|
||||||
|
class Root: pass
|
||||||
|
level_count = 100
|
||||||
|
expected = [('Root', 0)]
|
||||||
|
parent = Root
|
||||||
|
for level in range(1, level_count):
|
||||||
|
name = f'Sub{level}'
|
||||||
|
cls = type(name, (parent,), {})
|
||||||
|
expected.append((name, level))
|
||||||
|
parent = cls
|
||||||
|
|
||||||
|
result = list(tree(Root))
|
||||||
|
assert len(result) == level_count
|
||||||
|
assert result[0] == ('Root', 0)
|
||||||
|
assert result[-1] == ('Sub99', 99)
|
||||||
|
assert expected == result
|
||||||
14
17-it-generator/tree/step6/tree.py
Normal file
14
17-it-generator/tree/step6/tree.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
def tree(cls, level=0):
|
||||||
|
yield cls.__name__, level
|
||||||
|
for sub_cls in cls.__subclasses__():
|
||||||
|
yield from tree(sub_cls, level+1)
|
||||||
|
|
||||||
|
|
||||||
|
def display(cls):
|
||||||
|
for cls_name, level in tree(cls):
|
||||||
|
indent = ' ' * 4 * level
|
||||||
|
print(f'{indent}{cls_name}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
display(BaseException)
|
||||||
29
17-it-generator/yield_delegate_fail.py
Normal file
29
17-it-generator/yield_delegate_fail.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
""" Example from `Python: The Full Monty`__ -- A Tested Semantics for the
|
||||||
|
Python Programming Language
|
||||||
|
|
||||||
|
__ http://cs.brown.edu/~sk/Publications/Papers/Published/pmmwplck-python-full-monty/
|
||||||
|
|
||||||
|
"The following program, [...] seems to perform a simple abstraction over the
|
||||||
|
process of yielding:"
|
||||||
|
|
||||||
|
Citation:
|
||||||
|
|
||||||
|
Joe Gibbs Politz, Alejandro Martinez, Matthew Milano, Sumner Warren,
|
||||||
|
Daniel Patterson, Junsong Li, Anand Chitipothu, and Shriram Krishnamurthi.
|
||||||
|
2013. Python: the full monty. SIGPLAN Not. 48, 10 (October 2013), 217-232.
|
||||||
|
DOI=10.1145/2544173.2509536 http://doi.acm.org/10.1145/2544173.2509536
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::YIELD_DELEGATE_FAIL[]
|
||||||
|
def f():
|
||||||
|
def do_yield(n):
|
||||||
|
yield n
|
||||||
|
x = 0
|
||||||
|
while True:
|
||||||
|
x += 1
|
||||||
|
do_yield(x)
|
||||||
|
# end::YIELD_DELEGATE_FAIL[]
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('Invoking f() results in an infinite loop')
|
||||||
|
f()
|
||||||
24
17-it-generator/yield_delegate_fix.py
Normal file
24
17-it-generator/yield_delegate_fix.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
""" Example adapted from ``yield_delegate_fail.py``
|
||||||
|
|
||||||
|
The following program performs a simple abstraction over the process of
|
||||||
|
yielding.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::YIELD_DELEGATE_FIX[]
|
||||||
|
def f():
|
||||||
|
def do_yield(n):
|
||||||
|
yield n
|
||||||
|
x = 0
|
||||||
|
while True:
|
||||||
|
x += 1
|
||||||
|
yield from do_yield(x)
|
||||||
|
# end::YIELD_DELEGATE_FIX[]
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('Invoking f() now produces a generator')
|
||||||
|
g = f()
|
||||||
|
print(next(g))
|
||||||
|
print(next(g))
|
||||||
|
print(next(g))
|
||||||
|
|
||||||
4
18-context-mngr/README.rst
Normal file
4
18-context-mngr/README.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sample code for Chapter 15 - "Context managers and something else"
|
||||||
|
|
||||||
|
From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015)
|
||||||
|
http://shop.oreilly.com/product/0636920032519.do
|
||||||
92
18-context-mngr/mirror.py
Normal file
92
18-context-mngr/mirror.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
A "mirroring" ``stdout`` context.
|
||||||
|
|
||||||
|
While active, the context manager reverses text output to
|
||||||
|
``stdout``::
|
||||||
|
|
||||||
|
# tag::MIRROR_DEMO_1[]
|
||||||
|
|
||||||
|
>>> from mirror import LookingGlass
|
||||||
|
>>> with LookingGlass() as what: # <1>
|
||||||
|
... print('Alice, Kitty and Snowdrop') # <2>
|
||||||
|
... print(what)
|
||||||
|
...
|
||||||
|
pordwonS dna yttiK ,ecilA # <3>
|
||||||
|
YKCOWREBBAJ
|
||||||
|
>>> what # <4>
|
||||||
|
'JABBERWOCKY'
|
||||||
|
>>> print('Back to normal.') # <5>
|
||||||
|
Back to normal.
|
||||||
|
|
||||||
|
# end::MIRROR_DEMO_1[]
|
||||||
|
|
||||||
|
|
||||||
|
This exposes the context manager operation::
|
||||||
|
|
||||||
|
# tag::MIRROR_DEMO_2[]
|
||||||
|
|
||||||
|
>>> from mirror import LookingGlass
|
||||||
|
>>> manager = LookingGlass() # <1>
|
||||||
|
>>> manager
|
||||||
|
<mirror.LookingGlass object at 0x2a578ac>
|
||||||
|
>>> monster = manager.__enter__() # <2>
|
||||||
|
>>> monster == 'JABBERWOCKY' # <3>
|
||||||
|
eurT
|
||||||
|
>>> monster
|
||||||
|
'YKCOWREBBAJ'
|
||||||
|
>>> manager
|
||||||
|
>ca875a2x0 ta tcejbo ssalGgnikooL.rorrim<
|
||||||
|
>>> manager.__exit__(None, None, None) # <4>
|
||||||
|
>>> monster
|
||||||
|
'JABBERWOCKY'
|
||||||
|
|
||||||
|
# end::MIRROR_DEMO_2[]
|
||||||
|
|
||||||
|
The context manager can handle and "swallow" exceptions.
|
||||||
|
|
||||||
|
# tag::MIRROR_DEMO_3[]
|
||||||
|
|
||||||
|
>>> from mirror import LookingGlass
|
||||||
|
>>> with LookingGlass():
|
||||||
|
... print('Humpty Dumpty')
|
||||||
|
... x = 1/0 # <1>
|
||||||
|
... print('END') # <2>
|
||||||
|
...
|
||||||
|
ytpmuD ytpmuH
|
||||||
|
Please DO NOT divide by zero!
|
||||||
|
>>> with LookingGlass():
|
||||||
|
... print('Humpty Dumpty')
|
||||||
|
... x = no_such_name # <1>
|
||||||
|
... print('END') # <2>
|
||||||
|
...
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
NameError: name 'no_such_name' is not defined
|
||||||
|
|
||||||
|
# end::MIRROR_DEMO_3[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::MIRROR_EX[]
|
||||||
|
class LookingGlass:
|
||||||
|
|
||||||
|
def __enter__(self): # <1>
|
||||||
|
import sys
|
||||||
|
self.original_write = sys.stdout.write # <2>
|
||||||
|
sys.stdout.write = self.reverse_write # <3>
|
||||||
|
return 'JABBERWOCKY' # <4>
|
||||||
|
|
||||||
|
def reverse_write(self, text): # <5>
|
||||||
|
self.original_write(text[::-1])
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback): # <6>
|
||||||
|
import sys # <7>
|
||||||
|
sys.stdout.write = self.original_write # <8>
|
||||||
|
if exc_type is ZeroDivisionError: # <9>
|
||||||
|
print('Please DO NOT divide by zero!')
|
||||||
|
return True # <10>
|
||||||
|
# <11>
|
||||||
|
|
||||||
|
|
||||||
|
# end::MIRROR_EX[]
|
||||||
64
18-context-mngr/mirror_gen.py
Normal file
64
18-context-mngr/mirror_gen.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
A "mirroring" ``stdout`` context manager.
|
||||||
|
|
||||||
|
While active, the context manager reverses text output to
|
||||||
|
``stdout``::
|
||||||
|
|
||||||
|
# tag::MIRROR_GEN_DEMO_1[]
|
||||||
|
|
||||||
|
>>> from mirror_gen import looking_glass
|
||||||
|
>>> with looking_glass() as what: # <1>
|
||||||
|
... print('Alice, Kitty and Snowdrop')
|
||||||
|
... print(what)
|
||||||
|
...
|
||||||
|
pordwonS dna yttiK ,ecilA
|
||||||
|
YKCOWREBBAJ
|
||||||
|
>>> what
|
||||||
|
'JABBERWOCKY'
|
||||||
|
|
||||||
|
# end::MIRROR_GEN_DEMO_1[]
|
||||||
|
|
||||||
|
|
||||||
|
This exposes the context manager operation::
|
||||||
|
|
||||||
|
# tag::MIRROR_GEN_DEMO_2[]
|
||||||
|
|
||||||
|
>>> from mirror_gen import looking_glass
|
||||||
|
>>> manager = looking_glass() # <1>
|
||||||
|
>>> manager # doctest: +ELLIPSIS
|
||||||
|
<contextlib._GeneratorContextManager object at 0x...>
|
||||||
|
>>> monster = manager.__enter__() # <2>
|
||||||
|
>>> monster == 'JABBERWOCKY' # <3>
|
||||||
|
eurT
|
||||||
|
>>> monster
|
||||||
|
'YKCOWREBBAJ'
|
||||||
|
>>> manager # doctest: +ELLIPSIS
|
||||||
|
>...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc<
|
||||||
|
>>> manager.__exit__(None, None, None) # <4>
|
||||||
|
>>> monster
|
||||||
|
'JABBERWOCKY'
|
||||||
|
|
||||||
|
# end::MIRROR_GEN_DEMO_2[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::MIRROR_GEN_EX[]
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager # <1>
|
||||||
|
def looking_glass():
|
||||||
|
import sys
|
||||||
|
original_write = sys.stdout.write # <2>
|
||||||
|
|
||||||
|
def reverse_write(text): # <3>
|
||||||
|
original_write(text[::-1])
|
||||||
|
|
||||||
|
sys.stdout.write = reverse_write # <4>
|
||||||
|
yield 'JABBERWOCKY' # <5>
|
||||||
|
sys.stdout.write = original_write # <6>
|
||||||
|
|
||||||
|
|
||||||
|
# end::MIRROR_GEN_EX[]
|
||||||
101
18-context-mngr/mirror_gen_exc.py
Normal file
101
18-context-mngr/mirror_gen_exc.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
A "mirroring" ``stdout`` context manager.
|
||||||
|
|
||||||
|
While active, the context manager reverses text output to
|
||||||
|
``stdout``::
|
||||||
|
|
||||||
|
# tag::MIRROR_GEN_DEMO_1[]
|
||||||
|
|
||||||
|
>>> from mirror_gen import looking_glass
|
||||||
|
>>> with looking_glass() as what: # <1>
|
||||||
|
... print('Alice, Kitty and Snowdrop')
|
||||||
|
... print(what)
|
||||||
|
...
|
||||||
|
pordwonS dna yttiK ,ecilA
|
||||||
|
YKCOWREBBAJ
|
||||||
|
>>> what
|
||||||
|
'JABBERWOCKY'
|
||||||
|
|
||||||
|
# end::MIRROR_GEN_DEMO_1[]
|
||||||
|
|
||||||
|
|
||||||
|
This exposes the context manager operation::
|
||||||
|
|
||||||
|
# tag::MIRROR_GEN_DEMO_2[]
|
||||||
|
|
||||||
|
>>> from mirror_gen import looking_glass
|
||||||
|
>>> manager = looking_glass() # <1>
|
||||||
|
>>> manager # doctest: +ELLIPSIS
|
||||||
|
<contextlib._GeneratorContextManager object at 0x...>
|
||||||
|
>>> monster = manager.__enter__() # <2>
|
||||||
|
>>> monster == 'JABBERWOCKY' # <3>
|
||||||
|
eurT
|
||||||
|
>>> monster
|
||||||
|
'YKCOWREBBAJ'
|
||||||
|
>>> manager # doctest: +ELLIPSIS
|
||||||
|
>...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc<
|
||||||
|
>>> manager.__exit__(None, None, None) # <4>
|
||||||
|
>>> monster
|
||||||
|
'JABBERWOCKY'
|
||||||
|
|
||||||
|
# end::MIRROR_GEN_DEMO_2[]
|
||||||
|
|
||||||
|
The context manager can handle and "swallow" exceptions.
|
||||||
|
The following test does not pass under doctest (a
|
||||||
|
ZeroDivisionError is reported by doctest) but passes
|
||||||
|
if executed by hand in the Python 3 console (the exception
|
||||||
|
is handled by the context manager):
|
||||||
|
|
||||||
|
# tag::MIRROR_GEN_DEMO_3[]
|
||||||
|
|
||||||
|
>>> from mirror_gen import looking_glass
|
||||||
|
>>> with looking_glass():
|
||||||
|
... print('Humpty Dumpty')
|
||||||
|
... x = 1/0 # <1>
|
||||||
|
... print('END') # <2>
|
||||||
|
...
|
||||||
|
ytpmuD ytpmuH
|
||||||
|
Please DO NOT divide by zero!
|
||||||
|
|
||||||
|
# end::MIRROR_GEN_DEMO_3[]
|
||||||
|
|
||||||
|
>>> with looking_glass():
|
||||||
|
... print('Humpty Dumpty')
|
||||||
|
... x = no_such_name # <1>
|
||||||
|
... print('END') # <2>
|
||||||
|
...
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
NameError: name 'no_such_name' is not defined
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::MIRROR_GEN_EXC[]
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def looking_glass():
|
||||||
|
import sys
|
||||||
|
original_write = sys.stdout.write
|
||||||
|
|
||||||
|
def reverse_write(text):
|
||||||
|
original_write(text[::-1])
|
||||||
|
|
||||||
|
sys.stdout.write = reverse_write
|
||||||
|
msg = '' # <1>
|
||||||
|
try:
|
||||||
|
yield 'JABBERWOCKY'
|
||||||
|
except ZeroDivisionError: # <2>
|
||||||
|
msg = 'Please DO NOT divide by zero!'
|
||||||
|
finally:
|
||||||
|
sys.stdout.write = original_write # <3>
|
||||||
|
if msg:
|
||||||
|
print(msg) # <4>
|
||||||
|
|
||||||
|
|
||||||
|
# end::MIRROR_GEN_EXC[]
|
||||||
4
19-coroutine/README.rst
Normal file
4
19-coroutine/README.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sample code for Chapter 16 - "Coroutines"
|
||||||
|
|
||||||
|
From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015)
|
||||||
|
http://shop.oreilly.com/product/0636920032519.do
|
||||||
66
19-coroutine/coro_exc_demo.py
Normal file
66
19-coroutine/coro_exc_demo.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
Coroutine closing demonstration::
|
||||||
|
|
||||||
|
# tag::DEMO_CORO_EXC_1[]
|
||||||
|
>>> exc_coro = demo_exc_handling()
|
||||||
|
>>> next(exc_coro)
|
||||||
|
-> coroutine started
|
||||||
|
>>> exc_coro.send(11)
|
||||||
|
-> coroutine received: 11
|
||||||
|
>>> exc_coro.send(22)
|
||||||
|
-> coroutine received: 22
|
||||||
|
>>> exc_coro.close()
|
||||||
|
>>> from inspect import getgeneratorstate
|
||||||
|
>>> getgeneratorstate(exc_coro)
|
||||||
|
'GEN_CLOSED'
|
||||||
|
|
||||||
|
# end::DEMO_CORO_EXC_1[]
|
||||||
|
|
||||||
|
Coroutine handling exception::
|
||||||
|
|
||||||
|
# tag::DEMO_CORO_EXC_2[]
|
||||||
|
>>> exc_coro = demo_exc_handling()
|
||||||
|
>>> next(exc_coro)
|
||||||
|
-> coroutine started
|
||||||
|
>>> exc_coro.send(11)
|
||||||
|
-> coroutine received: 11
|
||||||
|
>>> exc_coro.throw(DemoException)
|
||||||
|
*** DemoException handled. Continuing...
|
||||||
|
>>> getgeneratorstate(exc_coro)
|
||||||
|
'GEN_SUSPENDED'
|
||||||
|
|
||||||
|
# end::DEMO_CORO_EXC_2[]
|
||||||
|
|
||||||
|
Coroutine not handling exception::
|
||||||
|
|
||||||
|
# tag::DEMO_CORO_EXC_3[]
|
||||||
|
>>> exc_coro = demo_exc_handling()
|
||||||
|
>>> next(exc_coro)
|
||||||
|
-> coroutine started
|
||||||
|
>>> exc_coro.send(11)
|
||||||
|
-> coroutine received: 11
|
||||||
|
>>> exc_coro.throw(ZeroDivisionError)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ZeroDivisionError
|
||||||
|
>>> getgeneratorstate(exc_coro)
|
||||||
|
'GEN_CLOSED'
|
||||||
|
|
||||||
|
# end::DEMO_CORO_EXC_3[]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::EX_CORO_EXC[]
|
||||||
|
class DemoException(Exception):
|
||||||
|
"""An exception type for the demonstration."""
|
||||||
|
|
||||||
|
def demo_exc_handling():
|
||||||
|
print('-> coroutine started')
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
x = yield
|
||||||
|
except DemoException: # <1>
|
||||||
|
print('*** DemoException handled. Continuing...')
|
||||||
|
else: # <2>
|
||||||
|
print(f'-> coroutine received: {x!r}')
|
||||||
|
raise RuntimeError('This line should never run.') # <3>
|
||||||
|
# end::EX_CORO_EXC[]
|
||||||
61
19-coroutine/coro_finally_demo.py
Normal file
61
19-coroutine/coro_finally_demo.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Second coroutine closing demonstration::
|
||||||
|
|
||||||
|
>>> fin_coro = demo_finally()
|
||||||
|
>>> next(fin_coro)
|
||||||
|
-> coroutine started
|
||||||
|
>>> fin_coro.send(11)
|
||||||
|
-> coroutine received: 11
|
||||||
|
>>> fin_coro.send(22)
|
||||||
|
-> coroutine received: 22
|
||||||
|
>>> fin_coro.close()
|
||||||
|
-> coroutine ending
|
||||||
|
|
||||||
|
|
||||||
|
Second coroutine not handling exception::
|
||||||
|
|
||||||
|
>>> fin_coro = demo_finally()
|
||||||
|
>>> next(fin_coro)
|
||||||
|
-> coroutine started
|
||||||
|
>>> fin_coro.send(11)
|
||||||
|
-> coroutine received: 11
|
||||||
|
>>> fin_coro.throw(ZeroDivisionError) # doctest: +SKIP
|
||||||
|
-> coroutine ending
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
File "coro_exception_demos.py", line 109, in demo_finally
|
||||||
|
print(f'-> coroutine received: {x!r}')
|
||||||
|
ZeroDivisionError
|
||||||
|
|
||||||
|
|
||||||
|
The last test above must be skipped because the output '-> coroutine ending'
|
||||||
|
is not detected by doctest, which raises a false error. However, if you
|
||||||
|
run this file as shown below, you'll see that output "leak" into standard
|
||||||
|
output::
|
||||||
|
|
||||||
|
|
||||||
|
$ python3 -m doctest coro_exception_demo.py
|
||||||
|
-> coroutine ending
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::EX_CORO_FINALLY[]
|
||||||
|
class DemoException(Exception):
|
||||||
|
"""An exception type for the demonstration."""
|
||||||
|
|
||||||
|
|
||||||
|
def demo_finally():
|
||||||
|
print('-> coroutine started')
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
x = yield
|
||||||
|
except DemoException:
|
||||||
|
print('*** DemoException handled. Continuing...')
|
||||||
|
else:
|
||||||
|
print(f'-> coroutine received: {x!r}')
|
||||||
|
finally:
|
||||||
|
print('-> coroutine ending')
|
||||||
|
|
||||||
|
# end::EX_CORO_FINALLY[]
|
||||||
28
19-coroutine/coroaverager0.py
Normal file
28
19-coroutine/coroaverager0.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
A coroutine to compute a running average
|
||||||
|
|
||||||
|
# tag::CORO_AVERAGER_TEST[]
|
||||||
|
>>> coro_avg = averager() # <1>
|
||||||
|
>>> next(coro_avg) # <2>
|
||||||
|
>>> coro_avg.send(10) # <3>
|
||||||
|
10.0
|
||||||
|
>>> coro_avg.send(30)
|
||||||
|
20.0
|
||||||
|
>>> coro_avg.send(5)
|
||||||
|
15.0
|
||||||
|
|
||||||
|
# end::CORO_AVERAGER_TEST[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::CORO_AVERAGER[]
|
||||||
|
def averager():
|
||||||
|
total = 0.0
|
||||||
|
count = 0
|
||||||
|
average = None
|
||||||
|
while True: # <1>
|
||||||
|
term = yield average # <2>
|
||||||
|
total += term
|
||||||
|
count += 1
|
||||||
|
average = total/count
|
||||||
|
# end::CORO_AVERAGER[]
|
||||||
30
19-coroutine/coroaverager1.py
Normal file
30
19-coroutine/coroaverager1.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# tag::DECORATED_AVERAGER[]
|
||||||
|
"""
|
||||||
|
A coroutine to compute a running average
|
||||||
|
|
||||||
|
>>> coro_avg = averager() # <1>
|
||||||
|
>>> from inspect import getgeneratorstate
|
||||||
|
>>> getgeneratorstate(coro_avg) # <2>
|
||||||
|
'GEN_SUSPENDED'
|
||||||
|
>>> coro_avg.send(10) # <3>
|
||||||
|
10.0
|
||||||
|
>>> coro_avg.send(30)
|
||||||
|
20.0
|
||||||
|
>>> coro_avg.send(5)
|
||||||
|
15.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from coroutil import coroutine # <4>
|
||||||
|
|
||||||
|
@coroutine # <5>
|
||||||
|
def averager(): # <6>
|
||||||
|
total = 0.0
|
||||||
|
count = 0
|
||||||
|
average = None
|
||||||
|
while True:
|
||||||
|
term = yield average
|
||||||
|
total += term
|
||||||
|
count += 1
|
||||||
|
average = total/count
|
||||||
|
# end::DECORATED_AVERAGER[]
|
||||||
61
19-coroutine/coroaverager2.py
Normal file
61
19-coroutine/coroaverager2.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
A coroutine to compute a running average.
|
||||||
|
|
||||||
|
Testing ``averager`` by itself::
|
||||||
|
|
||||||
|
# tag::RETURNING_AVERAGER_DEMO1[]
|
||||||
|
|
||||||
|
>>> coro_avg = averager()
|
||||||
|
>>> next(coro_avg)
|
||||||
|
>>> coro_avg.send(10) # <1>
|
||||||
|
>>> coro_avg.send(30)
|
||||||
|
>>> coro_avg.send(6.5)
|
||||||
|
>>> coro_avg.send(None) # <2>
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
StopIteration: Result(count=3, average=15.5)
|
||||||
|
|
||||||
|
# end::RETURNING_AVERAGER_DEMO1[]
|
||||||
|
|
||||||
|
Catching `StopIteration` to extract the value returned by
|
||||||
|
the coroutine::
|
||||||
|
|
||||||
|
# tag::RETURNING_AVERAGER_DEMO2[]
|
||||||
|
|
||||||
|
>>> coro_avg = averager()
|
||||||
|
>>> next(coro_avg)
|
||||||
|
>>> coro_avg.send(10)
|
||||||
|
>>> coro_avg.send(30)
|
||||||
|
>>> coro_avg.send(6.5)
|
||||||
|
>>> try:
|
||||||
|
... coro_avg.send(None)
|
||||||
|
... except StopIteration as exc:
|
||||||
|
... result = exc.value
|
||||||
|
...
|
||||||
|
>>> result
|
||||||
|
Result(count=3, average=15.5)
|
||||||
|
|
||||||
|
# end::RETURNING_AVERAGER_DEMO2[]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::RETURNING_AVERAGER[]
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
Result = namedtuple('Result', 'count average')
|
||||||
|
|
||||||
|
|
||||||
|
def averager():
|
||||||
|
total = 0.0
|
||||||
|
count = 0
|
||||||
|
average = None
|
||||||
|
while True:
|
||||||
|
term = yield
|
||||||
|
if term is None:
|
||||||
|
break # <1>
|
||||||
|
total += term
|
||||||
|
count += 1
|
||||||
|
average = total/count
|
||||||
|
return Result(count, average) # <2>
|
||||||
|
# end::RETURNING_AVERAGER[]
|
||||||
107
19-coroutine/coroaverager3.py
Normal file
107
19-coroutine/coroaverager3.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
A coroutine to compute a running average.
|
||||||
|
|
||||||
|
Testing ``averager`` by itself::
|
||||||
|
|
||||||
|
>>> coro_avg = averager()
|
||||||
|
>>> next(coro_avg)
|
||||||
|
>>> coro_avg.send(10)
|
||||||
|
>>> coro_avg.send(30)
|
||||||
|
>>> coro_avg.send(6.5)
|
||||||
|
>>> coro_avg.send(None)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
StopIteration: Result(count=3, average=15.5)
|
||||||
|
|
||||||
|
|
||||||
|
Driving it with ``yield from``::
|
||||||
|
|
||||||
|
>>> def summarize(results):
|
||||||
|
... while True:
|
||||||
|
... result = yield from averager()
|
||||||
|
... results.append(result)
|
||||||
|
...
|
||||||
|
>>> results = []
|
||||||
|
>>> summary = summarize(results)
|
||||||
|
>>> next(summary)
|
||||||
|
>>> for height in data['girls;m']:
|
||||||
|
... summary.send(height)
|
||||||
|
...
|
||||||
|
>>> summary.send(None)
|
||||||
|
>>> for height in data['boys;m']:
|
||||||
|
... summary.send(height)
|
||||||
|
...
|
||||||
|
>>> summary.send(None)
|
||||||
|
>>> results == [
|
||||||
|
... Result(count=10, average=1.4279999999999997),
|
||||||
|
... Result(count=9, average=1.3888888888888888)
|
||||||
|
... ]
|
||||||
|
True
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::YIELD_FROM_AVERAGER[]
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
Result = namedtuple('Result', 'count average')
|
||||||
|
|
||||||
|
|
||||||
|
# the subgenerator
|
||||||
|
def averager(): # <1>
|
||||||
|
total = 0.0
|
||||||
|
count = 0
|
||||||
|
average = None
|
||||||
|
while True:
|
||||||
|
term = yield # <2>
|
||||||
|
if term is None: # <3>
|
||||||
|
break
|
||||||
|
total += term
|
||||||
|
count += 1
|
||||||
|
average = total/count
|
||||||
|
return Result(count, average) # <4>
|
||||||
|
|
||||||
|
|
||||||
|
# the delegating generator
|
||||||
|
def grouper(results, key): # <5>
|
||||||
|
while True: # <6>
|
||||||
|
results[key] = yield from averager() # <7>
|
||||||
|
|
||||||
|
|
||||||
|
# the client code, a.k.a. the caller
|
||||||
|
def main(data): # <8>
|
||||||
|
results = {}
|
||||||
|
for key, values in data.items():
|
||||||
|
group = grouper(results, key) # <9>
|
||||||
|
next(group) # <10>
|
||||||
|
for value in values:
|
||||||
|
group.send(value) # <11>
|
||||||
|
group.send(None) # important! <12>
|
||||||
|
|
||||||
|
# print(results) # uncomment to debug
|
||||||
|
report(results)
|
||||||
|
|
||||||
|
|
||||||
|
# output report
|
||||||
|
def report(results):
|
||||||
|
for key, result in sorted(results.items()):
|
||||||
|
group, unit = key.split(';')
|
||||||
|
print(f'{result.count:2} {group:5}',
|
||||||
|
f'averaging {result.average:.2f}{unit}')
|
||||||
|
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'girls;kg':
|
||||||
|
[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
|
||||||
|
'girls;m':
|
||||||
|
[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
|
||||||
|
'boys;kg':
|
||||||
|
[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
|
||||||
|
'boys;m':
|
||||||
|
[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(data)
|
||||||
|
|
||||||
|
# end::YIELD_FROM_AVERAGER[]
|
||||||
12
19-coroutine/coroutil.py
Normal file
12
19-coroutine/coroutil.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# tag::CORO_DECO[]
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
def coroutine(func):
|
||||||
|
"""Decorator: primes `func` by advancing to first `yield`"""
|
||||||
|
@wraps(func)
|
||||||
|
def primer(*args,**kwargs): # <1>
|
||||||
|
gen = func(*args,**kwargs) # <2>
|
||||||
|
next(gen) # <3>
|
||||||
|
return gen # <4>
|
||||||
|
return primer
|
||||||
|
# end::CORO_DECO[]
|
||||||
203
19-coroutine/taxi_sim.py
Normal file
203
19-coroutine/taxi_sim.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
Taxi simulator
|
||||||
|
==============
|
||||||
|
|
||||||
|
Driving a taxi from the console::
|
||||||
|
|
||||||
|
>>> from taxi_sim import taxi_process
|
||||||
|
>>> taxi = taxi_process(ident=13, trips=2, start_time=0)
|
||||||
|
>>> next(taxi)
|
||||||
|
Event(time=0, proc=13, action='leave garage')
|
||||||
|
>>> taxi.send(_.time + 7)
|
||||||
|
Event(time=7, proc=13, action='pick up passenger')
|
||||||
|
>>> taxi.send(_.time + 23)
|
||||||
|
Event(time=30, proc=13, action='drop off passenger')
|
||||||
|
>>> taxi.send(_.time + 5)
|
||||||
|
Event(time=35, proc=13, action='pick up passenger')
|
||||||
|
>>> taxi.send(_.time + 48)
|
||||||
|
Event(time=83, proc=13, action='drop off passenger')
|
||||||
|
>>> taxi.send(_.time + 1)
|
||||||
|
Event(time=84, proc=13, action='going home')
|
||||||
|
>>> taxi.send(_.time + 10)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
StopIteration
|
||||||
|
|
||||||
|
Sample run with two cars, random seed 10. This is a valid doctest::
|
||||||
|
|
||||||
|
>>> main(num_taxis=2, seed=10)
|
||||||
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
||||||
|
taxi: 0 Event(time=5, proc=0, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
||||||
|
taxi: 1 Event(time=10, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=15, proc=1, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=17, proc=0, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=24, proc=1, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=26, proc=0, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=30, proc=0, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=34, proc=0, action='going home')
|
||||||
|
taxi: 1 Event(time=46, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=48, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=110, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=139, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=140, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=150, proc=1, action='going home')
|
||||||
|
*** end of events ***
|
||||||
|
|
||||||
|
See longer sample run at the end of this module.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import collections
|
||||||
|
import queue
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
DEFAULT_NUMBER_OF_TAXIS = 3
|
||||||
|
DEFAULT_END_TIME = 180
|
||||||
|
SEARCH_DURATION = 5
|
||||||
|
TRIP_DURATION = 20
|
||||||
|
DEPARTURE_INTERVAL = 5
|
||||||
|
|
||||||
|
Event = collections.namedtuple('Event', 'time proc action')
|
||||||
|
|
||||||
|
|
||||||
|
# tag::TAXI_PROCESS[]
|
||||||
|
def taxi_process(ident, trips, start_time=0): # <1>
|
||||||
|
"""Yield to simulator issuing event at each state change"""
|
||||||
|
time = yield Event(start_time, ident, 'leave garage') # <2>
|
||||||
|
for i in range(trips): # <3>
|
||||||
|
time = yield Event(time, ident, 'pick up passenger') # <4>
|
||||||
|
time = yield Event(time, ident, 'drop off passenger') # <5>
|
||||||
|
|
||||||
|
yield Event(time, ident, 'going home') # <6>
|
||||||
|
# end of taxi process # <7>
|
||||||
|
# end::TAXI_PROCESS[]
|
||||||
|
|
||||||
|
|
||||||
|
# tag::TAXI_SIMULATOR[]
|
||||||
|
class Simulator:
|
||||||
|
|
||||||
|
def __init__(self, procs_map):
|
||||||
|
self.events = queue.PriorityQueue()
|
||||||
|
self.procs = dict(procs_map)
|
||||||
|
|
||||||
|
def run(self, end_time): # <1>
|
||||||
|
"""Schedule and display events until time is up"""
|
||||||
|
# schedule the first event for each cab
|
||||||
|
for _, proc in sorted(self.procs.items()): # <2>
|
||||||
|
first_event = next(proc) # <3>
|
||||||
|
self.events.put(first_event) # <4>
|
||||||
|
|
||||||
|
# main loop of the simulation
|
||||||
|
sim_time = 0 # <5>
|
||||||
|
while sim_time < end_time: # <6>
|
||||||
|
if self.events.empty(): # <7>
|
||||||
|
print('*** end of events ***')
|
||||||
|
break
|
||||||
|
|
||||||
|
current_event = self.events.get() # <8>
|
||||||
|
sim_time, proc_id, previous_action = current_event # <9>
|
||||||
|
print('taxi:', proc_id, proc_id * ' ', current_event) # <10>
|
||||||
|
active_proc = self.procs[proc_id] # <11>
|
||||||
|
next_time = sim_time + compute_duration(previous_action) # <12>
|
||||||
|
try:
|
||||||
|
next_event = active_proc.send(next_time) # <13>
|
||||||
|
except StopIteration:
|
||||||
|
del self.procs[proc_id] # <14>
|
||||||
|
else:
|
||||||
|
self.events.put(next_event) # <15>
|
||||||
|
else: # <16>
|
||||||
|
msg = '*** end of simulation time: {} events pending ***'
|
||||||
|
print(msg.format(self.events.qsize()))
|
||||||
|
# end::TAXI_SIMULATOR[]
|
||||||
|
|
||||||
|
|
||||||
|
def compute_duration(previous_action):
|
||||||
|
"""Compute action duration using exponential distribution"""
|
||||||
|
if previous_action in ['leave garage', 'drop off passenger']:
|
||||||
|
# new state is prowling
|
||||||
|
interval = SEARCH_DURATION
|
||||||
|
elif previous_action == 'pick up passenger':
|
||||||
|
# new state is trip
|
||||||
|
interval = TRIP_DURATION
|
||||||
|
elif previous_action == 'going home':
|
||||||
|
interval = 1
|
||||||
|
else:
|
||||||
|
raise ValueError('Unknown previous_action: %s' % previous_action)
|
||||||
|
return int(random.expovariate(1/interval)) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
||||||
|
seed=None):
|
||||||
|
"""Initialize random generator, build procs and run simulation"""
|
||||||
|
if seed is not None:
|
||||||
|
random.seed(seed) # get reproducible results
|
||||||
|
|
||||||
|
taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
|
||||||
|
for i in range(num_taxis)}
|
||||||
|
sim = Simulator(taxis)
|
||||||
|
sim.run(end_time)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Taxi fleet simulator.')
|
||||||
|
parser.add_argument('-e', '--end-time', type=int,
|
||||||
|
default=DEFAULT_END_TIME,
|
||||||
|
help='simulation end time; default = %s'
|
||||||
|
% DEFAULT_END_TIME)
|
||||||
|
parser.add_argument('-t', '--taxis', type=int,
|
||||||
|
default=DEFAULT_NUMBER_OF_TAXIS,
|
||||||
|
help='number of taxis running; default = %s'
|
||||||
|
% DEFAULT_NUMBER_OF_TAXIS)
|
||||||
|
parser.add_argument('-s', '--seed', type=int, default=None,
|
||||||
|
help='random generator seed (for testing)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
main(args.end_time, args.taxis, args.seed)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
Sample run from the command line, seed=3, maximum elapsed time=120::
|
||||||
|
|
||||||
|
# tag::TAXI_SAMPLE_RUN[]
|
||||||
|
$ python3 taxi_sim.py -s 3 -e 120
|
||||||
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
||||||
|
taxi: 0 Event(time=2, proc=0, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
||||||
|
taxi: 1 Event(time=8, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=10, proc=2, action='leave garage')
|
||||||
|
taxi: 2 Event(time=15, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=17, proc=2, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=18, proc=0, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=18, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=25, proc=2, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=27, proc=1, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=27, proc=2, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=28, proc=0, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=40, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=44, proc=2, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=55, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=59, proc=1, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=65, proc=0, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=65, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=65, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=72, proc=2, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=76, proc=0, action='going home')
|
||||||
|
taxi: 1 Event(time=80, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=88, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=95, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=97, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=98, proc=2, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=106, proc=1, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=109, proc=2, action='going home')
|
||||||
|
taxi: 1 Event(time=110, proc=1, action='going home')
|
||||||
|
*** end of events ***
|
||||||
|
# end::TAXI_SAMPLE_RUN[]
|
||||||
|
|
||||||
|
"""
|
||||||
257
19-coroutine/taxi_sim0.py
Normal file
257
19-coroutine/taxi_sim0.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
Taxi simulator
|
||||||
|
|
||||||
|
Sample run with two cars, random seed 10. This is a valid doctest.
|
||||||
|
|
||||||
|
>>> main(num_taxis=2, seed=10)
|
||||||
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
||||||
|
taxi: 0 Event(time=4, proc=0, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
||||||
|
taxi: 1 Event(time=9, proc=1, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=10, proc=0, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=12, proc=1, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=17, proc=0, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=19, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=21, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=24, proc=1, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=28, proc=0, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=28, proc=1, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=29, proc=0, action='going home')
|
||||||
|
taxi: 1 Event(time=30, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=61, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=62, proc=1, action='going home')
|
||||||
|
*** end of events ***
|
||||||
|
|
||||||
|
See explanation and longer sample run at the end of this module.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
import collections
|
||||||
|
import queue
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
DEFAULT_NUMBER_OF_TAXIS = 3
|
||||||
|
DEFAULT_END_TIME = 80
|
||||||
|
SEARCH_DURATION = 4
|
||||||
|
TRIP_DURATION = 10
|
||||||
|
DEPARTURE_INTERVAL = 5
|
||||||
|
|
||||||
|
Event = collections.namedtuple('Event', 'time proc action')
|
||||||
|
|
||||||
|
|
||||||
|
def compute_delay(interval):
|
||||||
|
"""Compute action delay using exponential distribution"""
|
||||||
|
return int(random.expovariate(1/interval)) + 1
|
||||||
|
|
||||||
|
# BEGIN TAXI_PROCESS
|
||||||
|
def taxi_process(ident, trips, start_time=0): # <1>
|
||||||
|
"""Yield to simulator issuing event at each state change"""
|
||||||
|
time = yield Event(start_time, ident, 'leave garage') # <2>
|
||||||
|
for i in range(trips): # <3>
|
||||||
|
prowling_ends = time + compute_delay(SEARCH_DURATION) # <4>
|
||||||
|
time = yield Event(prowling_ends, ident, 'pick up passenger') # <5>
|
||||||
|
|
||||||
|
trip_ends = time + compute_delay(TRIP_DURATION) # <6>
|
||||||
|
time = yield Event(trip_ends, ident, 'drop off passenger') # <7>
|
||||||
|
|
||||||
|
yield Event(time + 1, ident, 'going home') # <8>
|
||||||
|
# end of taxi process # <9>
|
||||||
|
# END TAXI_PROCESS
|
||||||
|
|
||||||
|
# BEGIN TAXI_SIMULATOR
|
||||||
|
class Simulator:
|
||||||
|
|
||||||
|
def __init__(self, procs_map):
|
||||||
|
self.events = queue.PriorityQueue()
|
||||||
|
self.procs = dict(procs_map)
|
||||||
|
|
||||||
|
|
||||||
|
def run(self, end_time): # <1>
|
||||||
|
"""Schedule and display events until time is up"""
|
||||||
|
# schedule the first event for each cab
|
||||||
|
for _, proc in sorted(self.procs.items()): # <2>
|
||||||
|
first_event = next(proc) # <3>
|
||||||
|
self.events.put(first_event) # <4>
|
||||||
|
|
||||||
|
# main loop of the simulation
|
||||||
|
time = 0
|
||||||
|
while time < end_time: # <5>
|
||||||
|
if self.events.empty(): # <6>
|
||||||
|
print('*** end of events ***')
|
||||||
|
break
|
||||||
|
|
||||||
|
# get and display current event
|
||||||
|
current_event = self.events.get() # <7>
|
||||||
|
print('taxi:', current_event.proc, # <8>
|
||||||
|
current_event.proc * ' ', current_event)
|
||||||
|
|
||||||
|
# schedule next action for current proc
|
||||||
|
time = current_event.time # <9>
|
||||||
|
proc = self.procs[current_event.proc] # <10>
|
||||||
|
try:
|
||||||
|
next_event = proc.send(time) # <11>
|
||||||
|
except StopIteration:
|
||||||
|
del self.procs[current_event.proc] # <12>
|
||||||
|
else:
|
||||||
|
self.events.put(next_event) # <13>
|
||||||
|
else: # <14>
|
||||||
|
msg = '*** end of simulation time: {} events pending ***'
|
||||||
|
print(msg.format(self.events.qsize()))
|
||||||
|
# END TAXI_SIMULATOR
|
||||||
|
|
||||||
|
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
||||||
|
seed=None):
|
||||||
|
"""Initialize random generator, build procs and run simulation"""
|
||||||
|
if seed is not None:
|
||||||
|
random.seed(seed) # get reproducible results
|
||||||
|
|
||||||
|
taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
|
||||||
|
for i in range(num_taxis)}
|
||||||
|
sim = Simulator(taxis)
|
||||||
|
sim.run(end_time)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Taxi fleet simulator.')
|
||||||
|
parser.add_argument('-e', '--end-time', type=int,
|
||||||
|
default=DEFAULT_END_TIME,
|
||||||
|
help='simulation end time; default = %s'
|
||||||
|
% DEFAULT_END_TIME)
|
||||||
|
parser.add_argument('-t', '--taxis', type=int,
|
||||||
|
default=DEFAULT_NUMBER_OF_TAXIS,
|
||||||
|
help='number of taxis running; default = %s'
|
||||||
|
% DEFAULT_NUMBER_OF_TAXIS)
|
||||||
|
parser.add_argument('-s', '--seed', type=int, default=None,
|
||||||
|
help='random generator seed (for testing)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
main(args.end_time, args.taxis, args.seed)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Notes for the ``taxi_process`` coroutine::
|
||||||
|
|
||||||
|
<1> `taxi_process` will be called once per taxi, creating a generator
|
||||||
|
object to represent its operations. `ident` is the number of the taxi
|
||||||
|
(eg. 0, 1, 2 in the sample run); `trips` is the number of trips this
|
||||||
|
taxi will make before going home; `start_time` is when the taxi
|
||||||
|
leaves the garage.
|
||||||
|
|
||||||
|
<2> The first `Event` yielded is `'leave garage'`. This suspends the
|
||||||
|
coroutine, and lets the simulation main loop proceed to the next
|
||||||
|
scheduled event. When it's time to reactivate this process, the main
|
||||||
|
loop will `send` the current simulation time, which is assigned to
|
||||||
|
`time`.
|
||||||
|
|
||||||
|
<3> This block will be repeated once for each trip.
|
||||||
|
|
||||||
|
<4> The ending time of the search for a passenger is computed.
|
||||||
|
|
||||||
|
<5> An `Event` signaling passenger pick up is yielded. The coroutine
|
||||||
|
pauses here. When the time comes to reactivate this coroutine,
|
||||||
|
the main loop will again `send` the current time.
|
||||||
|
|
||||||
|
<6> The ending time of the trip is computed, taking into account the
|
||||||
|
current `time`.
|
||||||
|
|
||||||
|
<7> An `Event` signaling passenger drop off is yielded. Coroutine
|
||||||
|
suspended again, waiting for the main loop to send the time of when
|
||||||
|
it's time to continue.
|
||||||
|
|
||||||
|
<8> The `for` loop ends after the given number of trips, and a final
|
||||||
|
`'going home'` event is yielded, to happen 1 minute after the current
|
||||||
|
time. The coroutine will suspend for the last time. When reactivated,
|
||||||
|
it will be sent the time from the simulation main loop, but here I
|
||||||
|
don't assign it to any variable because it will not be useful.
|
||||||
|
|
||||||
|
<9> When the coroutine falls off the end, the coroutine object raises
|
||||||
|
`StopIteration`.
|
||||||
|
|
||||||
|
|
||||||
|
Notes for the ``Simulator.run`` method::
|
||||||
|
|
||||||
|
<1> The simulation `end_time` is the only required argument for `run`.
|
||||||
|
|
||||||
|
<2> Use `sorted` to retrieve the `self.procs` items ordered by the
|
||||||
|
integer key; we don't care about the key, so assign it to `_`.
|
||||||
|
|
||||||
|
<3> `next(proc)` primes each coroutine by advancing it to the first
|
||||||
|
yield, so it's ready to be sent data. An `Event` is yielded.
|
||||||
|
|
||||||
|
<4> Add each event to the `self.events` `PriorityQueue`. The first
|
||||||
|
event for each taxi is `'leave garage'`, as seen in the sample run
|
||||||
|
(ex_taxi_process>>).
|
||||||
|
|
||||||
|
<5> Main loop of the simulation: run until the current `time` equals
|
||||||
|
or exceeds the `end_time`.
|
||||||
|
|
||||||
|
<6> The main loop may also exit if there are no pending events in the
|
||||||
|
queue.
|
||||||
|
|
||||||
|
<7> Get `Event` with the smallest `time` in the queue; this is the
|
||||||
|
`current_event`.
|
||||||
|
|
||||||
|
<8> Display the `Event`, identifying the taxi and adding indentation
|
||||||
|
according to the taxi id.
|
||||||
|
|
||||||
|
<9> Update the simulation time with the time of the `current_event`.
|
||||||
|
|
||||||
|
<10> Retrieve the coroutine for this taxi from the `self.procs`
|
||||||
|
dictionary.
|
||||||
|
|
||||||
|
<11> Send the `time` to the coroutine. The coroutine will yield the
|
||||||
|
`next_event` or raise `StopIteration` it's finished.
|
||||||
|
|
||||||
|
<12> If `StopIteration` was raised, delete the coroutine from the
|
||||||
|
`self.procs` dictionary.
|
||||||
|
|
||||||
|
<13> Otherwise, put the `next_event` in the queue.
|
||||||
|
|
||||||
|
<14> If the loop exits because the simulation time passed, display the
|
||||||
|
number of events pending (which may be zero by coincidence,
|
||||||
|
sometimes).
|
||||||
|
|
||||||
|
|
||||||
|
Sample run from the command line, seed=24, total elapsed time=160::
|
||||||
|
|
||||||
|
# BEGIN TAXI_SAMPLE_RUN
|
||||||
|
$ python3 taxi_sim.py -s 24 -e 160
|
||||||
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
||||||
|
taxi: 0 Event(time=5, proc=0, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
||||||
|
taxi: 1 Event(time=6, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=10, proc=2, action='leave garage')
|
||||||
|
taxi: 2 Event(time=11, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=23, proc=2, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=24, proc=0, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=24, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=26, proc=2, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=30, proc=0, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=31, proc=2, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=43, proc=0, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=44, proc=0, action='going home')
|
||||||
|
taxi: 2 Event(time=46, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=49, proc=2, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=70, proc=1, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=70, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=71, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=79, proc=2, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=88, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=92, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=98, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=99, proc=2, action='going home')
|
||||||
|
taxi: 1 Event(time=102, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=104, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=135, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=136, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=151, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=152, proc=1, action='going home')
|
||||||
|
*** end of events ***
|
||||||
|
# END TAXI_SAMPLE_RUN
|
||||||
|
|
||||||
|
"""
|
||||||
215
19-coroutine/taxi_sim_delay.py
Normal file
215
19-coroutine/taxi_sim_delay.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
Taxi simulator with delay on output
|
||||||
|
===================================
|
||||||
|
|
||||||
|
This is a variation of ``taxi_sim.py`` which adds a ``-d`` comand-line
|
||||||
|
option. When given, that option adds a delay in the main loop, pausing
|
||||||
|
the simulation for .5s for each "minute" of simulation time.
|
||||||
|
|
||||||
|
|
||||||
|
Driving a taxi from the console::
|
||||||
|
|
||||||
|
>>> from taxi_sim import taxi_process
|
||||||
|
>>> taxi = taxi_process(ident=13, trips=2, start_time=0)
|
||||||
|
>>> next(taxi)
|
||||||
|
Event(time=0, proc=13, action='leave garage')
|
||||||
|
>>> taxi.send(_.time + 7)
|
||||||
|
Event(time=7, proc=13, action='pick up passenger')
|
||||||
|
>>> taxi.send(_.time + 23)
|
||||||
|
Event(time=30, proc=13, action='drop off passenger')
|
||||||
|
>>> taxi.send(_.time + 5)
|
||||||
|
Event(time=35, proc=13, action='pick up passenger')
|
||||||
|
>>> taxi.send(_.time + 48)
|
||||||
|
Event(time=83, proc=13, action='drop off passenger')
|
||||||
|
>>> taxi.send(_.time + 1)
|
||||||
|
Event(time=84, proc=13, action='going home')
|
||||||
|
>>> taxi.send(_.time + 10)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
StopIteration
|
||||||
|
|
||||||
|
Sample run with two cars, random seed 10. This is a valid doctest::
|
||||||
|
|
||||||
|
>>> main(num_taxis=2, seed=10)
|
||||||
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
||||||
|
taxi: 0 Event(time=5, proc=0, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
||||||
|
taxi: 1 Event(time=10, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=15, proc=1, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=17, proc=0, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=24, proc=1, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=26, proc=0, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=30, proc=0, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=34, proc=0, action='going home')
|
||||||
|
taxi: 1 Event(time=46, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=48, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=110, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=139, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=140, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=150, proc=1, action='going home')
|
||||||
|
*** end of events ***
|
||||||
|
|
||||||
|
See longer sample run at the end of this module.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import collections
|
||||||
|
import queue
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
DEFAULT_NUMBER_OF_TAXIS = 3
|
||||||
|
DEFAULT_END_TIME = 180
|
||||||
|
SEARCH_DURATION = 5
|
||||||
|
TRIP_DURATION = 20
|
||||||
|
DEPARTURE_INTERVAL = 5
|
||||||
|
|
||||||
|
Event = collections.namedtuple('Event', 'time proc action')
|
||||||
|
|
||||||
|
|
||||||
|
# BEGIN TAXI_PROCESS
|
||||||
|
def taxi_process(ident, trips, start_time=0): # <1>
|
||||||
|
"""Yield to simulator issuing event at each state change"""
|
||||||
|
time = yield Event(start_time, ident, 'leave garage') # <2>
|
||||||
|
for i in range(trips): # <3>
|
||||||
|
time = yield Event(time, ident, 'pick up passenger') # <4>
|
||||||
|
time = yield Event(time, ident, 'drop off passenger') # <5>
|
||||||
|
|
||||||
|
yield Event(time, ident, 'going home') # <6>
|
||||||
|
# end of taxi process # <7>
|
||||||
|
# END TAXI_PROCESS
|
||||||
|
|
||||||
|
|
||||||
|
# BEGIN TAXI_SIMULATOR
|
||||||
|
class Simulator:
|
||||||
|
|
||||||
|
def __init__(self, procs_map):
|
||||||
|
self.events = queue.PriorityQueue()
|
||||||
|
self.procs = dict(procs_map)
|
||||||
|
|
||||||
|
def run(self, end_time, delay=False): # <1>
|
||||||
|
"""Schedule and display events until time is up"""
|
||||||
|
# schedule the first event for each cab
|
||||||
|
for _, proc in sorted(self.procs.items()): # <2>
|
||||||
|
first_event = next(proc) # <3>
|
||||||
|
self.events.put(first_event) # <4>
|
||||||
|
|
||||||
|
# main loop of the simulation
|
||||||
|
sim_time = 0 # <5>
|
||||||
|
while sim_time < end_time: # <6>
|
||||||
|
if self.events.empty(): # <7>
|
||||||
|
print('*** end of events ***')
|
||||||
|
break
|
||||||
|
|
||||||
|
# get and display current event
|
||||||
|
current_event = self.events.get() # <8>
|
||||||
|
if delay:
|
||||||
|
time.sleep((current_event.time - sim_time) / 2)
|
||||||
|
# update the simulation time
|
||||||
|
sim_time, proc_id, previous_action = current_event
|
||||||
|
print('taxi:', proc_id, proc_id * ' ', current_event)
|
||||||
|
active_proc = self.procs[proc_id]
|
||||||
|
# schedule next action for current proc
|
||||||
|
next_time = sim_time + compute_duration(previous_action)
|
||||||
|
try:
|
||||||
|
next_event = active_proc.send(next_time) # <12>
|
||||||
|
except StopIteration:
|
||||||
|
del self.procs[proc_id] # <13>
|
||||||
|
else:
|
||||||
|
self.events.put(next_event) # <14>
|
||||||
|
else: # <15>
|
||||||
|
msg = '*** end of simulation time: {} events pending ***'
|
||||||
|
print(msg.format(self.events.qsize()))
|
||||||
|
# END TAXI_SIMULATOR
|
||||||
|
|
||||||
|
|
||||||
|
def compute_duration(previous_action):
|
||||||
|
"""Compute action duration using exponential distribution"""
|
||||||
|
if previous_action in ['leave garage', 'drop off passenger']:
|
||||||
|
# new state is prowling
|
||||||
|
interval = SEARCH_DURATION
|
||||||
|
elif previous_action == 'pick up passenger':
|
||||||
|
# new state is trip
|
||||||
|
interval = TRIP_DURATION
|
||||||
|
elif previous_action == 'going home':
|
||||||
|
interval = 1
|
||||||
|
else:
|
||||||
|
raise ValueError('Unknown previous_action: %s' % previous_action)
|
||||||
|
return int(random.expovariate(1/interval)) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
||||||
|
seed=None, delay=False):
|
||||||
|
"""Initialize random generator, build procs and run simulation"""
|
||||||
|
if seed is not None:
|
||||||
|
random.seed(seed) # get reproducible results
|
||||||
|
|
||||||
|
taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
|
||||||
|
for i in range(num_taxis)}
|
||||||
|
sim = Simulator(taxis)
|
||||||
|
sim.run(end_time, delay)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Taxi fleet simulator.')
|
||||||
|
parser.add_argument('-e', '--end-time', type=int,
|
||||||
|
default=DEFAULT_END_TIME,
|
||||||
|
help='simulation end time; default = %s'
|
||||||
|
% DEFAULT_END_TIME)
|
||||||
|
parser.add_argument('-t', '--taxis', type=int,
|
||||||
|
default=DEFAULT_NUMBER_OF_TAXIS,
|
||||||
|
help='number of taxis running; default = %s'
|
||||||
|
% DEFAULT_NUMBER_OF_TAXIS)
|
||||||
|
parser.add_argument('-s', '--seed', type=int, default=None,
|
||||||
|
help='random generator seed (for testing)')
|
||||||
|
parser.add_argument('-d', '--delay', action='store_true',
|
||||||
|
help='introduce delay proportional to simulation time')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
main(args.end_time, args.taxis, args.seed, args.delay)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
Sample run from the command line, seed=3, maximum elapsed time=120::
|
||||||
|
|
||||||
|
# BEGIN TAXI_SAMPLE_RUN
|
||||||
|
$ python3 taxi_sim.py -s 3 -e 120
|
||||||
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
||||||
|
taxi: 0 Event(time=2, proc=0, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
||||||
|
taxi: 1 Event(time=8, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=10, proc=2, action='leave garage')
|
||||||
|
taxi: 2 Event(time=15, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=17, proc=2, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=18, proc=0, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=18, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=25, proc=2, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=27, proc=1, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=27, proc=2, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=28, proc=0, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=40, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=44, proc=2, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=55, proc=1, action='pick up passenger')
|
||||||
|
taxi: 1 Event(time=59, proc=1, action='drop off passenger')
|
||||||
|
taxi: 0 Event(time=65, proc=0, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=65, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=65, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=72, proc=2, action='pick up passenger')
|
||||||
|
taxi: 0 Event(time=76, proc=0, action='going home')
|
||||||
|
taxi: 1 Event(time=80, proc=1, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=88, proc=1, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=95, proc=2, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=97, proc=2, action='pick up passenger')
|
||||||
|
taxi: 2 Event(time=98, proc=2, action='drop off passenger')
|
||||||
|
taxi: 1 Event(time=106, proc=1, action='drop off passenger')
|
||||||
|
taxi: 2 Event(time=109, proc=2, action='going home')
|
||||||
|
taxi: 1 Event(time=110, proc=1, action='going home')
|
||||||
|
*** end of events ***
|
||||||
|
# END TAXI_SAMPLE_RUN
|
||||||
|
|
||||||
|
"""
|
||||||
52
19-coroutine/yield_from_expansion.py
Normal file
52
19-coroutine/yield_from_expansion.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Code below is the expansion of the statement:
|
||||||
|
#
|
||||||
|
# RESULT = yield from EXPR
|
||||||
|
#
|
||||||
|
# Copied verbatim from the Formal Semantics section of
|
||||||
|
# PEP 380 -- Syntax for Delegating to a Subgenerator
|
||||||
|
#
|
||||||
|
# https://www.python.org/dev/peps/pep-0380/#formal-semantics
|
||||||
|
|
||||||
|
|
||||||
|
# tag::YIELD_FROM_EXPANSION[]
|
||||||
|
_i = iter(EXPR) # <1>
|
||||||
|
try:
|
||||||
|
_y = next(_i) # <2>
|
||||||
|
except StopIteration as _e:
|
||||||
|
_r = _e.value # <3>
|
||||||
|
else:
|
||||||
|
while 1: # <4>
|
||||||
|
try:
|
||||||
|
_s = yield _y # <5>
|
||||||
|
except GeneratorExit as _e: # <6>
|
||||||
|
try:
|
||||||
|
_m = _i.close
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
_m()
|
||||||
|
raise _e
|
||||||
|
except BaseException as _e: # <7>
|
||||||
|
_x = sys.exc_info()
|
||||||
|
try:
|
||||||
|
_m = _i.throw
|
||||||
|
except AttributeError:
|
||||||
|
raise _e
|
||||||
|
else: # <8>
|
||||||
|
try:
|
||||||
|
_y = _m(*_x)
|
||||||
|
except StopIteration as _e:
|
||||||
|
_r = _e.value
|
||||||
|
break
|
||||||
|
else: # <9>
|
||||||
|
try: # <10>
|
||||||
|
if _s is None: # <11>
|
||||||
|
_y = next(_i)
|
||||||
|
else:
|
||||||
|
_y = _i.send(_s)
|
||||||
|
except StopIteration as _e: # <12>
|
||||||
|
_r = _e.value
|
||||||
|
break
|
||||||
|
|
||||||
|
RESULT = _r # <13>
|
||||||
|
# end::YIELD_FROM_EXPANSION[]
|
||||||
32
19-coroutine/yield_from_expansion_simplified.py
Normal file
32
19-coroutine/yield_from_expansion_simplified.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Code below is a very simplified expansion of the statement:
|
||||||
|
#
|
||||||
|
# RESULT = yield from EXPR
|
||||||
|
#
|
||||||
|
# This code assumes that the subgenerator will run to completion,
|
||||||
|
# without the client ever calling ``.throw()`` or ``.close()``.
|
||||||
|
# Also, this code makes no distinction between the client
|
||||||
|
# calling ``next(subgen)`` or ``subgen.send(...)``
|
||||||
|
#
|
||||||
|
# The full expansion is in:
|
||||||
|
# PEP 380 -- Syntax for Delegating to a Subgenerator
|
||||||
|
#
|
||||||
|
# https://www.python.org/dev/peps/pep-0380/#formal-semantics
|
||||||
|
|
||||||
|
|
||||||
|
# tag::YIELD_FROM_EXPANSION_SIMPLIFIED[]
|
||||||
|
_i = iter(EXPR) # <1>
|
||||||
|
try:
|
||||||
|
_y = next(_i) # <2>
|
||||||
|
except StopIteration as _e:
|
||||||
|
_r = _e.value # <3>
|
||||||
|
else:
|
||||||
|
while 1: # <4>
|
||||||
|
_s = yield _y # <5>
|
||||||
|
try:
|
||||||
|
_y = _i.send(_s) # <6>
|
||||||
|
except StopIteration as _e: # <7>
|
||||||
|
_r = _e.value
|
||||||
|
break
|
||||||
|
|
||||||
|
RESULT = _r # <8>
|
||||||
|
# end::YIELD_FROM_EXPANSION_SIMPLIFIED[]
|
||||||
@@ -24,7 +24,7 @@ async def check(n: int) -> int:
|
|||||||
|
|
||||||
async def supervisor(n: int) -> int:
|
async def supervisor(n: int) -> int:
|
||||||
spinner = asyncio.create_task(spin('thinking!')) # <1>
|
spinner = asyncio.create_task(spin('thinking!')) # <1>
|
||||||
print(f'spinner object: {spinner}') # <2>
|
print('spinner object:', spinner) # <2>
|
||||||
result = await check(n) # <3>
|
result = await check(n) # <3>
|
||||||
spinner.cancel() # <5>
|
spinner.cancel() # <5>
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ async def check(n: int) -> int:
|
|||||||
|
|
||||||
async def supervisor(n: int) -> int:
|
async def supervisor(n: int) -> int:
|
||||||
spinner = asyncio.create_task(spin('thinking!')) # <1>
|
spinner = asyncio.create_task(spin('thinking!')) # <1>
|
||||||
print(f'spinner object: {spinner}') # <2>
|
print('spinner object:', spinner) # <2>
|
||||||
result = await check(n) # <3>
|
result = await check(n) # <3>
|
||||||
spinner.cancel() # <5>
|
spinner.cancel() # <5>
|
||||||
return result
|
return result
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user