========================================== Sec. . 2.2.0.0
for e,k in enumerate(l)
========================================== Sec. . 2.2.0.0
return l.sort()
========================================== Sec. II. 20.0.0.0
#! /usr/bin/env python3
========================================== Sec. II. 20.3.0.0
long_ass_function_name(with, many, parameters)
========================================== Sec. II. 20.3.0.0
>>> long_ass_function_name(0, 0, 0)
>>> long_ass_function_name(1, 0, 0)
>>> long_ass_function_name(1, 1, 0)
...
========================================== Sec. II. 21.1.0.0
>>> help()
...

help> keywords

Here is a list of the Python keywords.  Enter any keyword to get more help.

False               def                 if                  raise
None                del                 import              return
True                elif                in                  try
and                 else                is                  while
as                  except              lambda              with
assert              finally             nonlocal            yield
break               for                 not
class               from                or
continue            global              pass

async               await               # added in Python 3.7
match               case                # added in Python 3.10
========================================== Sec. II. 21.1.0.0
>>> help(len)
Help on built-in function len in module builtins:

len(obj, /)
    Return the number of items in a container.
========================================== Sec. II. 21.1.0.0
>>> help(int)
Help on class int in module builtins:

class int(object)
 |  int(x=0) -> integer
 |  int(x, base=10) -> integer
 ...
 |  __add__(self, value, /)
 |      Return self+value.
 ...
 |  conjugate(...)
 |      Returns self, the complex conjugate of any int.
 ...
========================================== Sec. II. 21.3.0.0
n = 1 + 2

l = [1, 2, 3, 4]

n = 1 + \
    2

n = (1 +
     2)

l = [1, 2,
      3, 4]
========================================== Sec. II. 21.3.0.0
if C:     # outer block
  if D:   # inner block
    pass  # inner-inner block
  else:   # inner block
    pass  # inner-inner block
else:     # outer block
  pass    # inner black
========================================== Sec. II. 21.3.0.0
>>> if True: print("it works")

it works
========================================== Sec. II. 21.4.0.0
>>> sqrt(10)
NameError: name 'sqrt' is not defined
========================================== Sec. II. 21.4.0.0
>>> import math
>>> sqrt(10)
NameError: name 'sqrt' is not defined
========================================== Sec. II. 21.4.0.0
>>> help(math)
========================================== Sec. II. 21.4.0.0
>>> math.sqrt(10)
3.1622776601683795
>>> math.ceil(math.sqrt(10))
4
========================================== Sec. II. 21.4.0.0
>>> import math as m
>>> m.sqrt(10)
3.1622776601683795
========================================== Sec. II. 21.4.0.0
>>> from math import sqrt
>>> math.sqrt(10)
NameError: name 'math' is not defined
>>> sqrt(10)
3.1622776601683795
========================================== Sec. II. 21.4.0.0
>>> from math import sqrt as s
>>> s(10)
3.1622776601683795
========================================== Sec. II. 21.4.0.0
>>> from math import *
>>> ceil(6.7)
7
========================================== Sec. II. 21.5.1.0
>>> IdontExist
NameError: name 'IdontExist' is not defined
>>> IdontExist = "And now I do!"
>>> IdontExist
'And now I do!'
========================================== Sec. II. 21.5.1.0
>>> x= 2
>>> x
2
>>> del x
>>> x
NameError: name 'x' is not defined
========================================== Sec. II. 21.5.2.0
>>> x=1; y=2
>>> x,y = y,x
>>> print(x,y)
2 1
========================================== Sec. II. 21.5.3.0
i += 1
========================================== Sec. II. 21.5.3.0
i = i + 1
========================================== Sec. II. 21.5.4.0
def <functionname> (<arg1>, ..., <argN>):
  """optional doc string for help(<functionname>),
     IDEs, generated HTML doc,..."""
  # you do your stuff here
  return <something nice>     # optional; no return => return None
========================================== Sec. II. 21.5.4.0
def f(a,b,c):
    """ My function f
    does some cool stuff, dude. """
    print("Call f:", a,b,c)
    return a + b - c

print( f(1,2,3) )
----------------------------
Call f: 1 2 3
0
>>> help(f)
Help on function f in module __main__:

f(a, b, c)
    My function f
    does some cool stuff, dude.
========================================== Sec. II. 21.5.5.0
def f(x): print("F",x)
def g(x): print("G",x)

def do(x,f):
    f(x)

do(0,f)
do(1,g)
----------------------
F 0
G 1
========================================== Sec. II. 21.5.5.0
def hFactory(letter):
    def my_h(x):
        print(letter,x)
    return my_h

do(2, hFactory('H'))
-----------------------
H 2
========================================== Sec. II. 21.5.6.0
lambda <arg1>, ..., <argN> : <returned expression>
========================================== Sec. II. 21.5.6.0
do(3, lambda x: print("L",x))
-----------------------------
L 3
========================================== Sec. II. 21.5.6.0
def add(x,y):
    return x + y
========================================== Sec. II. 21.5.6.0
add2 = lambda x,y : x+y
========================================== Sec. II. 21.5.6.0
>>> add
<function add at 0x7f873ffaff28>
>>> add2
<function <lambda> at 0x7f873ffbf048>
========================================== Sec. II. 21.5.6.0
>>> f = lambda x: lambda y: lambda z: f"{x}{y}{z}"
>>> f(1)(2)(3)
'123'
>>> ( (f(1))(2) )(3)
'123'
========================================== Sec. II. 21.5.7.0
def <functionname> (<arg1>, ..., <argN>,
                    <optarg1> = <defval1>,..., <optargM> = <defvalM>):
========================================== Sec. II. 21.5.7.0
def g(a,b=2,c=3):
    print("Call g:", a,b,c)
    return a + b - c
-----------------------------
>>> g(0)
Call g: 0 2 3
-1
>>> g(0,20)
Call g: 0 20 3
17
>>> g(0,20,30)
Call g: 0 20 30
-10
>>> g(0,c=30)
Call g: 0 2 30
-28
>>> g(0,c=30,b=20)
Call g: 0 20 30
-10
>>> g(c=30,a=100)
Call g: 100 2 30
72
========================================== Sec. II. 21.5.7.0
>>> g(a=1,2,3)
SyntaxError: positional argument follows keyword argument
========================================== Sec. II. 21.5.8.0
def x(n): print("arg",n)
print(x(1), x(2), x(3))
------------------------
arg 1
arg 2
arg 3
None None None # see the section on None
========================================== Sec. II. 21.5.9.1
a,b,c,d = 10, 20, 30, 40
l = [1,2]
print(globals())

def fun(a):
    a = 12
    b = 21
    global c
    c = 31
    global e
    e = 51
    ee = 61
    l.append(3)
    print(a,b,c,d,e,ee,l)
    print(locals())

fun(11)
print(a,b,c,d,e,l)
print(globals())
print(ee)

------------------------

{'__name__': '__main__',..., 'a': 10, 'b': 20, 'c': 30, 'd': 40, 'l': [1, 2]}
12 21 31 40 51 61 [1, 2, 3]
{'a': 12, 'b': 21, 'ee': 61}
10 20 31 40 51 [1, 2, 3]
{'__name__': '__main__',..., 'a': 10, 'b': 20, 'c': 31, 'd': 40,
    'l': [1, 2, 3], 'fun': <function fun at 0x7fa8def98dc0>, 'e': 51}
NameError: name 'ee' is not defined. Did you mean: 'e'?
========================================== Sec. II. 21.5.9.1
>>> help(globals)
Help on built-in function globals in module builtins:

globals()
    Return the dictionary containing the current scope's global variables.

    NOTE: Updates to this dictionary *will* affect name lookups in the current
    global scope and vice-versa.
========================================== Sec. II. 21.5.9.2
a = "Am I global or local?"
def f():
    print(a)
    # a = 2

f()

---------------

Am I global or local?
========================================== Sec. II. 21.5.9.2
UnboundLocalError: local variable 'a' referenced before assignment
========================================== Sec. II. 21.5.9.3
def f ():
    a,b,c,d = "abcd"
    print(locals())
    def g():
        print(a)
        nonlocal b
        b = b + "g"
        d = "D"
        def h():
            nonlocal b
            b = b + "h"
            nonlocal c
            c = c + "h"
            nonlocal d
            print(d)
        h()

    g()
    print(locals())

f()

---------------------------------------------------

{'a': 'a', 'd': 'd', 'b': 'b', 'c': 'c'}
a
D
{'a': 'a', 'd': 'd', 'b': 'bgh', 'c': 'ch',
    'g': <function f.<locals>.g at 0x7f5502a54ee0>}
========================================== Sec. II. 21.5.9.3
    def g():
        print(locals())
        # print(a) # comment and uncomment
        ....
========================================== Sec. II. 22.0.0.0
>>> type(42)
<class 'int'>
>>> type(42.)
<class 'float'>
>>> type(True)
<class 'bool'>
>>> type("Python")
<class 'str'>
>>> type([1,2])
<class 'list'>
>>> type( (1,2,3) )
<class 'tuple'>
>>> type(None)
<class 'NoneType'>
>>> type(print("Hello")) # see section on NoneType
Hello
<class 'NoneType'>
========================================== Sec. II. 22.0.0.0
>>> x=5
>>> type(x) == int    # avoid that
True
>>> type(x) is int    # is exactly of that class
True
>>> isinstance(x,int) # is that or subclass of that
True
========================================== Sec. II. 22.1.1.0
>>> 42          # decimal
42
>>> 0xDEADBEEF  # hexadecimal
3735928559
>>> 0o18        # octal -- here the digit '8' is out of place...
SyntaxError: invalid syntax
>>> 0o15
13
========================================== Sec. II. 22.1.2.0
>>> 18 / 6
3.0
>>> 18 / 7
2.5714285714285716
>>> 18 // 7
2
>>> 18. // 7
2.0
>>> 18 % 7
4
>>> 18. % 7
4.0
========================================== Sec. II. 22.1.3.0
>>> 2**10000
1995063116880758384883742162683585083823496831886192454
8520089498529438830221946631919961684036194597899331129...
...6826949787141316063210686391511681774304792596709376
========================================== Sec. II. 22.2.1.0
>>> 3.141592
3.141592
>>> 42**69  # integer
10097201832880355573875790863214833226
  896186369872326994250398570376877433
  686009543845316266007917815719968899072
>>> 42.**69 # float; note the .
1.0097201832880356e+112
>>> 42.**-69
9.903733891340244e-113
>>> 1e14
100000000000000.0
>>> float('inf') # infinity
inf
>>> float('inf') - float('inf') # Not a Number, undefined
nan
>>> float('inf') > 10**9999 # infinity is bigger than any number
True
========================================== Sec. II. 22.2.2.0
>>> 1/3 * 6
2.0
========================================== Sec. II. 22.2.2.0
>>> 1/3 + 1/3 + 1/3 + 1/3 + 1/3 + 1/3
1.9999999999999998
========================================== Sec. II. 22.2.2.0
>>> 1.1 + 2.2 - 3.3 == 0
False
>>> 1.1 + 2.2 - 3.3
4.440892098500626e-16
>>> 1e19 + 1000 == 1e19
True
========================================== Sec. II. 22.2.2.0
>>> import sys
>>> sys.float_info.min  # smallest normalised
2.2250738585072014e-308

>>> from math import ulp
>>> ulp(0.0)            # smallest denormalised
5e-324
========================================== Sec. II. 22.2.2.0
>>> x = 1000
>>> i = 0
>>> while x != 0:
...     x /= 2
...     i+=1

>>> print(i)
1085 # steps to get to zero
========================================== Sec. II. 22.2.2.0
>>> 5e-324/2
0.0
========================================== Sec. II. 22.2.2.0
>>> 1 - .42
0.5800000000000001

>>> 1 - .42 == .58
False
========================================== Sec. II. 22.3.0.0
>>> 27.1j
27.1j
>>> type(27.1j)
<class 'complex'>
>>> 2 + 3.14j
(2+3.14j)
>>> type (2+3.14j)
<class 'complex'>
========================================== Sec. II. 22.4.1.0
>>> "This is a 'single quote' in a double"
"This is a 'single quote' in a double"
>>> 'This is a "double quote" in a single one'
'This is a "double quote" in a single one'
========================================== Sec. II. 22.4.1.0
>>> """This is a triple quoted string,
where I can write carriage returns
without problem, and use 'single'
and "double" quotes as well."""
'This is a triple quoted string,\nwhere I can write carriage
  returns\nwithout problem, and use \'single\'\nand "double"
  quotes as well.'
========================================== Sec. II. 22.4.1.0
>>> "This i" 's a string' """ in multiple bits"""
'This is a string in multiple bits'
========================================== Sec. II. 22.4.1.0
>>> "This is a \
long string."
'This is a long string.'
========================================== Sec. II. 22.4.1.0
>>> "This is a \
     long string."
'This is a      long string.'
========================================== Sec. II. 22.4.1.0
>>> ("This is a "
     "long string.")
'This is a long string.'
========================================== Sec. II. 22.4.1.0
>>> "This is a "\
    "long string."
'This is a long string.'
========================================== Sec. II. 22.4.2.0
>>> x = 21*2
>>> print("a\nb{x}c")
a
b{x}c
>>> print(r"a\nb{x}c")
a\nb{x}c
========================================== Sec. II. 22.4.2.0
>>> print(f"a\nb{x}c")
a
b42c
>>> f"Hello, x={x}, x**2={x*x}"
'Hello, x=42, x**2=1764'
========================================== Sec. II. 22.4.2.0
>>> print(rf"a\nb{x}c")
a\nb42c
========================================== Sec. II. 22.4.2.0
>>> print(b"a\nb{x}c")
b'a\nb{x}c'

>>> print(b"a\nb{x}c".hex())
610a627b787d63
========================================== Sec. II. 22.4.2.0
>>> print(rb"a\nb{x}c")
b'a\\nb{x}c'
========================================== Sec. II. 22.4.3.0
>>> x=input("? ")
? Hello ! # Here I type 'Hello !'
>>> x
'Hello !'
========================================== Sec. II. 22.4.3.0
>>> n=int(input("Number please ? "))
Number please ? 42
>>> n + 8
50
========================================== Sec. II. 22.4.4.0
>>> "b" in "abcd"
True
>>> "bc" in "abcd"
True
>>> "ac" in "abcd"
False
========================================== Sec. II. 22.4.4.0
>>> "" in "abcd"
True
>>> "" in ""
True
========================================== Sec. II. 22.4.4.0
input() in "yY"
========================================== Sec. II. 22.4.4.0
input() in list("yY")
========================================== Sec. II. 22.4.5.0
>>> len("Python")
6
========================================== Sec. II. 22.4.5.0
>>> "Python"[0]
'P'
>>> "Python"[1]
'y'
========================================== Sec. II. 22.4.6.0
>>> "Python"[1:4]
'yth'
>>> "Python"[:]
'Python'
>>> "Python"[:42] # an overly large end index is truncated
'Python'
========================================== Sec. II. 22.4.6.0
>>> "Python"[-1]
'n'
>>> "Python"[-4]
't'
>>> "Python"[1:-4]
'y'
>>> "Python"[:-4]
'Py'
========================================== Sec. II. 22.4.6.0
>>> "Python"[0:6:2]
'Pto'
>>> "Python"[::2]
'Pto'
>>> "Python"[::3]
'Ph'
>>> "Python"[1::2]
'yhn'
========================================== Sec. II. 22.4.6.0
>>> "Python"[5::-2]
'nhy'
>>> "Python"[5:1:-2]
'nh' # excludes 'y' at index 1

>>> "Python"[4::-2]
'otP'
>>> "Python"[4:0:-2]
'ot' # note that end is not implicitly zero!

>>> "Python"[4:6:-2]
''
========================================== Sec. II. 22.4.6.0
>>> "Python"[4::-2]
'otP'
>>> "Python"[5::-2]
'nhy'
>>> "Python"[6::-2]
'nhy'  # you could legitimately expect 'otP'...
>>> "Python_"[6::-2]
'_otP' # ... just ignoring out-of-bounds index 6

>>> "Python"[7::-2]
'nhy'
>>> "Python"[-6::2]
'Pto'
>>> "Python"[-7::2]
'Pto'
========================================== Sec. II. 22.4.6.0
>>> myslice = slice(3, 10, 2)
>>> myslice
slice(3, 10, 2)  # does not do much except store those values

>>> l = list(range(15)) # list of numbers 0 <= .. < 15
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

>>> l[myslice] # but can be passed as index!
[3, 5, 7, 9]
>>> l[3:10:2]
[3, 5, 7, 9]
========================================== Sec. II. 22.4.6.0
>>> "Python" + " rules!"
'Python rules!'
>>> "Python" * 6
'PythonPythonPythonPythonPythonPython'
========================================== Sec. II. 22.4.6.0
>>> s="Python"
>>> s[:2] + 'T' + s[3:]
'PyThon'
========================================== Sec. II. 22.4.6.0
>>> "Python"[::2]
'Pto'
>>> "Python"[::-1]
'nohtyP'
>>> "Python"[::-2]
'nhy'
========================================== Sec. II. 22.4.7.0
>>> chr(65)
'A'
>>> ord('A')
65
>>> ord('a')
97
>>> ord('Python')
TypeError: ord() expected a character, but string of length 6 found
========================================== Sec. II. 22.4.7.0
>>> ord("A")
65
>>> ord("A")
913
>>> "A" == "A"
False
========================================== Sec. II. 22.4.9.0
>>> 'aa' < 'aaa'
>>> 'aaa' < 'ab'
>>> 'Etudiant' < 'Prof'
>>> 'Prof' < 'etudiant'   # case matters!
========================================== Sec. II. 22.4.10.1
>>> x = 1 ; y = None
>>> print("And then", "there was", x, ",and then there were", y,".")
And then there was 1 ,and then there were None .
========================================== Sec. II. 22.4.10.1
>>> print(x,y,y,x,sep='..',end='!!')
1..None..None..1!!
========================================== Sec. II. 22.4.10.1
>>> "And then " + " there was " + x + ", and then there were " + y + "."
TypeError: must be str, not int
>>> "And then" + " there was " + str(x) + ", and then there were" + str(y) + "."
'And then  there was 1, and then there were None.'
========================================== Sec. II. 22.4.10.2
>>> "Like %.2f a %d printf %s." % (3.14159265359, 42, "dream")
'Like 3.14 a 42 printf dream.'
========================================== Sec. II. 22.4.10.2
>>> print("Like %.2f a %d printf %s." % (3.14159265359, 42, "dream"))
Like 3.14 a 42 printf dream.
========================================== Sec. II. 22.4.10.3
>>> "Like {:.2f} a {:d} printf {:s}.".format(3.14159265359, 42, "dream")
'Like 3.14 a 42 printf dream.'
========================================== Sec. II. 22.4.10.3
>>> "Like {0:.2f} a {1:d} printf {2:s}.".format(3.14159265359, 42, "dream")
'Like 3.14 a 42 printf dream.'
>>> "Like {0:.2f} a {1:d} printf {2:s} {0:.3f}.".format(
      3.14159265359, 42, "dream")
'Like 3.14 a 42 printf dream 3.142.'
========================================== Sec. II. 22.4.10.3
printf("%1$s%1$s\n", "hello");
========================================== Sec. II. 22.4.10.3
>>> "{x:d}{y}{y}{x}".format(x=0,y=1)
'0110'
========================================== Sec. II. 22.4.10.4
>>> x = 'Python'
>>> v = 3.6
>>> f"Introduced in {x} version {v}."
'Introduced in Python version 3.6.'
========================================== Sec. II. 22.4.10.4
>>> f"Before {v} was {v-0.1}, and after was {v+0.1}."
'Before 3.6 was 3.5, and after was 3.7.'
========================================== Sec. II. 22.4.10.4
>>> f"fstring {v} " "regular string"
'fstring 3.6 regular string'
========================================== Sec. II. 22.4.10.4
>>> f'Introduced in {x} version {v}.'
'Introduced in Python version 3.6.'
========================================== Sec. II. 22.4.10.4
>>> f"""Introduced in {x}
version {v}."""
'Introduced in Python\nversion 3.6.'
========================================== Sec. II. 22.4.10.4
>>> d = { 'key' : 42 }
  # this is a dictionary; a key:value association
  # see the relevant section
>>> f"Value for 'key' is {d['key']}."
"Value for 'key' is 42."
>>> f'Value for \'key\' is {d['key']}.' # string eds at ...{d['
SyntaxError: invalid syntax
>>> f'Value for \'key\' is {d[\'key\']}.'
SyntaxError: f-string expression part cannot include a backslash
========================================== Sec. II. 22.4.10.4
>>> f"""Value for 'key' or "key" is {d['key']} or {d["key"]}."""
'Value for \'key\' or "key" is 42 or 42.'
========================================== Sec. II. 22.4.10.4
>>> f"{3.1415:.2f}"     # a formated expression
'3.14'
>>> f"{{3.1415:.2f}}"   # now it's just a literal string
'{3.1415:.2f}'
>>> f"{{{3.1415:.2f}}}" # combine the two.
'{3.14}'
========================================== Sec. II. 22.4.10.4
>>> f"[{x:<20}]"
'[Python              ]'
>>> f"[{x:>20}]"
'[              Python]'
>>> f"[{x:^20}]"
'[       Python       ]'
>>> f"[{x:*^20}]"   # fill with any character, here *
'[*******Python*******]'
>>> f"[{x:#^20}]"
'[#######Python#######]'
>>> f"[{x:\^20}]"   # it looks bigger but that's because \ is escaped
                    # in the string literal below
'[\\\\\\\\\\\\\\Python\\\\\\\\\\\\\\]'
>>> print(f"[{x:\^20}]") # when printed it's the right size
[\\\\\\\Python\\\\\\\]
========================================== Sec. II. 22.4.10.4
>>> for i in range(0,10+1,2):
      print(f"{i:6d} {i**2:6d} {i**3:6d} {i**4:6d}")

     0      0      0      0
     2      4      8     16
     4     16     64    256
     6     36    216   1296
     8     64    512   4096
    10    100   1000  10000
========================================== Sec. II. 22.4.10.4
>>> x,y,z = "abc"
>>> f"{z=} {y=} {x=}"
"z='c' y='b' x='a'"
========================================== Sec. II. 22.4.10.4
lines= [ "a", "bbbbbbb", "ccc"]
maxl = max(len(l) for l in lines)
for l in lines:
    print(f"|{l:^{maxl}}|")
---------------------------------
|   a   |
|bbbbbbb|
|  ccc  |
========================================== Sec. II. 22.4.10.4
>>> db = dict()
>>> y = 2021
>>> db[f"{y}_budget"] = 42e5

>>> print(f"{y}_budget : {db[f'{y}_budget']}")
2021_budget : 4200000.0
========================================== Sec. II. 22.4.10.4
>>> import datetime
>>> today = datetime.datetime.today()
>>> today
datetime.datetime(2019, 8, 20, 11, 53, 33, 122991)
>>> print(f"{today:%B %d, %Y}")
August 20, 2019
========================================== Sec. II. 22.5.0.0
x = 42      # create a memory space encoding 42, and bind it to x!
print(42)   # display that on the standard output!
pass        # do nothing!
========================================== Sec. II. 22.5.0.0
>>> print(print(42), print(69))
42
69
None None
========================================== Sec. II. 22.6.1.0
0 < x <= y < 1
========================================== Sec. II. 22.6.1.0
0 < x and x <= y and y < 1
========================================== Sec. II. 22.6.2.0
>>> 2 in {1,3,4}
False
========================================== Sec. II. 22.6.2.0
>>> "bc" in "abc"
True
========================================== Sec. II. 22.6.2.0
>>> (2,3) in (1,2,3)
False
>>> (2,3) in (1,(2,3))
True
========================================== Sec. II. 22.6.2.0
>>> 42 == 42.
True
========================================== Sec. II. 22.6.2.0
>>> 0 == False and 1 == True
True
>>> 2 == True or 2 == False
False
>>> 2 * True + True
3
========================================== Sec. II. 22.6.2.0
>>> {True, 1, 0, False}
{0, True}
  
========================================== Sec. II. 22.6.2.0
>>> "42" == 42
False
========================================== Sec. II. 22.6.2.0
>>> "42" < 42
TypeError: '<' not supported between instances of 'str' and 'int'
========================================== Sec. II. 22.6.2.0
>>> None==None is None
True
>>> (None==None) is None
False
>>> None==(None is None)
False
========================================== Sec. II. 22.6.2.0
>>> None==None and None is None
True
========================================== Sec. II. 22.6.2.0
>>> None==None is None in {None}
True
========================================== Sec. II. 22.6.3.0
>>> reply='y'
>>> reply == 'y' or 'yes'
True
========================================== Sec. II. 22.6.3.0
>>> reply='n'
>>> reply == 'y' or 'yes'
'yes' # non-empty, therefore True
========================================== Sec. II. 22.6.3.0
reply == 'y' or reply == 'yes'
========================================== Sec. II. 22.6.3.0
reply in ('y', 'yes')
========================================== Sec. II. 22.6.4.0
def p(a,b):
    test =   a == 1 and b == 2
      # or any computation that yields True or False
    if test == True:
        return True
    else:
        return False
========================================== Sec. II. 22.6.4.0
>>> (b == True) == True
>>> ((b == True) == True) == True
>>> # you get the idea
========================================== Sec. II. 22.6.4.0
if test :
    return True
else:
    return False
========================================== Sec. II. 22.6.4.0
def p(a,b):
    return a == 1 and b == 2
========================================== Sec. II. 22.6.5.0
>>> False or 42
42
>>> True and 42
42
>>> 42 or True
42
>>> True or 42
True
>>> not 42
False
>>> not not 42
True
>>> False or 0
0
>>> [] and 42
[]
========================================== Sec. II. 22.6.5.0
>>> a=0
>>> a != 0 and 1/a > 10
False
>>> 1/a > 10 and a != 0
ZeroDivisionError: division by zero
========================================== Sec. II. 22.6.5.0
default = 42
user = input('Enter a number, or press Enter for the default: ')
nb  = user or default
print('The number is', nb, user)
========================================== Sec. II. 22.6.5.0
def x(x): print(x,end=" "); return x
========================================== Sec. II. 22.6.5.0
>>> x(10) <= x(2) <= x(3)
10 2 False

>>> x(10) <= x(2) and x(2) <= x(3)
10 2 False
========================================== Sec. II. 22.6.5.0
>>> x(1) <= x(2) <= x(3)
1 2 3 True

>>> x(1) <= x(2) and x(2) <= x(3)
1 2 2 3 True
========================================== Sec. II. 22.6.6.0
assert <condition>
assert <condition> , <optional error data or message>
========================================== Sec. II. 22.6.6.0
if __debug__:  # you can't assign to this; use -O
    if not condition: raise AssertionError #(optional_message)
========================================== Sec. II. 22.6.6.0
>>> n = 52
>>> assert n == 42
Traceback (most recent call last):
  File "<pyshell#14>", line 1, in <module>
    assert n == 42
AssertionError
========================================== Sec. II. 22.6.6.0
>>> assert n == 42, "n should be 42"
Traceback (most recent call last):
  File "<pyshell#18>", line 1, in <module>
    assert n == 42, "n should be 42"
========================================== Sec. II. 22.6.6.0
>>> assert n == 42, n
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in <module>
    assert n == 42, n
AssertionError: 52
========================================== Sec. II. 22.6.6.0
def square(n): return n**2
========================================== Sec. II. 22.6.6.0
def square(n):
    assert n >= 0
    return n**2
========================================== Sec. II. 22.6.6.0
assert square(0)   ==    0
assert square(1)   ==    1
assert square(2)   ==    4
assert square(10)  ==  100
========================================== Sec. II. 22.6.6.0
for n in range(10): assert square(n) == n * n
========================================== Sec. II. 22.6.6.0
for n in range(10): pass
========================================== Sec. II. 22.6.6.0
assert all( square(n) == n*n  for n in range(10) )
========================================== Sec. II. 22.6.6.0
def square(n): return 8 if n==3 else n*n

assert all( (es:=square(en:=n)) == (ee:=n*n)
            for n in range(10) ), (en, es, ee)

----------------------------------------------

AssertionError: (3, 8, 9)
========================================== Sec. II. 22.6.7.0
def f(x): return 1/2 * (x**2 + 2)
========================================== Sec. II. 22.6.7.0
assert isinstance(x, float)
========================================== Sec. II. 22.6.7.0
assert isinstance(x, (int, float))
========================================== Sec. II. 22.6.7.0
x = np.linspace(-2,2,100) # plot between -2, and 2, with 100 samples
plot(x, f(x),...)
========================================== Sec. II. 22.6.7.0
>>> x = np.linspace(-1,2,4)

>>> x
array([-1.,  0.,  1.,  2.])

>>> f(x)
array([1.5, 1. , 1.5, 3. ])

>>> print(x, f(x))
[-1.  0.  1.  2.] [1.5 1.  1.5 3. ]

>>> type(x)
<class 'numpy.ndarray'>
========================================== Sec. II. 22.6.7.0
>>> x**2
array([1., 0., 1., 4.])

>>> 10*x
array([-10.,   0.,  10.,  20.])

>>> x+x
array([-2.,  0.,  2.,  4.])
========================================== Sec. II. 22.6.7.0
assert isinstance(x, (int, float))
========================================== Sec. II. 22.6.7.0
from decimal import Decimal
from fractions import Fraction

>>> f(Decimal(1)/Decimal(3))
TypeError: unsupported operand type(s) for *: 'float' and 'decimal.Decimal'
========================================== Sec. II. 22.6.7.0
def f(x): return (x**2 + 2) / 2
========================================== Sec. II. 22.6.7.0
>>> f(Decimal(1)/Decimal(3))
Decimal('1.055555555555555555555555556')
========================================== Sec. II. 22.6.7.0
>>> f(Fraction('1/3'))
Fraction(19, 18)
========================================== Sec. II. 22.6.8.0
assert (False, "It's a trap!")
========================================== Sec. II. 22.6.8.0
Warning (from warnings module):
SyntaxWarning: assertion is always true, perhaps remove parentheses?
========================================== Sec. II. 22.6.8.0
(False, "It's a trap!")
========================================== Sec. II. 23.1.0.0
[mathescape=true]
if <$c_1$>:
    <execute if $c_1$>
elif <$c_2$>:
    <execute if $\neg c_1 \land c_2$>
...
elif <$c_n$>:
    <execute if $\Und k1{n-1} \neg c_k \w\land c_n$>
else:
    <execute if $\Und k1{n} \neg c_k$>
========================================== Sec. II. 23.1.0.0
if test():
    instr1()
else:
    instr2()
========================================== Sec. II. 23.1.0.0
if test():
    instr1()
elif not(test()):
    instr2()
========================================== Sec. II. 23.2.0.0
[mathescape=true]
$<\xlbl v{true}>$ if $<B>$ else $<\xlbl v{false}>$
========================================== Sec. II. 23.2.0.0
>>> (1 if False else 2) **2
4
========================================== Sec. II. 23.2.0.0
return 1 if True else 2
========================================== Sec. II. 23.2.0.0
if True:
  return 1
else:
  return 2
========================================== Sec. II. 23.2.0.0
return 1 if True else return 2
========================================== Sec. II. 23.2.0.0
def default(x,y): return  1 if x else  2  if y else 3
def left   (x,y): return (1 if x else  2) if y else 3
def right  (x,y): return  1 if x else (2  if y else 3)

for x in (0,1):
    for y in (0,1):
        print(x,y,"  ", default(x,y), left(x,y), right(x,y))

------------------------------------------------------------

0 0    3 3 3
0 1    2 2 2
1 0    1 3 1
1 1    1 1 1
========================================== Sec. II. 23.2.0.0
>>> (str.lower if True  else str.upper)("Abacus")
'abacus'

>>> (str.lower if False else str.upper)("Abacus")
'ABACUS'
========================================== Sec. II. 23.3.0.0
while <condition>:
    <intructions block>
========================================== Sec. II. 23.4.0.0
for k in range(0,10,2):
    print(k, end=' ')
-----------------------
0 2 4 6 8
========================================== Sec. II. 23.4.0.0
for k in range(10,0,-2):
    print(k, end=' ')
------------------------
10 8 6 4 2
========================================== Sec. II. 23.4.0.0
s = "Python"
for k in range(len(s)):
    print(s[k], end=' ')
------------------------
P y t h o n
========================================== Sec. II. 23.4.0.0
>>> range(10)
range(0, 10)
>>> type(range(7))
<class 'range'>
========================================== Sec. II. 23.4.0.0
for <var> in <collection>
    <block in which var takes the value...
    ... of an element of the collection. >
========================================== Sec. II. 23.4.0.0
for c in "Python":
    print(c, end=' ')
---------------------
P y t h o n
========================================== Sec. II. 23.4.0.0
>>> c
'n'
========================================== Sec. II. 23.4.0.0
>>> l = [ (n, n**2) for n in range(5) ]

>>> l
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)]

>>> for x,y in l:
      print(f"{x} -> {y}   ", end='')

0 -> 0   1 -> 1   2 -> 4   3 -> 9   4 -> 16
========================================== Sec. II. 23.4.0.0
>>> list(enumerate("Python"))
[(0, 'P'), (1, 'y'), (2, 't'), (3, 'h'), (4, 'o'), (5, 'n')]

>>> for k,c in enumerate("Python"):
      print(f"{k}:{c}  ",end='')

0:P  1:y  2:t  3:h  4:o  5:n
========================================== Sec. II. 23.4.0.0
>>> for x,y,z in zip([1,2,3], "abcd", "XYZ"):
...     print(x,y,z)

1 a X
2 b Y
3 c Z
========================================== Sec. II. 23.4.0.0
>>> for x in reversed(range(3)):
...     print(x)

2
1
0
========================================== Sec. II. 23.4.0.0
for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'equals', x, '*', n//x)
            break # found a factor
    else:# if no break
         # loop fell through without finding a factor
        print(n, 'is a prime number')
-----------------------------------------------------
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
========================================== Sec. II. 23.5.0.0
try:
  <code that may fail>

except <ExceptionName1> :
  <what to do if this exception is raised>

except <ExceptionName2> as <var>: # as is optional,
  <what to do in that case>       # exception is bound as <var>

except (<Ex3, Ex4, ...):          # catch any of those
  <what to do in that case>

except :                          # catches all other exceptions
  <what to do in that case>

else:                             # optional
  <executes if the try block does not raise any exception>

finally: # optional
  <always executes after, regardless of exceptions and breaks>
========================================== Sec. II. 23.5.0.0
n = None
while n is None:
  try:
      n = int(input("Enter a number: "))
  except ValueError:
      print("Invalid number. Retry.")
========================================== Sec. II. 23.5.0.0
except Exception as e: print(repr(e))
========================================== Sec. II. 23.6.1.0
match expr:
    case pattern1: <execute if expr matches pattern1>
    case pattern2: <execute if expr matches pattern2 but not 1>
    ...
    case patternN: <execute if expr matches patternN but not 1..N-1>
========================================== Sec. II. 23.6.1.0
>>> def = 7
SyntaxError: invalid syntax
>>> if = 7
SyntaxError: invalid syntax
>>> match = 1
>>> match
1
========================================== Sec. II. 23.6.2.0
    match x:
        case 0:                         return "Ze Zero or Neo"
        case 1 | -1:                    return "Neo or Negative Neo"
        case int():                     return "integer != 0"
        case "INSA":                    return "lotta homework"
        case str() as s:                return f"a string '{s}'"
        case "a", 1, 3.0:               return "a very specific sequence"
        case [x, y, z]:                 return f"3 element sequence {x}-{y}-{z}"
        case [1|2 as x, 3|4 as y] as l: return f"or/as {x} {y} {l}"
        case x, [*l], y, z:             return f"4 elem seq, 2nd is seq {l}"
        case x, *rest:                  return f"at least 1 element {x}:{rest}"
        case {1:v, 2:V, 3:x} if x==v+V: return f"dict 123"
        case {2:v, **r}:                return f"dict 2 -> {v}; {r}"
        case {3:8}:                     return "38"
        case _:                         return "who knows?"

for x in [-1, 0, 1, 2,
          "INSA", "Meh",
          (1,2,3), ["a", 1, 3.0], ("a", 1, 3.0), {"a", 1, 3.0},
          [1, 3], [1, 4], [3, 1],
          (1,), [1,2], [1,2,3], [1,2,3,4], [1, 2, [3, 4], 5],
          [1, [2, 3], 4, 5], [1, (), 4, 5],
          {1, 2, 3}, {1:"a", 2:"b", 3:"c"},
          {1:4, 2:3, 3:7}, {1:4, 2:3, 3:8}, {1:4, 3:8},
          ]:
    print(f"{repr(x):>30} -> {match(x)}")

------------------------------------------------------------------------------

                            -1 -> Neo or Negative Neo
                             0 -> Ze Zero or Neo
                             1 -> Neo or Negative Neo
                             2 -> integer != 0
                        'INSA' -> lotta homework
                         'Meh' -> a string 'Meh'
                     (1, 2, 3) -> 3 element sequence 1-2-3
                 ['a', 1, 3.0] -> a very specific sequence
                 ('a', 1, 3.0) -> a very specific sequence
                 {1, 3.0, 'a'} -> who knows?
                        [1, 3] -> or/as 1 3 [1, 3]
                        [1, 4] -> or/as 1 4 [1, 4]
                        [3, 1] -> at least 1 element 3:[1]
                          (1,) -> at least 1 element 1:[]
                        [1, 2] -> at least 1 element 1:[2]
                     [1, 2, 3] -> 3 element sequence 1-2-3
                  [1, 2, 3, 4] -> at least 1 element 1:[2, 3, 4]
             [1, 2, [3, 4], 5] -> at least 1 element 1:[2, [3, 4], 5]
             [1, [2, 3], 4, 5] -> 4 elem seq, 2nd is seq [2, 3]
                 [1, (), 4, 5] -> 4 elem seq, 2nd is seq []
                     {1, 2, 3} -> who knows?
      {1: 'a', 2: 'b', 3: 'c'} -> dict 2 -> b; {1: 'a', 3: 'c'}
            {1: 4, 2: 3, 3: 7} -> dict 123
            {1: 4, 2: 3, 3: 8} -> dict 2 -> 3; {1: 4, 3: 8}
                  {1: 4, 3: 8} -> 38
========================================== Sec. II. 23.6.2.0
                             0 -> Ze Zero or Neo
========================================== Sec. II. 23.6.2.0
                            -1 -> Neo or Negative Neo
                             1 -> Neo or Negative Neo
========================================== Sec. II. 23.6.2.0
                             2 -> integer != 0
========================================== Sec. II. 23.6.2.0
    case int:                     return "integer != 0"
         ^^^
SyntaxError: name capture 'int' makes remaining patterns unreachable
========================================== Sec. II. 23.6.2.0
>>> isinstance(True, int)
True
>>> match True:
...     case int(): print("yes")

yes
========================================== Sec. II. 23.6.2.0
                        'INSA' -> lotta homework
                         'Meh' -> a string 'Meh'
========================================== Sec. II. 23.6.2.0
        case "a", 1, 3.0:   return "a very specific sequence"
        case [x, y, z]:     return f"3 element sequence {x}-{y}-{z}"
        case _:             return "who knows?"
--------------------------------------------------------------------
                     (1, 2, 3) -> 3 element sequence 1-2-3
                 ['a', 1, 3.0] -> a very specific sequence
                 ('a', 1, 3.0) -> a very specific sequence
                 {1, 3.0, 'a'} -> who knows?
========================================== Sec. II. 23.6.2.0
>>> match 1:
...     case {"a", 1, 3.0}: pass
...
SyntaxError: invalid syntax
========================================== Sec. II. 23.6.2.0
        case [x, y, z]:                 return f"3 element sequence {x}-{y}-{z}"
        case [1|2 as x, 3|4 as y] as l: return f"or/as {x} {y} {l}"
        case x, [*l], y, z:             return f"4 elem seq, 2nd is seq {l}"
        case x, *rest:                  return f"at least 1 element {x}:{rest}"
-------------------------------------------------------------------------------
                        [1, 3] -> or/as 1 3 [1, 3]
                        [1, 4] -> or/as 1 4 [1, 4]
                        [3, 1] -> at least 1 element 3:[1]
                          (1,) -> at least 1 element 1:[]
                        [1, 2] -> at least 1 element 1:[2]
                     [1, 2, 3] -> 3 element sequence 1-2-3
                  [1, 2, 3, 4] -> at least 1 element 1:[2, 3, 4]
             [1, 2, [3, 4], 5] -> at least 1 element 1:[2, [3, 4], 5]
             [1, [2, 3], 4, 5] -> 4 elem seq, 2nd is seq [2, 3]
========================================== Sec. II. 23.6.2.0
        case {1:v, 2:V, 3:x} if x==v+V: return f"dict 123"
        case {2:v, **r}:                return f"dict 2 -> {v}; {r}"
        case {3:8}:                     return "38"
        case _:                         return "who knows?"
---------------------------------------------------------------
                     {1, 2, 3} -> who knows?
      {1: 'a', 2: 'b', 3: 'c'} -> dict 2 -> b; {1: 'a', 3: 'c'}
            {1: 4, 2: 3, 3: 7} -> dict 123
            {1: 4, 2: 3, 3: 8} -> dict 2 -> 3; {1: 4, 3: 8}
                  {1: 4, 3: 8} -> 38
========================================== Sec. II. 23.6.2.0
>>> {a:b} = {1:2}
SyntaxError: cannot assign to dict literal here. Maybe you meant '==' instead of
'='?
========================================== Sec. II. 23.6.2.0
>>> match {"name":"Toto", "phones":[123,911], "sex":"safe"}:
...     case {"name":n, "phones":[p,*r]}: print(n,p)
...
Toto 123
========================================== Sec. II. 23.6.3.0
def cmdmatch(c):
    ops = {"cp":"copy", "mv":"move"}
    match c.split():
        case ["cp"|"mv" as c, *options, source, target]:
            for o in options:
                match o:
                    case "-v": print("I'm verbose")
                    case "-i": print("I'm interactive")
                    case _   : raise ValueError(o)
            print(f"I {ops[c]} {source} to {target}")
        case ["cp"|"mv" as c, *r]:
            raise TypeError(f"{ops[c]} needs at least 2 arguments")

-------------------------------------------------------------------

>>> cmdmatch("cp -i -v toto tata")
I'm interactive
I'm verbose
I copy toto to tata

>>> cmdmatch("mv toto tata")
I move toto to tata

>>> cmdmatch("mv -x toto tata")
ValueError: -x

>>> cmdmatch("mv toto")
TypeError: move needs at least 2 arguments
========================================== Sec. II. 24.1.0.0
>>> t = (1, "toto", 3.14) # note the heterogeneous types
>>> t
(1, 'toto', 3.14)
>>> type(t)
<class 'tuple'>
========================================== Sec. II. 24.1.0.0
>>> t = 1, "toto", 3.14
>>> t
(1, 'toto', 3.14)
========================================== Sec. II. 24.1.0.0
>>> t[1]
'toto'
>>> t[:2]
(1, 'toto')
>>> t[:-1]
(1, 'toto')
>>> t[:0]
()    # empty tuple
>>> t[:1]
(1,)  # singleton tuple
========================================== Sec. II. 24.1.0.0
>>> t = (1, (2,3) , 4)
>>> t[1][0]
2
========================================== Sec. II. 24.1.0.0
>>> (1,2) + (3,4)
(1, 2, 3, 4)
>>> (1,2) * 5
(1, 2, 1, 2, 1, 2, 1, 2, 1, 2)
========================================== Sec. II. 24.1.0.0
>>> t[0]=8
TypeError: 'tuple' object does not support item assignment
========================================== Sec. II. 24.1.0.0
>>> (8,) + t[1:]
(8, (2, 3), 4)
========================================== Sec. II. 24.1.0.0
>>> (a, (b,c), d) = (1, (2, 3), 4)
>>> print (a, b, c, d)
1 2 3 4

>>> (a, *m, b) = (1, 2, 3, 4)
>>> a, b, m
(1, 4, [2, 3])
========================================== Sec. II. 24.1.0.0
>>> tuple("Python")
('P', 'y', 't', 'h', 'o', 'n')
========================================== Sec. II. 24.1.0.0
>>> tuple(1)
TypeError: 'int' object is not iterable
========================================== Sec. II. 24.1.0.0
for x in (1,2,3):
  print(x,end='')
---------------------
123
========================================== Sec. II. 24.1.0.0
>>> 2 in (1,3,4)
False
>>> 3 in (1,3,4)
True
========================================== Sec. II. 24.1.0.0
>>> len( (1,2,3) )
3
>>> len(1,2,3) # don't forget the parentheses!
TypeError: len() takes exactly one argument (3 given)
========================================== Sec. II. 24.1.0.0
>>> t = (1,2,3)
>>> t == (1,2,3)
True
>>> (1, 2, 3) == (2, 1, 3)
False
>>> (1,2) == (1,2,3)
False
========================================== Sec. II. 24.1.0.0
>>> 1,2 == 1,2,3
(1, False, 2, 3)
========================================== Sec. II. 24.1.0.0
>>> (1,2,3) is (1,2,3)
False
========================================== Sec. II. 24.1.0.0
>>> 3 is 3
True
>>> "abc" is "abc"
True
========================================== Sec. II. 24.1.0.0
>>> (1,2) < (1,2,3)
True
>>> (2,2) < (1,2,3)
False
========================================== Sec. II. 24.1.0.0
>>> (2,3) < ("a str",1,2,3)
TypeError: '<' not supported between instances of 'int' and 'str'
========================================== Sec. II. 24.1.0.0
>>> (2,3) < (1,2,3,"a str")
False
========================================== Sec. II. 24.2.0.0
>>> l = [1, "toto", 3.14]
>>> type(l)
<class 'list'>
========================================== Sec. II. 24.2.0.0
>>> l[1] = True
>>> l
[1, True, 3.14]
========================================== Sec. II. 24.2.0.0
>>> l = [0, 1, 2, 3, 4]
>>> del l[2]
>>> l
[0, 1, 3, 4]
========================================== Sec. II. 24.2.0.0
>>> l = [0, 1, 2, 3, 4]
>>> del l[1], l[3]
>>> l
[0, 2, 3]
========================================== Sec. II. 24.2.0.0
>>> l = [1, 2, 3]
>>> l[1:] = list("abc")  # or simply l[1:] = "abc", any iterable will do
>>> l
[1, 'a', 'b', 'c']
========================================== Sec. II. 24.2.0.0
>>> l[1:3] = (1,2)
>>> l
[1, 1, 2, 'c']
>>> l[1:3] = range(5)
>>> l
[1, 0, 1, 2, 3, 4, 'c']
>>> l[1:3] = 5
TypeError: can only assign an iterable
========================================== Sec. II. 24.2.0.0
>>> l = list(range(5))
>>> l
[0, 1, 2, 3, 4]
>>> l[1:1] = list("abc")
>>> l
[0, 'a', 'b', 'c', 1, 2, 3, 4]
>>> l[1:2] = list("xyz")
>>> l
[0, 'x', 'y', 'z', 'b', 'c', 1, 2, 3, 4]
========================================== Sec. II. 24.2.0.0
>>> l[1] = list("xyz")
>>> l
[0, ['x', 'y', 'z'], 'y', 'z', 'b', 'c', 1, 2, 3, 4]
========================================== Sec. II. 24.2.1.0
>>> student = ("Julius", "Caesar", 55)
========================================== Sec. II. 24.2.1.0
>>> student[2]
55
========================================== Sec. II. 24.2.1.0
>>> from sys import getsizeof
>>> help(getsizeof)

getsizeof(...)
    getsizeof(object, default) -> int
    Return the size of object in bytes.

>>> t = tuple(range(10))
>>> l = list(range(10))
>>> getsizeof(t)
128
>>> getsizeof(l)
200
========================================== Sec. II. 24.2.2.1
>>> l = list(range(5))
>>> l
[0, 1, 2, 3, 4]

>>> m = l
>>> m
[0, 1, 2, 3, 4]

>>> m[1] = "Hello"

>>> m
[0, 'Hello', 2, 3, 4]
>>> l

[0, 'Hello', 2, 3, 4]

>>> m is l  # test whether they point to the same memory address
True
========================================== Sec. II. 24.2.2.2
>>> l = list(range(5))
>>> ll = [l,l]
>>> ll
[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]
>>> ll[0][2] = "X"
>>> ll
[[0, 1, 'X', 3, 4], [0, 1, 'X', 3, 4]]
========================================== Sec. II. 24.2.2.2
n = 5
>>> M = [ [0] * n ] * n
>>> mprint(M) # a matrix printing function I defined.
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
========================================== Sec. II. 24.2.2.2
>>> M[2][1] = 1
>>> mprint(M)
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
========================================== Sec. II. 24.2.2.2
>>> M = [ [0] * n  for _ in range(n) ]
>>> M[2][1] = 1
>>> mprint(M)
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
========================================== Sec. II. 24.2.2.3
>>> l = [1,2] ; m = ['a', 'b'] ; oldl = l
>>> l += m
>>> l
[1, 2, 'a', 'b']
>>> oldl
[1, 2, 'a', 'b']
========================================== Sec. II. 24.2.2.3
l = [1,2] ; m = ['a', 'b'] ; oldl = l

>>> l = l + m
>>> l
[1, 2, 'a', 'b']

>>> oldl
[1, 2]
========================================== Sec. II. 24.2.2.4
>>> il=[1]
>>> il.append(il)

>>> il
[1, [...]]

>>> len(il)
2
========================================== Sec. II. 24.2.2.4
[1, [1, [1, [1, .... ]... ]]]
========================================== Sec. II. 24.2.2.4
def recf(l):
    print(end='[')
    for e in l:
        if type(e) is list:
            recf(e)
        else:
            print(e,end='; ')
    print(end='] ')
-----------------------------
>>> recf([1,2,3])
[1; 2; 3; ]

>>> recf([1,list('abc'),3])
[1; [a; b; c; ] 3; ]

>>> recf(il)
[1; [1; [1; [1; [1; [1; [1; [1; [1;
KeyboardInterrupt  # I interrupted the program.
========================================== Sec. II. 24.2.3.0
>>> l = list(range(5))
>>> ll = list(l)
>>> l is ll # they are indeed different objects in memory
False
>>> l[2] = 'X'
>>> l,ll
([0, 1, 'X', 3, 4], [0, 1, 2, 3, 4])
========================================== Sec. II. 24.2.3.0
>>> n = 5
>>> M = [ [0] * n  for _ in range(n) ]
>>> MM = list(M)
>>> M is MM
False
>>> M[2][1] = 1
>>> mprint(M)
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

>>> mprint(MM)
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 0, 0, 0]  # what?
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
========================================== Sec. II. 24.2.3.0
>>> help(id)

id(obj, /)
    Return the identity of an object.

    This is guaranteed to be unique among simultaneously existing objects.
    (CPython uses the object`s memory address.)

>>> [ id(e) for e in M ]  # list comprehension
[140180209944968, 140180127633608, 140180210018824,
  140180210019208, 140180209944520]
>>> [ id(e) for e in MM ]
[140180209944968, 140180127633608, 140180210018824,
  140180210019208, 140180209944520]
========================================== Sec. II. 24.2.3.0
>>> M = [ [0] * n  for _ in range(n) ]
>>> from copy import deepcopy
>>> MM = deepcopy(M)
>>> M[2][1] = 1
>>> mprint(M)
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

>>> mprint(MM)
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

>>> [ id (e) for e in M ]
[140180233630728, 140180127751240, 140180127750856,
  140180127750664, 140180127751368]
>>> [ id (e) for e in MM ]
[140180127749320, 140180165209800, 140180127751560,
  140180127751752, 140180127751944]
========================================== Sec. II. 24.2.4.0
l = [1,2,3,2,2,1]

def remove_bad(l,bad):
    for i in range(len(l)):
        print(i,l) # for debugging purposes
        if l[i] == bad:
            l.remove(l[i])  # removes the first occurrence, in-place

print(remove_bad(l,2))

---------------------------
0 [1, 2, 3, 2, 2, 1]
1 [1, 2, 3, 2, 2, 1]
2 [1, 3, 2, 2, 1]   # we have successfully removed the first 2
3 [1, 3, 2, 1]      # and another
4 [1, 3, 2, 1]
IndexError: list index out of range
    ... in remove_bad, if l[i] == bad:
========================================== Sec. II. 24.2.5.0
>>> l = [2, 7, 4, 0, -6]
>>> sorted(l)
[-6, 0, 2, 4, 7]
>>> l
[2, 7, 4, 0, -6]
========================================== Sec. II. 24.2.5.0
>>> print(l.sort())
None
>>> l
[-6, 0, 2, 4, 7]
========================================== Sec. II. 24.2.5.0
>>> l = [2, 7, 4, 0, -6]

>>> sorted(l,reverse=True)
[7, 4, 2, 0, -6]

>>> l.sort(reverse=True)
>>> l
[7, 4, 2, 0, -6]
========================================== Sec. II. 24.2.5.0
>>> l = ['School', 'Platypus', 'Sleep']

>>> sorted(l) # the usual order
['Platypus', 'School', 'Sleep']

>>> sorted(l,key=len)
['Sleep', 'School', 'Platypus']

>>> sorted(l,key=lambda s:s[2])
['Platypus', 'Sleep', 'School']
========================================== Sec. II. 24.3.0.0
>>> s = set(range(5)) ; ss = set(range(3,8))
>>> s, ss
({0, 1, 2, 3, 4}, {3, 4, 5, 6, 7})

>>> 1 in s , 7 in s
(True, False)

>>> s & ss    # intersection
{3, 4}
>>> s | ss    # union
{0, 1, 2, 3, 4, 5, 6, 7}
>>> s - ss    # difference
{0, 1, 2}
>>> s ^ ss    # symmetric difference
{0, 1, 2, 5, 6, 7}

>>> {}          # you can't define an empty set that way
{}
>>> type({})
<class 'dict'>  # it's actually the empty dictionary
>>> set()       # this is how you make an empty set
set()

>>> s <= ss , s >= ss
(False, False)  # inclusion is of course a *partial* order
>>> set() <= s  # empty set is smaller than everybody
True
========================================== Sec. II. 24.3.0.0
>>> s[2]
TypeError: 'set' object does not support indexing
========================================== Sec. II. 24.3.0.0
>>> for e in s:
  print(e,end='')

01234  # iteration follows the same order as display

>>> { 'Python', 'abba', 'ABBA', 'A','a'}
{'a', 'ABBA', 'A', 'Python', 'abba'}

>>> { 'Python', 'abba', 'ABBA', 'A'}
{'A', 'ABBA', 'Python', 'abba'} # order has changed

>>> { 10010, 1, 86}
{1, 10010, 86} # order does not correspond to order on elements
========================================== Sec. II. 24.3.0.0
>>> t = (1,2)
>>> tt = (1,2)
>>> t is tt
False
>>> { t, tt }
{(1, 2)}
========================================== Sec. II. 24.3.0.0
>>> { 0, False, True, 1 }
{0, True}
========================================== Sec. II. 24.3.0.0
>>> { 1, 2, { 4, 5 } , 3 }
TypeError: unhashable type: 'set'
========================================== Sec. II. 24.3.0.0
>>> l = [2, 1, 2, 1, 1, 1, 3, 3, 2, 2]

>>> list(set(l))
[1, 2, 3]
========================================== Sec. II. 24.3.0.0
>>> l = [[1], [2], [2], [1], [1], [1], [3], [1], [2], [1]]

>>> list(set(l))
TypeError: unhashable type: 'list'

>>> [ e for k,e in enumerate(l) if e not in l[:k] ]
[[1], [2], [3]]
========================================== Sec. II. 24.3.1.0
>>> S = frozenset(s) ; SS = frozenset(ss)
>>> S, SS
(frozenset({0, 1, 2, 3, 4}), frozenset({3, 4, 5, 6, 7}))
>>> s == S, ss == SS
(True, True)
========================================== Sec. II. 24.3.1.0
>>> { S, SS }
{frozenset({0, 1, 2, 3, 4}), frozenset({3, 4, 5, 6, 7})}
========================================== Sec. II. 24.4.0.0
>>> {}
{}
>>> type({})
<class 'dict'>
>>> dict() == {}
True
========================================== Sec. II. 24.4.0.0
>>> age = {'Toto':15, 'Tata':27, 'Mamie':97 }
>>> age
{'Toto': 15, 'Tata': 27, 'Mamie': 97}
========================================== Sec. II. 24.4.0.0
>>> dict( [ ('Toto', 15), ('Tata', 27), ('Mamie', 97 ) ] )
{'Toto': 15, 'Tata': 27, 'Mamie': 97}
========================================== Sec. II. 24.4.0.0
>>> {'Toto': 15, 'Tata': 27, 'Mamie': 97, 'Toto':99}
{'Toto': 99, 'Tata': 27, 'Mamie': 97}
========================================== Sec. II. 24.4.0.0
>>> age['Tata']
27
========================================== Sec. II. 24.4.0.0
>>> age[1:3]
TypeError: unhashable type: 'slice'
========================================== Sec. II. 24.4.0.0
>>> age['IDONTEXIST!']
KeyError: 'IDONTEXIST!'
========================================== Sec. II. 24.4.0.0
>>> age['IDONTEXIST!'] = "Well, *now*, I do!"
>>> age
{'Toto': 15, 'Tata': 27, 'Mamie': 97, 'IDONTEXIST!': 'Well, *now*, I do!'}

>>> age['IDONTEXIST!']
'Well, *now*, I do!'

>>> del age['IDONTEXIST!']
>>> age
{'Toto': 15, 'Tata': 27, 'Mamie': 97}

>>> age['IDONTEXIST!']
KeyError: 'IDONTEXIST!'

>>> del age['IDONTEXIST!']
KeyError: 'IDONTEXIST!'
========================================== Sec. II. 24.4.0.0
>>> { age : 21 }
TypeError: unhashable type: 'dict'
>>> { 21 : age }
{21: {'Toto': 15, 'Tata': 27, 'Mamie': 97}}
========================================== Sec. II. 24.4.0.0
>>> for k in age:
      print(k,age[k])

Toto 15
Tata 27
Mamie 97
========================================== Sec. II. 24.4.0.0
>>> len(age)
3
========================================== Sec. II. 24.4.0.0
>>> 'Toto' in age
True
>>> 'toto' in age
False
========================================== Sec. II. 24.4.0.0
>>> age.items()
dict_items([('Toto', 15), ('Tata', 27), ('Mamie', 97)])

>>> for k,v in age.items():
      print(k,v)

Toto 15
Tata 27
Mamie 97
========================================== Sec. II. 24.4.0.0
>>> age.values()
dict_values([15, 27, 97])
========================================== Sec. II. 24.4.0.0
>>> {'Toto': 15, 'Tata': 99} | {'Tata': 27, 'Mamie': 97}
{'Toto': 15, 'Tata': 27, 'Mamie': 97}
========================================== Sec. II. 24.4.0.0
>>> d = {'Toto': 15}
>>> d |= { 'Tata': 27 }
>>> d
{'Toto': 15, 'Tata': 27}
>>> d |= [ ("Mamie", 97) ]
>>> d
{'Toto': 15, 'Tata': 27, 'Mamie': 97}
========================================== Sec. II. 24.4.0.0
>>> {'Toto': 15, 'Tata': 27} | [ ("Mamie", 97) ]
TypeError: unsupported operand type(s) for |: 'dict' and 'list'
========================================== Sec. II. 24.4.0.0
>>> age.keys()
dict_keys(['Toto', 'Tata', 'Mamie'])

>>> age & {'Toto', 'xx'}
TypeError: unsupported operand type(s) for &: 'dict' and 'set'

>>> age.keys() & {'Toto', 'xx'}
{'Toto'}
========================================== Sec. II. 24.4.0.0
>>> {'Toto': [] }.items() & age.items()
TypeError: unhashable type: 'list'

>>> {'Toto': 15 }.items() & age.items()
{('Toto', 15)}
========================================== Sec. II. 24.4.0.0
>>> K = age.keys()
>>> age['Banana'] = 10
>>> K
dict_keys(['Toto', 'Tata', 'Mamie', 'Banana'])

>>> del age['Banana']
>>> K
dict_keys(['Toto', 'Tata', 'Mamie'])
========================================== Sec. II. 24.4.0.0
>>> {'Toto': 15 } <= age
TypeError: '<=' not supported between instances of 'dict' and 'dict'
========================================== Sec. II. 24.4.0.0
>>> {'Toto': 15 }.items() <= age.items()
True
>>> {'Toto': [] }.items() <= age.items()
False  # this works, despite [] not being hashable
========================================== Sec. II. 24.4.0.0
>>> age.values() == age.values()
False # == always returns False on that view
========================================== Sec. II. 24.4.0.0
>>> set(age)
{'Toto', 'Mamie', 'Tata'}
>>> set(age.items())
{('Tata', 27), ('Mamie', 97), ('Toto', 15)}
>>> set(age.values())
{97, 27, 15}
>>> set({'Tata': 27 }.items()) <= set(age.items())
True
========================================== Sec. II. 24.4.1.1
s = "AABaaBAAA"
========================================== Sec. II. 24.4.1.1
{'A': 5, 'B': 2, 'a': 2}
========================================== Sec. II. 24.4.1.1
def count(s):
    d = {}
    for c in s:
        d[c] += 1
    return d
----------------------
Traceback in d[c] += 1
  KeyError: 'A'
========================================== Sec. II. 24.4.1.1
def count(s):
    d = {}
    for c in s:
        if c in d:
            d[c] += 1
        else:
            d[c] = 1
    return d
========================================== Sec. II. 24.4.1.1
>>> age.get('Toto')  # this item exists
15
>>> age.get('XXX')   # this one does not
None # here I wrote it explicitly;
     # the interactive mode won't print it unless requested
>>> age['XXX']
KeyError: 'XXX'
========================================== Sec. II. 24.4.1.1
>>> age.get('Toto',"I'm not here!")
15
>>> age.get('XXX',"I'm not here!")
"I'm not here!"
========================================== Sec. II. 24.4.1.1
def count(s):
    d = {}
    for c in s:
        d[c] = d.get(c,0) + 1
    return d
========================================== Sec. II. 24.4.1.1
d.setdefault(key, []).append(value)
========================================== Sec. II. 24.4.1.2
from collections import defaultdict
========================================== Sec. II. 24.4.1.2
>>> d = defaultdict(lambda: 0)
========================================== Sec. II. 24.4.1.2
>>> d = defaultdict(lambda: 0, D)
========================================== Sec. II. 24.4.1.2
>>> d
defaultdict(<function <lambda> at 0x7fb0a51bde18>, {})
========================================== Sec. II. 24.4.1.2
>>> d[5]
0
>>> d
defaultdict(<function <lambda> at 0x7fb0a51bde18>, {5: 0})
>>> d['hey']
0
>>> d
defaultdict(<function <lambda> at 0x7fb0a51bde18>, {5: 0, 'hey': 0})
========================================== Sec. II. 24.4.1.2
def count(s):
    d = defaultdict(lambda: 0)
    for c in s:
        d[c] += 1
    return d
========================================== Sec. II. 24.4.1.2
  {'A': [0, 1, 6, 7, 8], 'B': [2, 5], 'a': [3, 4]})
========================================== Sec. II. 24.4.1.2
def occs(s):
    d = defaultdict(list)
    for k,c in enumerate(s):
        d[c].append(k)
    return d
========================================== Sec. II. 24.4.1.2
l=[]
def occs(s):
    d = defaultdict(lambda:l)
    for k,c in enumerate(s):
        d[c].append(k)
    return d
print(occs(s))
----------------------------------------------------------------
defaultdict(<function occs.<locals>.<lambda> at 0x7f4c9fe3cea0>,
  {'A': [0, 1, 2, 3, 4, 5, 6, 7, 8],
   'B': [0, 1, 2, 3, 4, 5, 6, 7, 8],
   'a': [0, 1, 2, 3, 4, 5, 6, 7, 8]})
========================================== Sec. II. 24.4.1.3
>>> Counter(s)
Counter({'A': 5, 'B': 2, 'a': 2})
========================================== Sec. II. 24.4.1.3
>>> Counter(s)+Counter(s)
Counter({'A': 10, 'B': 4, 'a': 4})
========================================== Sec. II. 24.4.1.3
>>> def is_anagram(u,v):
      u,v = ( e.replace(" ","").upper() for e in (u,v) )
      return Counter(u) == Counter(v)

>>> u = "Counting Eh Tv"
>>> v = "Vincent Hugot"
>>> is_anagram(u,v)
True
========================================== Sec. II. 24.5.0.0
>>> S = { 0, 2, 4, 6, 8 }
========================================== Sec. II. 24.5.0.0
>>> S = set()
>>> for n in range(10):
      if n%2==0:
        S.add(n)
>>> S
{0, 2, 4, 6, 8}
========================================== Sec. II. 24.5.0.0
>>> { n for n in range(10) if n%2==0 }
{0, 2, 4, 6, 8}

>>> { 2*k for k in range(5) }
{0, 2, 4, 6, 8}
========================================== Sec. II. 24.5.1.0
>>> [n for n in range(10) if n%2==0]
[0, 2, 4, 6, 8]
========================================== Sec. II. 24.5.1.0
>>> {n : n**2 for n in range(10) if n%2==0}
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
========================================== Sec. II. 24.5.1.0
>>> (n for n in range(10) if n%2==0)
<generator object <genexpr> at 0x7fb79904d468>
========================================== Sec. II. 24.5.1.0
>>> tuple(n for n in range(10) if n%2==0)
(0, 2, 4, 6, 8)
========================================== Sec. II. 24.5.1.0
>>> list(n for n in range(10) if n%2==0)
[0, 2, 4, 6, 8]

>>> set(n for n in range(10) if n%2==0)
{0, 2, 4, 6, 8}
========================================== Sec. II. 24.5.1.0
>>> tuple((n for n in range(10) if n%2==0))
(0, 2, 4, 6, 8)
========================================== Sec. II. 24.5.1.0
>>> {n:n**2 for n in range(10) if n%2==0}
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

>>> dict(n:n**2 for n in range(10) if n%2==0)
SyntaxError: invalid syntax

>>> dict((n,n**2) for n in range(10) if n%2==0)
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

>>> {(n,n**2) for n in range(10) if n%2==0}   # not the same thing
{(6, 36), (0, 0), (8, 64), (4, 16), (2, 4)}
========================================== Sec. II. 24.5.1.0
>>> G=(n for n in range(5) if n%2==0)
>>> type(G)
<class 'generator'>
>>> next(G)
0
>>> next(G)
2
>>> next(G)
4
>>> next(G)
StopIteration # exception: the generator is exhausted
========================================== Sec. II. 24.5.2.0
>>> [n // 2 for n in range(10) if n%2==0]
[0, 1, 2, 3, 4]
========================================== Sec. II. 24.5.2.0
>>> [ (x,y) for x in 'ABCD' if x != 'D' for y in (0,1,2) ]
[('A', 0), ('A', 1), ('A', 2), ('B', 0), ('B', 1), ('B', 2),
  ('C', 0), ('C', 1), ('C', 2)]
========================================== Sec. II. 24.5.2.0
>>> [ (x,y) for y in (0,1,2) if x != 'D' for x in 'ABCD']
UnboundLocalError: local variable 'x' referenced before assignment
========================================== Sec. II. 24.5.2.0
[ (x,y) for x in 'ABCD' if x != 'D' for y in (0,1,2) ]
========================================== Sec. II. 24.5.2.0
for x in 'ABCD':
    if x != 'D':
        for y in (0,1,2):
            yield (x,y)
========================================== Sec. II. 24.5.2.0
L = []
for x in 'ABCD':
        if x != 'D':
            for y in (0,1,2):
                L.append( (x,y) )
print(L)
---------------------------------
[('A', 0), ('A', 1), ('A', 2), ('B', 0), ('B', 1), ('B', 2),
  ('C', 0), ('C', 1), ('C', 2)]
========================================== Sec. II. 24.5.2.0
>>> [ "Even" if n%2==0 else "Odd"    for n in range(5) ]
['Even', 'Odd', 'Even', 'Odd', 'Even']
========================================== Sec. II. 24.5.2.0
L = [ (x,y) for x in 'ABCD'
            if x != 'D'
            for y in (0,1,2) ]
========================================== Sec. II. 24.5.3.1
>>> [ (x,y) for x in (1,2,3) for y in (4,5,6) ]
[(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6),
  (3, 4), (3, 5), (3, 6)]
========================================== Sec. II. 24.5.3.1
>>> [ (x,y) for x in (1,2,3) and y in (4,5,6) ]
NameError: name 'y' is not defined
========================================== Sec. II. 24.5.3.1
>>> [ x for x in (1,2,3) and (4,5,6) ]
[4, 5, 6]
========================================== Sec. II. 24.5.3.2
>>> def f(x): return f"f({x})"
>>> [ f(x) for x in "abc" ]
['f(a)', 'f(b)', 'f(c)']
========================================== Sec. II. 24.5.3.2
>>> [ f"g({x})" for x in "abc" ]
['g(a)', 'g(b)', 'g(c)']

>>> [ (x, x//3, x%3) for x in range(5) ]
[(0, 0, 0), (1, 0, 1), (2, 0, 2), (3, 1, 0), (4, 1, 1)]
========================================== Sec. II. 24.5.3.3
>>> def P(x): return 65 <= ord(x) <= 65+26
>>> [ c for c in "loUPwPerER" if P(c) ]
['U', 'P', 'P', 'E', 'R']
========================================== Sec. II. 24.5.3.3
>>> [ c for c in "loUPwPerER" if 97 <= ord(c) <= 97+26 ]
['l', 'o', 'w', 'e', 'r']
========================================== Sec. II. 24.5.3.4
>>> sum( k for k in range(10+1) )
55
========================================== Sec. II. 24.5.3.4
>>> sum( [ k for k in range(10+1) ] )
55
========================================== Sec. II. 24.5.3.4
def prod(C):
    r = 1 # neutral element for *; just like 0 for +
    for e in C: r *= e
    return r

>>> prod(range(1, 5+1))
120

>>> prod( 1/k for k in range(1,5+1) )
0.008333333333333333

>>> 1/120
0.008333333333333333
========================================== Sec. II. 24.5.3.4
>>> min(1)
TypeError: 'int' object is not iterable
>>> min(3,1,2)
1
>>> min([3,1,2])
1
========================================== Sec. II. 24.5.3.4
>>> l = ['Platypus', 'School', 'Sleep'] # in the usual order

>>> min(l), max(l)
('Platypus', 'Sleep')

>>> sorted(l,key=lambda s:s[2])
['Platypus', 'Sleep', 'School']

>>> max(l,key=lambda s:s[2])
'School'
========================================== Sec. II. 24.5.3.4
>>> l = [75, 76, 99, 74, 11, 98, 85, 7, 5, 87]

>>> min(l)
5
>>> l.index(5)
8
>>> min(range(len(l)), key=lambda i:l[i])
8
========================================== Sec. II. 24.5.3.4
>>> l = ['A', 'C', 'B', 'B', 'C', 'C', 'C', 'C', 'C', 'A']

>>> max(set(l), key=l.count)
'C' # l would work instead of set(l), but less efficient
========================================== Sec. II. 24.5.3.4
>>> min({0},{0,1})
{0}

>>> min({0,2},{0,1})
{0, 2}

>>> min({0,1},{0,2})
{0, 1}
========================================== Sec. II. 24.5.3.4
>>> "".join( chr(k) for k in range(65,65+26) )
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
========================================== Sec. II. 24.5.3.4
>>> all([])
True  # neutral element for *and*
>>> all([True, False, True, True])
False
>>> all([True, True, True])
True

>>> any([])
False # neutral element for *or*
>>> any([True, False, True, True])
True
>>> any([False, False])
False
========================================== Sec. II. 24.5.3.4
>>> any([ (), () ])
False
>>> any([ (), (1,2) ])
True
========================================== Sec. II. 24.5.3.4
assert all( square(n) == n*n  for n in range(10) )
========================================== Sec. II. 24.5.3.5
>>> l = list(range(5))

>>> [ l[-i-1] for i in range(5) ]
[4, 3, 2, 1, 0]
========================================== Sec. II. 24.5.3.5
[mathescape=true]
l = list("ABCDE")

def $\gs\m1$(i): return (1,2,0,4,3).index(i)  # get the index where i appears
# I use $\gs\m1$ for clarity. In actual code, use a valid identifier

l$\gs\m1$ =$\ $[$\ $l[$\gs\m1$(i)] for i in range(5) ]

print( l$\gs\m1$ )

---------------------------------------------------------------

['C', 'A', 'B', 'E', 'D']
========================================== Sec. II. 24.5.3.6
>>> l = [ (1,2,3), (4,5), (6,7,8) ]

>>> [ e for sl in l for e in sl ] # sl = sublist
[1, 2, 3, 4, 5, 6, 7, 8]
========================================== Sec. II. 24.5.3.7
>>> [ (c,c,c) for c in list("ABCD") ]
[('A', 'A', 'A'), ('B', 'B', 'B'), ('C', 'C', 'C'), ('D', 'D', 'D')]
========================================== Sec. II. 24.5.3.7
>>> [ c,c,c for c in list("ABCD") ]
SyntaxError: invalid syntax

>>> [ *(c,c,c) for c in list("ABCD") ]  # unpacking attempt
SyntaxError: iterable unpacking cannot be used in comprehension
========================================== Sec. II. 24.5.3.7
>>> [ c for c in list("ABCD") for _ in range(3) ]
['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'D', 'D', 'D']
========================================== Sec. II. 24.6.1.0
>>> t = ("Hello", 24, True)
========================================== Sec. II. 24.6.1.0
>>> s,i,b = t     # or, equivalently, (s,i,b) = t
>>> print(s,i,b)
Hello 24 True
========================================== Sec. II. 24.6.1.0
>>> [s,i,b] = t
>>> s,i,b
('Hello', 24, True)

>>> {s,i,b} = t
SyntaxError: can't assign to literal
========================================== Sec. II. 24.6.1.0
>>> r = range(7)
>>> first, second, *middle, end = r

>>> first
0
>>> second
1
>>> middle
[2, 3, 4, 5]
>>> end
6

>>> [type(e) for e in (first,second,middle,end)]
[<class 'int'>, <class 'int'>, <class 'list'>, <class 'int'>]
========================================== Sec. II. 24.6.1.0
>>> r = range(7)

>>> first   = r[0]
>>> second  = r[1]
>>> middle  = r[2:-1]
>>> end     = r[-1]

>>> (first, second, middle, end)
(0, 1, range(2, 6), 6)

>>> (first, second, list(middle), end)
(0, 1, [2, 3, 4, 5], 6)
========================================== Sec. II. 24.6.1.0
>>> s = set(range(5))

>>> a, *rest = s

>>> a, rest
(0, [1, 2, 3, 4])
========================================== Sec. II. 24.6.1.0
>>> a = s[0] ; rest = s[1:]
TypeError: 'set' object does not support indexing
========================================== Sec. II. 24.6.1.0
>>> *a,*b = r
SyntaxError: two starred expressions in assignment
========================================== Sec. II. 24.6.1.0
>>> r = range(5)

>>> ['A', *r, 'Z']
['A', 0, 1, 2, 3, 4, 'Z']

>>> {'A', *r, 'Z'}
{0, 1, 'A', 2, 3, 4, 'Z'}

>>> print('A', *r, 'Z', sep=',')
A,0,1,2,3,4,Z
========================================== Sec. II. 24.6.1.0
>>> print(*{3.15, 6.7, 90.}, *r, 'A', *r, 'Z', sep=' , ')
90.0 , 3.15 , 6.7 , 0 , 1 , 2 , 3 , 4 , A , 0 , 1 , 2 , 3 , 4 , Z
========================================== Sec. II. 24.6.1.0
>>> def f(a,b,c): print(a,b,c)

>>> f(*[1,2], 3)
1 2 3

>>> f(*[1,2,3,4])
TypeError: f() takes 3 positional arguments but 4 were given
========================================== Sec. II. 24.6.2.0
>>> age = {'Toto':15, 'Tata':27, 'Mamie':97 }

>>> print(*age)
Toto Tata Mamie
========================================== Sec. II. 24.6.2.0
>>> { 'Jojo':15 , *age }
SyntaxError: invalid syntax
========================================== Sec. II. 24.6.2.0
>>> { 'Jojo':15 , **age }
{'Jojo': 15, 'Mamie': 97, 'Tata': 27, 'Toto': 15}
========================================== Sec. II. 24.6.2.0
>>> [*age]
['Toto', 'Tata', 'Mamie']

>>> [**age]
SyntaxError: invalid syntax
========================================== Sec. II. 24.6.2.0
>>> {'Mamie': 97, **d } = age
SyntaxError: can't assign to literal
========================================== Sec. II. 24.6.2.0
>>> [ a, *b ] = age
>>> b
['Tata', 'Mamie']
========================================== Sec. II. 24.6.2.0
def f(Toto, Tata, Mamie):
  print(Toto, Tata, Mamie)

def g(Mamie, Toto, Tata):
  print(Toto, Tata, Mamie)
========================================== Sec. II. 24.6.2.0
>>> f(**age)
15 27 97
>>> g(**age)
15 27 97
========================================== Sec. II. 24.6.2.0
>>> f(Toto=age['Toto'], Tata=age['Tata'], Mamie=age['Mamie'])
15 27 97
========================================== Sec. II. 24.6.2.0
>>> f(*age)
Toto Tata Mamie
>>> g(*age)
Tata Mamie Toto
========================================== Sec. II. 24.6.2.0
>>> age.update( { 'a' : 1, 'b': 2 } )
>>> age
{'Toto': 15, 'Tata': 27, 'Mamie': 97, 'a': 1, 'b': 2}
========================================== Sec. II. 24.6.2.0
>>> age.update(a=1,b=2)
>>> age
{'Mamie': 97, 'Tata': 27, 'Toto': 15, 'a': 1, 'b': 2}
========================================== Sec. II. 24.6.2.1
>>> age = {'Toto':15, 'Tata':27, 'Mamie':97 }
>>> d = { 'AA' * i : 10*i for i in range(1,3+1) }
>>> d
{'AA': 10, 'AAAA': 20, 'AAAAAA': 30}

>>> { **age, **d }
{'Toto': 15, 'Tata': 27, 'Mamie': 97, 'AA': 10, 'AAAA': 20, 'AAAAAA': 30}
========================================== Sec. II. 24.6.2.1
>>> { **{0:1}, **{0:2} }
{0: 2}
========================================== Sec. II. 24.6.2.1
>>> dict(**age, **d)  # or dict(age, **d)
{'Toto': 15, 'Tata': 27, 'Mamie': 97, 'AA': 10, 'AAAA': 20, 'AAAAAA': 30}
========================================== Sec. II. 24.6.2.1
>>> dict(a = 1, b = 2)
{'a': 1, 'b': 2}
========================================== Sec. II. 24.6.2.1
>>> dict(a = 1, b = 2)
{'a': 1, 'b': 2}

>>> dict(1=a, 2=a)
SyntaxError: keyword can t be an expression

>>> dict(**{1 : 'a' , 2 : 'b'})
TypeError: keyword arguments must be strings

>>> dict(**{'a':0},**{'a':1})
TypeError: type object got multiple values for keyword argument 'a'
========================================== Sec. II. 25.1.0.0
def <functionname> (<arg1>, ..., <argN>,
                    <optarg1> = <defval1>,... <optargM> = <defvalM>):
========================================== Sec. II. 25.1.0.0
def <fun> (
           <pk1>, ..., <pkN>,
           <pkd1> = <def1>,..., <pkdM> = <defM>,
           *<p>,
           **<k>
          ):
========================================== Sec. II. 25.1.0.0
def <fun> (
           <pk1>, ..., <pkN>,
           *<p>,
           <pkd1> = <def1>,..., <pkdM> = <defM>,
           **<k>
          ):
========================================== Sec. II. 25.1.0.0
def f( pk1, pk2,
       pkd1='d1', pkd2='d2',
       *p, **k
     ):
    print("f ",pk1,pk2,pkd1,pkd2,p,k)
========================================== Sec. II. 25.1.0.0
>>> f(1,2)         # ``normal'' positional call
f  1 2 d1 d2 () {}

>>> f(pk2=2,pk1=1) # keyword call
f  1 2 d1 d2 () {}

>>> f(1,pk2=2)     # partial positional, partial keyword
f  1 2 d1 d2 () {}

>>> f(1,pkd2=99,pk2=2) # overriding default via keyword
f  1 2 d1 99 () {}

>>> f(1,pkd2=99,pk2=2,a=78) # excess keyword is absorbed
f  1 2 d1 99 () {'a': 78}

>>> f(1,2,3)       # positional; override some defaults
f  1 2 3 d2 () {}

>>> f(1,2,3,4,5,6) # override all defaults; excess positionals absorbed
f  1 2 3 4 (5, 6) {}

>>> f(1,2,3,4,5,6,a=90,b=55) # same; excess keywords absorbed
f  1 2 3 4 (5, 6) {'a': 90, 'b': 55}
========================================== Sec. II. 25.1.0.0
def g( pk1, pk2, *p,
       pkd1='d1', pkd2='d2', **k
     ):
    print("g ",pk1,pk2,p,pkd1,pkd2,k)
========================================== Sec. II. 25.1.0.0
>>> g(1,2)
g  1 2 () d1 d2 {}

>>> g(pk2=2,pk1=1)
g  1 2 () d1 d2 {}

>>> g(1,pk2=2)
g  1 2 () d1 d2 {}

>>> g(1,pkd2=99,pk2=2)
g  1 2 () d1 99 {}

>>> g(1,pkd2=99,pk2=2,a=78)
g  1 2 () d1 99 {'a': 78}

>>> g(1,2,3)   # excess positional absorbed: defaults untouched
g  1 2 (3,) d1 d2 {}

>>> g(1,2,3,4,5,6) # defaults cannot be overridden positionally
g  1 2 (3, 4, 5, 6) d1 d2 {}

>>> g(1,2,3,4,5,6,pkd2=777) # but can be overridden by keyword
g  1 2 (3, 4, 5, 6) d1 777 {}

>>> g(1,2,3,4,5,6,a=90,b=55)
g  1 2 (3, 4, 5, 6) d1 d2 {'a': 90, 'b': 55}
========================================== Sec. II. 25.1.0.0
def myprint(*args):
    for a in args:
        print(a,"",end="")
    print() # newline
========================================== Sec. II. 25.1.0.0
>>> myprint()

>>> myprint(1)
1
>>> myprint(1,2)
1 2
>>> myprint(*range(5))
0 1 2 3 4
========================================== Sec. II. 25.1.0.0
def mydict(**kwargs):
    return kwargs
----------------------------------------------------
>>> d,D = dict(a=1,b=2), dict(c=3,d=4)

>>> mydict(**d,**D)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

>>> mydict(**d,**D,another='stuff')
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'another': 'stuff'}
========================================== Sec. II. 25.2.1.0
def announce_call(f):
    def wrap(*a,**k):
        print("Hello!")
        res = f(*a,**k)
        print("Goodbye!")
        return res
    return wrap
========================================== Sec. II. 25.2.1.0
def e(x): return  f"e({x})"
e = announce_call(e)

---------------------------

>>> e
<function announce_call.<locals>.wrap at 0x7fa6758c7be0>

>>> e(1)
Hello!
Goodbye!
'e(1)'
========================================== Sec. II. 25.2.1.0
@announce_call
def f(x): return  f"f({x})"

@announce_call
def g(x): return f"g({x})"

print([f(x) + g(x) + g(x) for x in range(2)])

---------------------------------------------

Hello!
Goodbye!
Hello!
Goodbye!
Hello!
Goodbye!
Hello!
Goodbye!
Hello!
Goodbye!
Hello!
Goodbye!
['f(0)g(0)g(0)', 'f(1)g(1)g(1)']
========================================== Sec. II. 25.2.2.0
def count_calls(f):
    i = 0
    def w(*a,**k):
        nonlocal i
        i += 1
        call = f"{f.__name__}[{i}]({','.join(map(repr,a+tuple(k.items())))})"
        print(f"@< {call}...")
        res = f(*a,**k)
        print(f"   {call} = {repr(res)} >@")
        return res
    return w

@count_calls
def f(x): return f"f({x})"
@count_calls
def g(x): return f"g({x})"

print([f(x) + g(x) + g(x) for x in range(2)])

---------------------------------------------------

@< f[1](0)...
   f[1](0) = 'f(0)' >@
@< g[1](0)...
   g[1](0) = 'g(0)' >@
@< g[2](0)...
   g[2](0) = 'g(0)' >@
@< f[2](1)...
   f[2](1) = 'f(1)' >@
@< g[3](1)...
   g[3](1) = 'g(1)' >@
@< g[4](1)...
   g[4](1) = 'g(1)' >@
['f(0)g(0)g(0)', 'f(1)g(1)g(1)']
========================================== Sec. II. 25.2.2.0
@count_calls
def fib(n):
    return n if n <= 1 else fib(n-1) + fib(n-2)

print(fib(3))

-----------------------------------------------

@< fib[1](3)...
@< fib[2](2)...
@< fib[3](1)...
   fib[3](1) = 1 >@
@< fib[4](0)...
   fib[4](0) = 0 >@
   fib[2](2) = 1 >@
@< fib[5](1)...
   fib[5](1) = 1 >@
   fib[1](3) = 2 >@
2
========================================== Sec. II. 25.2.3.0
@count_calls
def helpful():
    "I'm well-documented!"
    return "Happy!"

---------------------------------------------------

>>> helpful
<function count_calls.<locals>.w at 0x7fddbda6fa30>

>>> helpful.__name__
'w'

>>> help(helpful)
Help on function w in module __main__:

w(*a, **k)
========================================== Sec. II. 25.2.3.0
from functools import wraps
========================================== Sec. II. 25.2.3.0
def count_calls(f):
    i = 0
    @wraps(f)
    def w(*a,**k):
        nonlocal i
        ...
========================================== Sec. II. 25.2.3.0
>>> helpful
<function helpful at 0x7fe59e3bba30>
>>> helpful.__name__
'helpful'
>>> help(helpful)
Help on function helpful in module __main__:

helpful()
    I'm well-documented!
========================================== Sec. II. 25.2.3.0
>>> helpful()
@< helpful[1]()...
   helpful[1]() = 'Happy!' >@
'Happy!'

>>> helpful.__wrapped__
<function helpful at 0x7f3aad8a7d90>

>>> helpful.__wrapped__()
'Happy!'

>>> helpful()
@< helpful[2]()...
   helpful[2]() = 'Happy!' >@
'Happy!'
========================================== Sec. II. 25.2.3.0
@contextmanager
def unwrap(f):
    """Temporarily un-decorate global function"""
    if hasattr(f, "__wrapped__"):
        globals()[f.__name__] = f.__wrapped__
        yield
        globals()[f.__name__] = f
    else: yield

@trace
def fib(n):
    return n if n <= 1 else fib(n-1) + fib(n-2)

print("->", fib(5))

with unwrap(fib):
    assert [fib(n) for n in range(10)] == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

fib(2)
========================================== Sec. II. 25.2.4.0
@announce_call
@count_calls
def helpful():
    ...

--------------------------

>>> helpful()
Hello!
@< helpful[1]()...
   helpful[1]() = 'Happy!' >@
Goodbye!
'Happy!'
========================================== Sec. II. 25.2.5.0
def trace(f):
    lvl = 0
    @wraps(f)
    def w(*a,**k):
        nonlocal lvl
        tree = "|  " * lvl + "+"
        print(f"{tree} {f.__name__}({', '.join(map(repr,a))})")
        lvl += 1
        res = f(*a,**k)
        lvl -= 1
        return res
    return w

@trace
def fib(n):
    return n if n <= 1 else fib(n-1) + fib(n-2)

print(fib(5))

---------------------------------------------------------------

+ fib(5)
|  + fib(4)
|  |  + fib(3)
|  |  |  + fib(2)
|  |  |  |  + fib(1)
|  |  |  |  + fib(0)
|  |  |  + fib(1)
|  |  + fib(2)
|  |  |  + fib(1)
|  |  |  + fib(0)
|  + fib(3)
|  |  + fib(2)
|  |  |  + fib(1)
|  |  |  + fib(0)
|  |  + fib(1)
5
========================================== Sec. II. 25.2.6.0
@who_says("Simon")
def f(x): return  f"f({x})"

print(f(0))

---------------------------

Simon says Hello!
Simon says Goodbye!
f(0)
========================================== Sec. II. 25.2.6.0
def deco_with_params(params):
    def deco(f):
        @wraps(f)
        def wrap(*a,**k):
            ... params ... f(*a,**k)...
        return wrap
    return deco
========================================== Sec. II. 25.2.6.0
def who_says(who):
    def deco(f):
        @wraps(f)
        def wrap(*a,**k):
            print(who, "says Hello!")
            res = f(*a,**k)
            print(who, "says Goodbye!")
            return res
        return wrap
    return deco
========================================== Sec. II. 26.0.0.0
>>> f = open("mytext.txt")

>>> f
<_io.TextIOWrapper name='mytext.txt' mode='r' encoding='UTF-8'>
========================================== Sec. II. 26.0.0.0
>>> [l for l in f]
['This is\n', 'a\n', 'text file.\n']
========================================== Sec. II. 26.0.0.0
>>> [l for l in f]
[]

>>> next(f)
StopIteration

>>> f.seek(0)
0 # I'm now at position zero. Again.

>>> [l for l in f]
['This is\n', 'a\n', 'text file.\n']
========================================== Sec. II. 26.0.0.0
>>> [l.split() for l in f]
[['This', 'is'], ['a'], ['text', 'file.']]
========================================== Sec. II. 26.0.0.0
>>> f.read()
'This is\na\ntext file.\n'
>>> f.read()
'' # stream is exhausted
========================================== Sec. II. 26.0.0.0
>>> f = open("mytext.txt")
>>> [l for l in f]
['This is\n', 'a\n', 'text file.\n']
>>> f.read()
''

>>> f.seek(0)

>>> f.read()
'This is\na\ntext file.\n'
>>> [l for l in f]
[]
========================================== Sec. II. 26.0.0.0
>>> f.close()
========================================== Sec. II. 26.0.0.0
>>> g = open("blah.txt","w")

>>> g.write('This is\na\ntext file.\n')
21 # number of characters written
   # always equal to the length of the string.

>>> g.close()
========================================== Sec. II. 26.1.0.0
with open(ROfile) as f, open(RWfile,"w") as g:
  <block with f and g opened>
========================================== Sec. II. 27.1.0.0
class Person:
    pass
========================================== Sec. II. 27.1.0.0
>>> p = Person()
>>> p
<__main__.Person object at 0x7f82091eab70>
>>> type(p)
<class '__main__.Person'>
========================================== Sec. II. 27.1.0.0
>>> p.a
AttributeError: 'Person' object has no attribute 'a'

>>> p.a = 21  # on-the-fly new attribute. It works here,
              # but not on all objects.
>>> p.a
21

>>> del p.a

>>> p.a
AttributeError: 'Person' object has no attribute 'a'
========================================== Sec. II. 27.2.0.0
class Person:
    name = ''
    age = 0
========================================== Sec. II. 27.2.0.0
>>> p, q = Person(), Person()
>>> p.name
''
>>> p.name = "Toto"
>>> p.age = 25

>>> f"{p.name} {p.age} {q.name} {q.age}"
'Toto 25  0'
========================================== Sec. II. 27.2.0.0
class Person:
    name = ''
    age = 0
    hobbies = ['Knitting', 'Reading']
========================================== Sec. II. 27.2.0.0
>>> p, q = Person(), Person()
>>> p.hobbies
['Knitting', 'Reading']
>>> del p.hobbies[0]
>>> p.hobbies
['Reading']
>>> q.hobbies
['Reading']
========================================== Sec. II. 27.3.0.0
>>> Person.age
0
========================================== Sec. II. 27.3.0.0
>>> type(Person)
<class 'type'>
========================================== Sec. II. 27.3.0.0
>>> type(type)
<class 'type'>
========================================== Sec. II. 27.3.0.0
>>> help(type)
...
|  type(name, bases, dict) -> a new type
...

>>> C = type('C', (object,), dict(a=1))
>>> C
<class '__main__.C'>

>>> type(C)
<class 'type'>

>>> C.a
1
========================================== Sec. II. 27.3.0.0
>>> isinstance(type,object)
True
>>> type(object)
<class 'type'>
========================================== Sec. II. 27.4.0.0
class Person:
    def __init__(self, name='', age=0, hobbies=[]):
        self.name = name
        self.age = age
        self.hobbies = hobbies

p = Person("Toto", 25, ['Knitting', 'Reading'])

q = Person("Tata", 97, ['Snoozing'])
------------------------------------------------
>>> p.hobbies
['Knitting', 'Reading']
>>> q.hobbies
['Snoozing']
========================================== Sec. II. 27.4.0.0
>>> p, q = Person(), Person()
>>> p.hobbies.append(0)
>>> p.hobbies
[0]
>>> q.hobbies
[0] # oops, still the same list.
>>> r = Person()
>>> r.hobbies
[0] # everyone shares the same
========================================== Sec. II. 27.4.0.0
def __init__(self, name='', age=0, hobbies=[]):
========================================== Sec. II. 27.4.0.0
class Person:
    def __init__(self, name='', age=0, hobbies=None):
        self.name = name
        self.age = age
        self.hobbies = list() if hobbies is None else hobbies

p = Person("Toto", 25, ['Knitting', 'Reading'])
q = Person("Tata", 97, ['Snoozing'])
r, s = Person(), Person()
--------------------------------------------------------------
>>> r.hobbies.append(0)
>>> r.hobbies
[0]
>>> s.hobbies
[]
========================================== Sec. II. 27.5.0.0
p = Person("Toto", 25, ['Knitting', 'Reading'])

match p:
    case Person(age=25, hobbies=[x,y]): print(p.name, y)
--------------------------------------------------------
Toto Reading
========================================== Sec. II. 27.5.0.0
class C:
    def __init__(s,a,b):
        s.x = a+b

myobj = C(a=1, b=2)

match myobj:
    case C(a=1, b=2): print("yes")
    case C(x=3):      print("Beware!")
----------------------------------
Beware!
========================================== Sec. II. 27.5.0.0
TypeError: C() accepts 0 positional sub-patterns (2 given)
========================================== Sec. II. 27.5.0.0
TypeError: C() accepts 0 positional sub-patterns (1 given)
========================================== Sec. II. 27.6.0.0
class Person:
    ... # I don't repeat __init__

    def getold(self,years):
        self.age += years
        print(f"{self.name} says: I am now {self.age} years old.")

========================================== Sec. II. 27.6.0.0
>>> p = Person("Toto", 25, ['Knitting', 'Reading'])
>>> p.age
25
>>> p.getold(1)
Toto says: I am now 26 years old.
>>> p.age
26
========================================== Sec. II. 27.6.0.0
>>> Person.getold(p, 3)
Toto says: I am now 29 years old.

>>> p.age
29
========================================== Sec. II. 27.6.0.0
class Person:
    ...
    def avgage(*ps):
        return sum( p.age for p in ps ) / len(ps) if ps else None
-----------------------------------------------------------------
p = Person("Toto", 25, ['Knitting', 'Reading'])
q = Person("Tata", 97, ['Snoozing'])
r, s = Person(), Person()
========================================== Sec. II. 27.6.0.0
>>> Person.avgage()  # None
>>> Person.avgage(p)
25.0
>>> Person.avgage(p,q)
61.0
>>> Person.avgage(p,q,r,s)
30.5
========================================== Sec. II. 27.6.0.0
>>> p.avgage()
25.0
>>> p.avgage(q)
61.0
========================================== Sec. II. 27.6.0.0
>>> Person.avgage(p)
25.0
>>> Person.avgage(p,q)
61.0
========================================== Sec. II. 27.6.0.0
class Person:
    ...
    @staticmethod
    def avgage(*ps):
        return sum( p.age for p in ps ) / len(ps) if ps else None
========================================== Sec. II. 27.6.0.0
>>> p.avgage()
>>> p.avgage(q)
97.0
========================================== Sec. II. 27.7.0.0
>>> p
<__main__.Person object at 0x7f370d00dac8>
>>> q
<__main__.Person object at 0x7f3710d95b70>
========================================== Sec. II. 27.7.0.0
>>> str(p)
'<__main__.Person object at 0x7f370d00dac8>'
========================================== Sec. II. 27.7.0.0
>>> str('Hello')
'Hello'

>>> repr('Hello')
"'Hello'"

>>> print(str('Hello'), "and",repr('Hello'))
Hello and 'Hello'

>>> repr(repr('Hello'))
'"\'Hello\'"'
========================================== Sec. II. 27.7.0.0
class Person:
    ...
    def __repr__(self):
        return f"Person({repr(self.name)}, {self.age}, {self.hobbies})"
========================================== Sec. II. 27.7.0.0
>>> p
Person('Toto', 25, ['Knitting', 'Reading'])
>>> repr(p)
"Person('Toto', 25, ['Knitting', 'Reading'])"
>>> str(p)
"Person('Toto', 25, ['Knitting', 'Reading'])"
>>> p.getold(10)
Toto says: I am now 35 years old.
>>> p
Person('Toto', 35, ['Knitting', 'Reading'])
========================================== Sec. II. 27.8.0.0
class <DerivedClass> (<ParentClass>):
========================================== Sec. II. 27.8.0.0
class Person:
========================================== Sec. II. 27.8.0.0
class Person(object):
========================================== Sec. II. 27.8.0.0
class Teacher(Person):

    def lecture(self):
        print(f'{self.name} lectures: "You are doing it wrong!"')

========================================== Sec. II. 27.8.0.0
>>> VH = Teacher("V. Hugot", 33, [])

>>> VH
Person('V. Hugot', 33, [])

>>> VH.getold(1)
V. Hugot says: I am now 34 years old.

>>> VH.lecture()
V. Hugot lectures: "You are doing it wrong!"

>>> p.lecture()
AttributeError: 'Person' object has no attribute 'lecture'
========================================== Sec. II. 27.8.0.0
>>> isinstance(VH, Teacher)
True
>>> isinstance(VH, Person)
True # it is a subclass
>>> isinstance(VH, object)
True # of that too; always
>>> isinstance(VH, list)
False # just checking isinstance can return False ;-)
========================================== Sec. II. 27.8.0.0
class Teacher(Person):
    ..
    def __repr__(self):
        return f"Teacher({repr(self.name)}, {self.age}, {self.hobbies})"
========================================== Sec. II. 27.8.0.0
>>> VH
Teacher('V. Hugot', 33, [])
========================================== Sec. II. 27.8.0.0
    def __repr__(self):
        return Person.__repr__(self).replace("Person","Teacher",1)
========================================== Sec. II. 27.8.0.0
    def __repr__(self):
        return super().__repr__().replace("Person","Teacher",1)
========================================== Sec. II. 27.8.0.0
class Student(Person):

    def goof(self,hobby):
        self.hobbies.append(hobby)
        print(f'{self.name} goofs around with {hobby.lower()}')

    def __repr__(self):
        return super().__repr__().replace("Person","Student",1)
========================================== Sec. II. 27.8.0.0
>>> Bob = Student("Bob", 19, [])

>>> Bob.goof('Knitting')
Bob goofs around with knitting

>>> Bob
Student('Bob', 19, ['Knitting'])
========================================== Sec. II. 27.8.0.0
>>> all ( isinstance(o,Person) for o in (p,q,VH,Bob,PVH) )
True

>>> Person.avgage(p,q,VH,Bob,PVH)
41.4
========================================== Sec. II. 27.9.0.0
class <DerivedClass> (<ParentClass1>,...,<ParentClassN>):
========================================== Sec. II. 27.9.0.0
class Prof(Teacher,Student):
    pass
----------------------------
>>> PVH = Prof("V. Hugot", 33, [])

>>> PVH.goof('Video Games')
V. Hugot goofs around with video games

>>> PVH.lecture()
V. Hugot lectures: "You are doing it wrong!"
========================================== Sec. II. 27.9.0.0
>>> PVH
Teacher('V. Hugot', 33, [])
========================================== Sec. II. 27.9.0.0
>>> Prof.__mro__
( <class '__main__.Prof'>,
  <class '__main__.Teacher'>, <class '__main__.Student'>,
  <class '__main__.Person'>,
  <class 'object'>)
========================================== Sec. II. 27.10.0.0
class INT:
    def __init__(self, a): self.a = a
    def __add__(self, other):
        print("__add__")
        return self.a + other
========================================== Sec. II. 27.10.0.0
>>> INT(1) + 2
__add__
3

>>> 1 + INT(2)
TypeError: unsupported operand type(s) for +: 'int' and 'INT'
========================================== Sec. II. 27.10.0.0
class INT:
    ...
    def __radd__(self, other):
        print("__radd__")
        return other + self.a
========================================== Sec. II. 27.10.0.0
>>> 1 + INT(2)
__radd__
3
========================================== Sec. II. 27.10.0.0
>>> type(NotImplemented)
<class 'NotImplementedType'>
========================================== Sec. II. 27.10.0.0
class A:
    def __add__(s,o):
        print("Aadd")
        if isinstance(o,int): return "intOK"
        return NotImplemented

class B:
    # pass
    def __radd__(s,o):
        print("Bradd")
        if isinstance(o,A): return "yes"
========================================== Sec. II. 27.10.0.0
if isinstance(other, sometype):
    return # something
elif isinstance(other, someOtherType):
    return something_else
...
return NotImplemented
========================================== Sec. II. 27.10.0.0
class A:
    def __add__(s,o):
        print("A.add")
        if isinstance(o,int): return "Aa"
        return NotImplemented
    def __iadd__(s,o):
        print("A.iadd")
        return NotImplemented

class B:
    def __radd__(s,o):
        print("B.radd")
        if isinstance(o,A): return "yes"
----------------------------------------
>>> a = A()
>>> a += B()
A.iadd
A.add
B.radd
>>> a
'yes'
========================================== Sec. II. 28.0.0.0
(<operator>, <left operand>, <right operand>)
========================================== Sec. II. 28.0.0.0
(NOT, (IMPLIES, "x", (OR, "y", "z")))
========================================== Sec. II. 28.0.0.0
from dataclasses import dataclass

@dataclass
class BinOp:
    l: object
    r: object

class And       (BinOp):
    symb = "&"
class Or        (BinOp):
    symb = "|"
class Implies   (BinOp):
    symb = "->"

@dataclass
class Not:
    f: object

f = Not(Implies("x", Or("y", "z")))

------------------------------------

>>> f
Not(f=Implies(l='x', r=Or(l='y', r='z')))
========================================== Sec. II. 28.0.0.0
def fstr(f):
    match f:
        case str():         return f # variable
        case BinOp(l,r):    return f"({fstr(l)} {f.symb} {fstr(r)})"
        case Not(f):        return f"-{fstr(f)}"
        case _:             raise ValueError(f)

--------------------------------------------------------------------

>>> fstr(f)
-(x -> (y | z))
========================================== Sec. II. 28.0.0.0
case And(l,r):      return f"({fstr(l)} & {fstr(r)})"
case Or(l,r):       return f"({fstr(l)} | {fstr(r)})"
case Implies(l,r):  return f"({fstr(l)} -> {fstr(r)})"
========================================== Sec. II. 28.0.0.0
case _: raise ValueError(f)
========================================== Sec. II. 28.0.0.0
def rmimp(f):
    match f:
        case str():         return f
        case Implies(l,r):  return Or(Not(rmimp(l)), rmimp(r))
        case BinOp(l,r):    return type(f)(rmimp(l), rmimp(r))
        case Not(f):        return Not(rmimp(f))
        case _:             raise ValueError(f)

--------------------------------------------------------------

>>> fstr(F := rmimp(f))
-(-x | (y | z))
========================================== Sec. II. 28.0.0.0
case BinOp(l,r):    return type(f)(rmimp(l), rmimp(r))
========================================== Sec. II. 28.0.0.0
def nnf(f):
    z = nnf ; morgan = {And:Or, Or:And}
    match f:
        case Not(Not(f)):   return z(f)
        case Not(BinOp(l,r) as g):
            return morgan[type(g)](z(Not(l)), z(Not(r)))
        case BinOp(l,r):    return type(f)(z(l), z(r))
        case Not(f):        return Not(z(f))
        case str():         return f
        case _:             raise ValueError(f)

--------------------------------------------------------
>>> fstr(f)
-(x -> (y | z))
>>> fstr(F)
-(-x | (y | z))
>>> fstr(N := nnf(F))
(x & (-y & -z))
========================================== Sec. II. 28.1.0.0
case Not(f):        return Not(rmimp(f))
========================================== Sec. II. 28.1.0.0
def simp(e):
    def z(e):
        match e:
            case Plus(0,e) | Plus(e,0): return e
            ...
    return fixpoint(z,e)
========================================== Sec. II. 29.0.0.0
class X:
    def  __getitem__(s,o):  # s[o]
        if o in (0,1): return 'a'
        else: raise IndexError
x=X()
========================================== Sec. II. 29.0.0.0
>>> 'a' in x
True
>>> 'b' in x
False
>>> list(x)
['a', 'a']
>>> for c in x: print (c)
a
a
========================================== Sec. II. 29.1.0.0
class r:
    def __init__(s,i,j):
        s.i, s.j = i,j
        s.k = i

    def __iter__(s): return s

    def __next__(s):
        if s.k <= s.j:
            s.k += 1
            return s.k - 1
        else: raise StopIteration
========================================== Sec. II. 29.1.0.0
>>> list(r(2,7))
[2, 3, 4, 5, 6, 7]

>>> r(2,7)  # different iterator objects each time
<__main__.r object at 0x7fd85ffae860>
>>> r(2,7)
<__main__.r object at 0x7fd85ffb8eb8>

>>> it = r(2,7)
>>> next(it) # each call alters state
2
>>> next(it)
3
>>> list(it)
[4, 5, 6, 7]
>>> list(it)
[]
========================================== Sec. II. 29.1.0.0
class r:
    def __init__(s,i,j):
        s.i, s.j = i,j
        s.k = i

    def __iter__(s):
        return r_iterator(s.i,s.j)
========================================== Sec. II. 29.1.0.0
>>> it = r(2,7)

>>> list(it)
[2, 3, 4, 5, 6, 7]
>>> list(it)
[2, 3, 4, 5, 6, 7]

>>> iterator = iter(it)
>>> list(iterator)
[2, 3, 4, 5, 6, 7]
>>> list(iterator)
[]
========================================== Sec. II. 29.2.0.0
def r(i,j):
    while i <= j:
        yield i
        i += 1
========================================== Sec. II. 29.2.0.0
>>> r
<function r at 0x7f81332aed90>

>>> r(2,7)
<generator object r at 0x7f81332b44c0>
========================================== Sec. II. 29.2.0.0
>>> r(2,7) # different objects each time
<generator object r at 0x7f81332b4468>
>>> r(2,7)
<generator object r at 0x7f81332b4410>

>>> it = r(2,7)

>>> next(it)  # each call alters state
2
>>> next(it)
3
>>> list(it)
[4, 5, 6, 7]
>>> list(it)
[]
========================================== Sec. II. 29.2.0.0
def loop(i,j,n):
    for _ in range(n):
        yield r(i,j)
========================================== Sec. II. 29.2.0.0
>>> list(loop(1,3,3))
[<generator object r at 0x7fa5bc6cd410>,
 <generator object r at 0x7fa5bc6cd3b8>,
 <generator object r at 0x7fa5bc6cd468>]
========================================== Sec. II. 29.2.0.0
def loop(i,j,n):
    for _ in range(n):
        for x in r(i,j):
            yield x
---------------------------

>>> list( loop(1,3,3) )
[1, 2, 3, 1, 2, 3, 1, 2, 3]
========================================== Sec. II. 29.2.0.0
def loop(i,j,n):
    for _ in range(n):
        yield from r(i,j)
---------------------------

>>> list(loop(1,3,3))
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> g = loop(1,3,3)  # still an iterator
>>> next(g)
1
>>> next(g)
2
>>> list(g)
[3, 1, 2, 3, 1, 2, 3]
========================================== Sec. II. 29.4.0.0
data0 # our data source

data1 = f1(data0)
data2 = f2(data1)
...
dataN = fN(dataNm1)
========================================== Sec. II. 29.4.0.0
dataN = fN(fNm1(... f1(data0)...))
========================================== Sec. II. 29.4.0.0
def data(m):
    for i in range(m):
        print(f"Data yields 0; has {m-i-1} left")
        yield 0
    print("Data is exhausted")

def chaingens(g,lvl):
    while True:
        print(f"lvl {lvl} gen asks")
        d = next(g, None)
        if d is None: break
        res = d + 1
        print(f"lvl {lvl} gen obtains {d}, yields {res}")
        yield res

def genchain(n,m):
    if n == 0:
        return data(m)
    else:
        return chaingens(genchain(n-1,m),n)

g = genchain(3,3)

for v in g:
    print(f"Final computation depth: {v}\n")
-----------------------------------------------------------
lvl 3 gen asks
lvl 2 gen asks
lvl 1 gen asks
Data yields 0; has 2 left
lvl 1 gen obtains 0, yields 1
lvl 2 gen obtains 1, yields 2
lvl 3 gen obtains 2, yields 3
Final computation depth: 3

lvl 3 gen asks
lvl 2 gen asks
lvl 1 gen asks
Data yields 0; has 1 left
lvl 1 gen obtains 0, yields 1
lvl 2 gen obtains 1, yields 2
lvl 3 gen obtains 2, yields 3
Final computation depth: 3

lvl 3 gen asks
lvl 2 gen asks
lvl 1 gen asks
Data yields 0; has 0 left
lvl 1 gen obtains 0, yields 1
lvl 2 gen obtains 1, yields 2
lvl 3 gen obtains 2, yields 3
Final computation depth: 3

lvl 3 gen asks
lvl 2 gen asks
lvl 1 gen asks
Data is exhausted
========================================== Sec. II. 29.5.2.0
def N():
    i = 0
    while True:
        yield i
        i += 1
---------------------------------
>>> g = N()

>>> [ next(g) for _ in range(5) ]
[0, 1, 2, 3, 4]
========================================== Sec. II. 29.5.3.1
>>> g = r(1,1000)
>>> l = [ next(g) for _ in range(5) ]
>>> l
[1, 2, 3, 4, 5]
>>> next(g)
6
========================================== Sec. II. 29.5.3.1
>>> g = r(1,3)
>>> [ next(g) for _ in range(5) ]
StopIteration

>>> g = r(1,3)
>>> list( next(g) for _ in range(5) )
[1, 2, 3]  # in 3.6; StopIteration in 3.7+
========================================== Sec. II. 29.5.3.1
def upto(g,i): #3.7
    for k,e in enumerate(g):
        if k >= i: return
        yield e
========================================== Sec. II. 29.5.3.2
>>> g = r(1,1000)
>>> next(x for k,x in enumerate(g) if k == 5) # or k >= 5
6
>>> next(g)
7
========================================== Sec. II. 29.5.3.2
x for k,x in enumerate(g) if k == 5
========================================== Sec. II. 29.5.3.2
>>> g = r(1,3)
>>> ge = enumerate(g)
>>> next(ge)
(0, 1)
>>> next(ge)
(1, 2)
>>> next(g)
3
>>> next(ge)
StopIteration
========================================== Sec. II. 29.5.4.0
>>> g = r(1,10)

>>> len(g)
TypeError: object of type 'generator' has no len()
========================================== Sec. II. 29.5.4.0
>>> sum( 1 for _ in g )
10

>>> next(g)
StopIteration
========================================== Sec. II. 31.4.0.0
from time import sleep
import time
import concurrent.futures as cf
import os

def display_time(f):
    def F(*a,**k):
         x = time.perf_counter()
         res = f(*a,**k)
         y = time.perf_counter()
         print(f"TIME ELAPSED: {f.__name__}{a,k}: {y-x:.3f}s")
         return res
    return F
========================================== Sec. II. 31.4.0.0
@display_time
def io(id):
    print(f"IO-bound operation {id} START {os.getpid()}",flush=True)
    sleep(1)
    print(f"IO-bound operation {id} STOP  {os.getpid()}",flush=True)
    return f"{id}X"

@display_time
def cpu(id):
    print(f"CPU-bound operation {id} START {os.getpid()}",flush=True)
    for i in range(1000):
        x = 2**10**6
    print(f"CPU-bound operation {id} STOP {os.getpid()}",flush=True)
    return f"{id}Y"
========================================== Sec. II. 31.4.0.0
>>> io(0)
IO-bound operation 0 START 61699
IO-bound operation 0 STOP  61699
TIME ELAPSED: io((0,), {}): 1.028s
'0X'

>>> cpu(0)
CPU-bound operation 0 START 61699
CPU-bound operation 0 STOP 61699
TIME ELAPSED: cpu((0,), {}): 2.559s
'0Y'
========================================== Sec. II. 31.4.0.0
@display_time
def normalmap(*args): return list(map(*args))
========================================== Sec. II. 31.4.0.0
>>> normalmap(io, range(10))

IO-bound operation 0 START 49672
IO-bound operation 0 STOP  49672
IO-bound operation 1 START 49672
IO-bound operation 1 STOP  49672
...
IO-bound operation 9 STOP  49672
TIME ELAPSED: normalmap((<function io at ..>, range(0, 10)), {}): 10.011s
['0X', '1X', '2X', '3X', '4X', '5X', '6X', '7X', '8X', '9X']
========================================== Sec. II. 31.4.0.0
@display_time
def iomap(*args):
    with cf.ThreadPoolExecutor() as e: return list(e.map(*args))
========================================== Sec. II. 31.4.0.0
>>> iomap(io, range(10))

IO-bound operation 0 START 49672
...
IO-bound operation 9 START 49672
IO-bound operation 0 STOP  49672
IO-bound operation 2 STOP  49672
IO-bound operation 1 STOP  49672
...
IO-bound operation 8 STOP  49672
TIME ELAPSED: iomap((<function io at ..>, range(0, 10)), {}): 1.013s
['0X', '1X', '2X', '3X', '4X', '5X', '6X', '7X', '8X', '9X']
========================================== Sec. II. 31.4.0.0
TIME ELAPSED: iomap(..., range(0, 100)), {}): 9.017s
========================================== Sec. II. 31.4.0.0
@display_time
def cpumap(*args):
    with cf.ProcessPoolExecutor() as e: return list(e.map(*args))
========================================== Sec. II. 31.4.0.0
>>> cpumap(io, range(10))

AttributeError: Can't pickle local object 'display_time.<locals>.F'
========================================== Sec. II. 31.4.0.0
>>> cpumap(io, range(10))

IO-bound operation 0 START 49735
...
IO-bound operation 7 START 49744
IO-bound operation 0 STOP  49735
IO-bound operation 8 START 49735
IO-bound operation 1 STOP  49738
IO-bound operation 9 START 49738
IO-bound operation 2 STOP  49739
...
IO-bound operation 9 STOP  49738
TIME ELAPSED: cpumap((<function io at ..>, range(0, 10)), {}): 2.057s
['0X', '1X', '2X', '3X', '4X', '5X', '6X', '7X', '8X', '9X']
========================================== Sec. II. 31.4.0.0
TIME ELAPSED: cpumap((<function io at 0x7f9f0e8d95a0>, range(0, 100)), {}):
13.065s
========================================== Sec. II. 31.4.0.0
>>> normalmap(cpu, range(10))

CPU-bound operation 0 START 49672
...
CPU-bound operation 9 STOP 49672
TIME ELAPSED: normalmap((<function cpu at ..>, range(0, 10)), {}): 24.339s
['0Y', '1Y', '2Y', '3Y', '4Y', '5Y', '6Y', '7Y', '8Y', '9Y']
========================================== Sec. II. 31.4.0.0
>>> iomap(cpu, range(10))

CPU-bound operation 0 START 49672
...
CPU-bound operation 7 STOP 49672
TIME ELAPSED: iomap((<function cpu at ..>, range(0, 10)), {}): 40.133s
['0Y', '1Y', '2Y', '3Y', '4Y', '5Y', '6Y', '7Y', '8Y', '9Y']
========================================== Sec. II. 31.4.0.0
>>> cpumap(cpu, range(10))

CPU-bound operation 0 START 50000
...
CPU-bound operation 9 STOP 50008
TIME ELAPSED: cpumap((<function cpu at ..>, range(0, 10)), {}): 7.011s
['0Y', '1Y', '2Y', '3Y', '4Y', '5Y', '6Y', '7Y', '8Y', '9Y']
========================================== Sec. II. 31.4.0.0
TIME ELAPSED: cpumap((<..cpu..>, range(0, 4)), {}): 2.438s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 5)), {}): 3.586s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 6)), {}): 3.642s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 8)), {}): 4.689s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 9)), {}): 6.855s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 40)), {}): 22.917s
========================================== Sec. II. 31.4.0.0
def cpumap(*args):
    with cf.ProcessPoolExecutor(max_workers=4) as e: return list(e.map(*args))
========================================== Sec. II. 31.4.0.0
TIME ELAPSED: cpumap((<..cpu..>, range(0, 4)), {}): 2.451s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 5)), {}): 4.876s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 6)), {}): 4.822s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 8)), {}): 4.861s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 9)), {}): 7.224s
TIME ELAPSED: cpumap((<..cpu..>, range(0, 40)), {}): 24.451s
========================================== Sec. II. 31.5.0.0
shared = 0

def increment_shared(id):
    global shared
    for _ in range(10):     # each threads does shared += 10
        x = shared          # read resource
        time.sleep(0.0001)  # computation
        shared = x+1        # write to resource

def mciomap(*args):         # lots of worker threads
    with cf.ThreadPoolExecutor(max_workers=1000) as e:
        return list(e.map(*args))

from collections import Counter
c = Counter()

for _ in range(100):        # repeat experiment
    shared = 0
    mciomap(increment_shared, range(10))
    c[shared] += 1 # ten threads; shared should == 100

print(sorted(c.items()))
-------------------------------------------------------------
[(13, 6), (14, 27), (15, 39), (16, 17), (17, 6), (18, 1),
    (19, 1), (21, 2), (22, 1)]
========================================== Sec. II. 31.5.0.0
>>> from dis import dis # disassembler
>>> dis('x += 1')
  1           0 LOAD_NAME                0 (x)
              2 LOAD_CONST               0 (1)
              4 INPLACE_ADD
              6 STORE_NAME               0 (x)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

========================================== Sec. II. 32.0.0.0
 for elem, ekey in ((e, key(e)) for e in iterable):
========================================== Sec. II. 32.0.0.0
python3 -m cProfile -o profile.prof ./tests.py
snakeviz profile.prof
========================================== Sec. II. 32.0.0.0
if __name__ == '__main__':
========================================== Sec. II. 32.0.0.0
if __debug__
========================================== Sec. II. 32.0.0.0
@contextmanager
def cd(dir):
    currdir = os.getcwd()
    os.chdir(os.path.expanduser(dir))
    try: yield
    finally: os.chdir(currdir)
========================================== Sec. II. 32.0.0.0
return sum( f(x)*(h-l) for (l,h) in partition(a,b,n) for mid in [mid(l,h)] )
========================================== Sec. II. 32.0.0.0
assert all(factorial(X:=n) == forig(n) for n in range(10)), X
========================================== Sec. II. 32.0.0.0
for a,b,c in range(-5,5):
    print(a,b,c)

Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    for a,b,c in range(-5,5):
TypeError: cannot unpack non-iterable int object
bool( True for a,b,c in range(-5,5))
True
bool( prinnt(a,b,c) for a,b,c in range(-5,5))
True
bool( print(a,b,c) for a,b,c in range(-5,5))
True
bool( assert False for a,b,c in range(-5,5))
SyntaxError: invalid syntax
bool( 1/0 for a,b,c in range(-5,5))
True
bool( 1/0 for a,b,c in range(-5,1/0))
Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>
    bool( 1/0 for a,b,c in range(-5,1/0))
ZeroDivisionError: division by zero
========================================== Sec. II. 32.0.0.0
'REGEXP': r'/(?!/)(\\/|\\\\|[^/])*?/[%s]*' % _RE_FLAGS,
========================================== Sec. II. 32.0.0.0
l =["abc","ABC"]
zip(*zip(l)) == l
False
list(zip(*zip(l))) == l
False
list(zip(*zip(l)))
[('abc', 'ABC')]
========================================== Sec. III. 34.2.0.0
>>> F_to_C(-459.67) == -273.15
False

>>> F_to_C(-459.67)
-273.15000000000003
========================================== Sec. III. 34.2.0.0
>>> [ (a,b)
      for F in range(20)
      for a,b in [[5/9*(F-32), 5*(F-32)/9]]
      if a!=b ]

--------------------------------------------

[(-15.555555555555557, -15.555555555555555),
 (-12.222222222222223, -12.222222222222221),
 (-11.666666666666668, -11.666666666666666),
 ( -7.777777777777779,  -7.777777777777778)]
========================================== Sec. III. 34.2.0.0
assert all( isalmost(*(err := a)) for a in
    [(1,1), (1,1.1,.11)] +
    [(n,n-1e-14) for n in range(1,100)]
), err
assert not any( isalmost(*(err := a)) for a in
    [(1,2), (1,1.1), (5, 5.00008)] +
    [(n,n-1e-12) for n in range(1,100)]
), err
========================================== Sec. III. 34.2.0.0
assert isalmost ( F_to_C(-459.67) , -273.15 )
assert isalmost ( C_to_F(-273.15) , -459.67 )
assert all( isalmost( efc:=F_to_C(C_to_F(c)), ec:=c )
       and  isalmost( efc:=C_to_F(F_to_C(c)), c )
       for c in range(-273, 200) ), (ec, efc)
========================================== Sec. III. 34.3.1.0
assert greatest_root(1,1,1) == None
assert greatest_root(1,-2,1) == 1
assert greatest_root(4,-4,-24) == 3
# we can do direct comparison because we know the results are exact,
# at least in this case, and we only want to prevent regressions
# This is overspecification, and might refuse correct versions of
# the function. However, we can deal with those problems, using
# isalmost, as they arise.
========================================== Sec. III. 34.3.2.0
assert roots(1,1,1) == ()

assert roots(1,-2,1) in [ (1,1), (1,) ]
# I didn't specify whether single roots should be repeated,
# so both versions are valid

assert set(roots(4,-4,-24)) == {-2, 3}
# I did not specify the order of roots, hence the set test
========================================== Sec. III. 35.0.0.0
>>> d20, d6 = d(20), d(6)  # let d20 be a 20-sided die, and d6 a 6-sided one
>>> d20
d20: 14
>>> d20
d20: 4
>>> d20
d20: 7
...
========================================== Sec. III. 35.0.0.0
def d(n): return random.randint(...)
========================================== Sec. III. 35.0.0.0
>>> d20, d6 = d(20), d(6)   # let d20 be a 20-sided die, and d6 a 6-sided one
========================================== Sec. III. 35.0.0.0
>>> d20.N, d6.N
(20, 6)
========================================== Sec. III. 35.0.0.0
>>> d20.roll()
13
>>> d20.roll()
8
>>> d20.roll()
1

>>> type(d20)
<class '__main__.d'>

>>> type(d20.roll())
<class 'int'>
========================================== Sec. III. 35.0.0.0
>>> d20
d20: 14
...
========================================== Sec. III. 35.0.0.0
>>> f"I roll {d20}"
'I roll d20: 11'
...
========================================== Sec. III. 35.0.0.0
>>> d20
<__main__.d object at 0x7447d25af680>
========================================== Sec. III. 35.0.0.0
>>> int(d20)
15
>>> int(d20)
16
========================================== Sec. III. 35.0.0.0
>>> d20+100
106
>>> d20+100
101
>>> type(d20+100)
<class 'int'>
========================================== Sec. III. 35.0.0.0
TypeError: unsupported operand type(s) for +: 'int' and 'd'
========================================== Sec. III. 35.0.0.0
>>> d20+100
108

>>> 100+d20
TypeError: unsupported operand type(s) for +: 'int' and 'd'
========================================== Sec. III. 35.0.0.0
>>> 100+d20
111
>>> 100+d20
120
========================================== Sec. III. 35.0.0.0
>>> 10*d6
29
>>> 10*d6
34
========================================== Sec. III. 35.0.0.0
>>> -100 + 10*d20 + 40
72
>>> -100 + 10*d20 + 40
62
========================================== Sec. III. 35.0.0.0
[columns=flexible]
from collections import Counter

N = 100000
for v, c in (l := sorted(Counter( 3*d6 for _ in range(N) ).items())):
    print(f"{v:2} {'='*(c//500)}")

print("\navg", sum(c*v for v,c in l)/N,
      "\nexpected avg", 3/6*sum(range(1,6+1)), "=", 3*(6+1)/2)

---------------------------------------------------------------------
 3
 4 ==
 5 =====
 6 =========
 7 =============
 8 ===================
 9 ======================
10 =========================
11 ========================
12 =======================
13 ===================
14 ==============
15 =========
16 =====
17 ==
18

avg 10.48865
expected avg 10.5 = 10.5
========================================== Sec. III. 35.0.0.0
 3 =================================
 6 =================================
 9 ================================
12 ================================
15 =================================
18 =================================

avg 10.50894
========================================== Sec. III. 35.0.0.0
x = np.linspace(-2,2,100) # x varies in [-2,2], 100 uniform samples
========================================== Sec. III. 36.1.0.0
[basicstyle=\ttfamily\scriptsize]
from functools import wraps
from contextlib import contextmanager

def trace(l=-1):
    """Show tree of recursive calls and results
    l: 0 = do not show results, N = show up to level N, -1=show all"""
    if l < 0: l = float('inf')
    def tree(lvl, normal=1): return "|  " * lvl + "|" + ("-" if normal else "=")
    def trace(f):
        lvl = 0
        @wraps(f)
        def w(*a,**k):
            nonlocal lvl
            print(f"{tree(lvl)} {f.__name__}({', '.join(map(repr,a))})")
            lvl += 1
            res = f(*a,**k)
            if lvl <= l: print(f"{tree(lvl-1,0)} {res}")
            lvl -= 1
            return res
        w.__hasTrace__ = 1
        return w
    return trace

@contextmanager
def unwrap(f):
    """Temporarily un-decorate global function"""
    if hasattr(f, "__wrapped__"):
        globals()[f.__name__] = f.__wrapped__
        yield
        globals()[f.__name__] = f
    else: yield

def memoize(f):
    memo = {}
    @wraps(f)
    def w(*x):
        if x in memo: return memo[x]
        res = f(*x); memo[x] = res
        return res
    return w

def stress(f, timeout=1, S=10):
    """Stress test f: time how long it takes to compute increasingly high values.
    Stops when the computations start taking too long.
    S: nb of sample times to everage for each computation
    timeout: number of seconds beyond which the test is stopped."""
    if hasattr(f, "__hasTrace__"):
        print(f"Stress-test aborted. Remove @trace when stressing {f.__name__}.")
        return
    from timeit import timeit
    import resource, sys
    resource.setrlimit(resource.RLIMIT_STACK, [10 ** 9, resource.RLIM_INFINITY])
    sys.setrecursionlimit(10 ** 9)
    N = 0
    while True:
        t = timeit(lambda: f(N), number=S)
        print(f"{f.__name__}({N:3}) : {t/S:.1g}s")
        if t > timeout:
            print("Nobody got time for dat."); return
        if N > 100000:
            print("OK, we get the idea."); return
        elif t > timeout/2: N = 1+int(N * 1.05)
        elif t > timeout/100: N = 1+int(N * 1.1)
        else: N = 1+int(N * 1.5)
========================================== Sec. III. 36.2.0.0
# @trace()
# @memoize
def fib(n):
    return ... if ... else ...

with unwrap(fib): # black magic: I don't want @trace on in the unit test!
    assert [fib(n) for n in range(10)] == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
========================================== Sec. III. 36.2.0.0
@trace()                   @trace(1)

|- fib(5)                  |- fib(5)
|  |- fib(4)               |  |- fib(4)
|  |  |- fib(3)            |  |  |- fib(3)
|  |  |  |- fib(2)         |  |  |  |- fib(2)
|  |  |  |  |- fib(1)      |  |  |  |  |- fib(1)
|  |  |  |  |= 1           |  |  |  |  |- fib(0)
|  |  |  |  |- fib(0)      |  |  |  |- fib(1)
|  |  |  |  |= 0           |  |  |- fib(2)
|  |  |  |= 1              |  |  |  |- fib(1)
|  |  |  |- fib(1)         |  |  |  |- fib(0)
|  |  |  |= 1              |  |- fib(3)
|  |  |= 2                 |  |  |- fib(2)
|  |  |- fib(2)            |  |  |  |- fib(1)
|  |  |  |- fib(1)         |  |  |  |- fib(0)
|  |  |  |= 1              |  |  |- fib(1)
|  |  |  |- fib(0)         |= 5
|  |  |  |= 0
|  |  |= 1
|  |= 3
|  |- fib(3)
|  |  |- fib(2)
|  |  |  |- fib(1)
|  |  |  |= 1
|  |  |  |- fib(0)
|  |  |  |= 0
|  |  |= 1
|  |  |- fib(1)
|  |  |= 1
|  |= 2
|= 5
========================================== Sec. III. 36.2.0.0
>>> stress(fib)
fib(  0) : 4e-07s
fib(  1) : 1e-07s
fib(  2) : 2e-07s
fib(  4) : 3e-07s
fib(  7) : 1e-06s
fib( 11) : 8e-06s
fib( 17) : 0.0001s
fib( 26) : 0.01s
fib( 29) : 0.04s
fib( 32) : 0.2s
Nobody got time for dat.
========================================== Sec. III. 36.2.0.0
>>> fib(100)
354224848179261915075
========================================== Sec. III. 36.2.0.0
>>> stress(fib)
...
fib(106254) : 0.004s
OK, we get the idea.
========================================== Sec. III. 36.2.0.0
|- fib(5)
|  |- fib(4)
|  |  |- fib(3)
|  |  |  |- fib(2)
|  |  |  |  |- fib(1)
|  |  |  |  |= 1
|  |  |  |  |- fib(0)
|  |  |  |  |= 0
|  |  |  |= 1
|  |  |  |- fib(1)
|  |  |  |= 1
|  |  |= 2
|  |  |- fib(2)
|  |  |= 1
|  |= 3
|  |- fib(3)
|  |= 2
|= 5

|- fib(5)
|= 5
========================================== Sec. III. 36.2.0.0
m = {}
# @trace()
def fibm(n):
    if n <= 1: ...
    if n not in m: m[n] = ...
    return m[n]

with unwrap(fibm):
    assert [fibm(2**n -1) for n in range(6)] == [0, 1, 2, 13, 610, 1346269]
========================================== Sec. III. 36.2.0.0
....
fibm(98959) : 0.004s
fibm(106254) : 0.004s
========================================== Sec. III. 36.2.0.0
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
========================================== Sec. III. 36.2.0.0
>>> fibm(1000)
434665576869374564356885276750406258025646605173717804024817290895
365554179490518904038798400792551692959225930803226347752096896232
398733224711616429964409065331879382989696499285160037044761377951
66849228875
========================================== Sec. III. 36.2.0.0
# @trace()
def fiba(n):
    a = {0:0, 1:1}
    for k in range(2,n+1): a[k] = ...
    return ...

with unwrap(fiba):
    assert [fiba(2**n -1) for n in range(6)] == [0, 1, 2, 13, 610, 1346269]
========================================== Sec. III. 36.2.0.0
|- fiba(5)
|= 5
========================================== Sec. III. 36.2.0.0
# @trace()
def fibl(n):
    if n <= 1: ...
    a,b = 0,1
    for k in range(2,n+1): a,b = ...
    return ...

with unwrap(fibl):
    assert [fibl(2**n -1) for n in range(6)] == [0, 1, 2, 13, 610, 1346269]
========================================== Sec. III. 36.2.0.0
...
fibl(98832) : 0.05s
fibl(103774) : 0.06s
========================================== Sec. III. 37.1.0.0
from math import sqrt

def g(x): return x**2 - 2

>>> res = di(g,1,2)
1.414213562373095
>>> sqrt(2)
1.4142135623730951
>>> sqrt(2)-res
2.220446049250313e-16
========================================== Sec. III. 37.1.0.0
1 1.5 2 1
1 1.25 1.5 0.5
1.25 1.375 1.5 0.25
1.375 1.4375 1.5 0.125
....
1.414213562373095 1.4142135623730951 1.4142135623730954
                                            4.440892098500626e-16
1.414213562373095 1.414213562373095 1.4142135623730951
                                            2.220446049250313e-16
========================================== Sec. III. 37.1.0.0
from math import nextafter as na, inf
def neigh(f): return na(f,inf), na(f,-inf)
assert all( abs(n**.5 - (r:=di(lambda x:x**2-n, 0,99,d)) ) <= d
            or r in neigh(n**.5) # result is closest float
        # for di in [di, di_while]
        for n in range(20) for d in [1e-7, 1e-16, 1e-32] ), r
========================================== Sec. III. 37.1.0.0
for di in [di, di_while]
========================================== Sec. III. 37.1.0.0
assert find(3,[3]) == 0

assert all( (fe:=find(ie:=i,ir:=r)) == (i if 0<=i<N else None)
            for N in range(9) for r in [range(N)]
            for i in list(r)+[-1,N] ), (ir, ie, fe)

assert all( l[fe:=find(ie:=i,il:=l)] == i
            for N in range(5) for R in [2,3]
            for l in [[ e for e in range(N) for _ in range(R) ]]
            for i in range(N)), (il, ie, fe)
========================================== Sec. III. 37.1.0.0
def find(x,l,i=0):
    match l:
        case []:  return None
        case [a]: return i if a==x else None
        case _:
            if x < l[m := len(l) // 2]: return find(x,l[:m], i)
            return find(x, l[m:], i+m)
========================================== Sec. III. 37.1.0.0
def find(x,l):
    def z(a,b):
        ....
    return z(...)
========================================== Sec. III. 37.1.0.0
m = a + (b-a) // 2
========================================== Sec. III. 37.1.0.0
def time_test():
    from timeit import timeit
    print(f"len\tindex\tdicho\tratio")
    for N in [10**n for n in range(9)]:
        l = list(range(N))
        i = [i*N//10 for i in range(10)]+[N-1]
        ti = sum( timeit(lambda: l.index(k), number=1) for k in i )
        td = sum( timeit(lambda: find(k,l),  number=1) for k in i )
        print(f"{N}\t{ti}\t{td}\t{ti/td}")
========================================== Sec. III. 37.2.0.0
def f(x): return 2*x
def g(x): return x**2 - 2
========================================== Sec. III. 37.2.0.0
assert all( abs(deriv(g,x)-f(x)) <= 0.02 for x in range(100) )
========================================== Sec. III. 37.2.0.0
assert all( fderiv(lambda x:x**2)(x) == deriv(lambda x:x**2, x)
            for x in range(100) )
========================================== Sec. III. 37.2.0.0
 x   f(x)  G(x)         f(x)-G(x)
-2.0 -4.00 -3.99   -0.01000000000001755
-1.9 -3.80 -3.79   -0.009999999999995346
...
 1.9  3.80  3.81   -0.009999999999999343
 2.0  4.00  4.01   -0.009999999999888765
========================================== Sec. III. 37.2.0.0
sudo apt install python3-matplotlib
========================================== Sec. III. 37.2.0.0
sudo pacman -S python-matplotlib
========================================== Sec. III. 37.2.0.0
pip3 install matplotlib
========================================== Sec. III. 37.2.0.0
sudo apt install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
    libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \
    python3-tk libharfbuzz-dev libfribidi-dev libxcb1-dev
========================================== Sec. III. 37.2.0.0
import numpy as np, matplotlib.pyplot as plt
x = np.linspace(-2,2,100) # x varies in [-2,2], 100 uniform samples
npf = f(x) ; npg = g(x) ; npG = G(x) # our functions, with special object x

plt.figure(figsize=(12,12))
plt.rcParams.update({"font.size": 18 }) # I need glasses, OK?

plt.plot(x,npf,"b"      ,label="f", linewidth=4)
plt.plot(x,npG,"r"      ,label="G", linewidth=1)
plt.plot(x,npg,"black"  ,label="g", linewidth=3)

plt.title("Visualise Exact and Approximated Derivatives")
plt.legend(loc="best")
plt.axvline(0); plt.axhline(0) # draw abscissa and ordinate axes

# plt.savefig("../derivapprox.pdf", transparent=True)
plt.show()
========================================== Sec. III. 37.3.0.0
>>> res = newton(g,1) # here with optional printing of guesses
-> 1
-> 1.4999999999999996
-> 1.4166666666666667
-> 1.4142156862745099
-> 1.4142135623746899
-> 1.4142135623730951
>>> sqrt(2)
1.4142135623730951
>>> sqrt(2)-res
0.0
========================================== Sec. III. 38.0.0.0
{ i for i in range(n) if i%2==0 }
========================================== Sec. III. 38.0.0.0
def evens(n):
  return { i for i in range(n) if i%2==0 }
========================================== Sec. III. 38.1.0.0
>>> cart_prod({'a', 'b'}, {1,2,3})
{('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3)}
========================================== Sec. III. 38.1.0.0
assert cart_prod(range(3), []) == set()
assert type(cart_prod([1],[2])) is set
assert cart_prod(range(2), range(10, 12)) == {(0,10), (0,11), (1,10), (1,11)}
========================================== Sec. III. 38.1.0.0
>>> cart_prod("ab", {1,2,3})
========================================== Sec. III. 38.1.0.0
>>> cart_prod({'a', 'b'}, {1,2,3})
========================================== Sec. III. 38.1.0.0
assert squares(100) == [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
========================================== Sec. III. 38.1.0.0
assert [ i for i in range(30) if isprime(i) ] \
       == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
========================================== Sec. III. 38.2.0.0
assert palindrome('abba')
assert palindrome('abcba')
assert palindrome('')
assert palindrome('a')
assert not palindrome('ab')
========================================== Sec. III. 38.2.0.0
assert inverse('abc') == ['c', 'b', 'a']
assert inverse('') == []
========================================== Sec. III. 38.2.0.0
assert palinv('abba')
assert palinv('abcba')
assert palinv('')
assert palinv('a')
assert not palinv('ab')
========================================== Sec. III. 38.2.0.0
assert rmfrom('esope reste ici et se repose', 'aeiouy ') == \
       ['s', 'p', 'r', 's', 't', 'c', 't', 's', 'r', 'p', 's']
========================================== Sec. III. 38.2.0.0
assert rmspaces('esope reste ici et se repose') == \
       [ 'e', 's', 'o', 'p', 'e', 'r', 'e', 's', 't',
         'e', 'i', 'c', 'i', 'e', 't', 's', 'e', 'r',
         'e', 'p', 'o', 's', 'e']
========================================== Sec. III. 38.2.0.0
assert palindrome_sentence('esope reste ici et se repose')
assert not palindrome_sentence('esope reste ici et se reposes')
========================================== Sec. III. 38.2.0.0
assert fsum (lambda i:i, 0,10) == 55
assert fsum (lambda i:i**2, 0,10) == 385
========================================== Sec. III. 39.0.0.0
assert "".join(crange('A','Z')) == 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
assert next(crange("a","b")) == "a"
========================================== Sec. III. 39.0.0.0
assert "".join(charrange('A','Z','a','z','0','9')) == \
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
assert next(charrange("a","b")) == "a"
assert "".join(charrange()) == ''
========================================== Sec. III. 40.0.0.0
2+2
print(2+2)
print(print(2+2),print(2+2))
l= [ 1+i for i in range(3) ]
pl = [ print(1+i) for i in range(3) ]
print(l,pl)
========================================== Sec. III. 41.0.0.0
s = { print(i) for i in range(1,3) }
ss = { (i,print(i)) for i in range(1,3) }
sss = { (i,i,print(i)) for i in range(1,3) }
print(s,ss,sss,sep='\n')
========================================== Sec. III. 42.0.0.0
def spicy_function(X, Y):
 E = set()
 { E.add( (x,y) ) for x in X for y in Y }
 return E
========================================== Sec. III. 42.0.0.0
{ E.add( (x,y) ) for x in X for y in Y }
========================================== Sec. III. 42.0.0.0
True if len(p) == 0 else not False in {True if p[j] ==
          p[len(p)-j-1] else False for j in range (len(p)//2)}
========================================== Sec. III. 43.0.0.0
class matrix():
    def __init__(s, N=100):
        s.N = N
        s.m = [ [... for ...] for ... ]

    def __repr__(s): return f"matrix({repr(s.m)})"
========================================== Sec. III. 43.0.0.0
>>> matrix(3)
matrix([[0, 0, 0], [0, 1, 0], [0, 0, 2]])
========================================== Sec. III. 43.0.0.0
assert matrix(3).m == [[0, 0, 0], [0, 1, 0], [0, 0, 2]]
========================================== Sec. III. 43.0.0.0
>>> matrix([[0, 0, 0], [0, 1, 0], [0, 0, 2]])
TypeError: 'list' object cannot be interpreted as an integer
========================================== Sec. III. 43.0.0.0
    def sum(s):
        return sum(...)
========================================== Sec. III. 43.0.0.0
assert all( matrix(N).sum() == N*(N-1)//2 for N in range(10) )
========================================== Sec. III. 43.0.0.0
>>> [ (a,b) for N in range(10) if (a:= N*(N-1)//2) != (b:= N*((N-1)//2)) ]
[(1, 0), (6, 4), (15, 12), (28, 24)]
========================================== Sec. III. 43.0.0.0
class smatrix():
    def __init__(s, N=100):
        s.N = N
        s.m = defaultdict(..., { ... for ...  })

    def __repr__(s): return f"smatrix({repr(s.m)})"
========================================== Sec. III. 43.0.0.0
>>> smatrix(3)
smatrix(defaultdict(<function smatrix.__init__.<locals>.
    <lambda> at 0x7f54f0748220>, {(1, 1): 1, (2, 2): 2}))
========================================== Sec. III. 43.0.0.0
assert type(smatrix(3).m) is defaultdict
assert smatrix(3).m == { (1, 1): 1, (2, 2): 2 }
assert smatrix(3).m[(999, 999)] == 0
========================================== Sec. III. 43.0.0.0
    def sum(s):
        return sum(...)
========================================== Sec. III. 43.0.0.0
assert all( smatrix(N).sum() == N*(N-1)//2 for N in range(10) )
========================================== Sec. III. 43.0.0.0
>>> smatrix(3).full_matrix()
[[0, 0, 0], [0, 1, 0], [0, 0, 2]]
========================================== Sec. III. 43.0.0.0
assert all( smatrix(N).full_matrix() == matrix(N).m for N in range(10) )
========================================== Sec. III. 43.0.0.0
>>> smatrix(3)
smatrix([[0, 0, 0], [0, 1, 0], [0, 0, 2]])
========================================== Sec. III. 43.0.0.0
>>> smatrix(3)
smatrix(defaultdict(<function smatrix.__init__.<locals>.
    <lambda> at 0x7f54f0748220>, {(1, 1): 1, (2, 2): 2}))
========================================== Sec. III. 43.0.0.0
assert all( repr(smatrix(N)) == "s"+repr(matrix(N)) for N in range(10) )
========================================== Sec. III. 43.0.0.0
def test():
    from timeit import timeit
    for desc,f in [
    ("matrix init",            lambda: matrix()),
    ("matrix init+sum",        lambda: matrix().sum()),
    ("sparse matrix init",     lambda: smatrix()),
    ("sparse matrix init+sum", lambda: smatrix().sum()),
        ]:
        print(f"{desc:<25}{timeit(f,number=1000):.3f}")
========================================== Sec. III. 43.0.0.0
matrix init              0.183
matrix init+sum          0.446
sparse matrix init       0.005
sparse matrix init+sum   1.208
========================================== Sec. III. 43.0.0.0
matrix init              0.185
matrix init+sum          0.467
sparse matrix init       0.005
sparse matrix init+sum   0.006
========================================== Sec. III. 44.0.0.0
assert powerlist([]) == [[]]
assert sorted(powerlist([1,2,3]), key=lambda x:(len(x), x)) == \
    [[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]
assert sorted(powerlist([1,1,1]), key=len) == \
    [[], [1], [1], [1], [1, 1], [1, 1], [1, 1], [1, 1, 1]]
assert all( len(powerlist(range(n))) == 2**n for n in range(5) )
========================================== Sec. III. 44.0.0.0
for powerlist in (powerlist, powerlist2):
    assert powerlist([]) == [[]]
    assert sorted(powerlist([1,2,3]), key=lambda x:(len(x), x)) == \
        [[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]
    assert sorted(powerlist([1,1,1]), key=len) == \
        [[], [1], [1], [1], [1, 1], [1, 1], [1, 1], [1, 1, 1]]
    assert all( len(powerlist(range(n))) == 2**n for n in range(5) )
========================================== Sec. III. 44.0.0.0
assert all( powerset(r:=range(n)) == { frozenset(s) for s in powerlist(r) }
            for n in range(5) )
========================================== Sec. III. 44.0.0.0
assert type(powergen([])) is type(_ for _ in [])
assert all( type(s) is set for s in powergen(range(5)) )
assert all( set(map(frozenset, powergen(r:=range(n))))
            == { frozenset(s) for s in powerlist(r) }
            for n in range(5) )
========================================== Sec. III. 45.0.0.0
assert partition(-1, 1, 1) == [(-1.0, 1.0)]
assert partition(0,30,3)   == [(0.0, 10.0), (10.0, 20.0), (20.0, 30.0)]
assert partition(30,0,3)   == [(30.0, 20.0), (20.0, 10.0), (10.0, 0.0)]
assert partition(-1, 1, 4) == [(-1.0, -0.5), (-0.5, 0.0),
                                             (0.0, 0.5), (0.5, 1.0)]
========================================== Sec. III. 45.0.0.0
assert abs(riemann(lambda x:x, 0,1) - 0.5) < 1e-9
assert abs(riemann(sqrt, 0,1) - 2/3) < 1e-4
========================================== Sec. III. 45.0.0.0
assert all( abs( primitive(lambda x:x, x) - (x*x / 2)) < 1e-9
                 for x in [-32, 0, 1, 2, 8, 64] )
========================================== Sec. III. 45.0.0.0
assert all( fprimitive(sqrt)(x) == primitive(sqrt, x) for x in range(100) )
========================================== Sec. III. 46.1.0.0
@dataclass
class BinExpr:
    a: object
    b: object

class BinOp (BinExpr): pass

@dataclass
class UnOp:
    a: object
========================================== Sec. III. 46.1.0.0
>>> x = "x" # our main variable name
>>> f1 = Plus(1, Mul(2,Call("ln",Minus(Pow(x,2),1))))
>>> f1
Plus(a=1, b=Mul(a=2, b=Call(a='ln', b=Minus(a=Pow(a='x', b=2), b=1))))

>>> f2 = Mul(Call("ln",x), Plus(Mul(3,Pow(x,2)), 1))
>>> f2
Mul(a=Call(a='ln', b='x'), b=Plus(a=Mul(a=3, b=Pow(a='x', b=2)), b=1))
========================================== Sec. III. 46.1.0.0
>>> estr(f1)
'(1 + (2 * (ln(x^2 - 1))))'

>>> estr(f2)
'((ln x) * ((3 * x^2) + 1))'
========================================== Sec. III. 46.1.0.0
>>> eval(f2, x, 0)
inf

>>> eval(f2, x, 0.1)
-2.371662645783867

>>> eval(f2, x, 1)
0.0

>>> eval(f2, x, 2)
9.010913347279288

>>> eval(f2, x, 1.4)
2.3149289879539445

>>> eval(f2, "y", 1)
... an error of some sort
========================================== Sec. III. 46.1.0.0
>>> log(0)
ValueError: math domain error

>>> 1/0
ZeroDivisionError: division by zero
========================================== Sec. III. 46.1.0.0
except ValueError as e:
    if str(e) =="math domain error":
        return float('inf')
    raise e
========================================== Sec. III. 46.1.0.0
>>> eval([],x,1)
ValueError: []
========================================== Sec. III. 46.1.0.0
class Plus       (BinOp):
    symb = "+"
    sem = lambda x,y:x+y
========================================== Sec. III. 46.1.0.0
match e:
    case BinOp(l,r):    return f"({fstr(l)} {e.symb} {fstr(r)})"
========================================== Sec. III. 46.1.0.0
TypeError: Call.<lambda>() takes 2 positional arguments but 3 were given
========================================== Sec. III. 46.1.0.0
x = np.linspace(-2,2,100) # x varies in [-2,2], 100 uniform samples
npf = f(x)
========================================== Sec. III. 46.1.0.0
import matplotlib.pyplot as plt

plt.figure(figsize=(12,8))
plt.rcParams.update({"font.size": 18 })

X = [-2 + i/100 for i in range(500) ]
Yf1 = [eval(f1,x,X) for X in X]
Yf2 = [eval(f2,x,X) for X in X]
plt.ylim([-5, 10]) # limit the y axis
plt.plot(X,Yf1,"b",label=estr(f1), linewidth=2)
plt.plot(X,Yf2,"r",label=estr(f2), linewidth=2)

plt.legend(loc="best")
plt.axvline(0); plt.axhline(0)

##plt.savefig("../excasf1f2.pdf", transparent=True)
plt.show()
========================================== Sec. III. 46.1.0.0
print("f2:", estr(f2), "\n\t->")
print(estr(D(f2,x)))
---------------------------------------------------------------------
f2: ((ln x) * ((3 * x^2) + 1))
->
(((1 / x) * ((3 * x^2) + 1)) + ((ln x) * (((0 * x^2)
                                            + (3 * (2 * x^1))) + 0)))
========================================== Sec. III. 46.1.0.0
print(estr(D(f1,x)))
print(estr(Df1 := simp(D(f1,x))))
---------------------------------------------------------------------
(((1 / x) * ((3 * x^2) + 1)) + ((ln x) * (((0 * x^2)
                                            + (3 * (2 * x^1))) + 0)))
(((1 / x) * ((3 * x^2) + 1)) + ((ln x) * (6 * x)))
========================================== Sec. III. 46.1.0.0
Plus( Mul(..), Mul(..) )
========================================== Sec. III. 46.1.0.0
Plus( Mul(0, e)), Mul(..) )
Plus( Mul(e, 0)), Mul(..) )
...
========================================== Sec. III. 46.1.0.0
def simp(e):
    def z(e):
        match e:
            case Plus(0,e) | Plus(e,0): return e
            ...
    return fixpoint(z,e)
========================================== Sec. III. 46.1.0.0
(((1 / x) * ((3 * x^2) + 1)) + ((ln x) * (6 * x)))
========================================== Sec. III. 46.1.0.0
>>> estr( sub(Minus(Mul(2,x),x), x, Plus(1, Pow(x,3))) )
'((2 * (1 + x^3)) - (1 + x^3))'
========================================== Sec. III. 46.1.0.0
print("f1:", estr(f1), "\n\t->")
print(estr(D(f1,x)))
print(estr(Df1 := simp(D(f1,x))))
---------------------------------------------------------
f1: (1 + (2 * (ln(x^2 - 1))))
->
(0 + ((0 * (ln(x^2 - 1))) + (2 * ((1 / (x^2 - 1))
                                    * ((2 * x^1) - 0)))))
(2 * ((1 / (x^2 - 1)) * (2 * x)))
========================================== Sec. III. 46.2.0.0
X  = F('x')                         # declare a symbolic variable
ln = lambda x: F(Call("ln",x.f))    # declare a symbolic function

F1 = F(f1); F2 = F(f2)
FF1 = 1 + 2*ln(X**2 - 1)

-----------------------------------------------------------------

>>> FF1
(1 + (2 * (ln(x^2 - 1))))

>>> F1(X,2)
3.1972245773362196

>>> FF1.D(X)
(2 * ((1 / (x^2 - 1)) * (2 * x)))
========================================== Sec. III. 46.2.0.0
>>> f1
Plus(a=1, b=Mul(a=2, b=Call(a='ln', b=Minus(a=Pow(a='x', b=2), b=1))))

>>> F1
(1 + (2 * (ln(x^2 - 1))))

>>> F1.f  # the expression is stored internally as attribute f
Plus(a=1, b=Mul(a=2, b=Call(a='ln', b=Minus(a=Pow(a='x', b=2), b=1))))

>>> repr(F1)
'(1 + (2 * (ln(x^2 - 1))))'

>>> str(F1)
'(1 + (2 * (ln(x^2 - 1))))'
========================================== Sec. III. 46.2.0.0
>>> F1 + F1
((1 + (2 * (ln(x^2 - 1)))) + (1 + (2 * (ln(x^2 - 1)))))
========================================== Sec. III. 46.2.0.0
>>> F1 + 10
((1 + (2 * (ln(x^2 - 1)))) + 10)
========================================== Sec. III. 46.2.0.0
>>> 10 + F1
(10 + (1 + (2 * (ln(x^2 - 1)))))
========================================== Sec. III. 46.2.0.0
class F:
    ...
    def  __add__(s,o): return  disp(Plus,s,o)
    def __radd__(s,o): return rdisp(Plus,s,o)
========================================== Sec. III. 46.2.0.0
X  = F('x')                         # declare a symbolic variable
ln = lambda x: F(Call("ln",x.f))    # declare a symbolic function

FF1 = 1 + 2*ln(X**2 - 1)

-----------------------------------------------------------------

>>> FF1
(1 + (2 * (ln(x^2 - 1))))
========================================== Sec. III. 46.2.0.0
>>> FF1.D(X)
(2 * ((1 / (x^2 - 1)) * (2 * x)))
========================================== Sec. III. 46.2.0.0
>>> F1(X,2)
3.1972245773362196
========================================== Sec. III. 46.2.0.0
>>> eval(f1,x,2)
3.1972245773362196
========================================== Sec. III. 47.0.0.0
assert next(upto(range(3),8)) == 0

assert list(upto((x for x in range(3)),8)) == [0, 1, 2]

assert list(upto((n*n for n in range(100)),7)) == \
    [0, 1, 4, 9, 16, 25, 36]
========================================== Sec. III. 47.0.0.0
assert all( nth(g,i) == i*i
            for i in range(7)
            for g in [(n*n for n in range(7))] )
========================================== Sec. III. 47.0.0.0
assert next(powers(lambda x:2*x,1)) == 1

assert list(upto(powers(lambda x:2*x,1),7))  == \
    [1, 2, 4, 8, 16, 32, 64]
========================================== Sec. III. 47.0.0.0
assert next(group('a')) == ['a']

assert list(group('')) == []

assert list(group('a')) == [['a']]

assert list(group('aaba')) == [['a', 'a'], ['b'], ['a']]

assert list(group('aabbbcdaaaa')) == \
    [['a', 'a'], ['b', 'b', 'b'], ['c'], ['d'], ['a', 'a', 'a', 'a']]
========================================== Sec. III. 47.0.0.0
assert next(groupn('a')) == (1, 'a')

assert list(groupn('aabbbcdaaaa')) == \
    [(2, 'a'), (3, 'b'), (1, 'c'), (1, 'd'), (4, 'a')]
========================================== Sec. III. 47.0.0.0
assert next(groupl(groupn('aa'))) == ['a', 'a']

assert all ( tuple(group(s)) == tuple(groupl(groupn(s)))
             for s in ('','a','aaba','aabbbcdaaaa') )
========================================== Sec. III. 47.0.0.0
assert list(upto(powers(say,'1'),7)) == \
    ['1', '11', '21', '1211', '111221', '312211', '13112221']

assert list(upto(powers(say,'22'),7)) == \
    ['22', '22', '22', '22', '22', '22', '22']
========================================== Sec. III. 47.0.0.0
assert list(sayg(''))       == []
assert list(sayg('1'))      == ['1', '1']
assert list(sayg('1211'))   == ['1', '1', '1', '2', '2', '1']
========================================== Sec. III. 47.0.0.0
assert "".join( upto(nthpowerg(sayg,6,'1'),8) ) == '13112221'
========================================== Sec. III. 47.0.0.0
>>> perf(5,1)
Performance analysis: rank 5, 1 digit.
First 1 digit of C_5 = '3'
C_0 : 0 of 0
C_1 : 2 of 2
C_2 : 2 of 2
C_3 : 3 of 4
C_4 : 4 of 6
C_5 : 1 of 6
Total: 12 of 20, or 60.0%
========================================== Sec. III. 47.0.0.0
>>> perf(55,30)
Performance analysis: rank 55, 30 digits.
First 30 digits of C_55 = '111312211312111322212321121113'
C_0 : 0 of 0
..
C_4 : 6 of 6
..
C_10 : 5 of 26
..
C_20 : 4 of 408
..
C_30 : 4 of 5808
..
C_40 : 5 of 82350
..
C_50 : 12 of 1166642
C_51 : 12 of 1520986
C_52 : 17 of 1982710
C_53 : 20 of 2584304
C_54 : 21 of 3369156
C_55 : 30 of 4391702
Total: 343 of 18858434, or 0.0018188148602370697%
========================================== Sec. IV. 48.0.0.0
>>> range(-2, 2, .1)
TypeError: 'float' object cannot be interpreted as an integer
========================================== Sec. IV. 48.0.0.0
assert list(frange_inc(0,0,1))    == []
assert next(frange_inc(0,1,1))    == 0
assert list(frange_inc(-5,5,1.0)) == [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
assert list(frange_inc(.7,.8,.1)) == [0.7, 0.7999999999999999]
assert list(frange_inc(0,.8,.1))  == [0, 0.1, 0.2, 0.30000000000000004,
                                 0.4, 0.5, 0.6, 0.7, 0.7999999999999999]
========================================== Sec. IV. 48.0.0.0
list(frange_inc(.7,.8,.1)) == [0.7, 0.7999999999999999]
========================================== Sec. IV. 48.0.0.0
>>> .7 + .1
0.7999999999999999
>>> .7 + .1 < .8
True
>>> .2 + .1
0.30000000000000004
========================================== Sec. IV. 48.0.0.0
>>> ceil((.8-.7)/0.1)
2
========================================== Sec. IV. 48.0.0.0
>>> (.8-.7)/0.1
1.0000000000000009
========================================== Sec. IV. 48.0.0.0
>>> list(frange_inc(10**16, 10**16+2, 1.0))
========================================== Sec. IV. 48.0.0.0
KeyboardInterrupt
========================================== Sec. IV. 48.0.0.0
>>> g = frange_inc(10**16, 10**16+2, 1.0)
>>> next(g)
10000000000000000
>>> next(g)
1e+16
>>> next(g)
1e+16
...
========================================== Sec. IV. 48.0.0.0
>>> 10**16 + 1.0
1e+16
>>> 10**16 + 1.0 == 10**16
True
========================================== Sec. IV. 48.0.0.0
>>> 1e+18 + 10 == 1e+18
True
========================================== Sec. IV. 48.0.0.0
>>> 1e+19 + 1000 == 1e+19
True
========================================== Sec. IV. 48.0.0.0
assert list(frange_mul(0,0,1))    == []
assert next(frange_mul(0,1,1))    == 0
assert list(frange_mul(-5,5,1.0)) == [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
assert list(frange_mul(.7,.8,.1)) == [0.7, 0.7999999999999999]
assert list(frange_mul(0,.8,.1))  == [0, 0.1, 0.2, 0.30000000000000004,
                       0.4, 0.5, 0.6000000000000001, 0.7000000000000001]
========================================== Sec. IV. 48.0.0.0
assert list(frange(0,0,1))   == [0]
assert next(frange(0,1,1))   == 0.5
assert list(frange(0,1,1))   == [0.5]
assert list(frange(0,1,2))   == [0, 1]
assert list(frange(-5,4,10)) == [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
assert all( len(list(frange(0,1,n))) == n for n in range(100) )
assert list(frange(0,1,11))  == [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6,
                                                0.7, 0.8, 0.9, 1.0]
========================================== Sec. IV. 48.0.0.0
>>> len(set(frange(10**15, 10**15+10, 10)))
10
>>> len(set(frange(10**16, 10**16+10, 10)))
6
>>> len(set(frange(10**17, 10**17+10, 10)))
2
>>> set(list(frange(10**17, 10**17+10, 10)))
{1e+17, 1.0000000000000002e+17}
========================================== Sec. IV. 49.0.0.0
[deletekeywords={and}]
@slow(1)
def verbose(n=100):
    if n <= 0: return
    print(n, "I do stuff and I talk about it!")
    verbose(n-1)
---------------------------------------------------
                                    # wait a second
100 I do stuff and I talk about it! # wait a second
99 I do stuff and I talk about it!  # wait a second
...
========================================== Sec. IV. 49.0.0.0
[deletekeywords={and}]
100 I do stuff and I talk about it!
99 I do stuff and I talk about it!
98 I do stuff and I talk about it!
---------------------------------------------------------------------------
        # user presses ENTER
97 I do stuff and I talk about it!
96 I do stuff and I talk about it!
95 I do stuff and I talk about it!
---------------------------------------------------------------------------
        # user presses ENTER
94 I do stuff and I talk about it!
....
2 I do stuff and I talk about it!
---------------------------------------------------------------------------
        # user presses ENTER
1 I do stuff and I talk about it!
========================================== Sec. IV. 50.0.0.0
[mathescape=true]
D = disk(10)
D.print("$\varepsilon$ $\Delta$0 c (untouched)")
========================================== Sec. IV. 50.0.0.0
[mathescape=true]
__________ {}                   | $\varepsilon$ $\Delta$0 c (untouched)
========================================== Sec. IV. 50.0.0.0
>>> D[10]
'_'
========================================== Sec. IV. 50.0.0.0
>>> repr(D)
'{}'

>>> str(D)
'__________'
========================================== Sec. IV. 50.0.0.0
>>> D = disk(10)
>>> D[5] = 'A'

>>> print(D)
_____A_____

>>> D
{5: 'A'}
========================================== Sec. IV. 50.0.0.0
>>> D = disk(10)
>>> D.writeat(2, "blah")

>>> print(D)
__blah____
========================================== Sec. IV. 50.0.0.0
D = D.diff()
========================================== Sec. IV. 50.0.0.0
[mathescape=true]
D = disk(10)
D.print("$\varepsilon$ $\Delta$0 c (untouched)")
D.writeat(5, "AA")
D.print("$\varepsilon$ $\Delta$0 c (written to)")
D = D.diff()
D.print("$\varepsilon$ $\Delta$0 s1 $\Delta$1 c (snap just taken)")
D.writeat(4,"BB")
D.print("$\varepsilon$ $\Delta$0 s1 $\Delta$1 c (written)")
D = D.diff()
D.print("$\varepsilon$ $\Delta$0 s1 $\Delta$1 s2 $\Delta$2 c (snap just taken)")
D.writeat(5,"C")
D.print("$\varepsilon$ $\Delta$0 s1 $\Delta$1 s2 $\Delta$2 c (written)")
========================================== Sec. IV. 50.0.0.0
[mathescape=true]
__________ {}                   | $\varepsilon$ $\Delta$0 c (untouched)
_____AA___ {5: 'A', 6: 'A'}     | $\varepsilon$ $\Delta$0 c (written to)
_____AA___ {}                   | $\varepsilon$ $\Delta$0 s1 $\Delta$1 c (snap just taken)
____BBA___ {4: 'B', 5: 'B'}     | $\varepsilon$ $\Delta$0 s1 $\Delta$1 c (written)
____BBA___ {}                   | $\varepsilon$ $\Delta$0 s1 $\Delta$1 s2 $\Delta$2 c (snap just taken)
____BCA___ {5: 'C'}             | $\varepsilon$ $\Delta$0 s1 $\Delta$1 s2 $\Delta$2 c (written)
========================================== Sec. IV. 50.0.0.0
>>> D.history()

[{5: 'A', 6: 'A'}, {4: 'B', 5: 'B'}, {5: 'C'}]
========================================== Sec. IV. 50.0.0.0
####################
_____AA___ {5: 'A', 6: 'A'}
____BBA___ {4: 'B', 5: 'B'}
____BCA___ {5: 'C'}
####################
========================================== Sec. IV. 51.0.0.0
pip install pyexcel-ods3
========================================== Sec. IV. 51.0.0.0
#!/usr/bin/env python3

from pyexcel_ods3 import get_data
dbr = get_data("db.ods") # raw database
========================================== Sec. IV. 51.0.0.0
db> some arrant nonsense 10
? ['some', 'arrant', 'nonsense', '10']
========================================== Sec. IV. 51.0.0.0
[emph={[10]show}]
db> show tables
  * students
  * origins
========================================== Sec. IV. 51.0.0.0
[emph={[10]view}]
db> view students
  Name   Age   Python   TL   Origin
+------+-----+--------+----+--------+
| Toto | 20  | 15     | 5  | L2M    |
| Tata | 21  | 8      | 10 | DUTG   |
| Titi | 20  | 15     | 18 | L3I    |
| Bibi | 20  | 12     | 15 | L2M    |
| Baba | 18  | 15     | 11 | L2M    |
+------+-----+--------+----+--------+

db> view origins
  Origin     OriginName      Tutor
+--------+-----------------+-------+
| L3I    | Licence 3 Info  | No    |
| DUTG   | DUT GEII        | Yes   |
| L2M    | Licence 2 Maths | Maybe |
+--------+-----------------+-------+

db> view InvalidTable
KeyError('InvalidTable')
========================================== Sec. IV. 51.0.0.0
[emph={[10]select,where}]
db> select * from students
  Name   Age   Python   TL   Origin
+------+-----+--------+----+--------+
| Toto | 20  | 15     | 5  | L2M    |
| Tata | 21  | 8      | 10 | DUTG   |
| Titi | 20  | 15     | 18 | L3I    |
| Bibi | 20  | 12     | 15 | L2M    |
| Baba | 18  | 15     | 11 | L2M    |
+------+-----+--------+----+--------+
========================================== Sec. IV. 51.0.0.0
[emph={[10]select,where}]
db> select Name,Python from students
  Name   Python
+------+--------+
| Toto | 15     |
| Tata | 8      |
| Titi | 15     |
| Bibi | 12     |
| Baba | 15     |
+------+--------+
========================================== Sec. IV. 51.0.0.0
[emph={[10]select,where}]
db> select Name,Origin from students where Python=15,Age=20
  Name   Origin
+------+--------+
| Toto | L2M    |
| Titi | L3I    |
+------+--------+
========================================== Sec. IV. 51.0.0.0
db> select * from students|Origin|origins
  Name   Age   Python   TL   Origin     OriginName      Tutor
+------+-----+--------+----+--------+-----------------+-------+
| Toto | 20  | 15     | 5  | L2M    | Licence 2 Maths | Maybe |
| Tata | 21  | 8      | 10 | DUTG   | DUT GEII        | Yes   |
| Titi | 20  | 15     | 18 | L3I    | Licence 3 Info  | No    |
| Bibi | 20  | 12     | 15 | L2M    | Licence 2 Maths | Maybe |
| Baba | 18  | 15     | 11 | L2M    | Licence 2 Maths | Maybe |
+------+-----+--------+----+--------+-----------------+-------+
========================================== Sec. IV. 51.0.0.0
[emph={[10]select,where}]
db> select Name,Tutor from students|Origin|origins where Python=15,Age=20
  Name   Tutor
+------+-------+
| Toto | Maybe |
| Titi | No    |
+------+-------+
========================================== Sec. IV. 52.2.0.0
msg1 = "THESTUDENTSARENICEANDHARDWORKING"
key1 = "ORARETHEY"
cyp1 = crypt (msg1,key1)

>>> cyp1
'HYEJXNKILHJAIIGPGCOEDYEKKAMFBIEK'

>>> crypt(cyp1, key1 ,True)
'THESTUDENTSARENICEANDHARDWORKING'
========================================== Sec. IV. 52.3.0.0
2017:
MVUDHIVKSMREKSGMMEKOZXSVZVNMTATSLZTOITYGIROLZWMGFRIMIQCLXECSIXLASULCR

2018:
GVVMFEMMCTKYQBPZBDPYHJYYZIYSOHZRMNIOXMIQPYGBPMLUKVWZRFHAIWECJC

2019:
LXATDEMAFLIDVVFZKZHPBWARJEWXMAHSMZATGWPCJIDIWFSSVTMNAUTVJCYFDVVL

2019 bis:
LXATDEMAFLIDVVFZKZHPBWARJEZEHWQTIINWRMNUWEXMTTPMQZXHQDLIPKZSYMDHREVVXCZPX

2020:
XCZJLWVGIKEYQRBVTQNSPAFMWJEICSIHXNVRPLDIAKENVHFTAMTNGIGIDPWMPRCYUDIBKWHKW
    ...  TEWEXUCIGOCQAVS
========================================== Sec. IV. 52.3.0.0
import ngram_score as ns
fitness = ns.ngram_score()

>>> fitness.score('THISISACOHERENTSENTENCE')
-79.75074906594747
>>> fitness.score('LKFJLSDFJIOJZOJMIOFJNZA')
-176.05856134934515
========================================== Sec. IV. 52.3.0.0
>>> autobreak (cyp1)
          W LCINBROMPLNEMMKTKGSIHCIOOEQJFMIO -215.1410845557442
         XW KCHNARNMOLMELMJTJGRIGCHONEPJEMHO -192.46846034100733
        HEA AUECTNDELAFABEGICCHADRAKDWMYXIXG -182.68686138209173
       XUMC KESHATYGONXYLOUNJICCGESINGADEOSI -165.81846421022996
      WYAIT LAEBERMIDONCIANTICGLHAECREOFTPIM -157.3321135527277
      WYAIT LAEBERMIDONCIANTICGLHAECREOFTPIM -157.3321135527277
    GETYXAR BULLANTCHOLDIRALNEREMSARMDMOVELM -142.23665938186278
    GETYXAR BULLANTCHOLDIRALNEREMSARMDMOVELM -142.23665938186278
  ORARETHEY THESTUDENTSARENICEANDHARDWORKING -112.42451600254101
  ORARETHEY THESTUDENTSARENICEANDHARDWORKING -112.42451600254101
========================================== Sec. IV. 52.3.0.0
>>> c = crypt('THISISATEST','K')
>>> c
'DRSCSCKDOCD'
>>> autobreak (c)
          K THISISATEST -30.663467249176005
          K THISISATEST -30.663467249176005
          K THISISATEST -30.663467249176005
          K THISISATEST -30.663467249176005
      LKKKZ SHISTRATEDS -29.063122550537994
     DZKPMJ ASINGTHEENR -27.47716321489606
    ZOKPMJD EDINGTHEASO -26.043436663213683
   KKKKZVGM THISTHEREST -25.225017010062118
   KKKKZVGM THISTHEREST -25.225017010062118
 KKOLOPRVBW THERENTINGT -25.003927573660018
========================================== Sec. IV. 52.4.0.0
assert all ( isalpha(chr(n)) == chr(n).isalpha() for n in range(128) )
assert all (not isalpha(chr(n)) for n in range(128,256) )
========================================== Sec. IV. 52.4.0.0
assert tuple(gramiter("ATTACK")) == ('ATTA', 'TTAC', 'TACK')
assert tuple(gramiter("ATTACK",n=1)) == ('A', 'T', 'T', 'A', 'C', 'K')
========================================== Sec. IV. 52.4.0.0
process(text="WP.txt", out="quads.txt"):
========================================== Sec. IV. 52.4.0.0
<4GRAM> <nb of occurrences>
========================================== Sec. IV. 52.4.0.0
THAT 8285
THER 8187
WITH 6538
DTHE 6295
NTHE 5728
OTHE 5590
...
========================================== Sec. IV. 52.4.0.0
>>> score('THISISACOHERENTSENTENCE')
13537
>>> score('BLAHIBLAHBLOBYAAKNOWAHH')
1623
>>> score('LKFJLSDFJIOJZOJMIOFJNZA')
0
========================================== Sec. IV. 52.4.0.0
>>> score('THISISACOHERENTSENTENCE')
9.52447211241367e-85
>>> score('BLAHIBLAHBLOBYAAKNOWAHH')
7.907084778567561e-146
>>> score('LKFJLSDFJIOJZOJMIOFJNZA')
9.26233860365592e-169
>>> score('LKFJLSDFJIOJZOJMIOFJNZA'*2)
0.0
>>> score('THISISACOHERENTSENTENCE'*4)
0.0
========================================== Sec. IV. 52.4.0.0
>>> sys.float_info.min
2.2250738585072014e-308
========================================== Sec. IV. 52.4.0.0
from math import log10 as lg
========================================== Sec. IV. 52.4.0.0
>>> score('THISISACOHERENTSENTENCE')
-84.02115908547061
>>> score('BLAHIBLAHBLOBYAAKNOWAHH')
-145.10198360473777
>>> score('LKFJLSDFJIOJZOJMIOFJNZA')
-168.03327934653242
========================================== Sec. IV. 52.4.0.0
>>> score('THISISACOHERENTSENTENCE')
-79.75074906594747
>>> score('BLAHIBLAHBLOBYAAKNOWAHH')
-119.3170079814685
>>> score('LKFJLSDFJIOJZOJMIOFJNZA')
-176.05856134934515
========================================== Sec. IV. 53.1.0.0
assert [ isqrt_builtin(n) for n in range(30) ] == \
    [ 0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3,
      3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5 ]
========================================== Sec. IV. 53.1.0.0
for n in range (100):
    assert f(n) == isqrt_builtin(n), n
========================================== Sec. IV. 53.1.0.0
for f in (isqrt_hard, isqrt_dicho,...): # replace by your functions
    for n in range (100):
        assert f(n) == isqrt_builtin(n), (f,n)
========================================== Sec. IV. 53.5.0.0
from timeit import timeit
========================================== Sec. IV. 53.5.0.0
>>> N = 10000                           # repetitions

>>> timeit(lambda: isqrt_hard(1000) , number=N) / N
9.853858899987244e-06                   # average time, in seconds

>>> timeit(lambda: isqrt_hard(1000000) , number=N) / N
0.00029428713969991804
========================================== Sec. IV. 54.0.0.0
assert [ i for i in range(30) if isprime(i) ] \
       == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
========================================== Sec. IV. 54.0.0.0
comp = { i for i in range(...) if any(......) }
========================================== Sec. IV. 54.0.0.0
comp = { i for i in range(...) for j in range(...) if ... }
========================================== Sec. IV. 54.0.0.0
{ j for i in range(...) for j in range(...) }
========================================== Sec. IV. 54.0.0.0
{ i*j for i in range(...) for j in range(...) }
========================================== Sec. IV. 54.0.0.0
assert comp == comp2 == comp3, (comp^comp2, comp^comp3)
========================================== Sec. IV. 54.0.0.0
assert primes == tuple( k for k in range(1, n+1) if isprime(k) )
========================================== Sec. IV. 55.0.0.0
(y,m,d) < (Y,M,D)
========================================== Sec. IV. 55.2.0.0
x in (1,3,7,10)
========================================== Sec. IV. 55.2.0.0
elif x == 1:.. elif x == 3.. etc
========================================== Sec. IV. 55.2.0.0
match x:
    case 1|3|7|10: ...
========================================== Sec. IV. 55.5.0.0
assert (days_between(1985,10,21, 1985,10,21) ==      0)
assert (days_between(1985,10,20, 1985,10,21) ==      1)
assert (days_between(1985,10,21, 1985,10,20) ==     -1)
assert (days_between(1985,10,21, 2017,9,19)  ==  11656)
assert (days_between(2017,9,19,  1985,10,21) == -11656)
assert (days_between(1999,12, 5, 2000,3,1)   ==     87)
========================================== Sec. IV. 55.6.0.0
assert weekday(1900,1,1)   == 1
assert weekday(1985,10,21) == 1
assert weekday(2017,9,19)  == 2
assert weekday(1899,12,31) == 0
assert weekday(1700,1,1)   == 5
assert weekday(2019,9,14)  == 6
========================================== Sec. IV. 55.9.0.0
def approxrat(*p):
    ex = days_between(*p)
    ap = days_between_approx(*p)
    #print(ex,ap,ap/ex)
    return ap/ex

assert isalmost (approxrat(1985,10,21,2020,9,19) , 1 , 0.0001)
========================================== Sec. IV. 55.9.0.0
assert isalmost (approxrat(1800,1,1, 2100,1,1)   , 1 , 0.000005)
========================================== Sec. IV. 55.9.0.0
assert approxrat(....) > 1.7
========================================== Sec. IV. 55.10.0.0
assert next(daysgen(1899,12,31,1900,1,4)) == (1899, 12, 31)

assert  list(daysgen(1899,12,31,1900,1,4)) == \
    [(1899, 12, 31), (1900, 1, 1), (1900, 1, 2), (1900, 1, 3)]

assert list(daysgen(2020,2,28,2020,3,2)) == \
    [(2020, 2, 28), (2020, 2, 29), (2020, 3, 1)]

assert list(daysgen(2019,2,28,2019,3,2)) == \
    [(2019, 2, 28), (2019, 3, 1)]

assert sum(1 for _ in daysgen(1985,10,21, 2017,9,19)) == 11656
========================================== Sec. IV. 55.10.0.0
from datetime import date

assert all( (date(*t).weekday()+1)%7 == weekday(*t)
            for t in daysgen(1800,1,1, 2100,1,1) )
========================================== Sec. IV. 55.10.0.0
assert list(daysgen(1899,12,31,1900,1,4,True)) == \
    [((1899, 12, 31), 0), ((1900, 1, 1), 1),
     ((1900, 1, 2), 2), ((1900, 1, 3), 3)]

assert list(daysgen(1899,12,31,1905,1,4,True)) == \
       [ (t,weekday(*t)) for t in daysgen(1899,12,31,1905,1,4) ]
========================================== Sec. IV. 55.10.0.0
def approxdayrat(*p):
    N = days_between(*p)
    n = sum( 1 for (t,d) in daysgen(*p,True) if d == weekday_approx(*t) )
    #print(n,N,n/N)
    return n/N

assert isalmost( approxdayrat(1985,10,21, 2017,9,19), .49, .001 )
assert isalmost( approxdayrat(1800,1,1, 2100,1,1),    .35, .01 )
========================================== Sec. IV. 56.0.0.0
def sign(x):
    return 1 if x >= 0 else -1

def di(f,a,b):
    m = (a+b)/2
    if f(m) = 0:
        return m
    if sign(a) == sign(m):
        return di(f,m,b)
    else:
        return di(f,a,m)
========================================== Sec. IV. 57.0.0.0
l = list(range(1,6))
s = set(l)

print(l, sum_while(l), sum_for_range(l), sum_for(l))
print(s, sum_while(s), sum_for_range(s), sum_for(s))
========================================== Sec. IV. 57.0.0.0
def sum_reduce(l):
    return reduce(## COMPLETER ##)
========================================== Sec. IV. 57.0.0.0
print(union([set('abc'), set('baba'), set('coucou')]))
========================================== Sec. IV. 58.0.0.0
>>> mprint(M)
[0, 1, 1]
[1, 0, 1]
[0, 1, 0]
========================================== Sec. IV. 58.0.0.0
def mmul(A,B):
    n = check_squares(A,B)
    res = ## completer ##
        ## completer ##
        ## completer ##
            res[i][j] = ## completer ##
    return res
========================================== Sec. IV. 59.0.0.0
>>> testgen(allints(4,3),10)
[4, 7, 10, 13, 16, 19, 22, 25, 28, 31]
========================================== Sec. V. 60.2.3.0
class Zero: pass
Z = Zero()

@dataclass
class S:
    i: object

def plus(n,m):
    match n,m:
        case n, Zero() : return n
        case n, S(m)   : return S(plus(n,m))
========================================== Sec. V. 61.0.0.0
def f(args):
  ...         # no side effects on args
  f(args)
========================================== Sec. V. 61.0.0.0
>>> l=[1]
>>> l.append(l)
========================================== Sec. V. 61.0.0.0
l = 1 : l
========================================== Sec. V. 65.0.0.0
def f(n):
    print("call f", n) # afin d'imprimer une trace des appels recursifs
    return n if n <= 1 else f(n-1) + f(n-2)
========================================== Sec. V. 65.1.0.0
stack = []

def fstack(n):
    stack.append(n) ; print(*stack)
    r = n if n <= 1 else fstack(n-1) + fstack(n-2)
    stack.pop() ; print(*stack)
    return r

print(fstack(3),stack)
========================================== Sec. V. 65.2.0.0
memo = {}

def ff(n):
    print("call ff", n, memo)
    ...
========================================== Sec. V. 65.2.0.0
call ff 5 {}
call ff 4 {}
call ff 3 {}
call ff 2 {}
call ff 1 {}
call ff 0 {1: 1}
call ff 1 {1: 1, 0: 0, 2: 1}
call ff 2 {1: 1, 0: 0, 2: 1, 3: 2}
call ff 3 {1: 1, 0: 0, 2: 1, 3: 2, 4: 3}
5
# second call
call ff 5 {1: 1, 0: 0, 2: 1, 3: 2, 4: 3, 5: 5}
5
========================================== Sec. V. 65.2.0.0
def fff(n):
    print("call fff", n)
    if n == 0:
        return (0,1)
     .......
========================================== Sec. V. 65.2.0.0
call fff 5
call fff 4
call fff 3
call fff 2
call fff 1
call fff 0
5
========================================== Sec. V. 65.3.0.0
f = memoize(f)
print(f(5))
print(f(5))
 
========================================== Sec. V. 65.3.0.0
call f 5
call f 4
call f 3
call f 2
call f 1
call f 0
5
5
========================================== Sec. V. 65.3.0.0
g = memoize(f)
print(g(5))
print(g(5))
 
========================================== Sec. V. 65.3.0.0
@memoize
def g(n):
    print("call g", n)
    return n if n <= 1 else g(n-1) + g(n-2)
========================================== Sec. V. 66.0.0.0
def lin_diff_eq(n, init, *a):
    """This variadic function returns the list of the n first terms
      of the linear recurrence relation

        f(0) = init[0], ..., f(m) = init[m],
        f(n) = a[0]*f(n-1) + a[1]*f(n-2) + ... + a[k-1]*f(n-k) + a[k]
                                                            , for n > m

    where k = len(a)-1 and m = len(init)-1. If init is too small,
    an AssertionError should be raised.

    For instance, lin_diff_eq(10, [0, 1], 1, 1, 0) corresponds to
    the first terms of the Fibonacci sequence

        f(O) = 0,  f(1) = 1,  f(n) = f(n-1) + f(n-2), for n >= 2 .

    Furthermore, an *efficient* implementation, in O(n) time, is required.
    """
========================================== Sec. V. 66.0.0.0
>>> lin_diff_eq(10, [0, 1], 1, 1, 0)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
========================================== Sec. V. 66.0.0.0
>>> round(log10(lin_diff_eq(1000, [0,1], 1,1,0)[999]))
208
========================================== Sec. V. 66.0.0.0
>>> lin_diff_eq(10, [0, 1], 1, 1, 1)
[0, 1, 2, 4, 7, 12, 20, 33, 54, 88]
========================================== Sec. V. 66.0.0.0
>>> lin_diff_eq(10, [1], 2, 0)
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
========================================== Sec. V. 66.0.0.0
>>> lin_diff_eq(10, [0,1], 0, 2, 0)
[0, 1, 0, 2, 0, 4, 0, 8, 0, 16]
========================================== Sec. V. 66.0.0.0
>>> lin_diff_eq(18, [0,1,0,0], 1, 1, 1, 1, 0)
[0, 1, 0, 0, 1, 2, 3, 6, 12, 23, 44, 85, 164, 316, 609, 1174, 2263, 4362]
========================================== Sec. V. 66.0.0.0
[mathescape=true]
recrel(n, init, $\gp$, k=None)
========================================== Sec. V. 66.0.0.0
def rr_fac(n,u): return n*u

>>> recrel( 10, [1], rr_fac)
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
========================================== Sec. V. 66.0.0.0
def rr_fib(n,u,uu): return u+uu

>>> recrel( 10, [0,1], rr_fib)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
========================================== Sec. V. 66.0.0.0
def rr_fibs(n,u,uu): return u**2 + uu**2

>>> recrel( 10, [0,1], rr_fibs)
[0, 1, 1, 2, 5, 29, 866, 750797, 563696885165, 317754178345286893212434]
========================================== Sec. V. 66.0.0.0
def logistic(r):
    return lambda n,u : r*u*(1-u)
========================================== Sec. V. 66.0.0.0
# two values oscillation
>>> list(map(lambda x:round(x,1), recrel( 10, [.99], logistic(3) ) ))
[1.0, 0.0, 0.1, 0.2, 0.5, 0.7, 0.6, 0.7, 0.6, 0.7]
>>> list(map(lambda x:round(x,1), recrel( 10, [.3 ], logistic(3) ) ))
[0.3, 0.6, 0.7, 0.6, 0.7, 0.6, 0.7, 0.6, 0.7, 0.6]

# four values oscillation
>>> list(map(lambda x:round(x,1), recrel( 10, [.01], logistic(3.5) ) ))
[0.0, 0.0, 0.1, 0.4, 0.8, 0.5, 0.9, 0.4, 0.8, 0.5]

# chaotic behaviour
>>> list(map(lambda x:round(x,2), recrel( 100, [.01], logistic(3.99) ) ))
[0.01, 0.04, 0.15, 0.51, 1.0, 0.01, 0.05, 0.19, 0.6, 0.95, 0.18, 0.58,
          ..., 1.0, 0.01, 0.05, 0.18, 0.58, 0.97, 0.11, 0.4, 0.96, 0.16]
