ch11-24: clean up by @eumiro & sync with Atlas

This commit is contained in:
Luciano Ramalho 2021-02-14 20:58:46 -03:00
parent 03ace4f4ae
commit 47cafc801a
143 changed files with 21692 additions and 63 deletions

2
11-pythonic-obj/private/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.class
.jython_cache/

View 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
View 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[]

View File

@ -0,0 +1,2 @@
def double(x: object) -> object:
return x * 2

View 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

View File

@ -0,0 +1,6 @@
from collections import abc
from typing import Any
def double(x: abc.Sequence) -> Any:
return x * 2

View 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
View 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()

View 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
View 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[]

View 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[]

View 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[]

View 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)}

View 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

View 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>

View File

@ -0,0 +1,5 @@
from typing import Protocol, runtime_checkable, Any
@runtime_checkable
class RandomPicker(Protocol):
def pick(self) -> Any: ...

View 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)

View 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>

View 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)

View 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[]

View 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])

View 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[]

View 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())

View 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
View 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)

View 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

View 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

View 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[]

View 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

View 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)

View 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)

View 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

View 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[]

View 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

View 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

View 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

View 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)
[]

View 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))

View 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)

View 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

View 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[]

View 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[]

View 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[]

View 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()

View 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...')

View 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

View 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

View 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()

View 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()

View 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[]

View 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']

View 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[]

View 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[]

View 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()

View 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()

View 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

View 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)

View File

@ -0,0 +1,7 @@
def tree(cls):
yield cls.__name__
if __name__ == '__main__':
for cls_name in tree(BaseException):
print(cls_name)

View 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}')

View 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}')

View 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}')

View 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)

View 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

View 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

View 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)

View 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

View 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)

View 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

View 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)

View 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

View 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)

View 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

View 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)

View 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

View 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)

View 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

View 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)

View 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

View 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)

View 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()

View 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))

View 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
View 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[]

View 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[]

View 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
View 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

View 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[]

View 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[]

View 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[]

View 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[]

View 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[]

View 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
View 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
View 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
View 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
"""

View 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
"""

View 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[]

View 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[]

View File

@ -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

View File

@ -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