Python for Programmers

Python is a "scripting" language. This means it doesn't have funny types like unsigned short int who's only purpise is to save space. Commands focus on being easy to use and aren't always the most efficient. As a reaction to overcomplicated Perl, Python's feature set is minimal. There isn't even a do-while loop. It's also famous for having mandatory indentation. But the most unusual feature is that there's no type safety -- any variable can be any type at any time.

Variables are generic pointers to typed objects

Python data has types, but the variables don't. Because of that, they aren't even declared. Assigning to a variable creates it out of thin air. Examples:

n=4
type(n)  # <class 'int'>

n="cow" # assigning n to a different type is completely legal
type(n) # <class 'str'>

The language isn't completely insane. Trying to read from a non-existent variable gives an error. In n=m Python won't create m on the fly.

The no-type rule also applies inside of lists. There no such thing as a list of ints, only a generic list-of-whatever:

n=[4, 'frog', 4.5]  # different types in a list is legal
type(n)  # <class 'list'>, notice the lack of element type

Python often uses lists as a cheap way to avoid making a class. Here we abuse lists to give each cat a name and age, and even go nuts by including the name of the list as the first item:

Cats=[ "vet appointments", ["fritz",7], ["jub-jub",3] ]

Function parameters use the same rule -- no types. This means all Python functions are automatically virtual, or "duck-type". This legal function attempts to run whichever + will work on the inputs:

def sum(n1,n2):  # <- these parameters don't have types
  return n1+n2

sum("cat","fish")  # catfish

This function expects a list of numbers. It can take ints, floats or a mix of either. That's nice. But it can also take non-lists, which will crash it:

def posCount(L):  # L should be a list of numbers
  count=0
  for n in L:
    if n>0: count+=1

  return count

posCount( [-4, 3, 5, -2] )  # all ints
posCount( [4.7, 2.3, 99] )  # mixed float ints
posCount(-14)  # legal, but will crash

Basics

Basic types

Numeric types are int and float. That's it. Division is a bit odd: / is real division, // is int division, and modulo(%) works with float:

n=3;  f=8.2  # "declaring" an int and a float

9/2    # 4.5  single slash is real division
9//2   # 4    double slash is int division...
9.1//2   # 4.0 ... even with floats

8.5 % 2.1  # 0.1  (8.4, with 0.1 remainder) modulo with floats

++ is gone (and --). But we get the old ** for exponentiation: 2**4 is 16. Mixed int/float often takes care of itself, for example: n=3; n+=0.6 is fine: n was an int, but changes into a float.

Because of integer division's //, we get a really cool-looking //= operation:

n=28;  n/=10  # 2.8, oops! used real division
n=28;  n//=10 # 2

# Hmmm...int division on a float rounds but keeps as a float:
47.4 //= 10  # 4.0

Bools work the usual way, except they use capital True and False as in: happy=False.

Strings are str. They also work the usual way, but there's no char type! Strings are made of length 1 strings. "cat"[0] is the string "c". Like javascript, strings can use single or double quotes (which must match). Strangely, length is a normal function -- len(w) (the length of everything in Python works that way):

w1="camel"; w2='clutch'  # single or double quotes
type(w1)  # <class 'str'>

w3=w1+w2;  w3+="er"  # strings and + are normal

ch=w1[0]   # "c",  ch is just a small str
len(w1)    # 5  length is not a member function

All indexing in Python has an option to count from the right, w1[-1] is the last element, w1[-2] is the second-to-last, and so on. "abcde"[-2] is the string "d".

String indexes can't be assigned to. w[i]='x' is an error. You'll need to assign w to a new string with that change.

Lists

The only list type is list. There's no fixed-length array or linked-list (the list type is array-backed). Lists are created using []'s and commas. As usual, you don't declare them, you just make them:

N=[2,5,8]  # all there is to making a list
A=["car","truck","boat",plane"]
B=[1.6, 2, "frog", 6, ["x","y"], 7]  # mixed types are allowed

Indexing works as normal, including negative going from the right (L[-1] is the last item). They're range-checked -- L[999] throws an error. They allow insert and remove from anywhere, but preferably at the end:

Nums = []  # empty list

Nums.append(7)  # add a single item to the end
Nums.extend( [9,9,9] )  # add contents to end: [7,9,9,9]
# compare: Nums.append( [9,9,9] ) gives [7, [9,9,9] ]
Nums.insert(2, 942)  # add 942 before old slot #2

len(N)  # 5  [7,9,942,9,9]

n=Nums.pop()  # remove from end
Nums.pop(3)  # remove that index
Nums.remove(942)  # search for and remove that item

N+M is a shortcut for N.extend(M): [1,2,3]+[4,5,6] is a 1-6 list

All list-like objects use in as the contains check:

L=[2,6,9,12]
if 9 in L:  # True
5 in L  # False

hasX = "x" in "eckzactly"  # False

Tuples

Tuples are read-only lists, using parens instead of square brackets. Python uses these whenever it can. They work the same as lists:

c1 = ("fluffy", 8, 1.4)  # a tuple
len(c1)  # 3
c1[0]; c1[-1]  # "fluffy"  1.4 (1st and last)

type(c1)  # <class 'tuple'>,  again, notice absence of element type

c1[2]=5.6  # Error. Tuples are read-only

The ()'s around a tuple are optional. Whenever it looks as if multiple values are being sent, it's really sending 1 tuple:

a = 1, 9, 12  # same as a=(1,9,12)
a[2]  # 12 -- a really is a 3-part tuple

# at the end of some function:
  return w, "bird", True  # really: (w,"bird",True)

We'll see more tricks with this later.

Dictionary

Python dictionaries are the usual. They're made with curly-braces and key:value pairs. Assign and look-up use []'s. keys() and values() retrieve them:

D = { "cat":8, "horse":275, 'bird':2 }  # a dictionary
D['horse']  # 275
D["goat"]=140  # add new item

list(D.keys())  # ['cat', 'horse', 'bird']

Reading a non-existent dictionary item is an error. Luckily in works with dictionaries:

a='frog'
wt=D[a]  # error, no frog

# this is safe:
if a in D: wt=D[a]
else: wt=-99

Reference/Value Types

Python uses the standard Reference Type scheme (like Java). int, float, bool, str act as value-types, everything else is a pointer. Contrary to the usual way, == is a value-compare, while is checks pointer equality:

N1 = [1,2,3];  N2 = [1,2,3]  # different lists, with same contents

N1==N2  # True, same contents
N1 is N2  # False, not same objects
N1=N2  # both are now the same object
N1==N2; N1 is N2  # True True

Python's null is: None: c1=None. The preferred null-test is if c1 is None:. None is actually pre-made as the only value of the type NoneType. All variables which have been set to None point to it.

Python doesn't like to say Value and Reference type. It prefers the more confusing immutable and mutable, then says crazy things like "ints are immutable". That's just a fancy way to say that Python uses the same trick as Java and everyone else.

Technically, n=5; n+=2 doesn't change the 5 to a 7. It actually creates a 7 somewhere else and moves n to point to it. Likewise w="cow"; w+="bell" leaves the "cow" alone, creating a fresh "cowbell". So technically ints and strings can never change. But the entire point of the trick is to make pointers that act like value types. And it works. Everywhere you see immutable in a Python book, you can cross it out and put Value Type.

It's a little fun to to see how smart Python is about sharing things:

w1="cow"; w2="cow"
w1 is w2  # True -- w2 noticed we already had a cow and shared

# if we make "cow" in 2 parts, Python doesn't notice the old cow:
w3="c"; w3+="ow"
w3==w1  # True -- both are "cow"
w3 is w1  # False -- w3 didn't realize it could share

Numbers are even stranger. Like Java, Python pre-makes small ones. Here we make 3 in sneaky ways. All share the same 3:

n1=3;  n2=7-4;  n3=30; n3//=10  # all are 3
n1 is n2; n2 is n3  # True True

But if we make a pair of large numbers, not even being tricky at all, Python doesn't bother checking if they can share:

n1=758;  n2=758
n1==n2;  n1 is n2  # True False -- 2 copies of 758

# proving they can share:
n1=682;  n2=n1
n1 is n2  # True

This is all a long way of saying you can use is with "value types", but don't since it's totally unreliable.

if's

If statements don't use parens and end in a full colon. They have the usual compare operators (<=, ==, !=), but boolean logic is lower-case AND, OR, and NOT:

if w!="cow":
  print("no cow")

if not(n>=0 and n<=10):
  print("not 0-10")
  
if n==3 or n==7 or n==15: print('lucky')

Strangely, Python uses the old elif instead of else-if:

if n<10: print('low')
elif n<25: print('med')
else: print('hi')

Comparisons may be chained: 0<n<100 is actually legal and does what you think! They evaluate in pairs from left to right, and'ed together. It works for any compares, in a chain of any length:

if 10<=n<=20: ... # same as n>=10 and n<=20
if 0 <= i < len(A):  # a basic 'legal index' check

# test whether A is ascending:
if A[0]<A[1]<A[2]<A[3]<A[4]: ...

if a==b==c==0: ... # all are 0's

# mixed operators are allowed (but what a mess):
if x<y==z>=50:  ... # True for: 4,56,56

The technical term is interval comparison. It's lazy: 1 == 2 < 1/0 safely evaluates to False.

They can be confusing. if x!=y!=z!=0: looks like it checks for all non-zero but is actually junk. It only checks adjacent pairs so is true for things like [0,2,1] and [1,0,3] (x and y aren't the same, neither are y and z, and z isn't 0).

Python has the value-returning ?:, but it's more awkward than in other languages. The compare goes between the values:

w=  "positive" if n>=0 else "negative"

loops

Python has only 2 loops: while and for, which is really a foreach. If you're used to normal for-loops, seeing Python's for is confusing.

Whiles are nothing special. Like IF's, they don't use parens and end in a full colon:

x=1
while x<100:
  print(x)
  x*=2

The for loop (which is a foreach) can loop through any sort of sequence, even strings:

for x in [3,6,9,12]:  # looping through a list
  print(x)

for ch in "camel":  # every letter
  if ch=="a": aCount+=1

Typical counting loops can be faked using the range function. Range stops before the last number and defaults to starting at 0:

for n in range(10): print(n)  # 0 through 9
    range(90,100)  # 90 through 99
    range(20,30,2) # 20,22,24,26,28 (not 30)
    range(10,0,-2)  # 10,8,6,4,2 (not 0)

Range doesn't create a list -- it creates a range iterator -- but can easily cast into a list. list(range(10)) makes a real 0-9 list

functions

Functions start with def. The header ends with a full colon. Parameter names are listed, but with no types. Nothing lists the return value or whether there is one:

def maxOf3(a,b,c):
  if a>b:
    if a>c: return a
    return c
  if b>c: return b
  return c
  
max(5,9,4)
max("cat", "panther", 'cougar')  # > on str compares the length

Functions with no return send back None (Python's null). In other words, there are no void functions -- they always return something. You might recall that None is a new type. That's no problem -- Python functions can return any type, any time.

Default values use the usual = syntax: def f(n=0). In a call, name=value is used for Named Parameters (the same as how some other languages use f(n:3)):

def g(a=99, b=99): ...
# we can leave out the a, b or both. Nothing new

g(b=16, a=3)  # same as g(3,16)
g(b=12)  # this is fun, same as g(99,12)

There's no call-by-reference. But we rarely need it -- we mostly pass reference types anyway.

white-space

In Python, end-of-line ends statements. As we've seen, a full-colon is used for block headings: ifs, loops, functions, classes. Python has semi-colons, but typically used to put several statements on one line. Famously, blocks must be indented. Everything in a block must have the same indent (obviously), but the amount can vary for different blocks. Blank lines don't do anything:

if b>c: 
  b=0  # we choose a small indentation
  c=99; print("semicolon seperates same-line statements")
  
  if(d>10):
         d=0  # indented more here, since we felt like it
         c=999	 
  e=0 # back to 1st block (same indent is required)

Python's lack of block delimiters is a problem when you want an empty block. As a fix it adds a no-op named pass:

def doNothing():
  pass  # <-- the official Python "do nothing"
  
if a>99: pass
else: print("a is small enough")

Since line-breaks end statements, you can't easily split a statement over 2 lines. Python revives the old backslash line continue:

n = cats*2 + dogs*12 + \  # to be continued
    camels  # the indent here is unimportant

casting

Casting is the usual: str(43) is "43", int(4.9) is 4, int("12") is 12. To safely use these we have isdigit for ints, and try/catch for floats:

"436".isdigit()  # true -- contains only digits

if w.isdigit(): n=int(w)
else: n=-999

try: n=float(w)
except ValueError: n=-999  # testing: float("abc") gives ValueError

Strings combine with a +, but there's no implicit cast from numbers. "slot"+3 is an error. Either cast yourself, or use format:

w= "x=" + str(x) + " y=" + str(y)  # no implicit cast to str

# format fills {}'s with the input, casting to string:
w= "x={} y={}".format(x,y)

# {0} uses the first input, and so on:
phrase="There's a {0} in here. A {0}. Someone {1} that {0}!"
phrase.format("bear", "wrestle")

Python can treat length 1 strings as characters and cast then to and from acsii with ord and chr:

ord("a")  # 97
chr(98)  # "b"

w="c"
chr(ord(w)+7)  # "j"

# not quite right:
str(ord(w)+7)  # '106' opps! turned 106 into "106"

Most list-like types cast back and forth, string to list, list to tuple and so on:

A = list("frog")  # ["f", "r", "o", "g"]
B = tuple( [5,6,7] );  # (5,6,7) -- which is a tuple

# range creates an iterator, which can be cast into a list:
C = list( range(5) )  # [0,1,2,3,4]

# list(dict) returns the keys, just because:
list( {"hammer":8, "wrench":12}  # ['hammer', 'wrench']

Variables can also point to types. No special syntax -- just assign them. Check them using is:

t1=int

t2 = type(n)
if t1 is t2: ...
elif t1 is float: ...

Fun fact: the type of variables holding a type is type. For example: n=type(t1); type(n) gives < class 'type'>.

More tuples and Multiple assign

Python allows multi-assign through tuples. Since most people use the no-parens shortcut, it looks like every other multi-assign:

a, b = 5,12  # a=5; b=12
(a,b) = (5,12)  # what the computer really sees

N = [5,8,"cow"];
x,y,z = N  # x is 5, y is 8, z is "cow"

a,b,c = "CAT"  # a="C", b="A", c="T"

x,y = y,x  # a swap!!

Notice how it works on anything with a len. For strings it treats each letter as an item.

Functions which appear to return multiple values are actually returning tuples. User are allowed to catch them with a multi-assign:

def f(n):
  ...
  return val, status
  
v1, s1 = f(8)  # catches the returned val and status
# or catch a tuple in a tuple:
res = f(8)  # res is a tuple. res[1] is the status

The sizes must match exactly, but we have two tricks to get around that. By convention, underscore is used as a dummy variable. That's not much of a trick -- you still need to know the exact size -- but can sometimes help:

x, y, _, _ = N  # first 2 items, N must be length 4
_, age, wt = C  # last 2 items in C, which must be len 3

The real trick is adding a star before a variable to "glob" it -- it matches 0 or more items, becoming a list. This lets us multi-assign from variable length lists:

x, y, *_ = N  #  get first 2 items, N can be longer than 2  

*_, x, y = N  # last two items
x, *_, y = N  # another way to get first and last

# get 2nd-to-last and 3rd-to-last items:
*_, n1, n2, _ = N

As a note, x,y = N[0], N[-1] is an easier way to get the first and last.

We usually use *_ and ignore the globbed part, but it's returned as a list we can read, possibly an empty list:

a, *b, c = "FROGMAN"  # a="F", c="N",   b=["R", "O", "G", "M", "A"]
*x, y = [1,2,3]  # x=[1,2];  y=3
*x, y = "z"  # x=[], y="z"

You're allowed to dig into the structure using nested ()'s. ((x,y), *_) would get 4 and 5 from [ [4,5], 8, 7, 3]. Here's a tricky one. It gets the first 2 letters of the second word (read it in 2 steps: get 2nd word, then inside get 1st 2 letters):

_,(ch1, ch2,  *_), *_ = ["ant", "monkey", "duck", "pig"]
# ch1="m", ch2="o"

For loops can also use this trick when going through a list of lists. Assume each Cat in a list looks like ("Yaz",5, 7.2). This loop pulls them out into 3 vars:

for name, age, wt in Cats:  # auto-assigns all 3 from the current cat

# 1st letter of each word:
for ch,*_ in ["ant", "bear", "cow"]:  # 'a', 'b', 'c'

As usual, if each item in the list doesn't have exactly 3 parts, the Cat loop will crash. The other one crashes on empty strings.

Loops can even use the "digging-in" trick. for (ch,*_),*_ in Cats picks out the first letter of each cat's name (but doing it by hand might produce more readable code).

namespaces, importing modules

Python doesn't allow you to use anything without importing it first. Then you can use it with its full path:

import random  # a python built-in for random numbers

x=random.randInt(10,20) # random-dot is required

A standard "include" directive looks like from X include Y. We can then use imported items "naked":

from random import randInt, uniform

x=randInt(10,20)
y=uniform(1.5, 3.2)

Python doesn't have a syntax for you to make namespaces. Simply writing a file makes it importable. Python calls it a module. Here's a complete cats which we can import:

== cats.py: ===
totalCatCount=0

def randomCatName(): return "fluffy"

== a program using it: ==
import cats

def newCat():
  cats.totalCatCount+=1  # use imported variable
  newName=cats.randomCatName()  # use imported function

Nested modules are made by creating nested directories (the same way as Java). If you want Cats to be in the "namepsace" Pets, put it in a directory named Pets. It's new name will be pets.cats. Pets with everything under it is called a package.

Here's where it gets ugly. Python only recognizes a package if you put an empty file named __init__.py in each directory (double-underscores around init):

Pets
  __init__.py
  cats.py
  dogs.py
  Other       <-- if we had another directory...
    __init__.py       ...it needs it's own init
    hamsters.py

import pets.cats
pets.cats.totalCatCount=4

This allows you to leave things out of a package by leaving out the __init__ file.

Finally, Python only looks for imports in it's current path variable (or the current directory). You'll need to put the root of your packages into it. It's in sys.path which is a list of strings. The code below adds a new location:

import sys  # look! another import!

sys.path +=["/Users/bob/myPythonFiles"]

The other funny thing about how Python finds things is that there's no block scope. Loop variables or anything declared inside of an IF continue to exist. But this is rarely a problem: you don't have to worry about declaring them twice, and Python gladly changes the type:

for i in [1,7,8,9]: ...
# i is 9 after the loop

for i in "tree frog": ...  # i changes into a char
# i is "g"

Here's a crazy example of that, which isn't useful for anything. temp only exists if we take the IF:

if x<10:
  temp=8

n=temp # either 8, or gives a "no such variable" error

More on loops

Loops can have an else. It's very silly. It only fires when the loop exits normally. It's for those special searching loops when you use break when you find what you wanted. In those cases, it's like "else not found". Ex:

# does A has any positive numbers?:
for x in range(len(A)):  # each index in A
  if A[x]>0:
    print('found a positive number at '+str(x))
    break
else: # this only runs if the loop goes off the end of A
  print("no positive values in list")

I warned you it was silly. Notice how indentation clearly establishes what the else matches.

In that loop we used the index and looked up the value. Python has a trick for that: enumerate(A,0) pairs A's elements with numbers starting from 0, into tuples:

W=["red", "green", "blue"]
list( enumerate(W, 0) )
[(0,"red"), (1,"green"), (2,"blue")]

Notice how the index confusingly goes second in the call, but is first in the result. Also, enumerate gives an iterator. That's just fine for a loop, but we had to cast it to a list to print it.

Loops like to use enumerate with multi-assign anytime they need to get the value and the index:

for i,val in enumerate(W,0):
  # i is 0, val is "red", and so on

If you already know how to write an index loop, this seems more awkward and slower. But I suppose it's nice if you use lots of Python.

Function special pack/unpack

When calling a function, Python can convert lists and dictionaries into parameters, or the reverse -- a total of 4 possible conversions. To convert any number of inputs into a list, add a star before the last one (this is what many languages call variadic parameters):

def ff(x, *L):  # takes 1 or more parameters

ff(1,"cat",2,"dog")
# x is  1
# L is  ["cat", 2, "dog"]

To do the reverse (convert a list into several parameters), add a star before the list in the call (the function has nothing extra -- it doesn't need to know we might do this). For example:

def f(a,b,c):  # a normal 3-input function

A=[8,5,12]
f(*A)  # unpacks to become f(8,5,12)

f(*"cow")  # also legal, since it becomes: f("c", "o", "w")

B=[4,8];  f(5, *B )  # also legal: f(5, 4, 8)

It doesn't seem like much, but it saves typing out f(A[0], A[1], A[2]).

The other two versions are about using the parameter names. A dictionary can be unpacked into parameters by putting a double-star in front. As before, it works with any normal function:

def makeCat(name, age, cuteness):  # a normal function

# someone gives us a dictionary with the exact right names:
Cat1Info={"cuteness":7, "age":4, "name":"salem" }

makeCat(**Cat1Info)
# same as: makeCat(cuteness=7, age=4, name="salem")

This isn't useful for ordinary programs, but often some other program gives you inputs like Cat1Info, in dictionary format. This trick provides an easy way to use them.

The reverse of that trick is for a function to say it will take parameters with any names. Add a double-star in front. The parameters will be turned into a dictionary:

def fTakesAnyNames(**A):
  print(A)

fTakesAnyNames(cow=5, duck="quack")
# {"cow":5, "duck":"quack"}

fTakesAnyNames(red=1, purple=4, mediumGreyBlue=-3)
# {"red":1, "purple":4, "mediumGreyBlue":-3}

The trick is called keyword arguments, or kwargs. It's total fluff. We're sending the function a list of strings and values, which we could do plenty of ways, but it's so cool doing it like this.

For fun, the 2 dictionary tricks work together. Here we send all sorts of ways. The function assigns what it can, putting the rest into its kwargs dictionary:

# **C says this takes any inputs with a name:
def makeCat(name, age, cuteness, claws=5, **C): ....

Cat1Info={"size":4, "cuteness":7, "age":2, "hats":True }

makeCat("Tina", **Cat1Info, color="grey")

# name="Tina", age=2, cuteness=7, claws=5
# C={ "size":4, "hats":True, "color":"grey" }

More on Functions

Python functions have access to file-level variables. For example:

dogs=0

def checkDogs(n):
  if n<dogs: print("arf")

But if a function wants to change a file-level variable, there's a crazy problem -- Python assumes you want to create a local. It's a consequence of the no-declare rule:

dogs=0
cats=0

def addDog():
  cats=8  # create _local_ cats in the function
  dogs+=1  # ditto -- makes _local_ dogs

That's a weird problem. The fix is a new, janky global command which is like an un-declare:

def addDogs(n):
  global dogs  # dogs is global. Don't declare it
  dogs+=n  # adds to global dogs

Functions can have an overall description, plus one for each input and for the output. They use crazy shortcuts. The overall description is an optional bare string as the first line in the body:

def max(a,b):
  "finds largest of the inputs using > to compare"
  if a>b: return a
  return b

The rules for the paramters isn't too bad: they go after each, using a colon:

def chofinate(L1:"any list", L2:"a list of integers"):

The return value comment can go at the end of the first line, after a ->. It's sneaky because the -> really, really looks like it means something, but it's just part of the comment:

def max(a,b) -> "either a or b"

Every optional comment together looks like:

def swap(L:"a list", i1:"an index", i2:"an index") -> "nothing"
  "switches positions i1 and i2 in L"
  L[i1], L[i2] = L[i2], L[i1]  # a cool python swap

In theory, an IDE can pop those up for us. They real thing they're shortcuts for is f.__doc__ and f.__annotations__ (double underscores). The first is a string, the other is a dictionary. You can set them if you want:

swap.__doc__="switches the items in a list"
swap.__annotation__["L"]="a list"
swap.__annotation__["i1"]="an index"

The annotations can be types. This can make it look very official, like they're required (they're not):

def f2(x:int) -> str:  # these are merely annotations
  ...

Since they wrote it there, this function probably crashes if it gets anything besides an int, and probably always returns a string. But there's no rule saying that. They're the same as writing "x should be an int".

Mutable types as default parameters have a crazy rule - if you change them, they stay changed. That's completely nuts. This fucntions takes a list input, supplying an empty list as a default, so fits the rule. Each time this is called with no inputs, the actual default parameter is changed:

def AA(L=[]): # default list starts empty
    L.append(len(L))  # add length to end
    return L

AA( [5,5,5] )  # [5,5,5,3] nothing special	    
AA(); # [0], used the default, added the 0. so far so good

AA()  # [0,1]  yikes! It keeps growing
AA()  # [0,1,2] and more!

AA([6]);  # [6,1]  seems fine again
AA()  # [0,1,2,3] -- nope default is still growing

I guess it's a quick way to give a function a persistant private variable, but just make a class.

Function pointers, anon, closures

Function pointers are simple. since you don't need to declare the type. Just assign the function name to a variable:

f1 = max
n=f1(3,6) # 6  no special syntax to use it - just a normal call

Anonymous functions are limited. They're allowed a single expression which is implicitly returned. To shorten them, the parameters use no parens:

f1 = lambda x,y: x+y
f1(4,7) # 11

min3 = lambda a,b,c: min(min(a,b),c)

Functions are 1st-class objects. They can be passed or returned. This returns either the built-in min or max function, or, just to show we can, some crazy lambda function:

def getComparer(ch):
  if ch=="animal": return min
  elif ch=="mineral": return max
  else: return lambda x,y: x*x<y  # very contrived use of anon func
  
cc=getComparer("mineral");  cc(5,8)  # 8, since cc=max

Functions can have nested functions. That allows helper functions to be hidden, but it also allows them to be returned with closures. Here's the boring use, bestCat using helper isBadCat:

def bestCat(c1, c2):
  # nested helper function:
  def isBadCat(c): return c.lower() in ["boomer", "zubb"]
  # the 2 bad cats
  
  # handle cases where one or more are bad:
  b1=isBadCat(c1); b2=isBadCat(c2)  # using the helper function
  if b1 and b2: return  # None
  if b1: return c2
  if b2: return c1
  
  return c1.age > c2.age

Putting isBadCat inside just made things look a tad nicer. The really fun use is returning inner functions, capturing variables from the main function. Here clampMaker returns its inner clamp_aux function with lower and upper magically saved:

def clampMaker(lower, upper):
  def clamp_aux(n):
    # NOTE:  lower and upper are captured:
    if n<lower: return lower
    if n>upper: return upper
	return n
	
  return clamp_aux

c1 = clampMaker(1,10)
c2 = clampMaker(2,5)
c1(12)  # 10  c1 is set to 1-10
c2(12)  # 5   c2 is set to 2-5

Returned lambda functions also capture variables (but you have to figure out how to compute your result in a single expression).

More list tricks

Misc

x in L is a contains check for any sort of list. It even works for substrings: "sh" in "fishy" is true. But it won't work for sublists: [4,5] in [1,4,5,9] is false since it looks for [4,5] as a single sub-item.

+ adds any sorts of lists end-to-end (it's a shortcut for extend). Multiplying a list (or a string) by an int adds end-to-end copies:

L += [3,9,10]  # adds to end as 3 elements (not 1 list)
L += [n]  # a hack for L.append(n)

A=[1,2]; B=A*10  # [1,2,1,2,1,2 ... ]  # multiply by int
"Abc"*3  # AbcAbcAbc

for loops on dictionaries go through all of the keys, just because. D.items() creates a list of (key,value) tuples:

# key:value the normal way:
for key in D: 
  val = D[key]
 
# multi-assign trick:
for key,val in D.items():  # [("cat",5), ("dog",12) ... ]

slicing

As with any modern scripting language, Python supports slicing, including negative and missing indexes. The syntax is A[start:onePastEnd]:

w="catfish";
w[2,5]  # "tfi" -- items 2,3, and 4
w[:3]  # "cat" -- everything up to item 2
w[2:]  # "tfish" -- item 2 and beyond

w[:-1]  # "catfis" -- all but last item
w[-2:]  # "sh" -- last 2 items

Slices are new list objects (some Python-like languages make them iterators over part of the original list, but not Python).

Overall list functions

Python has the usual operations on lists. Nothing fancy. zip combines several lists (up to the shortest length) into a single list of tuples:

C=zip([1,2,3],["a","b","c"]) # [(1,"a"),(2,"b"),(3,"c")]

map creates a new list with an operation done to every element. Reduce combines every element into a single result:

# make a new list with everything upper-cased:
L2 = map(lambda w: w.upper(),  ["cow", "Arf", "RatTat"])

# make a sentence out of words in a list:
S=["the", "lonely", "fish"]
import functools  # reduce is here
addWords = lambda w1, w2: w1+" "+w2
n = functools.reduce(addWords, S)  # "the lonely fish"

The more serious use of reduce is when the answer isn't the same type as the elements. Then you need to give a starting value and know that it goes left to right. This adds the length of every word in a list:

# add length of new word to the previous sum:
lengthSum = lambda old, newword: old+len(newword)
n = functools.reduce(lengthSum, ["cat", "dog", "turtle"], 0)  # 12

These are for people who really enjoy built-ins. A regular loop would do this about as well.

Special list loops

Pythoners really enjoy special list and dictionary-making loops. List symbols around the whole thing emphasizes how you're creating a list. The first item represents the element to repeatedly add. The rest is a mostly-standard foreach loop. This makes a list with five 3's:

[3 for num in range(5)]  # [3,3,3,3,3]

Math is allowed in the element part. We're also allowed a single IF afterwards. Here's a complicated way to make [20, 40, 60, 80]:

[x*10 for x in range(1,9) if x%2==0]

We can make a filter with it. This makes a copy of L with no negative numbers:

# remove negative numbers:
L2=[n for n in L if n>=0]

There's no way to add extra items. For example [a,b for ... ] to turn each element into 2, won't work at all. At best you can create tuples or sublists: [(a,b) for ...].

Dictionaries can be created. Put {} around the whole thing and use naked key:val as the element. This turns a list into a dictionary with 0's for values:

{ w:0 for w in Animals}  # ["cat", "dog"] into {"cat":0, "dog":0}

Inner loops are allowed. This seems excessive, but some people really enjoy this style. As usual, rightmost loops are more inner. The first loop below makes N copies of each number N. The second writes each letter in each word, skipping words beginning with "x" and all spaces. Notice how the ch comes from the final loop:

[n for n in L for i in range(n)]  # [2,4] becomes [2,2,4,4,4,4]
# ex: if n is 4, the inner loop runs 4 times, making 4 4's

# this one is ugly: all words not starting with x, all letters except space:
[ch for word in W if len(word)!=0 and word[0]!='x' for ch in word if ch!=' ']

# ["a cow", "xort", "   Q"] to ["a","c","o","w","Q"]

This shows off about the worst part of Python: sure, it's mostly minimalistic, with the standard shortcuts. But it goes nuts for stuff that seems cool.

Sets

Items inside curly-braces, without the colon-pairs, makes a set. They use in OR(|) and AND(&). They don't allow [] look-ups:

A={1,2,3};  B={1,3,4}  # sets
C=A|B # {1,2,3,4}
C=A&B # {1,3}
if 2 in A:  # True

A.add(8);  A.remove(8)  # the usual

Regular expressions

Python has the usual regular expressions. re.search searches and returns a match object. A match casts to true/false and also tells you what and where it matched:

from re import search

# looks for a money amount ( $3.67 or $198.00, but not $.15 or $1.6):
# Notes: \d is a digit, \. is an escaped dot, \$ is an escaped dollar-sign
pattern = ""\$[\d]+\.\d\d"
match1 = search(pattern, "$12.5$.78$1.234")  # finds "$1.23"

if match1:
  text=match1.group(0)
  start=match1.start(0);  end=match1.end(0)
  
  w="found {} from {} to just before {}"
  print(w.format(text, start, end))
  # found $1.23 from 9 to just before 14

Reg-exps's have all the usual: .* for 0 or more of anything, [a|e|i|o|u]{2,4} for 2 to 4 vowels. Adding ? switches from a maximal to a minimal match: "X.*X?" gets the first thing surrounded by X's. It's even got groups with parens and \0, \1 to match a previous group. re.finditer is the same as search, but returns a list of everything matching your pattern.

classes

basic class

Python classes have two odd parts. Member variables aren't declared (since no Python variables are declared), and member functions are all fake (Python classes are very similar to how javascript ones). A sample cow class showing these. __init__ is the constructor (with double-underscores):

class Cow:
  # no variables are declared here, since vars aren't declared

  def __init__(self, w="", age=0): # the constructor
    self.name=w
    self.age=7

  def describe(self):  # called like: p.describe()
    return self.name + ", " + str(self.age) + " yrs. old"
	
  def addToAge(self, yrs):  # called like: p.addToAge(3)
    self.age += yrs

Every member function must have a reference to "itself" as the first input. It will automatically be sent. Calling c1.addToAge(3) actually runs addToAge(c1,3). This is because member functions are fake. They're just normal functions and you have to tell them which cow to use. They have use self.name and self.age to find that cow's name and age.

There's nothing special about self. It's just a variable name -- you could use s or theCow. But self is nice since people are used to it and if feels official.

The way you figure out a class's member variables is by seeing what the constructor creates. Anyone could add more (c1.milk=6 is legal anywhere and creates a new milk member variable), but it's better to agree that only the constructor will.

There's no new. Constructors are called like:

c1=Cow()
c2=Cow("bessy")
c3=Cow("lu-lu",4)

There's no public or private. Everything is automatically public. By convention variables starting with an underscore should be treated as private: _id, _age_in_days, and so on.

Python has static class variables (1 copy for the entire class). They're declared directly in the class:

class Frog:
  # these are static class variables:
  bestType="spotted croaker"
  count=0

Use them the normal way, with the classname in front:

ft = Frog.bestType 
Frog.count+=1

Python also has Static class functions, which are double weird. First they need @classmethod on the previous line. Second the class calling them is automatically the first parameter:

class Goat:
  count=0
  
  @classmethod
  def canEat(g, foodName):  # g will always be class Goat
    return True  # goats eat anything

We still call it with the class name, like Goat.canEat("tin cans"). That's turned into canEat(Goat, "tin cans"). We need the class name since we might need to look-up a class static (aarrrg!):

class Goat:
  count=0  # hasGoatArmy will need this
  
  @classmethod
  def hasGoatArmy(g):
    return g.count >= 100

Without g, we'd have no way of finding Goat.count since hasGoatArmy doesn't have special Goat access.

Python's __init__ can't be overloaded. Instead they use static functions as alternate constructors. Here makeHopper creates an alternate type of Frog:

class Frog:
  def __init__(self, name="frog"): self.name=name 

  # this is an alternate constructor:
  @classmethod
  def makeHopper(frogClass, jumpHt):
    theFrog = Frog()  # borrow the constructor
    theFrog.hopHt=10
    return theFrog	

f1 = Frog()
f2 = Frogs.makeHopper(4)  # an alternate "constructor"

One thing about static variables is just nuts. An instance is allowed to read them. Worse, instances can't change them -- instead it creates a new member variable. It's a confusing mess:

Frog.count=9  # correct way to use a static

f1 = Frog()
f1.count  # 9 !! This shouldn't work, but it does

Frog.count=27
f1.count  # 27 !! It's really tracking the class count

f1.count+=2
f1.count  # 29  seems to work, but...
Frog.count  # 27  ...Nooo!
# f1 accidentally made a personal count variable

It's pretty much a trap waiting to happen.

Inheritance

Regular object oriented languages need inheritance to get polymorphism. If you want to pass a Cat in place of an Animal, you need Cat to inherit. Python doesn't need that. You can already use everything interchangeably (as long as it has the parts your function uses).

In Python, inherit only if you want to "get" useful stuff from the base class. In other words, suppose in an OOP language you'd have 2 classes that work entirely diffferently but have the same public functions. You'd have them inherit from a common virtual class. In Python you don't need that extra step.

Inheritance uses parens around the base class. Base class constructors are called like: baseClassName.__init__. Here Cow inherits from Animal:

class Cow(Animal):
  def __init__(self, name="", age=1, milk=0):
    Animal.__init__(self, name, age)  # calling base constructor
    self.milk=milk
    
  def sellPrice(self):
    basePrice=Animal.sellPrice(self)  # calling base class function
    return basePrice*2

Multiple inheritance is allowed, with commas:

class RoboCow(Animal, Robot):  # multiple inheritance

What do we actually inherit? Well, functions are stored with the class, so we inherit all of them. We don't actually inherit variables. But if we call our base class constructor, it creates them, so effectively we inherit variables, too.

operators, casts

Python classes can define operators and casts. The special names are words surrounded by double-underscores (the same idea as __init__). Here's a typical string cast for Cow:

  def __str__(self):
    return "{}, {} years old".format(self.name, self.age)

Now str(c1) works.

+ and - can be overloaded using __add__ and __sub__. Another fun one is __call__ which overloads ()'s. Here they are for a Pair x,y class:

class Pair:
  def __init__(self, x=0, y=0):
   self.x, self.y = x,y  # using multiple assign trick
      
  // overloading Pair+Pair:
  def __add__(self, other):
    # add x's and y's of each to get new Pair:
    return Pair(self.x+other.x, self.y+other.y)
  
  def __call__(self): print("function call")

p3=p1+p2  # runs the __add__ function
p3()  # runs __call__, which is a bit silly in this case  

Properties

No scripting language can resist the lure of Properties -- functions that look like variables. In Python they have 3 parts: the get, the set, and a command to delete it (we'll see more on this). You list them 1-at-a-time with a decorator in front (@property means get, the other 2 are obvious):

class Cat:
  # assume pnds is the cats's weight in pounds
  
  @property
  def kilos(self): return self.pnds/2.2
  @kilos.setter
  def kilos(self, k): self.pnds=k*2.2
  @kilos.deleter
  def kilos(self): del self.k  # see below for "del"

Notice how they all have the same property name. It knows which is which because of the @'s in front. Also notice how they're all normal functions. setter and deleter are also optional.

This is a shortcut for the real way which no-one uses. You write kilos=property(getFunc, setFunc, delFunc). That creates a property object and assigns it to kilos. The @-macros create this for you.

More on storage

Python's variables are saved in dictionaries. Using a variable is really just a dictionary look-up. If you want, you can use the "all-variables" dictionary directly. globals() gives you the dictionary for the file (the "module"):

# first make some sample variables:
n=2;  c="cow"

# here's where Python keeps them:
globals()  # {'n':2, 'c':"cow" }  plus a lot more system stuff

# how to check if a variable exists:
if not "n1" in globals(): print("no such variable n1");

If you want to delete a variable, a normal pop command from the main dictionary will do that:

globals.pop("n");  # n is no longer a variable

That's right. Python allows you to delete variables, something no other language does. del n1 is a shortcut. Why do this? Maybe to save space?

locals() gives the variables in the current function (not even in the class -- just the current function).

Class member variables aren't technically variables -- they're attributes. Attributes are variables attached to objects, accessed with a dot. They can be listed and examined using vars(object):

# An empty class, for us to play with:
class A: pass

a=A();
a.x=7;  a.name="cow"  # member vars (actually attributes)

# list them:
vars(a)  # {"x":7, "name":"cow"}

You can check for one the normal way: "x" in vars(a), but Python adds shortcuts:

if hasattr(a, 'name'):  # does a.name exist?
n=getattr(a,"name")  # same as: n=a.name
setattr(a,"cost", 1.75)  # same as a.cost=1.75

Why use getattr and setattr when = is so much easier? Sometimes things "feel" better as attributes than as members.

We can delete member variables with the same old del command:

del a.x  # x is no longer a member var
vars(a).pop("x")  # the long way

  def f(self): del self.x  # in a member function

Every object has a few built-in attributes. __class__ holds the type (type(x) is the same as x.__class__). n.__doc__ holds the doc-string. Functions and classes have __name__ and __module__:

n=5;  n.__class__  # < class 'int'>
n.__doc__  # explains options for int() casts

Frog.__doc__='also used for toads'
Frog.__name__  # 'Frog'
max.__module__  # 'builtins'

These sometimes reach through to the type. For example n.__doc__ is actually showing you int.__doc__.

Deep-down, Python objects only have preset built-in attributes, including one named __dict__. That stores the ones you add. Whem you write a.n=5 it's actually a.__dict__["n"]=5. vals(a) is really just s shortcut for a.__dict__.

Your functions can also have attributes added.I'm not sure why you'd want to, but you can:

def knightMe(w): return "Sir "+w
f2.count=0  # we made up count, just now. An attribute
f2.abc="def"

Fun with strings into variables

Variables being in a dictionary means Python is comfortable converting strings to variables and back. We can read just any string and set an attribute with that name:

aName=input("enter atribute name: ")
setattr(a, aName, 10)
# if they entered "size", then a.size is now 10

Or we can search for attributes with certain patterns in their names. Here we search for f1.a through f1.z:

# check f1 for all 1-letter attributes:
for n in range(ord("a"), ord("z")+1):
  attr=chr(n)  # "a" to "z"
  if hasattr(f1, attr):
    print("f1."+attr+" exists")

# possible output:
# f1.n exists
# f1.y exists

We can abuse the ** trick to call a function using a parameter which we got from a string:

p0=input("enter a parameter name: ")
D={ p0:10 }  # dictionary
f(**D)  # unwrap it into a parameter

If they entered "cows" that would run function f(cows=10).