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 ⊕ y
for 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 < +inf
abs(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
x
as 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 | 4
crashes 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
x
to bey
ify
is “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 x
Notice 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
f
needs 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: break
iter(x)
⇒ Get an iterable for objectx
.next(it)
⇒ Get the current element and advance the iterableit
to 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
x
is a member ofxs
- src_python[:exports code]{x in y ≡ any(x == e for e in y)}, provided
y
is 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
start
up tostop-1
, skipping every otherstep-1
number.- 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
e
may 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
prod
from thenumpy
module 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) ≈ xs
xs.split(s)
⇒ split stringxs
into a list every times
is 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 wherex
occurs, 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
i
is 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
k
is in dictionaryd
. - src_python[:exports code]{del d[k]} ⇒ Remove the key-value pair at key
k
from dictionaryd
. d[k] = v
⇒ Add a new key-value pair tod
, or update the value at keyk
if 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.
start
may 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]
, bothstart
andstop
being 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])
⇒ strings
is 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
x
can 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
f
needs 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 annotations
Currying: 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 decorator
After 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 + x
Note 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 == kathy
We 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 usesum
to ‘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 Logic
Since @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 args
The 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 name
The 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