"""Python Cookbook

Chapter 8, Examples from the text.

•	Choosing between inheritance and extension – the “is-a” question
•	Separating concerns via multiple inheritance
•	Leveraging Python's duck typing
•	Managing global and singleton objects
•	Using more complex structures – maps of lists
•	Creating a class that has orderable objects
•	Improving performance with an an ordered collection
•	Deleting from a list of complex objects

"""

# Choosing between inheritance and extension – the “is-a” question

>>> from typing import NamedTuple
>>> class Card(NamedTuple):
...     rank: int
...     suit: str
>>> Spades, Hearts, Diamonds, Clubs = '\u2660\u2661\u2662\u2663'
>>> Card(2, Spades)
Card(rank=2, suit='♠')

>>> domain = list(
...     Card(r+1,s)
...         for r in range(13)
...             for s in (Spades, Hearts, Diamonds, Clubs))
>>> len(domain)
52

>>> import random
>>> from Chapter_08.ch08_r01 import Deck_W
>>> d = Deck_W(domain)

>>> random.seed(1)
>>> d.shuffle()
>>> [d.deal() for _ in range(5)] 
[Card(rank=13, suit='♡'), Card(rank=3, suit='♡'), Card(rank=10, suit='♡'), Card(rank=6, suit='♢'), Card(rank=1, suit='♢')]

>>> from Chapter_08.ch08_r01 import Deck_X
>>> d2 = Deck_X(
...     Card(r+1,s)
...         for r in range(13)
...             for s in (Spades, Hearts, Diamonds, Clubs))
>>> len(d2)
52

>>> random.seed(1)
>>> d2.shuffle()
>>> [d2.deal() for _ in range(5)] 
[Card(rank=13, suit='♡'), Card(rank=3, suit='♡'), Card(rank=10, suit='♡'), Card(rank=6, suit='♢'), Card(rank=1, suit='♢')]

>>> c_2s = Card(2, Spades)
>>> c_2s
Card(rank=2, suit='♠')
>>> another = c_2s
>>> another
Card(rank=2, suit='♠')

>>> id(c_2s) == id(another)
True
>>> c_2s is another
True

# Separating concerns via multiple inheritance

>>> from Chapter_08.ch08_r02 import make_cribbage_card, SUITS
>>> import random
>>> random.seed(1)
>>> deck = [make_cribbage_card(rank+1, suit) for rank in range(13) for suit in SUITS]
>>> random.shuffle(deck)
>>> len(deck)
52
>>> [str(c) for c in deck[:5]]
[' K ♡', ' 3 ♡', '10 ♡', ' 6 ♢', ' A ♢']

>>> sum(c.points() for c in deck[:5])
30

>>> c = deck[5]
>>> str(c)
'10 ♢'
>>> c.__class__.__name__
'CribbageCard'
>>> c.__class__.mro() 
[<class 'Chapter_08.ch08_r02.CribbageCard'>, <class 'Chapter_08.ch08_r02.Card'>, <class 'Chapter_08.ch08_r02.CribbagePoints'>, <class 'Chapter_08.ch08_r02.PointedCard'>, <class 'typing.Protocol'>, <class 'typing.Generic'>, <class 'object'>]

>>> cc_9h = make_cribbage_card(9, '♡')
>>> cc_9h
CribbageCard(rank=9, suit='♡')
>>> cc_9h == 9
False

# Leveraging Python's duck typing

>>> from Chapter_08.ch08_r03 import roller, Dice1, Dice2
>>> list(roller(Dice1, 1, samples=5))
[(1, 3), (1, 4), (4, 4), (6, 4), (2, 1)]
>>> list(roller(Dice2, 1, samples=5))
[(1, 3), (1, 4), (4, 4), (6, 4), (2, 1)]

>>> d1 = Dice1()
>>> d1.__class__
<class 'Chapter_08.ch08_r03.Dice1'>
>>> isinstance(d1, Dice1)
True
>>> issubclass(d1.__class__, Dice1)
True

# Managing global and singleton objects

>>> from Chapter_08.ch08_r04 import count, count_summary
>>> from Chapter_08.ch08_r03 import Dice1
>>> d = Dice1(1)
>>> for _ in range(1000):
...     if sum(d.roll()) == 7: count('seven')
...     else: count('other')

>>> print(count_summary())
[('other', 833), ('seven', 167)]

>>> from Chapter_08.ch08_r04 import EventCounter
>>> c1 = EventCounter()
>>> c1.count('input')
>>> c2 = EventCounter()
>>> c2.count('input')
>>> c3 = EventCounter()
>>> c3.summary()
[('input', 2)]

# Using more complex structures – maps of lists

# Creating a class that has orderable objects

>>> from Chapter_08.ch08_r06 import make_card
>>> c1 = make_card(9, '♡')
>>> c2 = make_card(10, '♡')
>>> c1 < c2
True
>>> c1 == c1
True
>>> c1 == c2
False
>>> c1 > c2
False

>>> from Chapter_08.ch08_r06 import make_deck
>>> deck = make_deck()
>>> len(deck)
48

>>> [str(c) for c in deck[:8]]
[' 9 ♠', ' 9 ♡', ' 9 ♢', ' 9 ♣', '10 ♠', '10 ♡', '10 ♢', '10 ♣']

>>> [str(c) for c in deck[24:32]]
[' 9 ♠', ' 9 ♡', ' 9 ♢', ' 9 ♣', '10 ♠', '10 ♡', '10 ♢', '10 ♣']

>>> import random
>>> random.seed(4)
>>> random.shuffle(deck)
>>> [str(c) for c in sorted(deck[:12])]
[' 9 ♣', '10 ♣', ' J ♠', ' J ♢', ' J ♢', ' Q ♠', ' Q ♣', ' K ♠', ' K ♠', ' K ♣', ' A ♡', ' A ♣']

>>> c1 = make_card(9, '♡')
>>> c1
PinochleNumber(rank=9, suit='♡')
>>> c1 == 9
False


# Improving performance with an an ordered collection

>>> from Chapter_08.ch08_r06 import make_deck, make_card
>>> from Chapter_08.ch08_r07 import Hand
>>> import random
>>> random.seed(4)
>>> deck = make_deck()
>>> random.shuffle(deck)
>>> h = Hand(deck[:12])
>>> [str(c) for c in h.cards]
[' 9 ♣', '10 ♣', ' J ♠', ' J ♢', ' J ♢', ' Q ♠', ' Q ♣', ' K ♠', ' K ♠', ' K ♣', ' A ♡', ' A ♣']

>>> pinochle = Hand([make_card(11,'♢'), make_card(12,'♠')])
>>> pinochle <= h
True

>>> sum(c.points() for c in h)
56

# Deleting from a list of mappings

>>> from Chapter_08.ch08_r08 import SongType
>>> from typing import List

>>> source: List[SongType] = [
...    {'title': 'Eruption', 'writer': ['Emerson'], 'time': '2:43'},
...    {'title': 'Stones of Years', 'writer': ['Emerson', 'Lake'], 'time': '3:43'},
...    {'title': 'Iconoclast', 'writer': ['Emerson'], 'time': '1:16'},
...    {'title': 'Mass', 'writer': ['Emerson', 'Lake'], 'time': '3:09'},
...    {'title': 'Manticore', 'writer': ['Emerson'], 'time': '1:49'},
...    {'title': 'Battlefield', 'writer': ['Lake'], 'time': '3:57'},
...    {'title': 'Aquatarkus', 'writer': ['Emerson'], 'time': '3:54'}
... ]


>>> from pprint import pprint

>>> data = source.copy()
>>> for item in data:
...     if 'Lake' in item['writer']:
...        print("remove", item['title'])
remove Stones of Years
remove Mass
remove Battlefield

Naïve Delete

>>> data = source.copy()
>>> for index in range(len(data)): 
...    if 'Lake' in data[index]['writer']:
...       del data[index]
Traceback (most recent call last):
  File "/Users/slott/miniconda3/envs/cookbook/lib/python3.8/doctest.py", line 1328, in __run
    compileflags, 1), test.globs)
  File "<doctest examples.txt[84]>", line 2, in <module>
    if 'Lake' in data[index]['writer']:
IndexError: list index out of range

Multiple passes through the data.

>>> from Chapter_08.ch08_r08 import index_of_writer

>>> data = source.copy()
>>> position = index_of_writer(data, 'Lake')
>>> while position is not None:
...    del data[position]  # or data.pop(position)
...    position = index_of_writer(data, 'Lake')
>>> pprint(data)
[{'time': '2:43', 'title': 'Eruption', 'writer': ['Emerson']},
 {'time': '1:16', 'title': 'Iconoclast', 'writer': ['Emerson']},
 {'time': '1:49', 'title': 'Manticore', 'writer': ['Emerson']},
 {'time': '3:54', 'title': 'Aquatarkus', 'writer': ['Emerson']}]

One pass through the data

>>> data = source.copy()
>>> i = 0
>>> while i != len(data):
...    if 'Lake' in data[i]['writer']:
...        del data[i]
...    else:
...        i += 1

>>> pprint(data)
[{'time': '2:43', 'title': 'Eruption', 'writer': ['Emerson']},
 {'time': '1:16', 'title': 'Iconoclast', 'writer': ['Emerson']},
 {'time': '1:49', 'title': 'Manticore', 'writer': ['Emerson']},
 {'time': '3:54', 'title': 'Aquatarkus', 'writer': ['Emerson']}]

Alternatives: Copying

>>> cleaned_copy = [item for item in source if not('Lake' in item['writer'])]
>>> pprint(cleaned_copy)
[{'time': '2:43', 'title': 'Eruption', 'writer': ['Emerson']},
 {'time': '1:16', 'title': 'Iconoclast', 'writer': ['Emerson']},
 {'time': '1:49', 'title': 'Manticore', 'writer': ['Emerson']},
 {'time': '3:54', 'title': 'Aquatarkus', 'writer': ['Emerson']}]

>>> cleaned_copy_2 = list(filter(lambda item: not('Lake' in item['writer']), source))
>>> pprint(cleaned_copy_2)
[{'time': '2:43', 'title': 'Eruption', 'writer': ['Emerson']},
 {'time': '1:16', 'title': 'Iconoclast', 'writer': ['Emerson']},
 {'time': '1:49', 'title': 'Manticore', 'writer': ['Emerson']},
 {'time': '3:54', 'title': 'Aquatarkus', 'writer': ['Emerson']}]


