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

4
18-with-match/README.rst Normal file
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

View File

@@ -0,0 +1,109 @@
"""
Doctests for `parse`:
>>> from lis import parse
# tag::PARSE_DEMO[]
>>> parse('1.5') # <1>
1.5
>>> parse('set!') # <2>
'set!'
>>> parse('(gcd 18 44)') # <3>
['gcd', 18, 44]
>>> parse('(- m (* n (// m n)))') # <4>
['-', 'm', ['*', 'n', ['//', 'm', 'n']]]
# end::PARSE_DEMO[]
"""
import math
from lis import run
fact_src = """
(define (! n)
(if (< n 2)
1
(* n (! (- n 1)))
)
)
(! 42)
"""
def test_factorial():
got = run(fact_src)
assert got == 1405006117752879898543142606244511569936384000000000
assert got == math.factorial(42)
gcd_src = """
(define (mod m n)
(- m (* n (// m n))))
(define (gcd m n)
(if (= n 0)
m
(gcd n (mod m n))))
(gcd 18 45)
"""
def test_gcd():
got = run(gcd_src)
assert got == 9
quicksort_src = """
(define (quicksort lst)
(if (null? lst)
lst
(begin
(define pivot (car lst))
(define rest (cdr lst))
(append
(quicksort
(filter (lambda (x) (< x pivot)) rest))
(list pivot)
(quicksort
(filter (lambda (x) (>= x pivot)) rest)))
)
)
)
(quicksort (list 2 1 6 3 4 0 8 9 7 5))
"""
def test_quicksort():
got = run(quicksort_src)
assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Example from Structure and Interpretation of Computer Programs
# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html
newton_src = """
(define (sqrt x)
(sqrt-iter 1.0 x))
(define (sqrt-iter guess x)
(if (good-enough? guess x)
guess
(sqrt-iter (improve guess x) x)))
(define (good-enough? guess x)
(< (abs (- (* guess guess) x)) 0.001))
(define (improve guess x)
(average guess (/ x guess)))
(define (average x y)
(/ (+ x y) 2))
(sqrt 123454321)
"""
def test_newton():
got = run(newton_src)
assert math.isclose(got, 11111)
closure_src = """
(define (make-adder increment)
(lambda (x) (+ increment x))
)
(define inc (make-adder 1))
(inc 99)
"""
def test_newton():
got = run(closure_src)
assert got == 100

View File

@@ -0,0 +1,192 @@
################ Lispy: Scheme Interpreter in Python 3.10
## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html
## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021)
## by Luciano Ramalho, adding type hints and pattern matching.
################ imports and types
import math
import operator as op
from collections import ChainMap
from collections.abc import MutableMapping, Iterator
from itertools import chain
from typing import Any, TypeAlias
Symbol: TypeAlias = str
Atom: TypeAlias = float | int | Symbol
Expression: TypeAlias = Atom | list
Environment: TypeAlias = MutableMapping[Symbol, object]
class Procedure:
"A user-defined Scheme procedure."
def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment):
self.parms = parms
self.body = body
self.env = env
def __call__(self, *args: Expression) -> Any:
local_env = dict(zip(self.parms, args))
env: Environment = ChainMap(local_env, self.env)
for exp in self.body:
result = evaluate(exp, env)
return result
################ global environment
def standard_env() -> Environment:
"An environment with some Scheme standard procedures."
env: Environment = {}
env.update(vars(math)) # sin, cos, sqrt, pi, ...
env.update({
'+': op.add,
'-': op.sub,
'*': op.mul,
'/': op.truediv,
'//': op.floordiv,
'>': op.gt,
'<': op.lt,
'>=': op.ge,
'<=': op.le,
'=': op.eq,
'abs': abs,
'append': lambda *args: list(chain(*args)),
'apply': lambda proc, args: proc(*args),
'begin': lambda *x: x[-1],
'car': lambda x: x[0],
'cdr': lambda x: x[1:],
'cons': lambda x, y: [x] + y,
'eq?': op.is_,
'equal?': op.eq,
'filter': lambda *args: list(filter(*args)),
'length': len,
'list': lambda *x: list(x),
'list?': lambda x: isinstance(x, list),
'map': lambda *args: list(map(*args)),
'max': max,
'min': min,
'not': op.not_,
'null?': lambda x: x == [],
'number?': lambda x: isinstance(x, (int, float)),
'procedure?': callable,
'round': round,
'symbol?': lambda x: isinstance(x, Symbol),
})
return env
################ parse, tokenize, and read_from_tokens
def parse(program: str) -> Expression:
"Read a Scheme expression from a string."
return read_from_tokens(tokenize(program))
def tokenize(s: str) -> list[str]:
"Convert a string into a list of tokens."
return s.replace('(', ' ( ').replace(')', ' ) ').split()
def read_from_tokens(tokens: list[str]) -> Expression:
"Read an expression from a sequence of tokens."
if len(tokens) == 0:
raise SyntaxError('unexpected EOF while reading')
token = tokens.pop(0)
if '(' == token:
exp = []
while tokens[0] != ')':
exp.append(read_from_tokens(tokens))
tokens.pop(0) # discard ')'
return exp
elif ')' == token:
raise SyntaxError('unexpected )')
else:
return parse_atom(token)
def parse_atom(token: str) -> Atom:
"Numbers become numbers; every other token is a symbol."
try:
return int(token)
except ValueError:
try:
return float(token)
except ValueError:
return Symbol(token)
################ interaction: a REPL
def repl(prompt: str = 'lis.py> ') -> None:
"A prompt-read-evaluate-print loop."
global_env: Environment = standard_env()
while True:
val = evaluate(parse(input(prompt)), global_env)
if val is not None:
print(lispstr(val))
def lispstr(exp: object) -> str:
"Convert a Python object back into a Lisp-readable string."
if isinstance(exp, list):
return '(' + ' '.join(map(lispstr, exp)) + ')'
else:
return str(exp)
################ eval
# tag::EVALUATE[]
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
match exp:
case int(x) | float(x):
return x
case Symbol(var):
return env[var]
case []:
return []
case ['quote', exp]:
return exp
case ['if', test, consequence, alternative]:
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
case ['define', Symbol(var), value_exp]:
env[var] = evaluate(value_exp, env)
case ['define', [Symbol(name), *parms], *body]:
env[name] = Procedure(parms, body, env)
case ['lambda', [*parms], *body]:
return Procedure(parms, body, env)
case [op, *args]:
proc = evaluate(op, env)
values = [evaluate(arg, env) for arg in args]
return proc(*values)
case _:
raise SyntaxError(repr(exp))
# end::EVALUATE[]
################ non-interactive execution
def run_lines(source: str) -> Iterator[Any]:
global_env: Environment = standard_env()
tokens = tokenize(source)
while tokens:
exp = read_from_tokens(tokens)
yield evaluate(exp, global_env)
def run(source: str) -> Any:
for result in run_lines(source):
pass
return result

View File

@@ -0,0 +1,182 @@
from typing import Optional
from pytest import mark, fixture
from lis import parse, evaluate, Expression, Environment, standard_env
############################################################# tests for parse
@mark.parametrize( 'source, expected', [
('7', 7),
('x', 'x'),
('(sum 1 2 3)', ['sum', 1, 2, 3]),
('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]),
('99 100', 99), # parse stops at the first complete expression
('(a)(b)', ['a']),
])
def test_parse(source: str, expected: Expression) -> None:
got = parse(source)
assert got == expected
########################################################## tests for evaluate
# Norvig's tests are not isolated: they assume the
# same environment from first to last test.
global_env_for_first_test = standard_env()
@mark.parametrize( 'source, expected', [
("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]),
("(+ 2 2)", 4),
("(+ (* 2 100) (* 1 10))", 210),
("(if (> 6 5) (+ 1 1) (+ 2 2))", 2),
("(if (< 6 5) (+ 1 1) (+ 2 2))", 4),
("(define x 3)", None),
("x", 3),
("(+ x x)", 6),
("((lambda (x) (+ x x)) 5)", 10),
("(define twice (lambda (x) (* 2 x)))", None),
("(twice 5)", 10),
("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None),
("((compose list twice) 5)", [10]),
("(define repeat (lambda (f) (compose f f)))", None),
("((repeat twice) 5)", 20),
("((repeat (repeat twice)) 5)", 80),
("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None),
("(fact 3)", 6),
("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000),
("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None),
("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]),
("""(define combine (lambda (f)
(lambda (x y)
(if (null? x) (quote ())
(f (list (car x) (car y))
((combine f) (cdr x) (cdr y)))))))""", None),
("(define zip (combine cons))", None),
("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]),
("""(define riff-shuffle (lambda (deck)
(begin
(define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq))))))
(define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq)))))
(define mid (lambda (seq) (/ (length seq) 2)))
((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None),
("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]),
("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]),
("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]),
])
def test_evaluate(source: str, expected: Optional[Expression]) -> None:
got = evaluate(parse(source), global_env_for_first_test)
assert got == expected
@fixture
def std_env() -> Environment:
return standard_env()
# tests for each of the cases in evaluate
def test_evaluate_variable() -> None:
env: Environment = dict(x=10)
source = 'x'
expected = 10
got = evaluate(parse(source), env)
assert got == expected
def test_evaluate_literal(std_env: Environment) -> None:
source = '3.3'
expected = 3.3
got = evaluate(parse(source), std_env)
assert got == expected
def test_evaluate_quote(std_env: Environment) -> None:
source = '(quote (1.1 is not 1))'
expected = [1.1, 'is', 'not', 1]
got = evaluate(parse(source), std_env)
assert got == expected
def test_evaluate_if_true(std_env: Environment) -> None:
source = '(if 1 10 no-such-thing)'
expected = 10
got = evaluate(parse(source), std_env)
assert got == expected
def test_evaluate_if_false(std_env: Environment) -> None:
source = '(if 0 no-such-thing 20)'
expected = 20
got = evaluate(parse(source), std_env)
assert got == expected
def test_define(std_env: Environment) -> None:
source = '(define answer (* 6 7))'
got = evaluate(parse(source), std_env)
assert got is None
assert std_env['answer'] == 42
def test_lambda(std_env: Environment) -> None:
source = '(lambda (a b) (if (>= a b) a b))'
func = evaluate(parse(source), std_env)
assert func.parms == ['a', 'b']
assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
assert func.env is std_env
assert func(1, 2) == 2
assert func(3, 2) == 3
def test_begin(std_env: Environment) -> None:
source = """
(begin
(define x (* 2 3))
(* x 7)
)
"""
got = evaluate(parse(source), std_env)
assert got == 42
def test_invocation_builtin_car(std_env: Environment) -> None:
source = '(car (quote (11 22 33)))'
got = evaluate(parse(source), std_env)
assert got == 11
def test_invocation_builtin_append(std_env: Environment) -> None:
source = '(append (quote (a b)) (quote (c d)))'
got = evaluate(parse(source), std_env)
assert got == ['a', 'b', 'c', 'd']
def test_invocation_builtin_map(std_env: Environment) -> None:
source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))'
got = evaluate(parse(source), std_env)
assert got == [2, 4, 6]
def test_invocation_user_procedure(std_env: Environment) -> None:
source = """
(begin
(define max (lambda (a b) (if (>= a b) a b)))
(max 22 11)
)
"""
got = evaluate(parse(source), std_env)
assert got == 22
###################################### for py3.10/lis.py only
def test_define_function(std_env: Environment) -> None:
source = '(define (max a b) (if (>= a b) a b))'
got = evaluate(parse(source), std_env)
assert got is None
max_fn = std_env['max']
assert max_fn.parms == ['a', 'b']
assert max_fn.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
assert max_fn.env is std_env
assert max_fn(1, 2) == 2
assert max_fn(3, 2) == 3

View File

@@ -0,0 +1,64 @@
import operator as op
from lis import run
env_scm = """
(define standard-env (list
(list (quote +) +)
(list (quote -) -)
))
standard-env
"""
def test_env_build():
got = run(env_scm)
assert got == [['+', op.add], ['-', op.sub]]
scan_scm = """
(define l (quote (a b c)))
(define (scan what where)
(if (null? where)
()
(if (eq? what (car where))
what
(scan what (cdr where))))
)
"""
def test_scan():
source = scan_scm + '(scan (quote a) l )'
got = run(source)
assert got == 'a'
def test_scan_not_found():
source = scan_scm + '(scan (quote z) l )'
got = run(source)
assert got == []
lookup_scm = """
(define env (list
(list (quote +) +)
(list (quote -) -)
))
(define (lookup what where)
(if (null? where)
()
(if (eq? what (car (car where)))
(car (cdr (car where)))
(lookup what (cdr where))))
)
"""
def test_lookup():
source = lookup_scm + '(lookup (quote +) env)'
got = run(source)
assert got == op.add
def test_lookup_not_found():
source = lookup_scm + '(lookup (quote z) env )'
got = run(source)
assert got == []

92
18-with-match/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[]