sync with O'Reilly Atlas

This commit is contained in:
Luciano Ramalho
2021-07-07 23:45:54 -03:00
parent f0f160844d
commit 23e78eeb82
64 changed files with 2087 additions and 124 deletions

View File

@@ -0,0 +1 @@
== Record-like Structures

View File

@@ -0,0 +1,22 @@
>>> from cards import Card
>>> helen = Card('Q', 'hearts')
>>> helen
Card(rank='Q', suit='hearts')
>>> cards = [Card(r, s) for s in Card.suits for r in Card.ranks]
>>> cards[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]
>>> sorted(cards)[:3]
[Card(rank='2', suit='clubs'), Card(rank='2', suit='diamonds'), Card(rank='2', suit='hearts')]
>>> from cards_enum import Card, Suit, Rank
>>> helen = Card('Q', 'hearts')
>>> helen
Card(rank='Q', suit='hearts')
>>> cards = [Card(r, s) for s in Suit for r in Rank]
>>> cards[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]
>>> sorted(cards)[:3]
[Card(rank='2', suit='clubs'), Card(rank='2', suit='diamonds'), Card(rank='2', suit='hearts')]
>>> for card in cards[12::13]: print(card)

9
05-data-classes/cards.py Normal file
View File

@@ -0,0 +1,9 @@
from dataclasses import dataclass
@dataclass(order=True)
class Card:
rank: str
suit: str
ranks = [str(n) for n in range(2, 10)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

View File

@@ -0,0 +1,14 @@
from dataclasses import dataclass
import enum
Suit = enum.IntEnum('Suit', 'spades diamonds clubs hearts')
Rank = enum.Enum('Rank', [str(n) for n in range(2, 10)] + list('JQKA'))
@dataclass(order=True)
class Card:
rank: Suit
suit: Rank
def __str__(self):
glyphs = [chr(x) for x in range(0x2660, 0x2664)]
return f'{self.rank} of {glyphs[self.suit-1]}'

View File

@@ -0,0 +1,16 @@
"""
``Coordinate``: a simple class with a custom ``__str__``::
>>> moscow = Coordinate(55.756, 37.617)
>>> print(moscow) # doctest:+ELLIPSIS
<coordinates.Coordinate object at 0x...>
"""
# tag::COORDINATE[]
class Coordinate:
def __init__(self, lat, lon):
self.lat = lat
self.lon = lon
# end::COORDINATE[]

View File

@@ -0,0 +1,9 @@
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)

View File

@@ -0,0 +1,6 @@
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list[str] = field(default_factory=list) # <1>

View File

@@ -0,0 +1,9 @@
from dataclasses import dataclass
# tag::CLUBMEMBER[]
@dataclass
class ClubMember:
name: str
guests: list = []
# end::CLUBMEMBER[]

View File

@@ -0,0 +1,23 @@
"""
``Coordinate``: simple class decorated with ``dataclass`` and a custom ``__str__``::
>>> moscow = Coordinate(55.756, 37.617)
>>> print(moscow)
55.8°N, 37.6°E
"""
# tag::COORDINATE[]
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
# end::COORDINATE[]

View File

@@ -0,0 +1,50 @@
# tag::DOCTESTS[]
"""
``HackerClubMember`` objects accept an optional ``handle`` argument::
>>> anna = HackerClubMember('Anna Ravenscroft', handle='AnnaRaven')
>>> anna
HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')
If ``handle`` is ommitted, it's set to the first part of the member's name::
>>> leo = HackerClubMember('Leo Rochael')
>>> leo
HackerClubMember(name='Leo Rochael', guests=[], handle='Leo')
Members must have a unique handle. The following ``leo2`` will not be created,
because its ``handle`` would be 'Leo', which was taken by ``leo``::
>>> leo2 = HackerClubMember('Leo DaVinci')
Traceback (most recent call last):
...
ValueError: handle 'Leo' already exists.
To fix, ``leo2`` must be created with an explicit ``handle``::
>>> leo2 = HackerClubMember('Leo DaVinci', handle='Neo')
>>> leo2
HackerClubMember(name='Leo DaVinci', guests=[], handle='Neo')
"""
# end::DOCTESTS[]
# tag::HACKERCLUB[]
from dataclasses import dataclass
from club import ClubMember
@dataclass
class HackerClubMember(ClubMember): # <1>
all_handles = set() # <2>
handle: str = '' # <3>
def __post_init__(self):
cls = self.__class__ # <4>
if self.handle == '': # <5>
self.handle = self.name.split()[0]
if self.handle in cls.all_handles: # <6>
msg = f'handle {self.handle!r} already exists.'
raise ValueError(msg)
cls.all_handles.add(self.handle) # <7>
# end::HACKERCLUB[]

View File

@@ -0,0 +1,51 @@
# tag::DOCTESTS[]
"""
``HackerClubMember`` objects can be created with a ``name`` and an optional ``handle``::
>>> anna = HackerClubMember('Anna Ravenscroft', handle='AnnaRaven')
>>> anna
HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')
If ``handle`` is ommitted, it's set to the first part of the member's name::
>>> leo = HackerClubMember('Leo Rochael')
>>> leo
HackerClubMember(name='Leo Rochael', guests=[], handle='Leo')
Members must have a unique handle. This ``leo2`` will not be created,
because its ``handle`` would be 'Leo', which was taken by ``leo``::
>>> leo2 = HackerClubMember('Leo DaVinci')
Traceback (most recent call last):
...
ValueError: handle 'Leo' already exists.
To fix, ``leo2`` must be created with an explicit ``handle``::
>>> leo2 = HackerClubMember('Leo DaVinci', handle='Neo')
>>> leo2
HackerClubMember(name='Leo DaVinci', guests=[], handle='Neo')
"""
# end::DOCTESTS[]
# tag::HACKERCLUB[]
from dataclasses import dataclass
from typing import ClassVar
from club import ClubMember
@dataclass
class HackerClubMember(ClubMember):
all_handles: ClassVar[set[str]] = set()
handle: str = ''
def __post_init__(self):
cls = self.__class__
if self.handle == '':
self.handle = self.name.split()[0]
if self.handle in cls.all_handles:
msg = f'handle {self.handle!r} already exists.'
raise ValueError(msg)
cls.all_handles.add(self.handle)
# end::HACKERCLUB[]

View File

@@ -0,0 +1,87 @@
"""
Media resource description class with subset of the Dublin Core fields.
Default field values:
>>> r = Resource('0')
>>> r # doctest: +NORMALIZE_WHITESPACE
Resource(identifier='0', title='<untitled>', creators=[], date=None,
type=<ResourceType.BOOK: 1>, description='', language='', subjects=[])
A complete resource record:
# tag::DOCTEST[]
>>> description = 'Improving the design of existing code'
>>> book = Resource('978-0-13-475759-9', 'Refactoring, 2nd Edition',
... ['Martin Fowler', 'Kent Beck'], date(2018, 11, 19),
... ResourceType.BOOK, description, 'EN',
... ['computer programming', 'OOP'])
>>> book # doctest: +NORMALIZE_WHITESPACE
Resource(identifier='978-0-13-475759-9', title='Refactoring, 2nd Edition',
creators=['Martin Fowler', 'Kent Beck'], date=datetime.date(2018, 11, 19),
type=<ResourceType.BOOK: 1>, description='Improving the design of existing code',
language='EN', subjects=['computer programming', 'OOP'])
# end::DOCTEST[]
"""
# tag::DATACLASS[]
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum, auto
from datetime import date
class ResourceType(Enum): # <1>
BOOK = auto()
EBOOK = auto()
VIDEO = auto()
@dataclass
class Resource:
"""Media resource description."""
identifier: str # <2>
title: str = '<untitled>' # <3>
creators: list[str] = field(default_factory=list)
date: Optional[date] = None # <4>
type: ResourceType = ResourceType.BOOK # <5>
description: str = ''
language: str = ''
subjects: list[str] = field(default_factory=list)
# end::DATACLASS[]
from typing import TypedDict
class ResourceDict(TypedDict):
identifier: str
title: str
creators: list[str]
date: Optional[date]
type: ResourceType
description: str
language: str
subjects: list[str]
if __name__ == '__main__':
r = Resource('0')
description = 'Improving the design of existing code'
book = Resource('978-0-13-475759-9', 'Refactoring, 2nd Edition',
['Martin Fowler', 'Kent Beck'], date(2018, 11, 19),
ResourceType.BOOK, description,
'EN', ['computer programming', 'OOP'])
print(book)
book_dict: ResourceDict = {
'identifier': '978-0-13-475759-9',
'title': 'Refactoring, 2nd Edition',
'creators': ['Martin Fowler', 'Kent Beck'],
'date': date(2018, 11, 19),
'type': ResourceType.BOOK,
'description': 'Improving the design of existing code',
'language': 'EN',
'subjects': ['computer programming', 'OOP']}
book2 = Resource(**book_dict)
print(book == book2)

View File

@@ -0,0 +1,111 @@
"""
Media resource description class with subset of the Dublin Core fields.
Default field values:
>>> r = Resource('0')
>>> r # doctest: +NORMALIZE_WHITESPACE
Resource(
identifier = '0',
title = '<untitled>',
creators = [],
date = None,
type = <ResourceType.BOOK: 1>,
description = '',
language = '',
subjects = [],
)
A complete resource record:
>>> description = 'Improving the design of existing code'
>>> book = Resource('978-0-13-475759-9', 'Refactoring, 2nd Edition',
... ['Martin Fowler', 'Kent Beck'], date(2018, 11, 19),
... ResourceType.BOOK, description, 'EN',
... ['computer programming', 'OOP'])
# tag::DOCTEST[]
>>> book # doctest: +NORMALIZE_WHITESPACE
Resource(
identifier = '978-0-13-475759-9',
title = 'Refactoring, 2nd Edition',
creators = ['Martin Fowler', 'Kent Beck'],
date = datetime.date(2018, 11, 19),
type = <ResourceType.BOOK: 1>,
description = 'Improving the design of existing code',
language = 'EN',
subjects = ['computer programming', 'OOP'],
)
# end::DOCTEST[]
"""
from dataclasses import dataclass, field, fields
from typing import Optional, TypedDict
from enum import Enum, auto
from datetime import date
class ResourceType(Enum):
BOOK = auto()
EBOOK = auto()
VIDEO = auto()
@dataclass
class Resource:
"""Media resource description."""
identifier: str
title: str = '<untitled>'
creators: list[str] = field(default_factory=list)
date: Optional[date] = None
type: ResourceType = ResourceType.BOOK
description: str = ''
language: str = ''
subjects: list[str] = field(default_factory=list)
# tag::REPR[]
def __repr__(self):
cls = self.__class__
cls_name = cls.__name__
indent = ' ' * 4
res = [f'{cls_name}('] # <1>
for f in fields(cls): # <2>
value = getattr(self, f.name) # <3>
res.append(f'{indent}{f.name} = {value!r},') # <4>
res.append(')') # <5>
return '\n'.join(res) # <6>
# end::REPR[]
class ResourceDict(TypedDict):
identifier: str
title: str
creators: list[str]
date: Optional[date]
type: ResourceType
description: str
language: str
subjects: list[str]
if __name__ == '__main__':
r = Resource('0')
description = 'Improving the design of existing code'
book = Resource('978-0-13-475759-9', 'Refactoring, 2nd Edition',
['Martin Fowler', 'Kent Beck'], date(2018, 11, 19),
ResourceType.BOOK, description,
'EN', ['computer programming', 'OOP'])
print(book)
book_dict: ResourceDict = {
'identifier': '978-0-13-475759-9',
'title': 'Refactoring, 2nd Edition',
'creators': ['Martin Fowler', 'Kent Beck'],
'date': date(2018, 11, 19),
'type': ResourceType.BOOK,
'description': 'Improving the design of existing code',
'language': 'EN',
'subjects': ['computer programming', 'OOP']}
book2 = Resource(**book_dict)
print(book == book2)

View File

@@ -0,0 +1,64 @@
>>> from frenchdeck import FrenchDeck, Card
>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')
>>> deck = FrenchDeck()
>>> len(deck)
52
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'), Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
>>> Card('Q', 'hearts') in deck
True
>>> Card('Z', 'clubs') in deck
False
>>> for card in deck: # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...
>>> for card in reversed(deck): # doctest: +ELLIPSIS
... print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...
>>> for n, card in enumerate(deck, 1): # doctest: +ELLIPSIS
... print(n, card)
1 Card(rank='2', suit='spades')
2 Card(rank='3', suit='spades')
3 Card(rank='4', suit='spades')
...
Sort with *spades high* overall ranking
# tag::SPADES_HIGH[]
>>> Card.suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) # <1>
>>> def spades_high(card): # <2>
... rank_value = FrenchDeck.ranks.index(card.rank)
... suit_value = card.suit_values[card.suit]
... return rank_value * len(card.suit_values) + suit_value
...
>>> Card.overall_rank = spades_high # <3>
>>> lowest_card = Card('2', 'clubs')
>>> highest_card = Card('A', 'spades')
>>> lowest_card.overall_rank() # <4>
0
>>> highest_card.overall_rank()
51
# end::SPADES_HIGH[]
>>> for card in sorted(deck, key=Card.overall_rank): # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
...
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')

View File

@@ -0,0 +1,17 @@
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
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]

View File

@@ -0,0 +1,92 @@
"""
match_cities.py
"""
# tag::CITY[]
import typing
class City(typing.NamedTuple):
continent: str
name: str
country: str
cities = [
City('Asia', 'Tokyo', 'JP'),
City('Asia', 'Delhi', 'IN'),
City('North America', 'Mexico City', 'MX'),
City('North America', 'New York', 'US'),
City('South America', 'São Paulo', 'BR'),
]
# end::CITY[]
# tag::ASIA[]
def match_asian_cities():
results = []
for city in cities:
match city:
case City(continent='Asia'):
results.append(city)
return results
# end::ASIA[]
# tag::ASIA_POSITIONAL[]
def match_asian_cities_pos():
results = []
for city in cities:
match city:
case City('Asia'):
results.append(city)
return results
# end::ASIA_POSITIONAL[]
# tag::ASIA_COUNTRIES[]
def match_asian_countries():
results = []
for city in cities:
match city:
case City(continent='Asia', country=cc):
results.append(cc)
return results
# end::ASIA_COUNTRIES[]
# tag::ASIA_COUNTRIES_POSITIONAL[]
def match_asian_countries_pos():
results = []
for city in cities:
match city:
case City('Asia', _, country):
results.append(country)
return results
# end::ASIA_COUNTRIES_POSITIONAL[]
def match_india():
results = []
for city in cities:
match city:
case City(_, name, 'IN'):
results.append(name)
return results
def match_brazil():
results = []
for city in cities:
match city:
case City(country='BR', name=name):
results.append(name)
return results
def main():
tests = ((n, f) for n, f in globals().items() if n.startswith('match_'))
for name, func in tests:
print(f'{name:15}\t{func()}')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,8 @@
from dataclasses import dataclass
@dataclass
class DemoDataClass:
a: int # <1>
b: float = 1.1 # <2>
c = 'spam' # <3>

View File

@@ -0,0 +1,7 @@
import typing
class DemoNTClass(typing.NamedTuple):
a: int # <1>
b: float = 1.1 # <2>
c = 'spam' # <3>

View File

@@ -0,0 +1,5 @@
class DemoPlainClass:
a: int # <1>
b: float = 1.1 # <2>
c = 'spam' # <3>

View File

@@ -0,0 +1,22 @@
"""
``Coordinate``: a simple ``NamedTuple`` subclass with a custom ``__str__``::
>>> moscow = Coordinate(55.756, 37.617)
>>> print(moscow)
55.8°N, 37.6°E
"""
# tag::COORDINATE[]
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float
lon: float
def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
# end::COORDINATE[]

View File

@@ -0,0 +1,20 @@
"""
``Coordinate``: a simple ``NamedTuple`` subclass
This version has a field with a default value::
>>> moscow = Coordinate(55.756, 37.617)
>>> moscow
Coordinate(lat=55.756, lon=37.617, reference='WGS84')
"""
# tag::COORDINATE[]
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float # <1>
lon: float
reference: str = 'WGS84' # <2>
# end::COORDINATE[]

View File

@@ -0,0 +1,9 @@
import typing
class Coordinate(typing.NamedTuple):
lat: float
lon: float
trash = Coordinate('Ni!', None) # <1>
print(trash)