Edit this page Backlinks This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong. ====== 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 <note> 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: <code python> def main(): ... # your code here if __name__ == "__main__": main() </code> </note> ==== Lists in python ==== Python does not have fixed-sized arrays, but it offers lists out of the box. <code python> 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 </code> ==== Slicing lists ==== Python supports various list slicing operations: <code python> # 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] </code> ==== Strings ==== Strings can be treated like lists, you can access elements and use slicing, but there are also a few specific string functions. <code python> 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" </code> 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: <code python> 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. </code> ==== 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: <code python> # 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) </code> ==== 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'': <code python> # 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 </code> ==== Tuples ==== Tuples are similar to lists, but they are immutable (can not be changed) <code python> 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 </code> ==== For loops ==== In Python for loops are different than the traditional **C** for loops. They simply iterate over sequences (like lists, strings, tuples). <code python> 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 </code> ==== Functions ==== Functions are defined similar to other languages, but unlike in C, they are values like any other value. <code python> 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"): ... # `rest' 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 </code> ==== Classes and inheritance ==== We discuss a few basics on classes and inheritance starting from the following example: <code python> 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 </code> 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 [[https://blog.teclado.com/python-abc-abstract-base-classes/|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**: <code python> lambda x: x + 1 lambda x,y: x + y lambda x,y: x if x < y else y </code> ==== Higher-order functions ==== The most used higher-order functions in Python are **map** and **reduce** (**reduce** is part of **functools** module): <code python> 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) </code> ==== List comprehensions ==== List comprehensions are widely used programming tools in Python. Usage examples: <code python> # 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] </code> Similarly, there also exist dictionary comprehensions, set comprehensions and a related concept, generator expressions (see [[https://www.geeksforgeeks.org/generator-expressions/|here]] and [[https://peps.python.org/pep-0289/|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]'': <code python> 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 </code> * 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 <code python> def hello() -> str: ... def get_item_at(index: int) -> Any: ... class MyCustomType(): def method(self, param1: Any, param2: int) -> str ... </code> * for generic classes: extend either a generic class (such as list[_], set[_] or tuple[_]) or extend the Generic class (introduced by the typing module): <code python> 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]): ... </code> 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) 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 * The official typing documentation [[https://docs.python.org/3/library/typing.html|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: <code> #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 </code> represents the following DFA: {{:lfa:2024:lab2-dfa-example.png?200|}} 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: <code python> 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() </code>