====== 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, they are labels that point to objects (similar with pointers in **C**) - 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 - 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''. ===== 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] ===== 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: '': [= expr]'': x: int y: int = 5 z: str = 'hello world' t: MyCustomType * for functions/methods: ''def ([: , ...]) -> '': 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 ''set''s and ''frozenset''s and the keys of dictionaries need to be hashable: see the [[lfa:dictionaries|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[ [...], ]'': this defines a function with a given signature * '' | '': 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]] ===== Exercises ===== **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.