Quick reference to a tremendously accessible high-level language —executable pseudocode!
The listing sheet, as PDF, can be found here, or as a single column portrait, while below is an unruly html rendition.
This reference sheet is built from a CheatSheets with Org-mode system.
⇒ Python is object oriented and dynamically type checked.
⇒ With dunder methods, every syntactic construct extends to user-defined datatypes, classes! —Including loops, comprehensions, and even function call notation!
⇒ Children block fragments are realised by consistent indentation, usually 4 spaces. No annoying semicolons or braces.
⇒ τ(x) to try to coerce x into a τ type element, crashes if conversion is not
possible. Usual types: src_python[:exports code]{bool, str, list, tuple, int, float, dict, set}.
Use src_python[:exports code]{type(x)} to get the type of an object x.
⇒ Identifier names are case sensitive; some unicode such as “α” is okay but not “⇒”.
⇒ If obj is an instance of type τ, then we may invoke an instance method f in
two ways: obj.f() or τ.f(obj). The latter hints at why src_python[:exports code]{“self”} is the usual name of the first argument to instance methods. The
former τ.f is the name proper.
⇒ Function and class definitions are the only way to introduce new, local, scope.
⇒ src_python[:exports code]{del x} deletes the object x, thereby removing the name x from scope.
⇒ src_python[:exports code]{print(x, end = e)} outputs x as a string followed by e; end is optional and
defaults to a newline. print(x₁, …, xₙ) prints a tuple without parentheses or
commas.
⇒ The src_python[:exports code]{NoneType} has only one value, src_python[:exports code]{None}. It’s used as the return type of functions that only perform a side-effect, like printing to the screen. Use src_python[:exports code]{type(None)} to refer to it.
Everything here works using Python3.
import sys
assert '3.8.1' == sys.version.split(' ')[0]We’ll use assert y == f(x) to show that the output of f(x) is y.
- Assertions are essentially “machine checked comments”.
Exploring built-in modules with dir and help
| Explore built-in modules with ~dir~ and ~help~ | |
dir(M) | List of string names of all elements in module M |
help(M.f) | Documentation string of function f in module M |
⇒ Print alphabetically all regular expression utilities that mention find.
help can be called directly on a name; no need for quotes.
Besides the usual operators +, *, **, /, //, %, abs, declare src_python[:exports code]{from math import *} to obtain
sqrt, loq10, factorial, … —use dir to learn more, as mentioned above.
- Augmented assignments:
x ⊕= y ≡ x = x ⊕ yfor any operator⊕. - Floating point numbers are numbers with a decimal point.
**for exponentiation and%for the remainder after division.//, floor division, discards the fractional part, whereas/keeps it.- Numeric addition and sequence catenation are both denoted by
+.- However:
1 + 'a' ⇒ error!.
- However:
# Scientific notation: 𝓍e𝓎 ≈ 𝓍 * (10 ** 𝓎)
assert 250e-2 == 2.5 == 1 + 2 * 3 / 4.0
from math import * # See below on imports
assert 2 == sqrt(4)
assert -inf < 123 < +infabs(x) ≈ x * (x > 0) - x * (x < 0)| Every “empty” collection is considered false! Non-empty values are truthy! |
- src_python[:exports code]{bool(x)} ⇒ Interpret object
xas either true or false. - E.g. 0,
None, and empty tuples/lists/strings/dictionaries are falsey.
In Boolean contexts:
| “x is empty” | ≡ | not bool(x) |
len(e) != 0 | ≡ | bool(e) |
bool(e) | ≡ | e |
x != 0 | ≡ | x |
User-defined types need to implement dunder methods __bool__ or __len__ .
Usual infix operations src_python[:exports code]{and, or, not} for control flow
whereas &, | are for Booleans only.
- src_python[:exports code]{None or 4 ≈ 4} but
None | 4crashes due to a type error.
s₁ and ⋯ and sₙ | ⇒ | Do sₙ only if all sᵢ “succeed” |
s₁ or ⋯ or sₙ | ⇒ | Do sₙ only if all sᵢ “fail” |
- src_python[:exports code]{x = y or z} ⇒ assign
xto beyifyis “non-empty” otherwise assign itz. - Precedence: src_python[:exports code]{A and not B or C ≈ (A and (not B)) or C}.
Value equality ==, discrepancy !=;
Chained comparisons are conjunctive; e.g.,
x < y <= z | ≡ | x < y and y <= z |
p == q == r | ≡ | p == q and q == r |
An iterable is an object which can return its members one at a time; this
includes the (finite and ordered) sequence types —lists, strings, tuples—
and non-sequence types —generators, sets, dictionaries. An iterable is any
class implementing __iter__ and __next__; an example is shown later.
- Zero-based indexing,
x[i], applies to sequence types only. - We must have src_python[:exports code]{-len(x) < i < len(x)} and src_python[:exports code]{xs[-i] ≈ xs[len(x) - i]}.
We shall cover the general iterable interface, then cover lists, strings, tuples, etc.
Comprehensions provide a concise way to create iterables; they consist of
brackets —() for generators, [] for lists, {} for sets and
dictionaries— containing an expression followed by a for clause, then zero or
more for or if clauses.
src_python[:exports code]{(f(x) for x in xs if p(x))}
⇒ A new iterable obtained by applying f to the elements of xs that satisfy p ⇐
E.g., the following prints a list of distinct pairs.
print ([(x, y) for x in [1,2,3] for y in (3,1,4) if x != y])Generators are “sequences whose elements are generated when needed”; i.e., are lazy lists.
If [,] are used in defining evens, the program will take forever
to make a list out of the infinitly many even numbers!
Comprehensions are known as monadic do-notation in Haskell and Linq syntax in C#.
Generators are functions which act as a lazy streams of data: Once a yield is
encountered, control-flow goes back to the caller and the function’s state is
persisted until another value is required.
xs = evens()
print (next (xs)) # ⇒ 0
print (next (xs)) # ⇒ 2
print (next (xs)) # ⇒ 4
# Print first 5 even numbers
for _, x in zip(range(5),evens()):
print xNotice that evens is just count(0, 2) from the itertools module.
| Unpacking operation |
- Iterables are “unpacked” with
*and dictionaries are “unpacked” with**. - Unpacking syntactically removes the outermost parenthesis ()/[]/{}.
- E.g., if
fneeds 3 arguments, thenf(*[x₁, x₂, x₃]) ≈ f(x₁, x₂, x₃). - E.g., printing a number of rows:
print(*rows, sep = '\n'). - E.g., coercing iterable
it:set(it) ≈ {*it}, list(it) ≈ [*it], tuple(it) ≈ (*it,)
Iterable unpacking syntax may also be used for assignments, where * yields
lists.
x, *y, z = it | ≡ | x = it[0]; z = it[-1]; y = list(it[1:len(it)-1]) |
| ⇒ | [x] + ys + [z] = list(it) |
E.g., head , *tail = xs to split a sequence.
In particular, since tuples only need parenthesis within expressions,
we may write x , y = e₁, e₂ thereby obtaining simultaneous assignment.
E.g., x, y = y , x to swap two values.
Any user-defined class implementing __iter__ and __next__ can use loop syntax.
for x in xs: f(x)
≈ it = iter(xs); while True: try: f(next(it)) except StopIteration: breakiter(x)⇒ Get an iterable for objectx.next(it)⇒ Get the current element and advance the iterableitto its next state.- Raise StopIteration exception when there are no more elements.
| Methods on Iterables |
- src_python[:exports code]{len} gives the length of (finite) iterables
len ((1, 2))⇒ 2; the extra parentheses make it clear we’re giving one tuple argument, not two integer arguments.
- src_python[:exports code]{x in xs} ⇒ check whether value
xis a member ofxs- src_python[:exports code]{x in y ≡ any(x == e for e in y)}, provided
yis a finite iterable. - src_python[:exports code]{x in y ≡ y.__contains__(x)}, provided
y’s class defines the method. - src_python[:exports code]{x not in y ≡ not x in y}
- src_python[:exports code]{x in y ≡ any(x == e for e in y)}, provided
- src_python[:exports code]{range(start, stop, step)} ⇒ An iterator of integers from
startup tostop-1, skipping every otherstep-1number.- Associated forms:
range(stop)andrange(start, stop).
- Associated forms:
- src_python[:exports code]{reversed(xs)} returns a reversed iterator for
xs; likewise src_python[:exports code]{sorted(xs)}. - src_python[:exports code]{enumerate(xs) ≈ zip(xs, range(len(xs)))}
- Pair elements with their indices.
- src_python[:exports code]{zip(xs₁, …, xsₙ)} is the iterator of tuples
(x₁, …, xₙ)wherexᵢis fromxsᵢ.- Useful for looping over multiple iterables at the same time.
- src_python[:exports code]{zip(xs, ys) ≈ ((x, y) for x in xs for y in ys)}
- src_python[:exports code]{xs₁ , …, xsₙ = zip(*𝓍𝓈)} ⇒ “unzip”
𝓍𝓈, an iterable of tuples, into a tuple of (abstract) iterablesxsᵢ, using the unpacking operation*.xs , τ = [ {1,2} , [3, 4] ] , list assert τ(map(tuple, xs)) == τ(zip(*(zip(*xs)))) == [(1,2) , (3,4)] # I claim the first “==” above is true for any xs with: assert len({len(x) for x in xs}) == 1
- src_python[:exports code]{map(f, xs₁, …, xₙ)} is the iterable of values
f x₁ … xₙwherexᵢis fromxsᵢ.- This is also known as zip with f, since it generalises the built-in
zip. - src_python[:exports code]{zip(xs, ys) ≈ map(lambda x, y: (x, y), xs, ys)}
- src_python[:exports code]{map(f, xs) ≈ (f(x) for x in xs)}
- This is also known as zip with f, since it generalises the built-in
- src_python[:exports code]{filter(p, xs) ≈ (x for x in xs if p(x))}
- src_python[:exports code]{reduce(⊕, [x₀, …, xₙ], e) ≈ e ⊕ x₀ ⊕ ⋯ ⊕ eₙ}; the
initial value
emay be omitted if the list is non-empty.from functools import reduce assert 'ABC' == reduce(lambda x, y: x + chr(ord(y) - 32), 'abc', '')
These are all instances of src_python[:exports code]{reduce}:
- src_python[:exports code]{sum, min/max, any/all} —remember “empty” values
are falsey!
# Sum of first 10 evens assert 90 == (sum(2*i for i in range(10)))
- Use
prodfrom thenumpymodule for the product of elements in an iterable.
- src_python[:exports code]{sum, min/max, any/all} —remember “empty” values
are falsey!
| Flattening |
Since,
src_python[:exports code]{sum(xs, e = 0) ≈ e + xs[0] + ⋯ + xs[len(xs)-1]}
We can use sum as a generic “list of τ → τ” operation by providing
a value for e. E.g., lists of lists are catenated via:
assert [1, 2, 3, 4] == sum([[1], [2, 3], [4]], [])
assert (1, 2, 3, 4) == sum([(1,), (2, 3), (4,)], ())
# List of numbers where each number is repeated as many times as its value
assert [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] == sum([i * [i] for i in range(5)], [])| Methods for sequences only |
| Sequences are Ordered |
Sequences of the same type are compared lexicographically: Where k = min(n, m),
[x₀, …, xₙ] < [y₀, …, yₘ] ≡ x₀ < y₀ or ⋯ or xₖ < yₖ
—recalling that Python’s or is lazy; i.e., later arguments are checked only if
earlier arguments fail to be true. Equality is component-wise.
assert [2, {}] != [3] # ⇒ Different lengths!
assert [2, {}] < [3] # ⇒ True since 2 < 3.
assert (1, 'b', [2, {}]) < (1, 'b', [3])A tuple consists of a number of values separated by commas —parenthesis are only required when the tuples appear in complex expressions.
Simultaneous assignment is really just tuple unpacking on the left and tuple packing on the right.
Strings are both "-enclosed and '-enclosed literals; the former easily allows us
to include apostrophes, but otherwise they are the same.
- There is no separate character type; a character is simply a string of size
one.
- src_python[:exports code]{assert ‘hello’ == ‘he’ + ‘l’ + ‘lo’ == ‘he’ ‘l’ ‘lo’}
- String literals separated by a space are automatically catenated.
- String characters can be accessed with [], but cannot be updated since strings are immutable. E.g., src_python[:exports code]{assert ‘i’ == ‘hi’[1]}.
- src_python[:exports code]{str(x)} returns a (pretty-printed) string representation of an object.
String comprehensions are formed by joining all the strings in the resulting iterable —we may join using any separator, but the empty string is common.
assert '5 ≤ 25 ≤ 125' == (' ≤ '.join(str(5 ** i) for i in [1, 2, 3]))s.join(xs).split(s) ≈ xsxs.split(s)⇒ split stringxsinto a list every timesis encountered
Useful string operations:
s.startswith(⋯) | s.endswith(⋯) |
s.upper() | s.lower() |
- src_python[:exports code]{ord/chr} to convert between characters and integers.
- src_python[:exports code]{input(x)} asks user for input with optional prompt
x. - E.g., src_python[:exports code]{i = int(input(“Enter int: “))} ⇒ gets an integer from user
f-strings are string literals that have an f before the starting quote and may
contain curly braces surrounding expressions that should be replaced by their
values.
name, age = "Abbas", 33.1
print(f"{name} is {age:.2f} years {'young' if age > 50 else 'old'}!")
# ⇒ Abbas is 33.10 years old!F-strings are expressions that are evaluated at runtime, and are generally faster than traditional formatted strings —which Python also supports.
The brace syntax is {expression:width.precision}, only the first is
mandatory and the last is either 𝓃f or 𝓃e to denote 𝓃-many decimal points or
scientific notation, respectively.
Besides all of the iterable methods above, for lists we have:
- src_python[:exports code]{list(cs)} ⇒ turns a string/tuple into the list of its characters/components
xs.remove(x)⇒ remove the first item from the list whose value isx.xs.index(x)⇒ get first index wherexoccurs, or error if it’s not there.xs.pop(i)≈(x := xs[i], xs := xs[:i] + xs[i+1:])[0]- Named Expressions are covered below;
if
iis omitted, it defaults tolen(xs)-1. - Lists are thus stacks with interface
append/pop.
- Named Expressions are covered below;
if
- For a list-like container with fast appends and pops on either end, see the deque collection type.
Note that {} denotes the empty dictionary, not the empty set.
A dictionary is like a list but indexed by user-chosen keys, which are members of any immutable type. It’s really a set of “key:value” pairs.
E.g., a dictionary of numbers along with their squares can be written explicitly (below left) or using a comprehension (below right).
assert {2: 4, 4: 16, 6: 36} == {x: x**2 for x in (2, 4, 6)}| “Case Statements” |
i, default = 'k' , "Dec"
x = { 'a': "Jan"
, 'k': "Feb"
, 'p': "Mar"
}.get(i, default)
assert x == 'Feb'Alternatively: Start with you = {} then later add key-value pairs: you[key] = value.
assert 'Bobert' == you["child2"]['child1'] # access via indices
del you['child2']['child2'] # Remove a key and its value
assert 'Mary' not in you['child2'].values()- src_python[:exports code]{list(d)} ⇒ list of keys in dictionary
d. d.keys(), d.values()⇒ get an iterable of the keys or the values.- src_python[:exports code]{k in d} ⇒ Check if key
kis in dictionaryd. - src_python[:exports code]{del d[k]} ⇒ Remove the key-value pair at key
kfrom dictionaryd. d[k] = v⇒ Add a new key-value pair tod, or update the value at keykif there is one.- src_python[:exports code]{dict(xs)} ⇒ Get a dictionary from a list of key-value tuples.
When the keys are strings, we can specify pairs using keyword arguments:
src_python[:exports code]{dict(me = 12, you = 41, them = 98)}.
Conversely,
d.items()gives a list of key-value pairs; which is useful to have when looping over dictionaries.
In dictionary literals, later values will always override earlier ones:
assert dict(x = 2) == {'x':1, 'x':2}Dictionary update: d = {**d, key₁:value₁, …, keyₙ:valueₙ}.
xs[start:stop:step] ≈ the subsequence of xs from start to stop-1 skipping
every step-1 element. All are optional, with start, stop, and step defaulting
to 0, len(xs), and 1; respectively.
- The start is always included and the end always excluded.
startmay be negative: -𝓃 means the 𝓃-th item from the end.- All slice operations return a new sequence containing the requested elements.
- One colon variant:
xs[start:stop], bothstartandstopbeing optional. - Slicing applies to sequence types only —i.e., types implementing
__getitem__.
assert "ola" == "hola"[1:]
assert (3, 2, 1) == (1, 2, 3)[::-1]
assert xs[-1::] == [55]
n, N = 10, len(xs)
assert xs[-n::] == xs[max(0, N - n)::]Assignment to slices is possible, resulting in sequences with possibly different sizes.
xs = list(range(10)) # ⇒ xs ≈ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
xs[3:7] = ['a', 'b'] # ⇒ xs ≈ [0, 1, 2, 'a', 'b', 7, 8, 9]Other operations via splicing:
0 == s.find(s[::-1])⇒ stringsis a palindrome- src_python[:exports code]{inits xs ≈ [xs[0:i] for i in range(1 + len(xs))]}
- src_python[:exports code]{segs xs ≈ [xs[i:j] for i in range(len(xs)) for j in range(i, len(xs))]}
Functions are first-class citizens: Python has one namespace for functions and variables, and so there is no special syntax to pass functions around or to use them anywhere, such as storing them in lists.
- src_python[:exports code]{return} clauses are optional; if there are none, a function returns src_python[:exports code]{None}.
- Function application always requires parentheses, even when there are no arguments.
- Any object
xcan be treated like a function, and use thex(⋯)application syntax, if it implements the__call__method:x(⋯) ≈ x.__call__(⋯). The src_python[:exports code]{callable} predicate indicates whether an object is callable or not. - Functions, and classes, can be nested without any special syntax; the nested functions are just new local values that happen to be functions. Indeed, nested functions can be done with src_python[:exports code]{def} or with assignment and src_python[:exports code]{lambda}.
- Functions can receive a variable number of arguments using
*.
def compose(*fs):
"""Given many functions f₀,…,fₙ return a new one: λ x. f₀(⋯(fₙ(x))⋯)"""
def seq(x):
seq.parts = [f.__name__ for f in fs]
for f in reversed(fs):
x = f(x)
return x
return seq
print (help(compose)) # ⇒ Shows the docstring with function type
compose.__doc__ = "Dynamically changing docstrings!"
# Apply the “compose” function;
# first define two argument functions in two ways.
g = lambda x: x + 1
def f(x): print(x)
h = compose(f, g, int)
h('3') # ⇒ Prints 4
print(h.parts) # ⇒ ['f', '<lambda>', 'int']
print (h.__code__.co_argcount) # ⇒ 1; h takes 1 argument!
# Redefine “f” from being a function to being an integer.
f = 3
# f(1) # ⇒ Error: “f” is not a function anymore!Note that compose() is just the identity function lambda x∶ x.
The first statement of a function body can optionally be a ‘docstring’, a string
enclosed in three double quotes. You can easily query such documentation with
help(functionName). In particular, f.__code__.co_argcount to obtain the number
of arguments f accepts.
That functions have attributes —state that could alter their behaviour—
is not at all unexpected: Functions are objects; Python objects have
attributes like __doc__ and can have arbitrary attributes (dynamically) attached
to them.
A src_python[:exports code]{lambda} is a single line expression; you are prohibited from writing statements like src_python[:exports code]{return}, but the semantics is to do the src_python[:exports code]{return}.
src_python[:exports code]{lambda args: (x₀ := e₀, …, xₙ := eₙ)[k]} is a way to
perform n-many stateful operations and return the value of the k-th one. See
pop above for lists; Named Expressions are covered below.
For fresh name x, a let-clause “let x = e in ⋯” can be simulated with x = e;
…; del x. However, in combination with Named Expressions, lambda’s ensure a
new local name: src_python[:exports code]{(lambda x = e: ⋯)()}.
| Default & keyword argument values are possible |
def go(a, b=1, c='two'):
"""Required 'a', optional 'b' and 'c'"""
print(a, b, c)Keyword arguments must follow positional arguments; order of keyword arguments (even required ones) is not important.
- Keywords cannot be repeated.
go('a') # ⇒ a 1 two ;; only required, positional
go(a='a') # ⇒ a 1 two ;; only required, keyword
go('a', c='c') # ⇒ a 1 c ;; out of order, keyword based
go('a', 'b') # ⇒ a b two ;; positional based
go(c='c', a='a') # ⇒ a 1 c ;; very out of order| Dictionary arguments |
After the required positional arguments, we can have an arbitrary number of
optional/ positional arguments (a tuple) with the syntax *args, after that we
may have an arbitrary number of optional keyword-arguments (a dictionary) with
the syntax **args.
The reverse situation is when arguments are already in a list or tuple but need
to be unpacked for a function call requiring separate positional
arguments. Recall, from above, that we do so using the * operator; likewise **
is used to unpack dictionaries.
- E.g., if
fneeds 3 arguments, thenf(*[x₁, x₂, x₃]) ≈ f(x₁, x₂, x₃).
def go(a, *more, this='∞', **kwds):
print (a)
for m in more: print(m)
print (this)
for k in kwds: print(f'{k} ↦ {kwds[k]}')
return kwds['neato'] if 'neato' in kwds else -1
# Elementary usage
go(0) # ⇒ 0 ∞
go(0, 1, 2, 3) # ⇒ 0 1 2 3 ∞
go(0, 1, 2, this = 8, three = 3) # ⇒ 0 1 2 8 three ↦ 3
go(0, 1, 2, three=3, four = 4) # ⇒ 0 1 2 ∞ three ↦ 3 four ↦ 4
# Using “**”
args = {'three': 3, 'four': 4}
go(0, 1, 2, **args) # ⇒ 0 1 2 ∞ three ↦ 3 four ↦ 4
# Making use of a return value
assert 5 == go (0, neato = 5)| Type Annotations |
We can annotate functions by expressions —these are essentially useful comments, and not enforced at all— e.g., to provide type hints. They’re useful to document to human readers the intended types, or used by third-party tools.
# A function taking two ints and returning a bool
def f(x:int, y : str = 'neat') -> bool:
return str(x) # Clearly not enforced!
print (f('hi')) # ⇒ hi; Typing clearly not enforced
print(f.__annotations__) # ⇒ Dictionary of annotationsCurrying: Fixing some arguments ahead of time.
from functools import partial
multiply = lambda x, y, z: z * y + x
twice = partial(multiply, 0, 2)
assert 10 == twice(5)Using decorators and classes, we can make an ‘improved’ partial application mechanism —see the final page.
The decoration syntax @ d f is a convenient syntax that emphasises
code acting on code.
Decorators can be helpful for functions we did not write, but we wish to advise
their behaviour; e.g., math.factorial = my_decorator(math.factorial) to make
the standard library’s factorial work in new ways.
When decorating, we may use *args and **kwargs in the inner wrapper function so
that it will accept an arbitrary number of positional and keyword arguments. See
typed below, whose inner function accepts any number of arguments and passes
them on to the function it decorates.
We can also use decorators to add a bit of type checking at runtime:
import functools
# “typed” makes decorators; “typed(s₁, …, sₙ, t)” is an actual decorator.
def typed(*types):
*inTys, outT = types
def decorator(fun):
@functools.wraps(fun)
def new(*args, **kwdargs):
# (1) Preprocessing stage
if any(type(𝓌 := arg) != ty for (arg, ty) in zip(args, inTys)):
nom = fun.__name__
raise TypeError (f"{nom}: Wrong input type for {𝓌!r}.")
# (2) Call original function
result = fun(*args, **kwdargs) # Not checking keyword args
# (3) Postprocessing stage
if type(result) != outT:
raise TypeError ("Wrong output type!")
return result
return new
return decoratorAfter being decorated, function attributes such as __name__ and __doc__ refer to
the decorator’s resulting function. In order to have it’s attributes preserved,
we copy them over using @functools.wraps decorator —or by declaring
functools.update_wrapper(newFun, oldFun).
# doit : str × list × bool → NoneType
@typed(str, list, bool, type(None))
def doit(x, y, z = False, *more):
print ((ord(x) + sum(y)) * z, *more)Notice we only typecheck as many positions as given, and the output; other arguments are not typechecked.
# ⇒ TypeError: doit: Wrong input type for 'bye'!
doit('a', [1, 2], 'bye')
# ⇒ 100 n i ;; typechecking succeeds
doit('a', [1, 2], True, 'n', 'i')
# ⇒ 0; Works with defaults too ;-)
doit(x, y)
# ⇒ 194; Backdoor: No typechecking on keyword arguments!
doit('a', z = 2, y = {})The implementation above matches the typed specification, but the one below does
not and so always crashes.
# This always crashes since
# the result is not a string.
@typed(int, str)
def always_crashes(x):
return 2 + xNote that typed could instead enforce type annotations, as shown before, at run
time ;-)
An easier way to define a family of decorators is to define a decorator-making-decorator!
Classes bundle up data and functions into a single entity; Objects are just values, or instances, of class types. That is, a class is a record-type and an object is a tuple value.
- A Python object is just like a real-world object: It’s an entity which has attributes —/a thing which has features/.
- We classify objects according to the features they share.
- A class specifies properties and behaviour, an implementation of which is called an object. Think class is a cookie cutter, and an actual cookie is an object.
- Classes are also known as “bundled up data”, structures, and records.
They let us treat a collection of data, including methods, as one semantic entity. E.g., rather than speak of name-age-address tuples, we might call them person objects.
Rather than “acting on” tuples of data, an object “knows how to act”; we shift from
doit(x₁, …, xₙ)to𝓍.doit(). We abstract away then-many details into 1 self-contained idea.
| What can we learn from an empty class? |
src_python[:exports code]{pass} is the “do nothing” statement. It’s useful for writing empty functions/classes that will be filled in later or for explicitly indicating do-nothing cases in complex conditionals.
# We defined a new type!
assert isinstance(Person, type)
# View information of the class
print (help(Person))
# Or use: Person.__name__,
# Person.__doc__, Person.__dict__
# Let's make a Person object
jasim = Person()
assert Person == type(jasim)
assert isinstance(jasim, Person)Instance (reference) equality is compared with src_python[:exports code]{is}.
src_python[:exports code]{x is y ≡ id(x) == id(y)}
id(x) is a unique number identifying object x; usually its address in memory.
jason = jasim
qasim = Person ()
assert jason is jasim and jasim is jason
assert qasim is not jasim# Check attributes exist before use
assert not hasattr(jasim, 'work')
# Dynamically add new (instance) attributes
jasim.work = 'farmer'
jasim.nick = 'jay'
# Delete a property
del jasim.nick
# View all attribute-values of an object
print(jasim.__dict__) # {'work': 'farmer'}| Look at that, classes are just fancy dictionary types! |
The converse is also true: src_python[:exports code]{class X: a = 1 ≈ X = type(‘X’, (object,), dict(a = 1))}
| Let’s add more features! |
src_python[:exports code]{# [0]} An __init__ method is called whenever a new
object is created via Person(name, age). It constructs the object by
initialising its necessary features.
src_python[:exports code]{# [1]} The argument src_python[:exports code]{self}
refers to the object instance being created and src_python[:exports code]{self.x
= y} is the creation of an attribute x with value y for the newly created object
instance. Compare src_python[:exports code]{self} with jasim above and
src_python[:exports code]{self.work} with jasim.work. It is convention to use
the name src_python[:exports code]{self} to refer to the current instance, you
can use whatever you want but it must be the first argument.
src_python[:exports code]{# [2]} Each Person instance has their own name and
work features, but they universally share the Person.__world feature.
Attributes starting with two underscores are private; they can only be altered
within the definition of the class. Names starting with no underscores are
public and can be accessed and altered using dot-notation. Names starting with
one underscore are protected; they can only be used and altered by children
classes.
class Person:
__world = 0 # [2]
def __init__(self, name, work): # [0]
self.name = name
self.work = work
Person.__world += 1
def speak(me): # [1] Note, not using “self”
print (f"I, {me.name}, have the world at my feet!")
# Implementing __str__ allows our class to be coerced as string
# and, in particular, to be printed.
def __str__(self):
return (f"In a world of {Person.__world} people, "
f"{self.name} works at {self.work}")
# [3] Any class implementing methods __eq__ or __lt__
# can use syntactic sugar == or <, respectively.
def __eq__(self, other):
return self.work == other.work
# We can loop over this class by defining __iter__,
# to setup iteration, and __next__ to obtain subsequent elements.
def __iter__(self):
self.x = -1
return self
def __next__(self):
self.x += 1
if self.x < len(self.name): return self.name[self.x]
else: raise StopIteration| Making People |
jason = Person('Jasim', "the old farm")
kathy = Person('Kalthum', "Jasim's farm")
print(kathy) # ⇒ In a world of 2 people, Kalthum works at Jasim's farm
# Two ways to use instance methods
jason.speak() # ⇒ I, Jasim, have the world at my feet!
Person.speak(jason)The following code creates a new public feature that happens to have the same name as the private one. This has no influence on the private feature of the same name! See src_python[:exports code]{# [2]} above.
Person.__world = -10
# Check that our world still has two people:
print(jason) # ⇒ In a world of 2 people, Jasim works at the old farm| Syntax Overloading: Dunder Methods |
src_python[:exports code]{# [3]} Even though jasim and kathy are distinct
people, in a dystopian world where people are unique up to contribution, they
are considered “the same”.
kathy.work = "the old farm"
assert jason is not kathy
assert jason == kathyWe can use any Python syntactic construct for new types by implementing the
dunder —“d”ouble “under”score— methods that they invoke. This way new types
become indistinguishable from built-in types. E.g., implementing __call__ makes
an object behave like a function whereas implementing __iter__ and __next__
make it iterable —possibly also implementing __getitem__ to use the slicing
syntax obj[start:stop] to get a ‘subsegment’ of an instance. Implementing
__eq__ and __lt__ lets us use ==, < which are enough to get <=, > if we
decorate the class by the @functools.total_ordering decorator. Reflected
operators __rℴ𝓅__ are used for arguments of different types: x ⊕ y ≈ y.__r⊕__(x) if x.__⊕__(y) is not implemented.
# Loop over the “jason” object; which just loops over the name's letters.
for e in jason:
print (e) # ⇒ J \n a \n s \n i \n m
# Other iterable methods all apply.
print(list(enumerate(jason)) # ⇒ [(0, 'J'), (1, 'a'), (2, 's'), …]One should not have attributes named such as __attribute__; the dunder naming
convention is for the Python implementation team.
- Here is a list of possible dunder methods.
__add__so we can use+to merge instances —then usesumto ‘add’ a list of elements.- Note:
𝒽(x) ≈ x.__𝒽__for 𝒽: src_python[:exports code]{len, iter, next, bool, str}.
| Extension Methods |
# “speak” is a public name, so we can assign to it:
# (1) Alter it for “jason” only
jason.speak = lambda: print(f"{jason.name}: Hola!")
# (2) Alter it for ALL Person instances
Person.speak = lambda p: print(f"{p.name}: Salam!")
jason.speak() # ⇒ Jasim: Hola!
kathy.speak() # ⇒ Kalthum: Salam!Notice how speak() above was altered. In general, we can “mix-in new
methods” either at the class level or at the instance level in the same
way.
This ability to extend classes with new functions does not work with the builtin
types like src_python[:exports code]{str} and src_python[:exports code]{int};
neither at the class level nor at the instance level. If we want to inject
functionality, we can simply make an empty class like the first incarnation of
Person above. An example, PartiallyAppliedFunction, for altering how function
calls work is shown on the right column ⇒
| Inheritance |
A class may inherit the features of another class; this is essentially
automatic copy-pasting of code. This gives rise to polymorphism,
the ability to “use the same function on different objects”:
If class A has method f(), and classes B and C are children of A,
then we can call f on B- and on C-instances; moreover B and C might
redefine f, thereby ‘overloading’ the name, to specialise it further.
class Teacher(Person):
# Overriding the inherited __init__ method.
def __init__(self, name, subject):
super().__init__(name, f'the university teaching {subject}')
self.subject = subject
assert isinstance(Teacher, type)
assert issubclass(Teacher, Person)
assert issubclass(Person, object)
# The greatest-grandparent of all classes is called “object”.
moe = Teacher('Ali', 'Logic')
assert isinstance(moe, Teacher) # By construction.
assert isinstance(moe, Person) # By inheritance.
print(moe)
# ⇒ In a world of 3 people, Ali works at the university teaching LogicSince @C f stands for f = C(f), we can decorate via classes C whose __init__
method takes a function. Then @C f will be a class! If the class implements
__call__ then we can continue to treat @C f as if it were a (stateful)
function.
In turn, we can also decorate class methods in the usual way. E.g., when a
method 𝓍(self) is decorated @property , we may attach logic to its setter
obj.𝓍 = ⋯ and to its getter obj.𝓍!
We can decorate an entire class C as usual; @dec C still behaves as update via
function application: C = dec(C). This is one way to change the definition of a
class dynamically.
- E.g., to implement design patterns like the singleton pattern.
A class decorator is a function from classes to classes; if we apply a function decorator, then only the class’ constructor is decorated —which makes sense, since the constructor and class share the same name.
| Example: Currying via Class Decoration |
Goal: We want to apply functions in many ways, such as f(x₁, …, xₙ)
and f(x₁, …, xᵢ)(xᵢ₊₁, …, xₙ); i.e., all the calls on the right below
are equivalent.
doit(1)(2)(3)
doit(1, 2)(3)
doit(1)(2, 3)
doit(1, 2, 3)
doit(1, 2, 3, 666, '∞') # Ignore extra argsThe simplest thing to do is to transform src_python[:exports code]{f = lambda x₁, …, xₙ: body} into src_python[:exports code]{nestLambdas(f, [], f.__code__.co_argcount) = lambda x₁: …: lambda xₙ: body}.
def nestLambdas (func, args, remaining):
if remaining == 0: return func(*args)
else: return lambda x: nestLambdas(func, args + [x], remaining - 1)However, the calls shift from f(v₁, …, vₖ) to f(v₁)(v₂)⋯(vₖ); so we need to
change what it means to call a function.
As already mentioned, we cannot extend built-in classes,
so we’ll make a wrapper to slightly alter what it means to
call a function on a smaller than necessary amount of arguments.
class PartiallyAppliedFunction():
def __init__(self, func):
self.value = nestLambdas(func, [], func.__code__.co_argcount)
def __mul__ (self, other):
return PartiallyAppliedFunction(lambda x: self(other(x)))
apply = lambda self, other: other * self if callable(other) else self(other)
def __rshift__(self, other): return self.apply(other)
def __rrshift__(self, other): return self.apply(other)
def __call__(self, *args):
value = self.value
for a in args:
if callable(value):
value = value(a)
return PartiallyAppliedFunction(value) if (callable(value)) else value
curry = PartiallyAppliedFunction # Shorter convenience nameThe above invocation styles, for doit, now all work ^_^
Multiplication now denotes function composition and the (‘r’eflected) ‘r’ight-shift denotes forward-composition/application:
(g * f(v₁, …, vₘ))(x₁, …, xₙ) = g(f (v₁, …, vₘ, x₁))(x₂, …, xₙ) |
assert( (g * f(3, 1))(9, 4)
== (f(3, 1) >> g)(9, 4)
== [13, 13, 13, 13])
assert ( ['a', 'a', 'b']
== 2 >> g('a')
>> curry(lambda x: x + ['b']))The value of a “walrus” expression x := e is the value of e, but it also
introduces the name x into scope. The name x must be an atomic identifier; e.g.,
not an unpacked pattern or indexing; moreover x cannot be a for-bound name.
“if-let”
x = e; if p(x): f(x)
≈ if p(x := e): f(x)This can be useful to capture the value of a truthy item so as to use it in the body:
if e: x = e; f(x)
≈ if (x := e): f(x)“while-let”
while True:
x = input(); if p(x): break; f(x)
≈
while p(x := input()): f(x)“witness/counterexample capture”
if any(p(witness := x) for x in xs):
print(f"{witness} satisfies p")
if not all(p(witness := x) for x in xs):
print(f"{witness} falsifies p")“Stateful Comprehensions”
partial sums of xs
≈ [sum(xs[: i + 1]) for i in range(len(xs))]
≈ (total := 0, [total := total + x for x in xs])[1]Walrus introduces new names, what if we wanted to check if a name already exists?
# alter x if it's defined else, use 7 as default value
x = x + 2 if 'x' in vars() else 7⇒ Each Python file myfile.py determines a module whose contents can be used in
other files, which declare src_python[:exports code]{import myfile}, in the form myfile.component.
⇒ To use a function f from myfile without qualifying it each time, we may use the
from import declaration: src_python[:exports code]{from myfile import f}.
⇒ Moreover, src_python[:exports code]{from myfile import *} brings into scope
all contents of myfile and so no qualification is necessary.
⇒ To use a smaller qualifier, or have conditional imports that alias the imported modules with the same qualifier, we may use src_python[:exports code]{import thefile as newNameHere}.
⇒ A Python package is a directory of files —i.e., Python modules— with
a (possibly empty) file named __init__.py to declare the directory as a package.
⇒ If P is a package and M is a module in it, then we can use src_python[:exports code]{import P.M} or src_python[:exports code]{from P import M}, with the same
behaviour as for modules. The init file can mark some modules as private and not
for use by other packages.
- Dan Bader’s Python Tutorials —bite-sized lessons
- Likewise: w3schools Python Tutorial
- www.learnpython.org —an interactive and tremendously accessible tutorial
- The Python Tutorial —really good introduction from python.org
- https://realpython.com/ —real-world Python tutorials
- Python for Lisp Programmers
- A gallery of interesting Jupyter Notebooks —interactive, ‘live’, Python tutorials
- How to think like a computer scientist —Python tutorial that covers turtle graphics as well as drag-and-drop interactive coding and interactive quizzes along the way to check your understanding; there are also videos too!
- Monads in Python —Colourful Python tutorials converted from Haskell
- Teach Yourself Programming in Ten Years
