Python primer

5 February 2019

Prelude

Python is a language which is powerful, terse, and nice to work with. Arguably it’s built for comfort rather than speed of execution, but in terms of your time rather than the computer’s many find it speedy and comfortable in comparison to other languages.

This primer is designed to be an aide-memoir to the beginning Pythonista or a navigational aide for someone with familiarity of another language. It refers throughout to Python 3 which is the current version of Python at the time of writing. Python 2 has different syntax and is not interoperable with Python 3.

If you are just getting started there are a myriad of ways to run Python code: the vanilla python interpreter, ipython, and jupyter notebooks. Personally my preference while learning the language is to use a jupyter notebook because this allows you to easily and conveniently edit code, execute it and see the output. A good guide to jupyter notebooks can be found here: https://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/index.html

White space

White space is important in Python as indentation is used with e.g. functions, loops and flow control statements.

If you want to extend a statement to the following line, you can use \.

Variables

Can’t have spaces or symbols in names other than underscore and cannot begin with numbers.

No need to declare types as Python infers these. To force float just use the decimal point with nothing after

x = 5
y = 5.56
z = 5.
my_string = "hello"

etc

Data types in Python are: int, float, str, list, dict. More on those later.

Mathematical operators

Addition +, subtraction -, multiplication * division \ are as expected. Python converts int to float types when performing division.

To print you can just do

print(2*3)

etc

** means to the exponent of e.g.

c = 3*10**8
print(c)

% means modulo (e.g. 2 / 2 modulo 0)

Useful when we want to perform an action every nth time. Modulo 2 returns 0 for even, 1 for odd numbers.

+= will add to the variable value

Strings

# denotes comment

+ or += will also concatenate strings

Can use str() to convert variables to strings

""" denotes multi-line string

str() is useful for converting type to a string so it can be string-concatenated

For an empty string just assign an empty string:

empty_string = ""

Functions

print() prints to stdout

Functions start with def, end with colon, and have contents are indented by two spaces. What’s neat is that you can return more than one thing.

def my_function(parama, paramb):
  # Do stuff here
  print(parama + paramb)
  # Note the below is a nonsensical suggestion
  return parama, paramb

Parama and Paramb are both positional arguments when the function is called

my_function("first", "second")

Alternitively can use keyword arguments

my_function(parama = "first", paramb = "second")

This same syntax in the function definition defines default arguments.

Functions can optionally return a value

Flow control

Note that flow control statements, loops, etc use indentation in the same way as functions.

Boolean expressions are True or False and type bool

== equals, != does not equal

Be mindful of type when making comparisons (string ‘7’ does not equal integer 7)

Boolean operators are and, or and not

elif is used as a combination of else and if, with an else then executed if none of the previous conditions have been met.

Try and except are shown below.

try:
    # some statement
except ErrorName:
    # some statement

Ex

def applicant_selector(gpa, ps_score, ec_count):
  if gpa >= 3 and ps_score >= 90 and ec_count >= 3:
    return "This applicant should be accepted."
  elif gpa >= 3 and ps_score >= 90:
    return "This applicant should be given an in-person interview."
  else:
    return "This applicant should be rejected."

Lists

Lists look like this:

numbers = [1, 3, 6, 9]
names = ['Bill', 'Ben', 'AN Other', 'Jill']
empty_list = []

Lists can contain multiples data types e.g. with ints and strings in the same list

Lists can contain lists e.g.

heights = [['Dave', 51], ['Freda', 71], ['Jimmy', 57]]

zip can be used to combine two lists into a list of pairs; to print one then has to convert it back to a list print(list(zippped))`

Use append to add to lists e.g. empty_list.append(x)

Use + to combine lists

range(n) can be used to create lists of numbers; it will generate values from 0 to n-1. Like zip to print etc. if needs to be converted into a list. With two arguments the starting number can be changed. With three arguments the increment can be changed.

my_list.pop() removes the last item on the list and returns it

Operations on lists

len() returns the length (# of items) in a list

Lists are zero-indexed

To select the nth element of a list my_list[n-1]

Selecting an element that does not exist yields IndexError

my_list[-1] allows us to select the last item in a list

Can slice a list (i.e. select a subset) using my_list[start:end], where start is where the start the subset and end is the number of the last element to be included +1. Given the zero-indexed nature of the list, this is actually more helpful in use than it may look.

When starting at the beginning fine to omit the 0, so my_list[:4] is same is my_list[0:4]

Ending at the end the same logic applies, so my_list[2:] will give you all items from the 3rd item onwards.

Can also use negative indexes to count backwards from the last element, so my_list[-3:] will give you the last 3 elements of the list.

my_list.count(x) allows you to count the number of instance of x in a list.

my_list.sort() will sort in place.

sorted() will generate a new sorted list.

You can flatten a list of lists (e.g. l) with the iterator:

flat_list = [item for sublist in l for item in sublist]

.join is used to create a string from the list of substrings (which could be individual characters)

my_string = ''.join(my_chars)

Loops

To print each item of a list (iterate).

for x in my_list:
  print(x)

Because we need something to iterate through, if this is not already to hand, we can use range e.g.

for i in range(3):
  print("Hello world!")

e.g. 2 To loop through one list and append to another

for i in range(len(my_list_A)):
  my_list_B.append(my_list_A[i])

Note that break can be used to stop the loop, and continue can be used to move to the next iteration.

For a list of lists just nest two for loops

for my_list in my_list_of_lists:
  print(my_list)
  for x in my_list:
    print(x)

List comprehensions allow a conditional and a for loop to be tersely expressed:

twitter_handles = [word for word in words_list if word[0] == '@']

or to append a string

messages = [user + " please follow me!" for user in twitter_handles]

or to cube numbers

cubes = [n ** 3 for n in list(range(10))]

Strings

Strings can be thought of as lists of characters.

Strings can be sliced just like lists.

in

my_string[a:b]

a, the first index, is included but b the last index, is not.

E.g to get the last 4 letters of the string we can use my_string[-4:]

Strings can be concatenated with +

len(my_string) returns the string length

Note that strings in Python are immutable, so an already existing string cannot be changed; instead it is used to help create other strings.

Escape character is \, unsurprisingly; double-quotes in strings need to be escaped.

As well as iterating through the string like we can with any other list, we can use in for convenience.

e.g.

"blue" in "blueberry"

will evaluate to True

String methods

Python has nice built-in string methods, shown below.

Note the methods all return the value.

my_string.upper()
my_string.lower()
my_string.title() # Mixed case
my_string.split(delim) # Splits with delim as delimiter (uses ' ' by default)
'delim'.join(my_list) # Joins items in mylist using delim as the delimiter
my_string.replace('F','T') # Replace ~F with T
my_string.strip() # Removes white space
format_string.format(...) # Formats
my_string.find('f') # Returns location of 'f' in my_string

You can get some pretty neat results by combining split with iterators e.g.

# Gets surnames of authors from a list of authors

authors = "Jane Austen, JK Rowling, JRR Tolkein, George Orwell, CS Lewis, Agatha Cristie"

author_names = list(authors.split(', '))

author_last_names = [s.split()[-1] for s in author_names]

print(author_last_names)

Escape sequences are e.g. \n for newline, \r for tab

Format uses format strings like so:

"{} is in department {}".format("Bob","Finance")

Or you can use use keywords, in which case values can be passed in any order.

"{name} is in department {dept}".format(name="Jill",dept="HR")

Modules

Libraries in python are referred to as modules

The syntax is

from module_name import object_name

It makes sense to only import what you need. Note that sometimes the module_name and the object_name will be the same.

Where Python offers a namespace, we then call e.g.

module_name.function()

namespace isolates the functions, classes, and variables defined in the module from the code in the file doing the importing

If we want to use our own module name, we can specify it like this

import module_name as m
m.function()

If you want to just dump everything in to the current namespace (not often recommended), you can use:

from math import *

and then just use the functions as though they were defined in your source code file.

It is much neater to do e.g. (as above)

from datetime import datetime
# Stuff here
current_time = datetime.now()

to keep the namespace separate, however.

Putting the two together we get e.g.

from matplotlib import pyplot as plt

Note that source code files (.py) are treated as modules, so variables or functions that are out of scope in the current source code file can be imported to give access.

So to import e.g. lib.py

from lib import *
func_in_lib() # In scope due to import

To install modules you may need to use pip3 (vanilla Python3) or conda ([?]).

Dictionaries

A dictionary is an unordered set of key + value pairs.

Looks like

coffee_menu = {"latte": 3, \
  "flat white": 3, \
  "filter": 4, \
  "espresso": 2}

The keys can be strings, as above, or numbers. The values can be any type; string, number, list or another dictionary.

Can also mix and match key and value types.

To add/update a single value:

coffee_menu["Pumpkin Spiced latte"] = 5

Multiple values:

coffee_menu.update( \
  {"V60": 5, \
  "aeropress": 5 \
  "special stuff": 6 \
  })

Can also use a list comprehension:

coffee_drinks = ["V60", "aeropress", "special stuff"]
coffee_prices = [5, 5, 6]
coffee_menu = {key:value for key, value in zip (coffee_drinks, coffee_prices)}

To access the value just use dict[key] To find out if a key is in the dictionary:

if "V60" in coffee_menu:
  print (coffee_menu["V60"])

OR

try: 
  print (coffee_menu["V60"])
except KeyError:
  print("Key not in dict")

You can also use dict.get(key) which will either return the associated value, or None if the key is not in the dictionary. With a second argument, dict.get() will return that instead of None.

dict.pop(key) will return the value associated with the key and remove the key + value pair from the dictionary. As with dict.get() a default value can be provided too.

list(dict) will return a list of the keys from the dict

To iterate, dict.keys() can be used, albeit this does not allow modification of the keys

for k in coffee_menu.keys():
  print("We serve " + k + " coffee")

dict.values() works similarly. Both dict.values() and dict.keys() return a dict_list object

dict.items() returns keys and values, in a dict_list object, which consists of tuples of (key, value)

File I/O

with open ('my_text_file.txt') as fd:
  text_data = fd.read()
  
print(text_data)

fd.read() reads the whole file into a single string

fd.readlines() reads the file in lines and can be iterated as below

with open ('my_text_file.txt') as fd:
  for line in fd.readlines():
    print(line)

fd.readline() reads one line; subsequent calls will return subsequent lines, until the end of the file at which point it returns an empty string

To write we used the ‘w’ flat with open; to append to the end of the file use ‘a’:

with open('bad_bands.txt', 'w') as bad_bands_doc:
  bad_bands_doc.write("U2")

with is handy because it takes care of opening and then closing the file descriptor for us. Alternately we can close it manually ourselves.

fd = open('file.txt', 'a')
fd.write('Appended data')
fd.close()

CSV example:

import csv

with open('cool_csv.csv',newline='') as cool_csv_file:
  cool_csv_dict = csv.DictReader(cool_csv_file,delimiter=',')
  for row in cool_csv_dict:
    print(row['Cool Fact'])

Write CSV

import csv

access_log = [{'time': '08:39:37', 'limit': 844404, 'address': '1.227.124.181'}, {'time': '13:13:35', 'limit': 543871, 'address': '198.51.139.193'}, {'time': '19:40:45', 'limit': 3021, 'address': '172.1.254.208'}, {'time': '18:57:16', 'limit': 67031769, 'address': '172.58.247.219'}, {'time': '21:17:13', 'limit': 9083, 'address': '124.144.20.113'}, {'time': '23:34:17', 'limit': 65913, 'address': '203.236.149.220'}, {'time': '13:58:05', 'limit': 1541474, 'address': '192.52.206.76'}, {'time': '10:52:00', 'limit': 11465607, 'address': '104.47.149.93'}, {'time': '14:56:12', 'limit': 109, 'address': '192.31.185.7'}, {'time': '18:56:35', 'limit': 6207, 'address': '2.228.164.197'}]
fields = ['time', 'address', 'limit']

with open('logger.csv','w') as logger_csv:
  log_writer = csv.DictWriter(logger_csv, fieldnames = fields)

  log_writer.writeheader() 
  for item in access_log:
    log_writer.writerow(item)

Reading JSON:

import json

with open('message.json') as message_json:
  message = json.load(message_json)
  print(message['text'])
  print(message)

Writing JSON:

import json

data_payload = [
  {'interesting message': 'What is JSON? A web application\'s little pile of secrets.',
   'follow up': 'But enough talk!'}
]

with open('data.json','w') as data_json:
  json.dump(data_payload, data_json)

Classes

Can use type(my_variable) to get type of variable e.g. print(type(my_variable))

e.g.

print(type(5))
my_dict = {}
print(type(my_dict))
my_list = []
print(type(my_list))

A class is like a template of the data type, describing the kinds of information the class will hold and how it will be interacted with.

Classes are defined with the class keyword and the PEP8 Style Guide recommends camel case for class names: LikeThis.

Classes must be instantiated i.e. an instance of that class created in order to do anything with it.

class_instance = MyClass()

Instantiation can be thought of as taking a class and making it into an object … hence object-orientated programming.

class MyClass:
  my_variable = "Infos"

class_instance = MyClass()

print(class_instance.my_variable)

Methods are functions inside of classes. The first argument of a method is always the object that is calling the method and convention dictates that this argument is self

Note: pass means intentionally left blank, in effect, and is handily provided to be used as a placeholder.

class MyClass:
  def my_method(self):
    pass

class_instance = MyClass()

class_instance.my_method() # self is implicitly passed

self can used to access e.g. class variables within the class method.

There are certain special methods that can be used within classes, sometimes called ‘magic methods’ or ‘dunder methods’ (as ’__’ can be referred to as dunder).

__init__ is used to initialise a newly created object and predictably enough called automatically on instantiation. Any arguments passed to the class on instantiation will also be passed to the __init__ method.

Data held by object is referred to as ‘instance variable’. Instance variables and class variables are both accessed similarly.

hasattr(class_instance, "attribute") will return a boolean value reflecting the attribute belonging to the class or not.

Similarly getattr(class_instance, "attribute") will return the attribute, just like class_instance.attribute, if it exists.

Note that in both cases the attribute is passed as a string.

dir() can be used to show attributes

There is another dunder method, __repr__ which relates to the string representation of the class. __repr__ can only have one parameter, self and returns the string which is to be the string representation of the object.

Once defined, print(class_instance)) will use the string from __repr__.

Note a list can be a list of objects of a given class.

Inheritance and polymorphism

You want a class that looks like a class you already have … have no fear, inheritance is here.

A base class/parent class comes first and a subclass/child class inherits from it.

To do this, we simply include the class it inherits from as an argument to the class.

class BaseClass:
  pass

class SubClass(BaseClass):
  pass 

Can check is a class is a subclass with issubclass(SubClass, BaseClass) which returns True if SubClass a subclass of BaseClass, or False if not.

The various types of Python exception, like ZeroDivisionError, are subclasses of the Exception built-in class.

This means we can define our own exceptions, inheriting from Exception:

class StupidReferendum(Exception):
  print("Uhoh ... P(idiot|voted) > 0.5")

if PM == 'David Cameron':
  raise StupidReferendum

Inheritance is clearly useful where further class variables are profitably added. In certain situations it might also be useful if a class method is different for the child class. This is easily done; to override the method we simply include the new one in the subclass.

class BaseClass:
  def override_me(self):
    pass

class SubClass(BaseClass):
  def override_me(self):
    print("Duly overridden")

It would be super if we could call a method from the parent class from the child class, and Python provides super() to allow us to do this:

class BaseClass:
  def override_me(self):
    pass

class SubClass(BaseClass):
  def override_me(self):
    print("Duly overridden")

e.g. we can use super() to use the parent’s initialisation method:

class BaseClass:
  def __init__(self):
    print("Initialised")

class SubClass(BaseClass):
  def __init__(self):
    super().__init__()

Two classes are said to provide the same interface, strictly, if they have the same method names and attributes, however the key thing here is that they present the same methods, i.e. same names and arguments.

Polymorphism is the term used to describe the same syntax doing different things depending on the types involved. E.g. ’+’ does different things depending on type; it adds integers and floats, it concatenates strings, it copies a list to the end of another list, etc

Python allows us to use dunder methods such that our classes can behave like python built-ins e.g.

class MyCass:
  def __add__(self, other):
    # Add the thingies
    return MyClass(args)

Other dunder methods available include __iter__, __len__, __contains__.

These allows us to write data types (i.e. classes) that look and feel like native data types, which makes them more convenient to use.

e.g.

class LawFirm:
  def __init__(self, practice,
 lawyers):
    self.practice = practice
    self.lawyers = lawyers
  def __len__(self):
    return len(self.lawyers)
  def __iter__(self):
    return iter(self.lawyers)

dgp_and_h = LawFirm("Commercial", ["Dewy", "Getpaid", "Howe"])

n = 0
for lawyer in dgp_and_h: 
  n += 1
  if (n == len(dgp_and_h)):
    print("and {}".format(lawyer), end='')
    break
  print("{}, ".format(lawyer), end='')

Note, to override e.g. .append() you simply override it — i.e. provide your own method. You don’t need to use dunder methods for this.

If you make your own class extending list you can use super().whatever() to access the methods associated with list.


Appendix


Useful modules

random

  • random numbers
  • import random as random
  • random.choice(list)
  • random.randint(min, max)

datetime

  • date and time functions

matplotlib

  • plots charts
from matplotlib import pyplot as plt
plt.plot(numbers_a,numbers_b)
plt.show()

decimal

  • works around Python’s built in floating-point arithmetic (which can give funny roundings); useful for monetary values etc
# Import Decimal below:
from decimal import Decimal as D

# Fix the floating point maths below:
three_decimal_points = D('0.2') + D('0.69')
print(three_decimal_points)

Other resources

Some further resources I can recommend would include: