Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision | |||
lfa:2023:lab01 [2024/10/05 20:30] stefan.sterea |
lfa:2023:lab01 [2024/10/06 21:29] (current) stefan.sterea old revision restored (2023/10/08 15:08) |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== 1. Deterministic Finite Automata ====== | + | ====== Programming introduction - Python ====== |
- | ===== 2.1. DFA practice ===== | + | **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 | ||
<note> | <note> | ||
- | * Double-circle = final state | + | 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. |
- | * Hanging arrow entering state = initial state | + | |
- | * Sink state = a state that is not final; once reached, there is no transition that leaves it, thus the DFA will reject; there is a transition that loops in this state for each possible 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: | ||
+ | |||
+ | <code python> | ||
+ | def main(): | ||
+ | ... # your code here | ||
+ | |||
+ | if __name__ == "__main__": | ||
+ | main() | ||
+ | </code> | ||
</note> | </note> | ||
+ | ==== Lists in python ==== | ||
+ | |||
+ | Python does **NOT** offer fixed-sized arrays, but they offer 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 **dispersed** using a hash-function into buckets. Each bucket will hold its respective values. Some examples of dictionary usage: | ||
+ | |||
+ | <code python> | ||
+ | # 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) | ||
+ | |||
+ | </code> | ||
+ | |||
+ | ==== 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'': | ||
+ | <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) | ||
+ | </code> | ||
+ | |||
+ | ==== Tuples === | ||
+ | = | ||
+ | |||
+ | Tuples are very similar to lists and can be substituted by the latter in many cases: | ||
+ | |||
+ | <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 | ||
+ | |||
+ | </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 a index with the element | ||
+ | for idx, e in enumerate(l): | ||
+ | ... | ||
+ | |||
+ | x = [1, 2, 3, 4] | ||
+ | y = [5, 6, 7] | ||
- | === Write DFAs for the following languages: === | + | for ex, ey in zip(x, y): |
+ | ... iterates through [(1, 5), (2, 6), (3, 7)] | ||
- | **2.1.1.** $ L=\{w \in \{0,1\}^* \text{ | w contains an odd number of ones} \} $ | + | </code> |
+ | ==== Functions ==== | ||
- | /*<hidden> | + | Functions are very similar with *C* functions, but you don't need to add static typing to arguments or return values. |
- | {{:lfa:2022:lfa2022_lab2_ex1.png?400|}} | + | |
- | * State 0 = currently counted an even number of 1s [initial state] | + | |
- | * State 1 = currently counted an odd number of 1s [final state] | + | |
- | * When we read a 0, we stay on the same state, as we are only counting ones. | + | |
- | * When we find a one: | + | |
- | * If we were on state 0, it means we counted until the previous step an even number of ones. Now we have an odd number of ones, so we need to move to state 1. | + | |
- | * If we were on state 1, it means we counted until the previous step an odd number of ones. Now we have an even number of ones, so we need to move to state 0. | + | |
- | </hidden>*/ | + | |
+ | <code python> | ||
+ | def f(x, y): | ||
+ | a = 2 * x + y | ||
+ | b = 3 * a + 2 * x * y | ||
+ | return b * b | ||
+ | | ||
+ | result = f(5, 8) | ||
+ | </code> | ||
- | **2.1.2.** The language of binary words which contain **exactly** two ones | + | You can also provide default values for parameters: |
+ | <code python> | ||
+ | def f(x, y=0, name="no name provided"): | ||
+ | ... | ||
+ | </code> | ||
+ | ==== Classes and inheritance ==== | ||
- | /*<hidden> | + | We discuss a few basics on classes and inheritance starting from the following example: |
- | {{:lfa:2022:lfa2022_lab2_ex2.png?550|}} | + | |
- | * State 0: no ones have been found yet [initial state] | + | |
- | * State 1: 1 one have been found so far | + | |
- | * State 2: 2 ones have been found so far [final state] | + | |
- | * State 3: more than 2 ones have been found so far [sink state] | + | |
- | </hidden>*/ | + | |
+ | <code python> | ||
+ | class Tree: | ||
+ | def size(self): | ||
+ | pass | ||
+ | | ||
+ | def contains(self, key): | ||
+ | pass | ||
- | **2.1.3.** The language of binary words which encode odd numbers (the last digit is least significative) | + | 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 | ||
- | /*<hidden> | + | </code> |
- | {{:lfa:2022:lfa2022_lab2_ex3.png?300|}} | + | |
- | * We pass through the word, marking the parity of the last character that we have read. | + | |
- | </hidden>*/ | + | |
+ | 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. | ||
- | **2.1.4.** (hard) The language of words which encode numbers divisible by 3 | + | 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. | ||
- | <hidden> | + | The method ''def __str__(self):'' is the Python equivalent for ''toString()''. An object can be displayed using the function ''str''. |
- | {{:lfa:2022:lfa2022_lab2_ex4.png?400|}} | + | |
- | * State 0: 3k [initial state and final state] | + | |
- | * State 1: 3k + 1 | + | |
- | * State 2: 3k + 2 | + | |
- | * When we read a 0, the number we had so far is multiplied by 2: | + | |
- | * 3k --(*2)--> 3k | + | |
- | * 3k + 1 --(*2)--> 3k + 2 | + | |
- | * 3k + 2 --(*2)--> 3k + 1 | + | |
- | * When we read a 1, the number we had so far is multiplied by 2 and we also add 1: | + | |
- | * 3k --(*2)--> 3k --(+1)--> 3k + 1 | + | |
- | * 3k + 1 --(*2)--> 3k + 2 --(+1)--> 3k | + | |
- | * 3k + 2 --(*2)--> 3k + 1 --(+1)--> 3k + 2 | + | |
- | </hidden> | + | |
+ | ===== The functional style ===== | ||
- | **2.1.5.** The language of words that encode a calendar date (DD/MM/YYYY) | + | ==== Lambda functions ==== |
- | (do not overthink about the actual number of days in a month) | + | |
+ | You can declare lambda functions with the keyword **lambda**: | ||
+ | <code python> | ||
- | /*<hidden> | + | lambda x: x + 1 |
- | {{:lfa:2022:lfa2022_lab2_ex5.png?600|}} | + | |
- | * Note: Checking for months with 30 or 31 days can be done with a slightly bigger DFA. | + | |
- | * Note: Even checking for bisect years (29 Feb) can still be done with a DFA, but it's too big to do as a seminar example. | + | |
- | </hidden>*/ | + | |
+ | lambda x,y: x + y | ||
- | **2.1.6.** (HARD) $ L=\{w \in \{0,1,2,3\}^* \text{ | w follows the rule that every zero is immediately followed by a sequence of at least 2 consecutive threes and every one is immediately followed by a sequence of at most 2 consecutive twos} \} $ | + | lambda x,y: x if x < y else y |
+ | </code> | ||
- | <hidden> | + | ==== Higher-order functions ==== |
- | {{:lfa:2022:lfa2022_lab2_ex6_v2.png?400|}} | + | The most used higher-order functions in Python are **map** and **reduce** (**reduce** is part of **functools** module): |
- | * injuraturile redirectati-le catre AU :)) (si MP) | + | <code python> |
- | * All missing transitions lead to a sink state (where there is a transition that loops in that state for any character) | + | |
- | </hidden> | + | |
+ | from functools import reduce | ||
- | **2.1.7.** The set of all binary strings having the substring 00101 | + | # 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> | ||
- | /*<hidden> | + | ==== List comprehensions ==== |
- | {{:lfa:2022:ex_8_dfa.png?400|}} | + | |
- | * Every state corresponds to a certain current configuration (eg. state **B** translates to a sequence of **0**; **C** to a seq of **00**; **D** -> **001** and so on) | + | |
- | * Analyze how each incoming character changes the current available sequence. For example, if we are in state **E** and we read character **1** we reach a final state, but if we read **0** we go back to state **C** since the available seq will be **00** | + | |
- | </hidden>*/ | + | |
+ | List comprehensions are widely used programming tools in Python. | ||
- | === Describe the language of the following DFA and simulate them on the given inputs: === | + | Usage examples: |
- | All states are final | + | <code python> |
+ | # adding 1 to each element of a list | ||
+ | l1 = [x + 1 for x in [1, 2, 3]] | ||
+ | # [2, 3, 4] | ||
- | **2.1.8.** w<sub>1</sub> = 10011110, w<sub>2</sub> = 00110010 | + | # packing elements into pairs |
+ | l2 = [(x, x + 1) for x in [1, 2, 3]] | ||
+ | # [(1, 2), (2, 3), (3, 4)] | ||
- | {{:lfa:2022:lfa2022_lab2_ex8.png?400|}} | + | # unpacking pairs in the for notation |
+ | l3 = [x + y for (x, y) in [(1, 2), (2, 3), (3, 4)]] | ||
+ | # [3, 5, 7] | ||
- | /*<hidden> | + | # combined list comprehensions |
- | * The language of binary words where there are no sequences in which the same character repeats 3 or more times. | + | l4 = [(x, y) for x in [1, 2, 3] for y in [4, 5, 6]] |
- | * regex: $math[(11 \cup 1 \cup \epsilon) ( (00 \cup 0) 1 (1 \cup \epsilon) )^* (00 \cup 0 \cup \epsilon)] | + | # [(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)] |
- | * (0, **10011110**) |--<sub>A</sub> (11, 0011110) |--<sub>A</sub> (01, 011110) |--<sub>A</sub> (02, 11110) |--<sub>A</sub> (11, 1110) |--<sub>A</sub> (12, 110) [There is no transition, so we go to a sink state which is not figured on the drawing.] |--<sub>A</sub> (sink, 10) |--<sub>A</sub> (sink, 0) |--<sub>A</sub> (sink, $math[\epsilon]) The sink state is not a final state, so this word is **rejected**. | + | |
- | * (0, **00110010**) |--<sub>A</sub> (01, 0110010) |--<sub>A</sub> (02, 110010) |--<sub>A</sub> (11, 10010) |--<sub>A</sub> (12, 0010) |--<sub>A</sub> (01, 010) |--<sub>A</sub> (02, 10) |--<sub>A</sub> (11, 0) |--<sub>A</sub> (01, $math[\epsilon]) The word is **accepted** by the DFA because we reached a final state (blue). | + | |
- | </hidden>*/ | + | |
- | ===== 2.2. Describing the language of DFAs ===== | + | # filters |
+ | l5 = [x for x in [1, 2, 3, 4] if x > 2] | ||
+ | # [3, 4] | ||
+ | </code> | ||
- | Describe in natural speak the language accepted by these DFAs. Make sure your description is fit to the DFA and not too broad (covers all words accepted and no other word). | + | ===== Python typing annotations ===== |
- | **2.2.1** | + | 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. | ||
- | {{:lfa:2024:lab1-2_2_1.png?500|}} | + | To create such hints the syntax is as follows: |
- | /* Cuvinte nevide în care după un grup de 0-uri consecutive urmează fie nimic, fie un număr impar de 1-uri consecutive, | + | |
- | și după un grup consecutiv de 1-uri urmează fie nimic, fie un număr par de 0-uri consecutive */ | + | |
- | **2.2.2** | + | * for variables: ''<var_name>: <type> [= expr]'': |
+ | <code python> | ||
+ | x: int | ||
+ | y: int = 5 | ||
+ | z: str = 'hello world' | ||
+ | t: MyCustomType | ||
+ | </code> | ||
+ | * for functions/methods: ''def <func_name>([<param>: <type>, ...]) -> <return_type>'': | ||
+ | <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 | ||
- | {{:lfa:2024:lab1-2_2_2.png?300|}} | + | StateType = TypeVar("StateType") # !!! important: type variables must be declared and initialised before they are used |
- | /* Cel puțin un grup de: 0 urmat de un număr impar de 1-uri urmat de un număr par de 0-uri */ | + | 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]): | ||
+ | ... | ||
+ | </code> | ||
- | **2.2.3** | + | 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[ [<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]] | ||
- | {{:lfa:2024:lab1-2_2_3.png?300|}} | + | ===== Exercises ===== |
- | /* Oricâte grupuri de a sau ab sau abc */ | + | |
- | **2.2.4** | + | **1.1.** Write a function ''max_elem(l, start, stop)'' that determines the maximum element between the start and stop positions. |
- | {{:lfa:2024:lab1-2_2_4.png?300|}} | + | **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]''. |
- | /* Cuvinte nevide peste {0,1}* care nu încep și se termină cu aceași cifră */ | + | |
- | **2.2.5** | + | **1.3.** Write a function ''is_palindrome(l)'' that checks if a list is a palindrome (it returns True/False). |
- | {{:lfa:2024:lab1-2_2_5.png?400|}} | + | **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"''. |
- | /* Cuvinte de lungime pară care conțin cel puțin un b */ | + | |
- | **2.2.6** | + | **1.5.** Write a function ''get_frequency(s)'' that returns a dictionary with the frequency of every character that appears in the string. |
- | {{:lfa:2024:lab1-2_2_6.png?400|}} | + | **1.6.** Starting from the example in the **Classes and inheritance** section, implemented the ''Node'' class (that inherits from ''Tree'') and it's methods. |
- | /* Cuvinte care încep cu a sau b și sunt formate din secvențe alternative (posibil vide) separate prin c în care | + | |
- | după orice a urmează un b sau după orice b urmează un a; dacă cuvântul începe cu a, prima secvență este de primul tip, | + | |
- | iar dacă începe cu b. prima secvență este de al doilea tip */ | + |