sync with O'Reilly Atlas
This commit is contained in:
109
02-array-seq/lispy/examples_test.py
Normal file
109
02-array-seq/lispy/examples_test.py
Normal 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
|
||||
190
02-array-seq/lispy/lis.py
Normal file
190
02-array-seq/lispy/lis.py
Normal file
@@ -0,0 +1,190 @@
|
||||
################ 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: 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)
|
||||
return evaluate(self.body, env)
|
||||
|
||||
|
||||
################ 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
|
||||
182
02-array-seq/lispy/lis_test.py
Normal file
182
02-array-seq/lispy/lis_test.py
Normal 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
|
||||
64
02-array-seq/lispy/meta_test.py
Normal file
64
02-array-seq/lispy/meta_test.py
Normal 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 == []
|
||||
|
||||
32
02-array-seq/match_lat_lon.py
Normal file
32
02-array-seq/match_lat_lon.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
metro_lat_long.py
|
||||
|
||||
Demonstration of nested tuple unpacking::
|
||||
|
||||
>>> main()
|
||||
| latitude | longitude
|
||||
Mexico City | 19.4333 | -99.1333
|
||||
New York-Newark | 40.8086 | -74.0204
|
||||
Sao Paulo | -23.5478 | -46.6358
|
||||
|
||||
"""
|
||||
|
||||
# tag::MAIN[]
|
||||
metro_areas = [
|
||||
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
|
||||
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
|
||||
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
|
||||
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
|
||||
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
|
||||
]
|
||||
|
||||
def main():
|
||||
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
|
||||
for record in metro_areas:
|
||||
match record:
|
||||
case [name, _, _, (lat, lon)] if lon <= 0:
|
||||
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
|
||||
# end::MAIN[]
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -4,7 +4,7 @@ metro_lat_long.py
|
||||
Demonstration of nested tuple unpacking::
|
||||
|
||||
>>> main()
|
||||
| lat. | long.
|
||||
| latitude | longitude
|
||||
Mexico City | 19.4333 | -99.1333
|
||||
New York-Newark | 40.8086 | -74.0204
|
||||
Sao Paulo | -23.5478 | -46.6358
|
||||
@@ -12,7 +12,7 @@ Demonstration of nested tuple unpacking::
|
||||
"""
|
||||
|
||||
metro_areas = [
|
||||
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
|
||||
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
|
||||
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
|
||||
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
|
||||
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
|
||||
@@ -20,10 +20,10 @@ metro_areas = [
|
||||
]
|
||||
|
||||
def main():
|
||||
print(f'{"":15} | {"lat.":^9} | {"long.":^9}')
|
||||
for name, cc, pop, (latitude, longitude) in metro_areas: # <2>
|
||||
if longitude <= 0: # <3>
|
||||
print(f'{name:15} | {latitude:9.4f} | {longitude:9.4f}')
|
||||
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
|
||||
for name, _, _, (lat, lon) in metro_areas: # <2>
|
||||
if lon <= 0: # <3>
|
||||
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user