ch11-24: clean up by @eumiro & sync with Atlas
This commit is contained in:
parent
03ace4f4ae
commit
47cafc801a
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:
|
||||
spinner = asyncio.create_task(spin('thinking!')) # <1>
|
||||
print(f'spinner object: {spinner}') # <2>
|
||||
print('spinner object:', spinner) # <2>
|
||||
result = await check(n) # <3>
|
||||
spinner.cancel() # <5>
|
||||
return result
|
||||
|
@ -43,7 +43,7 @@ async def check(n: int) -> int:
|
||||
|
||||
async def supervisor(n: int) -> int:
|
||||
spinner = asyncio.create_task(spin('thinking!')) # <1>
|
||||
print(f'spinner object: {spinner}') # <2>
|
||||
print('spinner object:', spinner) # <2>
|
||||
result = await check(n) # <3>
|
||||
spinner.cancel() # <5>
|
||||
return result
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user