Linked handout: 2. Programming

Python programming primer

Python programming primer

    What is Python?

  • Interpreted language
  • As high level as you can get
  • Dynamically type-checked
  • Garbage-collected
  • "Batteries included"
  • Modular
  • Recommendation

  • Also learn a natively compiled language with memory management
    C/C++/Rust/Odin/Zig/FORTRAN/...

    Pros

  • Easy to learn
    (but with a high skill-ceiling)
  • Powerful
  • Huge user community
  • Easy to debug and prototype
  • Cons

  • Interpreters are slow
  • Dynamic typing can be dangerous
  • Optimization is less obvious
  • Environments can be a bit of a mess

Python programming primer

How Python works

  • Indentation (tab or number of spaces) decides to what "block" your code belongs
  • You can write code ”live” in the interpreter or write scripts, and then run the scripts trough the interpreter
  • Each row is executed in sequence (by being interpreted)
  • When memory can no longer be reached - python frees it (reference counting)
                    
                        def my_function(x):
                            x = x + 2
                            y = 5
                            return x
                    
                

Python programming primer

Dynamic typing

  • Every object has a type
  • Every object knows its type
  • Types are not declared (but can be since Python 3)
  • Names refer to an object
  • Names are not typed
                    
                        a = 5  # a is the name of an object of type integer
                        a = 5.0  # a is now the name of an object of type float
                    
                

Python programming primer

Try this

                    
                        a = 5
                        print(type(a))

                        a = 5.0
                        print(type(a))

                        a = "5"
                        print(type(a))

                        print(type(print))

                        def func(a):
                            return(a * 5)

                        print(type(func))
                    
                
                    
                        print(func(5))
                        print(func("Hello "))
                        print(func("Na") + " Batman!")
                    
                

Python programming primer

Basic data types

Integer, Float, String, Boolean, Tuple, List, Ditionary, Set

                    
                        my_dict = {"key": 1.0, "x": "this is a string"}
                        print(my_dict["key"])
                        my_dict["x"] = 2.0
                    
                
                    
                        my_list = [1, 2, "string"]
                        print(my_list[0])

                        my_list[0] = "10"
                    
                
                    
                        my_tuple = (2, 3, 4)
                        print(my_list[0])

                        my_tuple[0] = 10  # TypeError
                    
                

Python programming primer

Variable assignment

Multiple return values

                        
                            # Return type is tuple when more than one argument is returned
                            def func(a):
                                return a, a + 2

                            x = func(5)
                            print(type(x), x)
                        
                    

Multiple variable assignment

                        
                            x, y = func(5)
                            print(f"{x=} {y=}")
                        
                    

Python programming primer

Function arguments

Positional and keyword arguments

                        
                            # Keyword arguments have a default
                            def func(a, b=2):
                                return a + b

                            print(func(5), func(5, b=10))
                        
                    

Lets talk about immutability and pointers...

Advanced topic

Python programming primer

Lets talk about immutability and pointers...

Immutable data types: numbers, booleans, tuples, and strings

                    
                        a = "hello"
                        b = a  # pointer is copied
                        print(id(a))
                        print(id(b))  # Same memory!

                        b = "world"  # immutable -> new object is created
                        print(id(a))
                        print(id(b))  # Now different!
                    
                

methods return a new object - does not modify in-place!

                    
                        a = "hello"
                        a.upper()
                        print(a)

                        b = a.upper()
                        print(a, b)

                        print(a[2])  # can access
                        a[2] = "X"  # TypeError - but not modify
                    
                
Advanced topic

Python programming primer

Lets talk about immutability and pointers...

Mutable types (the rest) CAN modify their memory

                    
                        a = [3, 2, 1]
                        b = a  # pointer is copied
                        print(id(a), id(b))  # Same memory

                        b.sort()
                        print(a)
                    
                

All pointers to the memory can modify it

Advanced topic

Python programming primer

Lets talk about immutability and pointers...

Be careful about side-effects!

                    
                        def func(x=[]):
                            x.append("world!")
                            print(x)
                        
                        # what does this print?
                        a = ["hello "]
                        func(a)
                        func(a)

                        # what does this print?
                        func()
                        func()
                    
                
                    
                        # Same as above but more clear what is going on
                        DEFAULT_LIST = []

                        def func(x=DEFAULT_LIST):
                            x.append("world!")
                            print(x)
                    
                
Advanced topic

Python programming primer

Argument catching and expanding

                    
                        def func(*args, **kwargs):
                            print(args)
                            print(kwargs)
                        
                        a = (1, 2, 3)
                        func(*a)

                        b = {"hello": None}
                        func(**b)
                    
                

You should probably not do this!

But sometimes it is useful (e.g. decorators)

Advanced topic

Python programming primer

Decorators!

                    
                        def say_hello(func):
                            def hello_func(*args, **kwargs):
                                print("hello!")
                                return func(*args, **kwargs)

                            return hello_func

                        def add(a, b):
                            return a + b

                        hello_add = say_hello(add)

                        x = hello_add(1, 2)
                    
                
Advanced topic

Python programming primer

Decorators!

                    
                        def say_hello(func):
                            def hello_func(*args, **kwargs):
                                print("hello!")
                                return func(*args, **kwargs)

                            return hello_func

                        @say_hello
                        def add(a, b):
                            return a + b

                        x = add(1, 2)
                    
                

This is an advanced technique but useful for some structural patterns - do not over-use!

One thing I like to use it for is registering functions

Advanced topic

Python programming primer

Decorators!

                    
                        FUNCS = {}

                        def register(func):
                            # Side-effect! GASP!
                            FUNCS[func.__name__] = func
                            return func

                        @register
                        def sub(a, b):
                            return a - b

                        @register
                        def add(a, b):
                            return a + b

                        print(FUNCS)
                    
                

Python programming primer

Error handling

                    
                        def add(a, b):
                            if not isinstance(a, float):
                                raise TypeError("a is not a float!")
                            return a + b

                        x = add(1, 1)
                    
                

isinstance is very useful for doing type checks (returns true for all sub-classes too)

There is an extensive list of standard exceptions in the python docs

The python error tutorial is also a good read

But what if we want to handle the error?

Python programming primer

Error handling

                    
                        def add(a, b):
                            if not isinstance(a, float):
                                raise TypeError("a is not a float!")
                            return a + b
                        
                        try:
                            x = add(1, 1)
                        except TypeError:
                            x = None
                    
                

But what if we have more behaviour?

Python programming primer

Error handling

                    
                        def odd_add(a, b):
                            if not isinstance(a, float):
                                raise TypeError("a is not a float!")
                            if b % 2 == 0:
                                raise ValueError("b has to be odd!")
                            return a + b
                        
                        try:
                            x = odd_add(1.0, 2)
                        except TypeError:
                            x = None
                    
                

Python programming primer

Error handling

                    
                        def odd_add(a, b):
                            if not isinstance(a, float):
                                raise TypeError("a is not a float!")
                            if b % 2 == 0:
                                raise ValueError("b has to be odd!")
                            return a + b
                        
                        try:
                            x = odd_add(1.0, 2)
                        except TypeError:
                            x = None
                        except ValueError as e:
                            x = str(e)
                        else:
                            x += 1
                        finally:
                            print("this will always get run!")
                        print(x)
                    
                

Try to get every block of this to execute!

Python programming primer

Creating your own "types" - classes

                    
                        class Square:
                            def __init__(self, length):
                                self.length = length
                            
                            def area(self):
                                return self.length ** 2

                        s = Square(10)
                        print(s.area())
                    
                

You can define all behaviour of this class, e.g. see operators

Advanced topic

Python programming primer

Creating your own "types" - classes

                    
                        class Square:
                            ...
                            def __add__(self, value):
                                if isinstance(value, float):
                                    new_square = Square(self.length + value)
                                else:
                                    raise TypeError(f"Can only add floats, not {type(value)}")
                                return new_square

                        s = Square(10)
                        q = s + 10.5
                        print(f"{s.area()=} {q.area()=}")
                    
                

You should probably never overload operators - unless you have a very good reason

Classes are good for some things and bad for others - we will deep dive later

Advanced topic

Python programming primer

Data classes are nice for specifying containers!

                    
                        from dataclasses import dataclass
                        from typing import Tuple
                        import numpy.typing as npt
                        import numpy as np

                        @dataclass
                        class Meteors:
                            """Class for meteor data from an observer."""
                            observer_name: str
                            observer_geo: Tuple[float, float]
                            start_velocity: npt.NDArray[np.float64]
                            instrument: str = "radar"

                        m = Meteors("me", (1.0, 2.0), np.zeros((100, )))
                    
                
Advanced topic

Python programming primer

Also named tuples and enums!

collections in python

                    
                        from collections import namedtuple
                        from enum import Enum

                        Point = namedtuple('Point', ['x', 'y'])
                        p = Point(1.0, 2.0)  # Not mutable!
                        print(p)

                        class Color(Enum):
                            RED = 1
                            GREEN = 2
                            BLUE = 3

                        print(Color["BLUE"])  # Nice identifier declaration if needed!
                    
                

Python programming primer

Control flow

For loop

                    
                        for i in range(10):
                            print(f"Hello world number {i}")
                    
                

While loop

                    
                        a = 0
                        while a < 10:
                            print(f"Looping {a=}")
                            a += 1
                    
                

Python programming primer

Control flow

Most sequences can be iterated

                    
                        for c in "HELLO":
                            print(f"{c}={ord(c)}")
                    
                
                    
                        d = {"a": 3.14, "b": 42, "c": 1}

                        # Iterating a dict iterates the keys
                        for c in d:
                            print(c)

                        # dict.items return a iterable of tuples with both keys and values
                        for item in d.items():
                            print(f"{item[0]}={item[1]}")

                        # multiple variable assignment in loops too!
                        for key, value in d.items():
                            print(f"{key}={value}")
                    
                

Python programming primer

Iterator tricks!

                    
                        c = 0
                        for a, b in zip(range(10), range(20, 30)):
                            c += a * b

                        for ind, ch in enumerate(str(c)):
                            print(ind, ch)
                    
                

We can combine however we like

Python programming primer

Iterator tricks!

                    
                        c = 0
                        for ind, (a, b) in enumerate(zip(range(10), range(20, 30))):
                            c += a * b + ind
                    
                

Just don't overdo it!

Then we have generators

Advanced topic

Python programming primer

Iterator tricks!

                    
                        def my_iter(num):
                            c = num
                            for ind in range(num - 1, 0, -1):
                                c *= ind
                                yield c
                        
                        for x in my_iter(10):
                            print(x)

                        print(type(my_iter(100)))
                    
                
Advanced topic

Python programming primer

Iterator tricks!

                    
                        def my_iter(num):
                            c = num
                            for ind in range(num - 1, 0, -1):
                                print(f"now at {ind=}")
                                c *= ind
                                yield c
                        
                        g = my_iter(20)
                        x = next(g)
                        print(f"{x=}")
                        x = next(g)
                    
                
Advanced topic

Python programming primer

Iterator tricks!

                    
                        def my_iter(num):
                            c = num
                            for ind in range(num - 1, 0, -1):
                                print(f"now at {ind=}")
                                c *= ind
                                yield c
                        
                        g = my_iter(2)
                        x = next(g)
                        print(f"{x=}")
                        x = next(g) # StopIteration!
                    
                

Python programming primer

Control flow

If statement

                        
                            import numpy as np

                            x = np.random.randn(1)

                            if x < -1 or x > 1:
                                print("Shocked!")
                            elif x < -2 and x > 2:
                                print("WHAT?")
                            else:
                                print("Completely normal phenomenon")
                        
                    
Advanced topic

Python programming primer

List and dict comprehension!

                    
                        squares = [x ** 2 for x in range(10)]
                        print(squares)


                        some_strs = [f"{x}" for x in squares if x > 10]
                        print(some_strs)
                    
                
                    
                        dicts_also = {c: ord(c) for c in "abcd"}
                        print(dicts_also)
                    
                

Python programming primer

Math in C

numpy arrays are our bread and butter!

                    
                        import numpy as np

                        a = np.random.randn(2, 5)  # 2 rows, 5 columns
                        print(type(a))

                        # Array slicing
                        print(a[:, 3])  # print column 4

                        b = a[0, :] * 2  # Multiply a row and assign result
                        print(f"{a[0, :]=} {b=}")
                    
                

Python programming primer

Math in C

numpy matrix operations

                    
                        import numpy as np

                        a = np.array([1, 1, 1], dtype=np.float64)
                        M = np.eye(3)
                        M[1, 1] = 2

                        print(f"{M=}")

                        x = M @ a  # Matrix multiplication!
                        print(f"{x=}")

                        # Broadcasting! ( or maybe brotcasting ;] )
                        M = M[:, :] + x[:, None]
                        print(f"{M=}")
                    
                

Python programming primer

Ok bear with me here...

matmul_python.py
                    
                        import numpy as np
                        import time

                        np.random.seed(12345)
                        x = np.random.randn(3, 10_000).T.tolist()
                        M = np.eye(3).T.tolist()

                        repeats = 1_000

                        t0 = time.time()

                        # do work a lot!
                        for ri in range(repeats):
                            out = []  # calculate the matrix multiplication and sum
                            for ind in range(10_000):
                                x1, x2, x3 = x[ind]
                                y = [
                                    x1 * M[0][dim] + x2 * M[1][dim] + x3 * M[2][dim]
                                    for dim in range(3)
                                ]
                                out.append(sum(y))

                        dt = time.time() - t0

                        print(f"Execution time [native] {dt} seconds")
                    
                

Python programming primer

I am speed!

matmul_numpy.py
                    
                        import numpy as np
                        import time

                        np.random.seed(12345)
                        x = np.random.randn(3, 10_000)
                        M = np.eye(3)

                        repeats = 1_000

                        t0 = time.time()

                        # do work a lot!
                        for ri in range(repeats):
                            out = np.sum(M @ x, axis=0)

                        dt = time.time() - t0

                        print(f"Execution time [numpy ] {dt} seconds")
                    
                

~100 times faster!

Python programming primer

Packages and the import statement

                    
                        import blargh
                    
                

How does python find "blargh"?

                    
                        import sys
                        for path in sys.path:
                            print(repr(path))
                    
                

For more info see sys path init

pip installs into "site_packages"

Python programming primer

Packages and the import statement

So that's modules - what is a "package"?

  • project
    • script.py
    • my_package
      • __init__.py

__init__.py is executed upon import my_package

We can of course nest this!

Advanced topic

Python programming primer

Packages and the import statement

  • project
    • script.py
    • my_package
      • __init__.py
      • foo
        • __init__.py
                    
                        # my_package/__init__.py
                        from . import foo
                    
                
                    
                        # script.py
                        import my_package

                        my_package.foo.bar()
                    
                
Advanced topic

Python programming primer

Packages and the import statement

pyproject.toml
                    
                        [project]
                        name = "spam-eggs"
                        version = "1.0.0"
                        dependencies = [
                          "httpx",
                          "gidgethub[httpx]>4.0.0",
                        ]
                        requires-python = ">=3.8"
                        authors = [
                          {name = "Jane Doe", email = "obviously_not_my@email.com"},
                        ]
                        maintainers = [
                          {name = "Someone else", email = "bob@example.com"}
                        ]
                        description = "Lovely Spam! Wonderful Spam!"
                        readme = "README.rst"
                        license = "MIT"


                        [project.urls]
                        Homepage = "https://example.com"
                        Repository = "https://github.com/me/spam.git"

                        [project.scripts]
                        spam-cli = "spam:main_cli"
                    
                
Advanced topic

Python programming primer

Packages and the import statement

pyproject.toml
                    
                        [project]
                        name = "spam-eggs"
                        version = "1.0.0"
                        dependencies = [
                          "httpx",
                          "gidgethub[httpx]>4.0.0",
                        ]
                        requires-python = ">=3.8"
                        authors = [
                          {name = "Jane Doe", email = "obviously_not_my@email.com"},
                        ]
                        maintainers = [
                          {name = "Someone else", email = "bob@example.com"}
                        ]
                        description = "Lovely Spam! Wonderful Spam!"
                        readme = "README.rst"
                        license = "MIT"


                        [project.urls]
                        Homepage = "https://example.com"
                        Repository = "https://github.com/me/spam.git"

                        [project.scripts]
                        spam-cli = "spam:main_cli"
                    
                

pip install .

Read more in the handouts

Python programming primer

there are excellent tutorials online

Lets check out the handout above!