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:
- there are no curly braces, Python uses a combination of identation and colons (:) to inidicate blocks of code
- variables DO NOT have associated types (although in newer version there are ways to annotate typing information that can be used by static type checkers, more on that later), they are generic containers for values of any type
- Python works like a scripting language, code is interpreted in textual order; to follow best practices encapsulate your code in functions and classes
- comments are made using the # character
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 have fixed-sized arrays, but it offers 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 hashable values. Every key is associated to a value. Some examples of dictionary usage:
# create an empty dictionary d = {} # create a dictionary with 2 entries (key-value pairs) d1 = { "key1": 10, "key2": [] } # 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, but they can not be indexed. 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) # checking whether an element is in a set: 3 in s # this operation has O(1) amortized cost
Tuples
Tuples are similar to lists, but they are immutable (can not be changed)
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 # the tuple constructor is actually the comma; the brackets are not neccessary in many contexts fst, snd = p p = 1, 2 # p is the pair (1, 2) x, y = y, x # interchange x and y
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 an index with the element for idx, e in enumerate(l): ... # the above will actually iterate only once through the list, so it's efficient x = [1, 2, 3, 4] y = [5, 6, 7] for ex, ey in zip(x, y): ... iterates through [(1, 5), (2, 6), (3, 7)] # same as above, this only iterates through the two lists once
Functions
Functions are defined similar to other languages, but unlike in C, they are values like any other value.
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 g(x, y=0, name="no name provided"): ... # 0114est' will catch all other arguments passed into a tuple def h(x, y, *rest): return rest h(1, 2, 3, 4, 5) # (3, 4, 5) # you can also spread a list into arguments h(*[1, 2, 3, 4, 5]) # the same as the call above
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
(though there are ways to enforce it at runtime. See here).
The class constructor is the __init__(self):
method. Also note the mandatory presence of self
, which is the Python equivalent of this
. Each method will receive the object it is called on as the first argument.
The method def __str__(self):
is the Python equivalent for toString()
. An object can be displayed using the function str
.
The functional style
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]
Similarly, there also exist dictionary comprehensions, set comprehensions and a related concept, generator expressions (see here and here) (there are no tuple comrehensions)
Python typing annotations
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 d: dict # dictionary of unknown types for keys and values d1: dict[str, int] # dictionary with string keys and integer values
- for functions/methods:
def <func_name>([<param>: <type>, …]) → <return_type>
- you can also type a variable with
Callable[[<param_type1>, ...], <return_type>]
to hold functions or lambdas
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]): # you can also write 'class DFA[StateType]:' instead 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 (characters 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
- *
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) typesNone
: 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- The official typing documentation here
Practice
The task of this lab will be to implement the accepting procedure of a DFA in Python, in order to get familiar with Python's language constructs and get a start into working with DFAs in code.
Reading a textual representation of a DFA and a word, output whether or not the DFA accepts the word. The DFA will be represented as:
#states
the labels of the states, each on a separate line
#initial
the label of the initial state
#accepting
the labels of the final states, each on a separate line
#alphabet
the symbols of the alphabet of the DFA, each on a separate line
#transitions
source state : read character > destination state (each transition on a separate line)
Example:
#states s0 s1 s2 #initial s0 #accepting s1 #alphabet a b c #transitions s0:b>s2 s0:c>s0 s1:a>s2 s1:c>s1 s2:a>s1 s2:b>s1 s2:c>s2
You should write a DFA class to keep the internal state and input left unconsumed and implement an accept
method.
You may choose how to take input: you can read the DFA from a file, you can receive the path to the file as an argument to the Python program, you can take the word as another argument or in the same file as the DFA.
Example of reading command line arguments and working with files:
import sys def read_lines(file_path: str) -> list[str]: try: # 'with' creates a context manager which handles file closing with open(file_path, 'r') as file: return file.readlines() except FileNotFoundError: print(f"Error: File '{file_path}' not found.") def main(): if len(sys.argv) != 2: print(f"Usage: python3 <file_path>") sys.exit(1) file_path = sys.argv[1] lines = read_file(file_path) print(lines) if __name__ == "__main__": main()