You are on page 1of 7

Python Notes/Cheat Sheet

Comments Operators
# from the hash symbol to the end of a line Operator Functionality
+ Addition (also string, tuple, list, and other
Code blocks sequence concatenation)
Delineated by colons and indented code; and not the - Subtraction (also set difference)
curly brackets of C, C++ and Java. * Multiplication (also string, tuple, list
def is_fish_as_string(argument): replication)
if argument:
/ Division
return 'fish'
else: % Modulus (also a string format function,
return 'not fish' but this use deprecated)
Note: Four spaces per indentation level is the Python // Integer division rounded towards minus
standard. Never use tabs: mixing tabs and spaces infinity
produces hard-to-find errors. Set your editor to convert ** Exponentiation
tabs to spaces. =, -=, +=, /=, Assignment operators
*=, %=, //=,
Line breaks **=
Typically, a statement must be on one line. Bracketed ==, !=, <, <=, Boolean comparisons
code - (), [] or {} - can be split across lines; or (if you >=, >
must) use a backslash \ at the end of a line to continue a and, or, not Boolean operators
statement on to the next line (but this can result in hard in, not in Containment test operators
to debug code). is, is not Object identity operators
|, ^, &, ~ Bitwise: or, xor, and, compliment
Naming conventions
<<, >> Left and right bit shift
Style Use
; Inline statement separator
StudlyCase Class names # inline statements discouraged
joined_lower Identifiers, functions; and class Hint: float('inf') always tests as larger than any number,
methods, attributes including integers.
_joined_lower Internal class attributes
__joined_lower Private class attributes Modules
# this use not recommended Modules open up a world of Python extensions that can
joined_lower Constants be imported and used. Access to the functions, variables
ALL_CAPS and classes of a module depend on how the module
was imported.
Basic object types (not a complete list) Import method Access/Use syntax
Type Examples import math math.cos(math.pi/3)
None None # singleton null object import math as m m.cos(m.pi/3)
Boolean True, False # import using an alias
integer -1, 0, 1, sys.maxint from math import cos, pi cos(pi/3)
long 1L, 9787L # arbitrary length ints # only import specifics
float 3.14159265 from math import * log(e)
inf, float('inf') # infinity # BADish global import
-inf # neg infinity Global imports make for unreadable code!!!
nan, float('nan') # not a number
complex 2+3j # note use of j Oft used modules
string 'I am a string', "me too" Module Purpose
'''multi-line string''', """+1""" datetime Date and time functions
r'raw string', b'ASCII string' time
u'unicode string' math Core math functions and the constants pi
tuple empty = () # empty tuple and e
(1, True, 'dog') # immutable list pickle Serialise objects to a file
list empty = [] # empty list os Operating system interfaces
[1, True, 'dog'] # mutable list os.path
set empty = set() # the empty set re A library of Perl-like regular expression
set(1, True, 'a') # mutable operations
dictionary empty = {} # mutable object string Useful constants and classes
{'a': 'dog', 7: 'seven’, True: 1} sys System parameters and functions
file f = open('filename', 'rb') numpy Numerical python library
Note: Python has four numeric types (integer, float, long pandas R DataFrames for Python
and complex) and several sequence types including matplotlib Plotting/charting for Python
strings, lists, tuples, bytearrays, buffers, and range
objects.

Version 25 April 2017 - [Draft – Mark Graph – mark dot the dot graph at gmail dot com – @Mark_Graph on twitter] 1
If - flow control Objects and variables (AKA identifiers)
if condition: # for example: if x < 5:  Everything is an object in Python (in the sense that it
statements can be assigned to a variable or passed as an
elif condition: # optional – can be multiple argument to a function)
statements  Most Python objects have methods and attributes.
else: # optional
For example, all functions have the built-in attribute
statements
__doc__, which returns the doc string defined in the
function's source code.
For - flow control  All variables are effectively "pointers", not "locations".
for x in iterable: They are references to objects; and often called
statements
identifiers.
else: # optional completion code
statements  Objects are strongly typed, not identifiers
 Some objects are immutable (int, float, string, tuple,
While - flow control frozenset). But most are mutable (including: list, set,
while condition:
dictionary, NumPy arrays, etc.)
statements  You can create our own object types by defining a
else: # optional completion code new class (see below).
statements
Booleans and truthiness
Ternary statement Most Python objects have a notion of "truth".
id = expression if condition else expression False True
x = y if a > b else z - 5 None
0 Any number other than 0
Some useful adjuncts: int(False) #  0 int(True) #  1
 pass - a statement that does nothing "" " ", 'fred', 'False'
 continue - moves to the next loop iteration # the empty string # all other strings
 break - to exit for and while loop () [] {} set() [None], (False), {1, 1}
Trap: break skips the else completion code # empty containers # non-empty containers,
including those containing
Exceptions – flow control False or None.
try:
statements You can use bool() to discover the truth status of an
except (tuple_of_errors): # can be multiple object.
statements a = bool(obj) # the truth of obj
else: # optional no exceptions
statements
It is pythonic to use the truth of objects.
finally: # optional all
statements if container: # test not empty
# do something
while items: # common looping idiom
Common exceptions (not a complete list) item = items.pop()
Exception Why it happens # process item
AsserionError Assert statement failed
AttributeError Class attribute assignment or Specify the truth of the classes you write using the
reference failed __nonzero__() magic method.
IOError Failed I/O operation
ImportError Failed module import Comparisons
IndexError Subscript out of range Python lets you compare ranges, for example
KeyError Dictionary key not found assert(1 <= x <= 100)
MemoryError Ran out of memory
NameError Name not found Tuples
TypeError Value of the wrong type Tuples are immutable lists. They can be searched,
ValueError indexed and iterated much like lists (see below). List
Right type but wrong value
methods that do not change the list also work on tuples.
a = () # the empty tuple
Raising errors
a = (1,) #  note comma # one item tuple
Errors are raised using the raise statement a = (1, 2, 3) # multi-item tuple
raise ValueError(value) a = ((1, 2), (3, 4)) # nested tuple
a = tuple(['a', 'b']) # conversion
Creating new errors Note: the comma is the tuple constructor, not the
class MyError(Exception): parentheses. The parentheses add clarity.
def __init__(self, value):
self.value = value The Python swap variable idiom
def __str__(self):
a, b = b, a # no need for a temp variable
return repr(self.value)
This syntax uses tuples to achieve its magic.

Version 25 April 2017 - [Draft – Mark Graph – mark dot the dot graph at gmail dot com – @Mark_Graph on twitter] 2
String (immutable, ordered, characters) List (mutable, indexed, ordered container)
s = 'string'.upper() # STRING Indexed from zero to length-1
s = 'fred'+'was'+'here' # concatenation a = [] # the empty list
s = ''.join(['fred', 'was', 'here']) # ditto a = ['dog', 'cat', 'bird'] # simple list
s = 'spam' * 3 # replication a = [[1, 2], ['a', 'b']] # nested lists
s = str(x) # conversion a = [1, 2, 3] + [4, 5, 6] # concatenation
a = [1, 2, 3] * 456 # replication
String iteration and sub-string searching a = list(x) # conversion
for character in 'str': # iteration
print (ord(character)) # 115 116 114 List comprehensions (can be nested)
for index, character in enumerate('str') Comprehensions: a tight way of creating lists
print (index, character) t3 = [x*3 for x in [5, 6, 7]] # [15, 18, 21]
if 'red' in 'Fred': # searching z = [complex(x, y) for x in range(0, 4, 1)
print ('Fred is red') # it prints! for y in range(4, 0, -1) if x > y]
# z --> [(2+1j), (3+2j), (3+1j)]
String methods (not a complete list)
capitalize, center, count, decode, encode, endswith, Iterating lists
expandtabs, find, format, index, isalnum, isalpha, isdigit, L = ['dog', 'cat', 'turtle']
islower, isspace, istitle, isupper, join, ljust, lower, lstrip, for item in L
partition, replace, rfind, rindex, rjust, rpartition, rsplit, print (item)
rstrip, split, splitlines, startswith, strip, swapcase, title, for index, item in enumerate(L):
translate, upper, zfill print (index, item)

String constants (not a complete list) Searching lists


from string import * # global import is not good L = ['dog', 'cat', 'turtle']
print ([digits, hexdigits, ascii_letters, value = 'cat'
ascii_lowercase, ascii_uppercase, if value in L:
punctuation]) count = L.count(value)
first_occurrence = L.index(value)
Old school string formatting (using % oper) if value not in L:
print 'list is missing {}'.format(value)
print ("It %s %d times" % ('occurred', 5))
# prints: 'It occurred 5 times'
List methods (not a complete list)
Code Meaning Method What it does
s String or string conversion l.append(x) Add x to end of list
c Character l.extend(other) Append items from other
d Signed decimal integer l.insert(pos, x) Insert x at position
u Unsigned decimal integer del l[pos] Delete item at pos
H or h Hex integer (upper or lower case) l.remove(x) Remove first occurrence of x; An
error if no x
f Floating point
l.pop([pos]) Remove last item from list (or
E or e Exponent (upper or lower case E)
item from pos); An error if empty
G or g The shorter of e and f (u/l case) list
% Literal '%' l.index(x) Get index of first occurrence of
x; An error if x not found
import math l.count(x) Count the number of times x is
'%s' % math.pi # '3.14159265359'
found in the list
'%f' % math.pi # '3.141593'
'%.2f' % math.pi # '3.14' l.sort() In place list sort
'%.2e' % 3000 # '3.00e+03' l.reverse(x) In place list reversal
'%03d' % 5 # '005'
List slicing
New string formatting (using format method) x = [0, 1, 2, 3, 4, 5, 6, 7, 8] # play data
Uses: 'template-string'.format(arguments) x[2] # 3rd element - reference not slice
Examples (using similar codes as above): x[1:3] # 2nd to 3rd element (1, 2)
import math x[:3] # the first three elements (0, 1, 2)
'Hello {}'.format('World') # 'Hello World' x[-3:] # last three elements
'{}'.format(math.pi) # ' 3.14159265359' x[:-3] # all but the last three elements
'{0:.2f}'.format(math.pi) # '3.14' x[:] # every element of x – copies x
'{0:+.2f}'.format(5) # '+5.00' x[1:-1] # all but first and last element
'{:.2e}'.format(3000) # '3.00e+03' x[::3] # (0, 3, 6, 9, …) 1st then every 3rd
'{:0>2d}'.format(5) # '05' (left pad) x[1:5:2] # (1,3) start 1, stop >= 5, by every 2nd
'{:x<3d}'.format(5) # '5xx' (rt. pad) Note: All Python sequence types support the above
'{:,}'.format(1000000) # '1,000,000' index slicing (strings, lists, tuples, bytearrays, buffers,
'{:.1%}'.format(0.25) # '25.0%' and xrange objects)
'{0}{1}'.format('a', 'b') # 'ab'
'{1}{0}'.format('a', 'b') # 'ba'
'{num:}'.format(num=7) # '7' (named args)

Version 25 April 2017 - [Draft – Mark Graph – mark dot the dot graph at gmail dot com – @Mark_Graph on twitter] 3
Set (unique, unordered container) Dictionary (indexed, unordered map-container)
A Python set is an unordered, mutable collection of A mutable hash map of unique key=value pairs.
unique hashable objects. a = {} # empty dictionary
a = set() # empty set a = {1: 1, 2: 4, 3: 9} # simple dict
a = {'red', 'white', 'blue'} # simple set a = dict(x) # convert paired data
a = set(x) # convert list # next example – create from a list
Trap: {} creates empty dict, not an empty set l = ['alpha', 'beta', 'gamma', 'delta']
a = dict(zip(range(len(l)), l))
# Example using string & generator expression
Set comprehensions
s = 'a=apple,b=bird,c=cat,d=dog,e=egg'
# a set of selected letters... a = dict(i.split("=") for i in s.split(","))
s = {e for e in 'ABCHJADC' if e not in 'AB'} # {'a': 'apple', 'c': 'cat', 'b': 'bird',
# --> {'H', 'C', 'J', 'D'} # 'e': 'egg', 'd': 'dog'}
# a set of tuples ...
s = {(x,y) for x in range(-1,2)
for y in range (-1,2)} Dictionary comprehensions
Trap: set contents need to be immutable to be Conceptually like list comprehensions; but it constructs a
hashable. So you can have a set of dictionary rather than a list
a = { n: n*n for n in range(7) }
tuples, but not a set of lists.
# a -> {0:0, 1:1, 2:4, 3:9, 4:16, 5:25,6:36}
odd_sq = { n: n*n for n in range(7) if n%2 }
Iterating a set # odd_sq -> {1: 1, 3: 9, 5: 25}
for item in set: # next example -> swaps the key:value pairs
print (item) a = { val:key for key, val in a.items() }
# next example -> count list occurrences
Searching a set l = [11,12,13,11,15,19,15,11,20,13,11,11,12,10]
if item in set: c = { key: l.count(key) for key in set(l) }
print (item)
if item not in set: Iterating a dictionary
print ('{} is missing'.format(item)) for key in dictionary.keys():
print (key)
Set methods (not a complete list) for key, value in dictionary.items():
Method What it does print (key, value)
for value in dictionary.values():
len(s) Number of items in set
print(value)
s.add(item) Add item to set
s.remove(item) Remove item from set. Raise
Searching a dictionary
KeyError if item not found.
if key in dictionary:
s.discard(item) Remove item from set if present. print (key)
s.pop() Remove and return an arbitrary
item. Raise KeyError on empty Merging two dictionaries
set. merged = dict_1.copy()
s.clear() Remove all items from set merged.update(dict_2)
item in s True or False
item not in s True or False Dictionary methods (not a complete list)
iter(s) An iterator over the items in the Method What it does
set (arbitrary order) len(d) Number of items in d
s.copy() Get shallow copy of set d[key] Get value for key or raise the
s.isdisjoint(o) True if s has not items in KeyError exception
common with other set o d[key] = value Set key to value
s.issubset(o) Same as set <= other del d[key] deletion
s.issuperset(o) Same as set >= other key in d True or False
s.union(o[, ...]) Return new union set key not in d True or False
s.intersection(o) Return new intersection iter(d) An iterator over the keys
s.difference(o) Get net set of items in s but not d.clear() Remove all items from d
others (Same as set – other) d.copy() Shallow copy of dictionary
d.get(key[, def]) Get value else default
Frozenset
d.items() Dictionary's (k,v) pairs
Similar to a Python set above, but immutable (and
d.keys() Dictionary's keys
therefore hashable).
f = frozenset(s) # convert set d.pop(key[, def]) Get value else default; remove
f = frozenset(o) # convert other key from dictionary
d.popitem() Remove and return an arbitrary
(k, v) pair
d.setdefault(k[,def])) If k in dict return its value
otherwise set def
d.update(other_d) Update d with key:val pairs
from other
d.values() The values from dict

Version 25 April 2017 - [Draft – Mark Graph – mark dot the dot graph at gmail dot com – @Mark_Graph on twitter] 4
Key functions (not a complete list) Using functions
Function What it does When called, functions can take positional and named
abs(num) Absolute value of num arguments.
all(iterable) True if all are True
any(iterable) True if any are True For example:
bytearray(source) result = function(32, aVar, c='see', d={})
A mutable array of bytes
callable(obj) True if obj is callable
chr(int) Character for ASCII int Arguments are passed by reference (ie. the objects are
not copied, just the references).
complex(re[, im]) Create a complex number
divmod(a, b) Get (quotient, remainder) Writing a simple function
enumerate(seq) Get an enumerate object, with def funct(arg1, arg2=None, *args, **kwargs):
next() method returns an (index, """explain what this function does"""
element) tuple statements
eval(string) Evaluate an expression return x # optional statement
filter(fn, iter) Construct a list of elements from Note: functions are first class objects that get
iter for which fn() returns True instantiated with attributes and they can be referenced
float(x) Convert from int/string by variables.
getattr(obj, str) Like obj.str
hasattr(obj, str) True if obj has attribute Avoid named default mutable arguments
hex(x) From in to hex string Avoid mutable objects as default arguments.
id(obj) Return unique (run-time) Expressions in default arguments are evaluated when
identifier for an object the function is defined, not when it’s called. Changes to
mutable default arguments survive between function
int(x) Convert from float/string
calls.
isinstance(o, c) Eg. isinstance(2.1, float)
def nasty(value=[]): # <-- mutable arg
len(x) Number of items in x; x is string, value.append('a')
tuple, list, dict return value
list(iterable) Make a list print (nasty ()) # --> ['a']
long(x) Convert a string or number to a print (nasty ()) # --> ['a', 'a']
long integer
map(fn, iterable) Apply fn() to every item in def better(val=None):
val = [] if val is None else val
iterable; return results in a list
value.append('a')
max(a,b) What it says on the tin return value
max(iterable)
min(a,b) Ditto Lambda (inline expression) functions:
min(iterable) g = lambda x: x ** 2 # Note: no return
next(iterator) Get next item from an iter print(g(8)) # prints 64
open(name[,mode]) Open a file object mul = lambda a, b: a * b # two arguments
ord(c) Opposite of chr(int) mul(4, 5) == 4 * 5 # --> True
pow(x, y) Same as x ** y Note: only for expressions, not statements.
print (objects) What it says on the tin Lambdas are often used with the Python functions
takes end arg (default \n) filter(), map() and reduce().
and sep arg (default ' ') # get only those numbers divisible by three
range(stop) integer list; stops < stop div3 = filter(lambda x: x%3==0,range(1,101))
range(start,stop) default start=0; Typically, you can put a lambda function anywhere you
range(fr,to,step) default step=1 put a normal function call.
reduce(fn, iter) Applies the two argument fn(x, y)
cumulatively to the items of iter. Closures
repr(object) Printable representation of an Closures are functions that have inner functions with
object data fixed in the inner function by the lexical scope of
reversed(seq) Get a reversed iterator the outer. They are useful for avoiding hard constants.
Wikipedia has a derivative function for changeable
round(n[,digits]) Round to number of digits after
values of dx, using a closure.
the decimal place
def derivative(f, dx):
setattr(obj,n,v) Like obj.n = v #name/value """Return a function that approximates
sorted(iterable) Get new sorted list the derivative of f using an interval
str(object) Get a string for an object of dx, which should be appropriately
sum(iterable) Sum list of numbers small.
type(object) Get the type of object """
def _function(x):
xrange() Like range() but better: returns
return (f(x + dx) - f(x)) / dx
an iterator return _function #from derivative(f, dx)
zip(x, y[, z]) Return a list of tuples
f_dash_x = derivative(lambda x: x*x,0.00001)
f_dash_x(5) # yields approx. 10 (ie. y'=2x)

Version 25 April 2017 - [Draft – Mark Graph – mark dot the dot graph at gmail dot com – @Mark_Graph on twitter] 5
An iterable object Classes
The contents of an iterable object can be selected one Python is an object-oriented language with a multiple
at a time. Such objects include the Python sequence inheritance class mechanism that encapsulates program
types and classes with the magic method __iter__(), code and data.
which returns an iterator. An iterable object will produce
a fresh iterator with each call to iter(). Methods and attributes
iterator = iter(iterable_object) Most objects have associated functions or “methods”
that are called using dot syntax:
Iterators obj.method(arg)
Objects with a next() or __next__() method, that: Objects also often have attributes or values that are
 returns the next value in the iteration directly accessed without using getters and setters
 updates the internal note of the next value (most unlike Java or C++)
 raises a StopIteration exception when done instance = Example_Class()
Note: with the loop for x in y: if y is not an iterator; Python print (instance.attribute)
calls iter() to get one. With each loop, it calls next() on
the iterator until a StopIteration exception. Simple example
x = iter('XY') # iterate a string by hand import math
print (next(x)) # --> X class Point:
print (next(x)) # --> Y # static class variable, point count
print (next(x)) # --> StopIteration exception count = 0

Generators def __init__(self, x, y):


self.x = float(x)
Generator functions are resumable functions that work
self.y = float(y)
like iterators. They can be more space or time efficient Point.count += 1
than iterating over a list, (especially a very large list), as
they only produce items as they are needed. def __str__(self):
def fib(max=None): return \
""" generator for Fibonacci sequence""" '(x={}, y={})'.format(self.x, self.y)
a, b = 0, 1
while max is None or b <= max: def to_polar(self):
yield b #  yield is like return r = math.sqrt(self.x**2 + self.y**2)
a, b = b, a+b theta = math.atan2(self.y, self.x)
return(r, theta)
[i for i in fib(10)] #  [1, 1, 2, 3, 5, 8]
Note: a return statement (or getting to the end of the # static method – trivial example ...
function) ends the iteration. def static_eg(n):
Trap: a yield statement is not allowed in the try clause of print ('{}'.format(n))
static_eg = staticmethod(static_eg)
a try/finally construct.
# Instantiate 9 points & get polar coords
Messaging the generator for x in range(-1, 2):
def resetableCounter(max=None): for y in range(-1, 2):
j = 0 p = Point(x, y)
while max is None or j <= max: print (p) # uses __str__() method
x = yield j #  x gets the sent arg print (p.to_polar())
j = j+1 if x is None else x print (Point.count) # check static variable
Point.static_eg(9) # check static method
x = resetableCounter(10)
print x.send(None) #  0
print x.send(5) #  5
The self
print x.send(None) #  6 Class methods have an extra argument over functions.
print x.send(11) #  StopIteration Usually named 'self'; it is a reference to the instance. It
Trap: must send None on first send() call is not used in the method call; and is provided by Python
to the method. Self is like 'this' in C++ & Java
Generator expressions
Generator expressions build generators, just like Public and private methods and variables
building a list from a comprehension. You can turn a list Python does not enforce the public v private data
comprehension into a generator expression simply by distinction. By convention, variables and methods that
replacing the square brackets [] with parentheses (). begin with an underscore should be treated as private
[i for i in range(10)] # list comprehension
(unless you really know what you are doing). Variables
list(i for i in range(10)) # generated list that begin with double underscore are mangled by the
compiler (and hence more private).

Version 25 April 2017 - [Draft – Mark Graph – mark dot the dot graph at gmail dot com – @Mark_Graph on twitter] 6
Inheritance Magic class methods (not a complete list)
class DerivedClass1(BaseClass): Magic methods (which begin and end with double
statements underscore) add functionality to your classes consistent
class DerivedClass2(module_name.BaseClass): with the broader language.
statements Magic method What it does
__init__(self,[...]) Constructor
Multiple inheritance __del__(self) Destructor pre-garbage
class DerivedClass(Base1, Base2, Base3): collection
statements
__str__(self) Human readable string for
class contents. Called by
Decorators str(self)
Technically, decorators are just functions (or classes), __repr__(self) Machine readable
that take a callable object as an argument, and return an unambiguous Python
analogous object with the decoration. We will skip how string expression for class
to write them, and focus on using a couple of common contents. Called by
built in decorators. repr(self) Note: str(self)
will call __repr__ if
Practically, decorators are syntactic sugar for more __str__ is not defined.
readable code. The @wrapper is used to transform the
__eq__(self, other) Behaviour for ==
existing code. For example, the following two method
definitions are semantically equivalent. __ne__(self, other) Behaviour for !=
def f(...): __lt__(self, other) Behaviour for <
... __gt__(self, other) Behaviour for >
f = staticmethod(f) __le__(self, other) Behaviour for <=
__ge__(self, other) Behaviour for >=
@staticmethod __add__(self, other) Behaviour for +
def f(...):
__sub__(self, other) Behaviour for -
...
__mul__(self, other) Behaviour for *
Getters and setters __div__(self, other) Behaviour for /
Although class attributes can be directly accessed, the __mod__(self, other) Behaviour for %
property function creates a property manager. __pow__(self, other) Behaviour for **
class Example: __pos__(self, other) Behaviour for unary +
def __init__(self): __neg__(self, other) Behaviour for unary -
self._x = None __hash__(self) Returns an int when
hash() called. Allows class
def getx(self): instance to be put in a
return self._x
dictionary
def setx(self, value):
self._x = value __len__(self) Length of container
def delx(self): __contains__(self, i) Behaviour for in and not in
del self._x operators
x = property(getx, setx, delx,"Doc txt") __missing__(self, i) What to do when dict key i
is missing
Which can be rewritten with decorators as: __copy__(self) Shallow copy constructor
class Example: __deepcopy__(self, Deep copy constructor
def __init__(self): memodict={})
self._x = None __iter__(self) Provide an iterator
@property
__nonzero__(self) Called by bool(self)
def x(self): __index__(self) Called by x[self]
"""Doc txt: I'm the 'x' property.""" __setattr__(self, Called by
return self._x name, val) self.name = val
__getattribute__(self, Called by self.name
@x.setter name)
def x(self, value):
__getattr__(self, Called when self.name
self._x = value
name) does not exist
@x.deleter __delattr__(self, Called by
def x(self): name) del self.name
del self._x __getitem__(self, key) Called by self[key]
__setitem__(self, key, Called by
val) self[key] = val
__delitem__(self, key) del self[key]

Version 25 April 2017 - [Draft – Mark Graph – mark dot the dot graph at gmail dot com – @Mark_Graph on twitter] 7

You might also like