moved lispy from 02 to 18

This commit is contained in:
Luciano Ramalho 2021-09-15 22:48:08 -03:00
parent 00e4741926
commit 6527037ae7
38 changed files with 590 additions and 1219 deletions

View File

@ -1,109 +0,0 @@
"""
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

@ -1,69 +0,0 @@
"""
Tests for developing a meta-circular interpreter, step-by-step.
"""
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 == []

View File

@ -1,33 +0,0 @@
# Changes from the original
While adapting Peter Norvig's [lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) for
use in _Fluent Python, Second Edition_, I made a few changes for didactic reasons.
_Luciano Ramalho_
## Significant changes
* Make the `lambda` form accept more than one expression as the body. This is consistent with _Scheme_ syntax, and provides a useful example for the book. To implement this:
* In `Procedure.__call__`: evaluate `self.body` as a list of expressions, instead of a single expression. Return the value of the last expression.
* In `evaluate()`: when processing `lambda`, unpack expression into `(_, parms, *body)`, to accept a list of expressions as the body.
* Remove the `global_env` global `dict`. It is only used as a default value for the `env` parameter in `evaluate()`, but it is unsafe to use mutable data structures as parameter default values. To implement this:
* In `repl()`: create local variable `global_env` and pass it as the `env` paramater of `evaluate()`.
* In `evaluate()`, remove `global_env` default value for `env`.
* Rewrite the custom test script
[lispytest.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py) as
[lis_test.py](https://github.com/fluentpython/example-code-2e/blob/master/02-array-seq/lispy/py3.9/lis_test.py):
a standard [pytest](https://docs.pytest.org) test suite including new test cases, preserving all Norvig's the test cases for
[lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py)
but removing the test cases for the features implemented only in
[lispy.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py)
## Name changes
Cosmetic changes to make the code look more familiar to
Python programmers, the audience of _Fluent Python_.
* Rename `eval()` to `evaluate()`, to avoid confusion with Python's `eval` built-in function.
* Refer to the list class as `list` instead of aliasing as `List`, to avoid confusion with `typing.List` which is often imported as `List`.
* Import `collections.ChainMap` as `ChainMap` instead of `Environment`.

View File

@ -1,142 +0,0 @@
################ Lispy: Scheme Interpreter in Python 3.9
## (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 typing import Any
Symbol = str # A Lisp Symbol is implemented as a Python str
Number = (int, float) # A Lisp Number is implemented as a Python int or float
class Procedure:
"A user-defined Scheme procedure."
def __init__(self, parms, body, env):
self.parms, self.body, self.env = parms, body, env
def __call__(self, *args):
env = ChainMap(dict(zip(self.parms, args)), self.env)
for exp in self.body:
result = evaluate(exp, env)
return result
################ Global Environment
def standard_env():
"An environment with some Scheme standard procedures."
env = {}
env.update(vars(math)) # sin, cos, sqrt, pi, ...
env.update({
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv,
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
'abs': abs,
'append': op.add,
'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,
'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, Number),
'procedure?': callable,
'round': round,
'symbol?': lambda x: isinstance(x, Symbol),
})
return env
################ Parsing: parse, tokenize, and read_from_tokens
def parse(program):
"Read a Scheme expression from a string."
return read_from_tokens(tokenize(program))
def tokenize(s):
"Convert a string into a list of tokens."
return s.replace('(',' ( ').replace(')',' ) ').split()
def read_from_tokens(tokens):
"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):
"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-eval-print loop."
global_env = standard_env()
while True:
val = evaluate(parse(input(prompt)), global_env)
if val is not None:
print(lispstr(val))
def lispstr(exp):
"Convert a Python object back into a Lisp-readable string."
if isinstance(exp, list):
return '(' + ' '.join(map(lispstr, exp)) + ')'
else:
return str(exp)
################ eval
def evaluate(x, env):
"Evaluate an expression in an environment."
if isinstance(x, Symbol): # variable reference
return env[x]
elif not isinstance(x, list): # constant literal
return x
elif x[0] == 'quote': # (quote exp)
(_, exp) = x
return exp
elif x[0] == 'if': # (if test conseq alt)
(_, test, conseq, alt) = x
exp = (conseq if evaluate(test, env) else alt)
return evaluate(exp, env)
elif x[0] == 'define': # (define var exp)
(_, var, exp) = x
env[var] = evaluate(exp, env)
elif x[0] == 'lambda': # (lambda (var...) body)
(_, parms, *body) = x
return Procedure(parms, body, env)
else: # (proc arg...)
proc = evaluate(x[0], env)
args = [evaluate(exp, env) for exp in x[1:]]
return proc(*args)

View File

@ -1,166 +0,0 @@
from pytest import mark, fixture
from lis import parse, evaluate, 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):
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, expected):
got = evaluate(parse(source), global_env_for_first_test)
assert got == expected
@fixture
def std_env():
return standard_env()
# tests for each of the cases in evaluate
def test_evaluate_variable():
env = dict(x=10)
source = 'x'
expected = 10
got = evaluate(parse(source), env)
assert got == expected
def test_evaluate_literal(std_env):
source = '3.3'
expected = 3.3
got = evaluate(parse(source), std_env)
assert got == expected
def test_evaluate_quote(std_env):
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) -> 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) -> None:
source = '(if 0 no-such-thing 20)'
expected = 20
got = evaluate(parse(source), std_env)
assert got == expected
def test_define(std_env) -> 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) -> 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) -> 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) -> None:
source = '(car (quote (11 22 33)))'
got = evaluate(parse(source), std_env)
assert got == 11
def test_invocation_builtin_append(std_env) -> 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) -> 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):
source = """
(begin
(define max (lambda (a b) (if (>= a b) a b)))
(max 22 11)
)
"""
got = evaluate(parse(source), std_env)
assert got == 22

View File

@ -6,7 +6,7 @@
>>> anna
HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')
If ``handle`` is ommitted, it's set to the first part of the member's name::
If ``handle`` is omitted, it's set to the first part of the member's name::
>>> leo = HackerClubMember('Leo Rochael')
>>> leo

View File

@ -6,7 +6,7 @@
>>> anna
HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')
If ``handle`` is ommitted, it's set to the first part of the member's name::
If ``handle`` is omitted, it's set to the first part of the member's name::
>>> leo = HackerClubMember('Leo Rochael')
>>> leo

View File

@ -5,7 +5,7 @@ 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
if end is None:
return ap_gen
return itertools.takewhile(lambda n: n < end, ap_gen)
# end::ARITPROG_ITERTOOLS[]

View File

@ -0,0 +1,43 @@
"""
A coroutine to compute a running average
# tag::CORO_AVERAGER_TEST[]
>>> coro_avg = averager() # <1>
>>> next(coro_avg) # <2>
0.0
>>> 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_TEST_CONT[]
>>> coro_avg.send(20) # <1>
16.25
>>> coro_avg.close() # <2>
>>> coro_avg.close() # <3>
>>> coro_avg.send(5) # <4>
Traceback (most recent call last):
...
StopIteration
# end::CORO_AVERAGER_TEST_CONT[]
"""
# tag::CORO_AVERAGER[]
from collections.abc import Generator
def averager() -> Generator[float, float, None]: # <1>
total = 0.0
count = 0
average = 0.0
while True: # <2>
term = yield average # <3>
total += term
count += 1
average = total/count
# end::CORO_AVERAGER[]

View File

@ -0,0 +1,96 @@
"""
A coroutine to compute a running average.
Testing ``averager2`` by itself::
# tag::RETURNING_AVERAGER_DEMO_1[]
>>> coro_avg = averager2()
>>> next(coro_avg)
>>> coro_avg.send(10) # <1>
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> coro_avg.close() # <2>
# end::RETURNING_AVERAGER_DEMO_1[]
Catching `StopIteration` to extract the value returned by
the coroutine::
# tag::RETURNING_AVERAGER_DEMO_2[]
>>> coro_avg = averager2()
>>> next(coro_avg)
>>> coro_avg.send(10)
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> try:
... coro_avg.send(STOP) # <1>
... except StopIteration as exc:
... result = exc.value # <2>
...
>>> result # <3>
Result(count=3, average=15.5)
# end::RETURNING_AVERAGER_DEMO_2[]
Using `yield from`:
# tag::RETURNING_AVERAGER_DEMO_3[]
>>> def compute():
... res = yield from averager2(True) # <1>
... print('computed:', res) # <2>
... return res # <3>
...
>>> comp = compute() # <4>
>>> for v in [None, 10, 20, 30, STOP]: # <5>
... try:
... comp.send(v) # <6>
... except StopIteration as exc: # <7>
... result = exc.value
received: 10
received: 20
received: 30
received: <Sentinel>
computed: Result(count=3, average=20.0)
>>> result # <8>
Result(count=3, average=20.0)
# end::RETURNING_AVERAGER_DEMO_3[]
"""
# tag::RETURNING_AVERAGER_TOP[]
from collections.abc import Generator
from typing import Union, NamedTuple
class Result(NamedTuple): # <1>
count: int # type: ignore # <2>
average: float
class Sentinel: # <3>
def __repr__(self):
return f'<Sentinel>'
STOP = Sentinel() # <4>
SendType = Union[float, Sentinel] # <5>
# end::RETURNING_AVERAGER_TOP[]
# tag::RETURNING_AVERAGER[]
def averager2(verbose: bool = False) -> Generator[None, SendType, Result]: # <1>
total = 0.0
count = 0
average = 0.0
while True:
term = yield # <2>
if verbose:
print('received:', term)
if isinstance(term, Sentinel): # <3>
break
total += term # <4>
count += 1
average = total / count
return Result(count, average) # <5>
# end::RETURNING_AVERAGER[]

View File

@ -0,0 +1,7 @@
from collections.abc import Iterator
def fibonacci() -> Iterator[int]:
a, b = 0, 1
while True:
yield a
a, b = b, a + b

View File

@ -21,7 +21,7 @@ class Sentence:
def __iter__(self):
for word in self.words: # <1>
yield word # <2>
return # <3>
# <3>
# done! <4>

View File

@ -1,109 +0,0 @@
"""
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

@ -1,192 +0,0 @@
################ 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

@ -1,182 +0,0 @@
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

@ -1,64 +0,0 @@
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 == []

View File

@ -0,0 +1,259 @@
"""
Doctests for `parse`
--------------------
# tag::PARSE_ATOM[]
>>> from lis import parse
>>> parse('1.5')
1.5
>>> parse('ni!')
'ni!'
# end::PARSE_ATOM[]
# tag::PARSE_LIST[]
>>> parse('(gcd 18 45)')
['gcd', 18, 45]
>>> parse('''
... (define double
... (lambda (n)
... (* n 2)))
... ''')
['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]]
# end::PARSE_LIST[]
Doctest for `Environment`
-------------------------
# tag::ENVIRONMENT[]
>>> from lis import Environment
>>> outer_env = {'a': 0, 'b': 1}
>>> inner_env = {'a': 2}
>>> env = Environment(inner_env, outer_env)
>>> env['a'] = 111 # <1>
>>> env['c'] = 222
>>> env
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1})
>>> env.change('b', 333) # <2>
>>> env
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333})
# end::ENVIRONMENT[]
Doctests for `evaluate`
-----------------------
# tag::EVAL_NUMBER[]
>>> from lis import parse, evaluate, standard_env
>>> evaluate(parse('1.5'), {})
1.5
# end::EVAL_NUMBER[]
# tag::EVAL_SYMBOL[]
>>> from lis import standard_env
>>> evaluate(parse('+'), standard_env())
<built-in function add>
>>> evaluate(parse('ni!'), standard_env())
Traceback (most recent call last):
...
KeyError: 'ni!'
# end::EVAL_SYMBOL[]
# tag::EVAL_QUOTE[]
>>> evaluate(parse('(quote no-such-name)'), {})
'no-such-name'
>>> evaluate(parse('(quote (99 bottles of beer))'), {})
[99, 'bottles', 'of', 'beer']
>>> evaluate(parse('(quote (/ 10 0))'), {})
['/', 10, 0]
# end::EVAL_QUOTE[]
# tag::EVAL_IF[]
>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env())
1
>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env())
0
# end::EVAL_IF[]
# tag::EVAL_LAMBDA[]
>>> expr = '(lambda (a b) (* (/ a b) 100))'
>>> f = evaluate(parse(expr), standard_env())
>>> f # doctest: +ELLIPSIS
<lis.Procedure object at 0x...>
>>> f(15, 20)
75.0
# end::EVAL_LAMBDA[]
# tag::EVAL_DEFINE[]
>>> global_env = standard_env()
>>> evaluate(parse('(define answer (* 7 6))'), global_env)
>>> global_env['answer']
42
# end::EVAL_DEFINE[]
# tag::EVAL_DEFUN[]
>>> global_env = standard_env()
>>> percent = '(define (% a b) (* (/ a b) 100))'
>>> evaluate(parse(percent), global_env)
>>> global_env['%'] # doctest: +ELLIPSIS
<lis.Procedure object at 0x...>
>>> global_env['%'](170, 200)
85.0
# end::EVAL_DEFUN[]
function call:
# tag::EVAL_CALL[]
>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env)
42.0
# end::EVAL_CALL[]
# tag::EVAL_SYNTAX_ERROR[]
>>> evaluate(parse('(lambda is not like this)'), standard_env())
Traceback (most recent call last):
...
SyntaxError: (lambda is not like this)
# end::EVAL_SYNTAX_ERROR[]
"""
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
closure_with_change_src = """
(define (make-counter)
(define n 0)
(lambda ()
(set! n (+ n 1))
n)
)
(define counter (make-counter))
(counter)
(counter)
(counter)
"""
def test_closure_with_change():
got = run(closure_with_change_src)
assert got == 3
# tag::RUN_AVERAGER[]
closure_averager_src = """
(define (make-averager)
(define count 0)
(define total 0)
(lambda (new-value)
(set! count (+ count 1))
(set! total (+ total new-value))
(/ total count)
)
)
(define avg (make-averager))
(avg 10)
(avg 11)
(avg 15)
"""
def test_closure_averager():
got = run(closure_averager_src)
assert got == 12.0
# end::RUN_AVERAGER[]

View File

@ -1,46 +1,82 @@
#!/usr/bin/env python
################ 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
# tag::IMPORTS[]
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
from typing import Any, TypeAlias, NoReturn
Symbol: TypeAlias = str
Atom: TypeAlias = float | int | Symbol
Expression: TypeAlias = Atom | list
Environment: TypeAlias = MutableMapping[Symbol, object]
# end::IMPORTS[]
class Procedure:
"A user-defined Scheme procedure."
################ Parsing: parse, tokenize, and read_from_tokens
def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment):
self.parms = parms
self.body = body
self.env = env
def parse(program: str) -> Expression:
"Read a Scheme expression from a string."
return read_from_tokens(tokenize(program))
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
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)
################ Global Environment
# tag::ENV_CLASS[]
class Environment(ChainMap):
"A ChainMap that allows changing an item in-place."
def change(self, key: Symbol, value: object) -> None:
"Find where key is defined and change the value there."
for map in self.maps:
if key in map:
map[key] = value
return
raise KeyError(key)
# end::ENV_CLASS[]
def standard_env() -> Environment:
"An environment with some Scheme standard procedures."
env: Environment = {}
env = Environment()
env.update(vars(math)) # sin, cos, sqrt, pi, ...
env.update({
'+': op.add,
@ -60,6 +96,7 @@ def standard_env() -> Environment:
'car': lambda x: x[0],
'cdr': lambda x: x[1:],
'cons': lambda x, y: [x] + y,
'display': lambda x: print(lispstr(x)),
'eq?': op.is_,
'equal?': op.eq,
'filter': lambda *args: list(filter(*args)),
@ -78,68 +115,33 @@ def standard_env() -> Environment:
})
return env
################ Parsing: 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."
# tag::REPL[]
def repl() -> NoReturn:
"A prompt-read-eval-print loop."
global_env = standard_env()
while True:
val = evaluate(parse(input(prompt)), global_env)
ast = parse(input('lis.py> '))
val = evaluate(ast, 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)
# end::REPL[]
################ eval
################ Evaluator
# tag::EVALUATE[]
KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!']
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
match exp:
@ -149,40 +151,66 @@ def evaluate(exp: Expression, env: Environment) -> Any:
return env[var]
case []:
return []
case ['quote', exp]:
return exp
case ['quote', x]:
return x
case ['if', test, consequence, alternative]:
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
case ['lambda', [*parms], *body] if body:
return Procedure(parms, body, env)
case ['define', Symbol(var), value_exp]:
env[var] = evaluate(value_exp, env)
case ['define', [Symbol(name), *parms], *body]:
case ['define', [Symbol(name), *parms], *body] if body:
env[name] = Procedure(parms, body, env)
case ['lambda', [*parms], *body]:
return Procedure(parms, body, env)
case [op, *args]:
case ['set!', Symbol(var), value_exp]:
env.change(var, evaluate(value_exp, env))
case [op, *args] if op not in KEYWORDS:
proc = evaluate(op, env)
values = [evaluate(arg, env) for arg in args]
return proc(*values)
case _:
raise SyntaxError(repr(exp))
raise SyntaxError(lispstr(exp))
# end::EVALUATE[]
# tag::PROCEDURE[]
class Procedure:
"A user-defined Scheme procedure."
################ non-interactive execution
def __init__( # <1>
self, parms: list[Symbol], body: list[Expression], env: Environment
):
self.parms = parms # <2>
self.body = body
self.env = env
def __call__(self, *args: Expression) -> Any: # <3>
local_env = dict(zip(self.parms, args)) # <4>
env = Environment(local_env, self.env) # <5>
for exp in self.body: # <6>
result = evaluate(exp, env)
return result # <7>
# end::PROCEDURE[]
def run_lines(source: str) -> Iterator[Any]:
global_env: Environment = standard_env()
################ command-line interface
def run(source: str) -> Any:
global_env = 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
result = evaluate(exp, global_env)
return result
def main(args: list[str]) -> None:
if len(args) == 1:
with open(args[0]) as fp:
run(fp.read())
else:
repl()
if __name__ == '__main__':
import sys
main(sys.argv[1:])

View File

@ -0,0 +1,17 @@
(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)))
)
)
)
(display
(quicksort (list 2 1 6 3 4 0 8 9 7 5)))

View File

@ -140,16 +140,18 @@ def evaluate(x: Expression, env: Environment) -> Any:
(_, exp) = x
return exp
elif x[0] == 'if': # (if test conseq alt)
(_, test, conseq, alt) = x
exp = (conseq if evaluate(test, env) else alt)
return evaluate(exp, env)
elif x[0] == 'define': # (define var exp)
(_, var, exp) = x
env[var] = evaluate(exp, env)
elif x[0] == 'lambda': # (lambda (var...) body)
(_, test, consequence, alternative) = x
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
elif x[0] == 'define': # (define name exp)
(_, name, exp) = x
env[name] = evaluate(exp, env)
elif x[0] == 'lambda': # (lambda (parm…) body)
(_, parms, *body) = x
return Procedure(parms, body, env)
else: # (proc arg...)
else: # (proc arg)
proc = evaluate(x[0], env)
args = [evaluate(exp, env) for exp in x[1:]]
return proc(*args)

View File

@ -11,11 +11,11 @@ While active, the context manager reverses text output to
... print('Alice, Kitty and Snowdrop') # <2>
... print(what)
...
pordwonS dna yttiK ,ecilA # <3>
pordwonS dna yttiK ,ecilA
YKCOWREBBAJ
>>> what # <4>
>>> what # <3>
'JABBERWOCKY'
>>> print('Back to normal.') # <5>
>>> print('Back to normal.') # <4>
Back to normal.
# end::MIRROR_DEMO_1[]
@ -27,15 +27,15 @@ This exposes the context manager operation::
>>> from mirror import LookingGlass
>>> manager = LookingGlass() # <1>
>>> manager
<mirror.LookingGlass object at 0x2a578ac>
>>> manager # doctest: +ELLIPSIS
<mirror.LookingGlass object at 0x...>
>>> monster = manager.__enter__() # <2>
>>> monster == 'JABBERWOCKY' # <3>
eurT
>>> monster
'YKCOWREBBAJ'
>>> manager
>ca875a2x0 ta tcejbo ssalGgnikooL.rorrim<
>>> manager # doctest: +ELLIPSIS
>... ta tcejbo ssalGgnikooL.rorrim<
>>> manager.__exit__(None, None, None) # <4>
>>> monster
'JABBERWOCKY'
@ -69,10 +69,11 @@ The context manager can handle and "swallow" exceptions.
# tag::MIRROR_EX[]
import sys
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>
@ -81,12 +82,9 @@ class LookingGlass:
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>
sys.stdout.write = self.original_write # <7>
if exc_type is ZeroDivisionError: # <8>
print('Please DO NOT divide by zero!')
return True # <10>
# <11>
return True # <9>
# <10>
# end::MIRROR_EX[]

View File

@ -35,22 +35,36 @@ This exposes the context manager operation::
>>> manager # doctest: +ELLIPSIS
>...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc<
>>> manager.__exit__(None, None, None) # <4>
False
>>> monster
'JABBERWOCKY'
# end::MIRROR_GEN_DEMO_2[]
The decorated generator also works as a decorator:
# tag::MIRROR_GEN_DECO[]
>>> @looking_glass()
... def verse():
... print('The time has come')
...
>>> verse() # <1>
emoc sah emit ehT
>>> print('back to normal') # <2>
back to normal
# end::MIRROR_GEN_DECO[]
"""
# tag::MIRROR_GEN_EX[]
import contextlib
import sys
@contextlib.contextmanager # <1>
def looking_glass():
import sys
original_write = sys.stdout.write # <2>
def reverse_write(text): # <3>
@ -59,6 +73,4 @@ def looking_glass():
sys.stdout.write = reverse_write # <4>
yield 'JABBERWOCKY' # <5>
sys.stdout.write = original_write # <6>
# end::MIRROR_GEN_EX[]

View File

@ -35,6 +35,7 @@ This exposes the context manager operation::
>>> manager # doctest: +ELLIPSIS
>...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc<
>>> manager.__exit__(None, None, None) # <4>
False
>>> monster
'JABBERWOCKY'
@ -48,7 +49,7 @@ is handled by the context manager):
# tag::MIRROR_GEN_DEMO_3[]
>>> from mirror_gen import looking_glass
>>> from mirror_gen_exc import looking_glass
>>> with looking_glass():
... print('Humpty Dumpty')
... x = 1/0 # <1>
@ -74,13 +75,11 @@ is handled by the context manager):
# tag::MIRROR_GEN_EXC[]
import contextlib
import sys
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
@ -96,6 +95,4 @@ def looking_glass():
sys.stdout.write = original_write # <3>
if msg:
print(msg) # <4>
# end::MIRROR_GEN_EXC[]

View File

@ -36,10 +36,10 @@ async def get_flag(session: aiohttp.ClientSession, # <2>
resp.raise_for_status() # <3>
return bytes()
async def download_one(session: aiohttp.ClientSession, # <4>
async def download_one(session: aiohttp.ClientSession,
cc: str,
base_url: str,
semaphore: asyncio.Semaphore,
semaphore: asyncio.Semaphore, # <4>
verbose: bool) -> Result:
try:
async with semaphore: # <5>

View File

@ -36,9 +36,9 @@ async def get_flag(session: aiohttp.ClientSession,
return bytes()
# tag::FLAGS3_ASYNCIO_GET_COUNTRY[]
async def get_country(session: aiohttp.ClientSession, # <1>
async def get_country(session: aiohttp.ClientSession,
base_url: str,
cc: str) -> str:
cc: str) -> str: # <1>
url = f'{base_url}/{cc}/metadata.json'
async with session.get(url) as resp:
if resp.status == 200:

View File

@ -1,11 +1,13 @@
aiohttp==3.7.4
aiohttp==3.7.4.post0
async-timeout==3.0.1
attrs==20.3.0
certifi==2020.12.5
attrs==21.2.0
certifi==2021.5.30
chardet==4.0.0
idna==2.10
requests==2.25.1
urllib3==1.26.5
tqdm==4.56.2
charset-normalizer==2.0.4
idna==3.2
multidict==5.1.0
requests==2.26.0
tqdm==4.62.2
typing-extensions==3.10.0.2
urllib3==1.26.6
yarl==1.6.3

View File

@ -38,6 +38,7 @@ class SlowHTTPRequestHandler(SimpleHTTPRequestHandler):
"""Serve a GET request."""
time.sleep(.5)
if random() < self.error_rate:
# HTTPStatus.IM_A_TEAPOT requires Python >= 3.9
self.send_error(HTTPStatus.IM_A_TEAPOT, "I'm a Teapot")
else:
f = self.send_head()

View File

@ -1,25 +0,0 @@
#!/usr/bin/env python3
import shelve
from schedule_v2 import DB_NAME, CONFERENCE, load_db
from schedule_v2 import DbRecord, Event
with shelve.open(DB_NAME) as db:
if CONFERENCE not in db:
load_db(db)
DbRecord.set_db(db)
event = DbRecord.fetch('event.33950')
print(event)
print(event.venue)
print(event.venue.name)
for spkr in event.speakers:
print(f'{spkr.serial}:', spkr.name)
print(repr(Event.venue))
event2 = DbRecord.fetch('event.33451')
print(event2)
print(event2.fetch)
print(event2.venue)