2. Programming introduction - Python

Python3 is a multi-paradigm language, that combines the imperative style with the functional style. It also supports Object-Oriented programming, albeit in a limited way.

Python3 offers a few programming constructs that:

  • make life real easy for the programmer,
  • yield efficient programs,
  • sum-up to a programming style called pythonic (a blend of imperative and functional features).

This lab will introduce some of these constructs.

For Python beginners

If you are completely unfamiliar with Python, some of the biggest differences to a language like C are:

  1. there are no curly braces, Python uses a combination of identation and colons (:) to inidicate blocks of code
  2. variables DO NOT have associated types, they are labels that point to objects (similar with pointers in C)
  3. Python works like a scripting language, every line of code is interpreted in order, to follow best practices encapsulate your code in functions and classes
  4. comments are made using the # character
Any code that is outside of a function will get interpreted every time that file gets run, but also when it's imported in another project.

To avoid this, encapsulate your code using functions.

To run the code ONLY when you want to run it directly, not when it gets imported in other files, you can use:

def main():
    ...  # your code here
 
if __name__ == "__main__":
    main()

Lists in python

Python does NOT offer fixed-sized arrays, but they offer lists out of the box.

l = [1, 2, 3, 4]  # defining a list
 
l.append(5)
# l is now [1, 2, 3, 4, 5]
 
l.extend([6, 7, 8])
# l is now [1, 2, 3, 4, 5, 6, 7, 8]
 
# delete the second element
del l[2]
# l is now [1, 2, 4, 5, 6, 7, 8]
 
# accesing lists
l[0] # 1
l[1] # 2
l[-1] # 8 (you can also acces them from the back)
 
lr = l.reverse()  # [8, 7, 6, 5, 4, 2, 1]
 
# checking if a element is in a list
2 in [1, 2, 4]  # true
3 in [1, 2, 4]  # false

Slicing lists

Python supports various list slicing operations:

# the sublist from positions 0 to x of l:
l[0:x]
# the very same list:
l[:x]
# the last three elements of a list:
l[-3:]
# the slice from n-3 to n-1, where n is the number of elements from the list
l[-3:-1]
# every second element from the second to the seventh
l[2:7:2]

Strings

Strings can be treated like lists, you can access elements and use slicing, but there are also a few specific string functions.

s = "Limbaje formale si automate"
 
s[2] # "m"
 
s[:7] # "Limbaje"
 
len(s)  # 27
 
s.split() ["Limbaje", "formale", "si", "automate"]
 
s.starswith("Limbaje")  # True
 
s.endswith("automate")  # True
 
s.replace("formale", "neformale")  # "Limbaje neformale si automate"

The most widely used datatypes in Python are lists and dictionaries. Additionally, at LFA, we will also use: sets, stacks.

Stacks

In Python, lists are also used as stacks:

l = []
 
l.append(x)   # add x at the TOP of the stack (this will be the last element of the list)
 
l[-1]         # this is the top of the stack
 
x = l.pop()   # removes and returns an element from the top of the stack.

Dictionaries

Python dictionaries are actually hash maps (or hash tables). The keys k are dispersed using a hash-function into buckets. Each bucket will hold its respective values. Some examples of dictionary usage:

# create an empty dictionary
d = {}
 
# add or modify a key-value
d[k] = v
 
# search if a key k is defined in a dictionary d
if k in d:
  # do something
 
# get a key or a default value if key is not in dictionary
d.get(k, 0)

Sets

Sets are collections where each element is unique. Sets can be traversed and indexed exactly as lists. They are initialised using the constructor set:

# the empty set:
s = set()
 
# set constructed from a list:
s = set([1,2,3])
 
# normal set construction (you cannot use {} to create an empty set as that would create an empty dict)
s = {1,2,3}
 
# adding a new element to the set:
s.add(4)

Tuples

=

Tuples are very similar to lists and can be substituted by the latter in many cases:

p = (x, y)     # a pair where the first element is x and the second is y
t = (x, y, z)  # a tuple
s = (x,)       # a singleton (you have to add the comma, if not python will interpret it as a variable)
 
fst = p[0]
snd = p[1]
 
# tuple unpacking
(fst, snd) = p

For loops

In Python for loops are different than the traditional C for loops. They simply iterate over sequences (like lists, strings, tuples).

l = [1, 2, 3, 4]
for e in l:
    ...
 
# some useful constructs to use with for loops in python
 
# range will construct a list of integers to iterate over
for i in range(10):
    ...  # iterates through [0, 1, 2, 3, ..., 9]
 
for i in range(5, 10):
    ...  # iterates through [5, 6, 7, 8, 9] 
 
for i in range(5, 10, 2):
    ...  # iterates through [5, 7, 9] 
 
# enumerate will keep a index with the element
for idx, e in enumerate(l):
    ...
 
x = [1, 2, 3, 4]
y = [5, 6, 7]
 
for ex, ey in zip(x, y):
   ... iterates through [(1, 5), (2, 6), (3, 7)]

Functions

Functions are very similar with *C* functions, but you don't need to add static typing to arguments or return values.

def f(x, y):
    a = 2 * x + y
    b = 3 * a + 2 * x * y
    return b * b
 
result = f(5, 8)

You can also provide default values for parameters:

def f(x, y=0, name="no name provided"):
   ...

Classes and inheritance

We discuss a few basics on classes and inheritance starting from the following example:

class Tree:
    def size(self):
        pass
 
    def contains(self, key):
        pass
 
class Void(Tree):
    def __init__(self, name):
        self.name = name
 
    def __str__(self):
        return self.name
 
    def size(self):
        return 0
 
    def contains(self, key):
        return False

In the previous example, the class Tree acts as an interface. Python does not natively support interfaces, but class inheritance is supported. The instruction pass does nothing, and it helps us defer the method implementation.

The definition class Void(Tree) tells us that class Void inherits Tree. Note that this contract is not binding in Python. The program will be interpreted even if Void does not correctly implement the methods in Tree.

The class construct is the init(self): method. Also note the mandatory presence of self, which is the Python equivalent of this. Each member class must mark as first argument self, otherwise it is not a proper member of the class, and just a nested function.

The method def str(self): is the Python equivalent for toString(). An object can be displayed using the function str.

Lambda functions

You can declare lambda functions with the keyword lambda:

lambda x: x + 1
 
lambda x,y: x + y
 
lambda x,y: x if x < y else y

Higher-order functions

The most used higher-order functions in Python are map and reduce (reduce is part of functools module):

from functools import reduce
 
# adds 1 to each element of a list
def allplus1(l):
   return map(lambda x: x + 1, l)
 
# computes the sum of elements of a list
def sum_list(l):
   # inner functions are very useful for defining local, reusable functionality
   def plus(x, y):
      return x + y
   return reduce(plus, l)  # reduce is a little different from Haskell folds: it does not use an initial value
 
def short_sum(l):
   return reduce(lambda x,y: x + y, l)
 

List comprehensions

List comprehensions are widely used programming tools in Python.

Usage examples:

# adding 1 to each element of a list
l1 = [x + 1 for x in [1, 2, 3]]
# [2, 3, 4]
 
# packing elements into pairs 
l2 = [(x, x + 1) for x in [1, 2, 3]]
# [(1, 2), (2, 3), (3, 4)]
 
# unpacking pairs in the for notation
l3 = [x + y for (x, y) in [(1, 2), (2, 3), (3, 4)]]
# [3, 5, 7]
 
# combined list comprehensions
l4 = [(x, y) for x in [1, 2, 3] for y in [4, 5, 6]]
# [(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)]
 
# filters
l5 = [x for x in [1, 2, 3, 4] if x > 2]
# [3, 4]

Newer versions of python support type annotations and pre-runtime type checking (supported by some code editors). The typing module allows programmers to provide hints regarding the types of different objects, the signature (parameters and return value types) of functions and methods, and create generic classes.

To create such hints the syntax is as follows:

  • for variables: <var_name>: <type> [= expr]:
x: int
y: int = 5
z: str = 'hello world'
t: MyCustomType
  • for functions/methods: def <func_name>([<param>: <type>, …]) → <return_type>:
def hello() -> str:
    ...
def get_item_at(index: int) -> Any:
    ...
class MyCustomType():
    def method(self, param1: Any, param2: int) -> str
        ...
  • for generic classes: extend either a generic class (such as list[_], set[_] or tuple[_]) or extend the Generic class (introduced by the typing module):
from typing import TypeVar, Generic
 
StateType = TypeVar("StateType") # !!! important: type variables must be declared and initialised before they are used
class DFA(Generic[StateType]):
    def next_state(self, from_state: StateType, token: str) -> StateType:
        ...
    ...
 
dfa: DFA[int] = DFA[int](...)
 
A = TypeVar("A")
B = TypeVar("B")
 
class TupleLinkedList(Generic[A,B]):
    value: tuple[A,B]
    next: 'TupleLinkedList[A,B]' | None # !!! important: in most python versions, if you need to use the
                                        #                type of the class within its own definition, you
                                        #                need to quote its name (this delays the evaluation
                                        #                of the hint until after the class is fully defined)
    ...
 
class IntList(list[int]):
    ...

Useful type hints:

  • int, str, bool, float etc. : the basic datatypes (charaters are hinted as strings, as python does not make the distinction)
  • list[type], set[type], frozenset[type], dict[key_type, val_type]: basic data collections (items introduced in sets and frozensets and the keys of dictionaries need to be hashable: see the appendix for python hashing)
  • Any: a type is compatible with any other type (if no type hint is specified, this is what type checkers usually default to)
  • Callable[ [<param_types>…], <return_type>]: this defines a function with a given signature
  • <type1> | <type2>: the | operator allows us to indicate that a given object can be one of two(or more) types
  • None: represents the lack of a value (or an explicit None value); useful to mark functions which have no return value, or that a value may be purposefully missing (in combination with the | operator), and a None check may be necessary

full documentation of the python typing module: https://docs.python.org/3/library/typing.html

1.1. Write a function max_elem(l, start, stop) that determines the maximum element between the start and stop positions.

1.2. Write a function longest_positive_sequence(l) that determines the longest sequence of positive integers from a list. For example for the list [1,3,-1,2,0,1,5,4,-2,4,5,-3,0,1,2] it returns [2,0,1,5,4].

1.3. Write a function is_palindrome(l) that checks if a list is a palindrome (it returns True/False).

1.4. Write a function chr_list(s) that determines the list of characters which occur in a string. For example for the string “limbaje formale” it returns “limbaje for”.

1.5. Write a function get_frequency(s) that returns a dictionary with the frequency of every character that appears in the string.

1.6. Starting from the example in the Classes and inheritance section, implemented the Node class (that inherits from Tree) and it's methods.