├── .gitignore ├── README.md └── docs └── multiprocessing ├── README.md ├── chef.png ├── example ├── example01.py ├── example02.py ├── example03.py ├── example04.py ├── pipe_example.py ├── queue_example.py └── queue_example_lock.py ├── multi-chef.png ├── multiprocess-queue-diagram.png ├── multiprocessing-pipe-diagram.png ├── multiprocessing-queue.png └── multiprocessing.png /.gitignore: -------------------------------------------------------------------------------- 1 | mypy_cache/ 2 | venv/ 3 | .venv/ 4 | __pycache__/ 5 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn Python 2 | 3 | I use this repo to track all the lessons I learned about python 4 | 5 | ## Reference documents 6 | 7 | * https://www.geeksforgeeks.org/python-programming-language-tutorial 8 | * https://www.w3schools.com/python/default.asp 9 | * https://learnxinyminutes.com/docs/python/ 10 | * https://docs.python.org/3/tutorial/index.html 11 | * https://www.youtube.com/watch?v=eWRfhZUzrAc&t=386s 12 | 13 | ## Online playground tools 14 | 15 | * https://programiz.pro/ide/python 16 | * https://replit.com 17 | 18 | ## Main content 19 | 20 | ### Comment 21 | 22 | ```python 23 | # Single line comment 24 | 25 | """ 26 | Multiple 27 | line 28 | comment 29 | This kind of comment very helpful in comment in class and method 30 | Scroll to Class section to read more 31 | """ 32 | def func_name(name: str, age: int) -> None: 33 | """ 34 | func_name which print a string 35 | 36 | Parameters: 37 | name: str 38 | age: int 39 | 40 | Returns: 41 | None 42 | """ 43 | print(f'Hello {name}, you are {age} age') 44 | ``` 45 | 46 | ### Data types 47 | 48 | #### Numbers 49 | 50 | There are 2 kind of numbers in Python: integer, float 51 | 52 | ```python 53 | # integer 54 | # -2, -1, 0, 1, 2, 3, 4, 5 55 | tax = 20 56 | 57 | # you can use underscore to make readable number 58 | salary = 100_000_000_000 # 1000000000 59 | print(f"{salary: _}") # 100_000_000_000 60 | print(f"{salary: ,}") # 100,000,000,000 61 | 62 | # float 63 | # -1.25, -1.0, --0.5, 0.0, 0.5, 1.0, 1.25 64 | buy_price = 1.68 65 | 66 | # you can use underscore to make readable number 67 | income = 88_000_000.123_456 # 88000000.123456 68 | print(f"{income: _.2f}") # 88_000_000.12 69 | print(f"{income: ,.6f}") # 88,000,000.123456 70 | 71 | # you can declare multiple variables on a single line, separated by commas 72 | sell_price, sold_amount = 1.99, 2123 73 | 74 | co = buy_price * sold_amount 75 | revenue = sold_amount * sell_price 76 | profit_before_tax = (sell_price - buy_price) * sold_amount 77 | profit_after_tax = profit_before_tax - (profit_before_tax * tax / 100) 78 | 79 | print(str.format("CO: ${0:,.2f}", co)) 80 | print(str.format("Revenue: ${0:,.2f}", revenue)) 81 | print(str.format("Profit before tax: ${0:,.2f}", profit_before_tax)) 82 | print(str.format("Profit after tax: ${0:,.2f}", profit_after_tax)) 83 | 84 | # CO: $3,566.64 85 | # Revenue: $4,224.77 86 | # Profit before tax: $658.13 87 | # Profit after tax: $526.50 88 | 89 | latitude = 20.577180620745487 90 | longitude = 106.04136437153396 91 | print(f"Geopoint is {latitude}, {longitude}") 92 | # Geopoint is 20.577180620745487, 106.04136437153396 93 | 94 | # classic division / always return a float 95 | print(6 / 2) # 3.0 96 | print(9 / 5) # 1.8 97 | 98 | # floor division // discards the fractional part 99 | print(6 // 3) # 2 100 | print(6 // 4) # 1 101 | 102 | # % operator returns the remainder of the division 103 | print(6 % 3) # 0 104 | print(6 % 4) # 2 105 | 106 | # use ** to calculate the powers 107 | print(4 ** 4) # 256 108 | print(f'4 ** 4 = {4 ** 4}') # 4 ** 4 = 256 109 | print(f'{4 ** 4 = }') # 4 ** 4 = 256 110 | 111 | print(2 ** 7) # 128 112 | print(f'2 ** 7 = {2 ** 7}') # 2 ** 7 = 128 113 | print(f'{2 ** 7 = }') # 2 ** 7 = 128 114 | 115 | # casting data using `int(), float()` methods 116 | int("123") # 117 | float("123.123") # 118 | 119 | # check type 120 | type(12) # 121 | type(3.14) # 122 | ``` 123 | 124 | #### String 125 | 126 | ```python 127 | address = 'Halong Bay, Quang Ninh, Vietnam' 128 | name = "Tuan" 129 | # find lenght of a string 130 | len(name) # 4 131 | 132 | # string can be treated like a list characters 133 | # character in position 0 134 | print(name[0]) # T 135 | # last character 136 | print(name[-1]) # n 137 | # second last character 138 | print(name[-2]) # a 139 | # character from position 0 (included) to 2 (excluded) 140 | print(name[0:2]) # Tu 141 | # character from position 1 (included) to 3 (excluded) 142 | print(name[1:3]) # ua 143 | # character from beginning to 2 (excluded) 144 | print(name[:2]) # Tu 145 | # character from 2 (exclude 2) to the end 146 | print(name[2:]) # an 147 | 148 | # Python string cannot be changed, they are immutable, therefore assigning to an indexed position in the string result in an error 149 | name[0] = "H" # error 150 | # Traceback (most recent call last): 151 | # File "/home/runner/RockPaperScissors/main.py", line 2, in 152 | # name[0] = "H" 153 | # TypeError: 'str' object does not support item assignment 154 | 155 | # concat string 156 | greeting_one = "Hello " + name 157 | greeting_two = str.format("Hello {0}", name) 158 | concat_string = f"Hello {name}" 159 | print(greeting_one) 160 | print(greeting_two) 161 | print(concat_string) 162 | # Hello Tuan 163 | # Hello Tuan 164 | # Hello Tuan 165 | 166 | # string methods 167 | greeting_three = str.format("Hello {0}", name.upper()) 168 | print(greeting_three) 169 | print(greeting_three.split(" ")) 170 | 171 | # Hello TUAN 172 | # ['Hello', 'TUAN'] 173 | 174 | # pad string 175 | name = 'TUAN' 176 | print(f'{name:_<10}') # TUAN______ 177 | print(f'{name:_>10}') # ______TUAN 178 | print(f'{name:_^10}') # ___TUAN___ 179 | 180 | # you need to cast data to string before concat 181 | age = 30 182 | greeting_f_our = "Hello " + name + ", age: " + str(age) 183 | # but you can use str.format to concat other data type into string 184 | greeting_five = str.format("Hello {0}, age: {1}", name, age) 185 | 186 | # split line 187 | print("Multiple\nline\n\tstring") 188 | # Multiple 189 | # line 190 | # string 191 | 192 | # trim whitespace from lead and tail 193 | " this is a string with spacing ".strip() # this is a string with spacing 194 | 195 | # casting data using `str()` method 196 | str(123) # 197 | str(123.123) # 198 | str("123") # 199 | 200 | # check type 201 | type("Hello world") # 202 | ``` 203 | 204 | #### Boolean 205 | 206 | It have 2 value: True/False 207 | 208 | ```python 209 | exceeded_quota = True 210 | published = False 211 | 212 | # negative with not 213 | new_value = not exceeded_quota # False 214 | 215 | # Boolean Operators 216 | True & True # True 217 | True and True # True 218 | False & True # False 219 | False and True # False 220 | 221 | # True/False is 1 and 0 but with different keywords 222 | True + True # 2 223 | True + False # 1 224 | True * 8 # 8 225 | False - 5 # -5 226 | True - 5 # -4 227 | 228 | # comparision operators look at the numerical value of True and False 229 | 0 == False # True 230 | 1 == False # False 231 | 1 == True # True 232 | 0 == True # False 233 | 2 > True # True 234 | 2 >= True # True 235 | 2 != True # True 236 | 237 | # None, 0, and empty strings/list/dics/tuples/sets all evaluate to False 238 | # All other values are True 239 | print(bool(None)) # False 240 | print(bool(0)) # False 241 | print(bool("")) # False 242 | print(bool([])) # False 243 | print(bool({})) # False 244 | print(bool(())) # False 245 | print(bool(set())) # False 246 | 247 | # int 248 | print(bool(5)) # True 249 | # string 250 | print(bool("tuan")) # True 251 | # list 252 | print(bool(["tuan", "nguyen"])) # True 253 | # dics 254 | print(bool({"foo": "bar"})) # True 255 | # tuple 256 | print(bool((1, 2, 3, 4, 5))) # True 257 | # set 258 | print(bool({120, 120, 10, 30})) # True 259 | 260 | # equality is == 261 | 1 == True # True 262 | 1 == 1 # True 263 | 0 == True # False 264 | 265 | # inequality is != 266 | 1 != True # False 267 | 1 != False # True 268 | 1 != 0 # True 269 | 270 | # More comparision 271 | 1 < 10 # True 272 | 1 > 10 # False 273 | 1 <= 10 # True 274 | 1 >= 10 # False 275 | 276 | # Seeing whether a value is in a range 277 | 1 < 2 and 2 < 3 # True 278 | 2 < 3 and 3 < 2 # False 279 | # Chaining make this look nicer 280 | 1 < 2 < 3 # True 281 | 2 < 3 < 2 # False 282 | 283 | # is vs == 284 | # is check if 2 variables refer to the same object 285 | # == check if the objects pointed to the same value 286 | list_one = [1, 2, 3, 4] 287 | list_two = list_one 288 | 289 | list_two is list_one # True, list_one and list_two point to the same object 290 | list_two == list_one # True, list_one and list_two are equal because they have the same value 291 | 292 | list_three = [1, 2, 3, 4] 293 | 294 | list_three is list_one # False, list_three is not point to the same object as list_one 295 | list_three == list_one # True, list_three and list_one have the same value 296 | 297 | # check type 298 | type(True) # 299 | ``` 300 | 301 | #### None 302 | 303 | None is an object 304 | 305 | ```python 306 | None # None 307 | 308 | # Don't use the equality `==` to compare objects to None 309 | # Use `is` instead. `is` check for equality of object identity 310 | 311 | "etc" is None # False 312 | None is None # True 313 | 314 | # check type 315 | type(None) # 316 | ``` 317 | 318 | #### Lists 319 | 320 | List store sequences 321 | 322 | ```python 323 | days_of_week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 324 | days_of_month = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30] 325 | 326 | # append element 327 | days_of_week.append("Lazy") 328 | 329 | append_result = days_of_month.append(31) # None 330 | print(days_of_month) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] 331 | 332 | # remove from the end with pop 333 | pop_result = days_of_month.pop() # 31 334 | 335 | print(days_of_month) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30] 336 | 337 | # access a list like you would any array 338 | days_of_month[0] # 1 339 | 340 | # look at the last element 341 | days_of_month[-1] # 30 342 | 343 | # looking out of bounds is an IndexError 344 | days_of_month[50] # IndexError: list index out of range 345 | 346 | # You can look at ranges with slice syntax. 347 | # the start index is included, the end index is not 348 | # (It's a closed/open range for you mathy types.) 349 | days_of_month[1:3] # return list from 1 (excluded) to 3 (included) => [2, 3] 350 | days_of_month[28:] # return list from 28 (excluded) to the end => [29, 30] 351 | days_of_month[:3] # return list from beginning to index 3 (excluded) => [1, 2, 3] 352 | days_of_month[::3] # return list selecting elements with a step size of 3 => [1, 4, 7, 10, 13, 16, 19, 22, 25, 28] 353 | days_of_month[::-1] # return list in reverse order => [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1] 354 | 355 | # make one layer deep copy using slices 356 | days_of_month_copy = days_of_month[:] 357 | 358 | # remove arbitrary elements from a list with `del` 359 | del days_of_month[0] # [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30] 360 | 361 | # remove first occurrence of a value, raise ValueError if absent 362 | list_with_duplicate_value = [1, 2, 3, 4, 5, 4, 3, 8] 363 | list_with_duplicate_value.remove(4) # [1, 2, 3, 5, 4, 3, 8] 364 | list_with_duplicate_value.remove(100) # ValueError: list.remove(x): x not in list 365 | 366 | # insert an element at a specific index 367 | list_with_duplicate_value.insert(2, 100) # [1, 2, 100, 3, 4, 5, 4, 3, 8] 368 | 369 | # get the index of the first item found matching the argument, raise ValueError if absent 370 | list_item = ["Apple", "Banana", "Peach", "Guava", "Avocado", "Peach"] 371 | list_item.index("Peach") # 3 372 | list_item.index("Peachy") # ValueError: 'Peachy' is not in list 373 | 374 | # use `+` to add multiple list to make a new list, original list will not being modified 375 | list_poultry = ["Chickens", "Ducks", "Turkeys", "Geese", "Quails", "Pheasants", "Pigeons", "Ostriches"] 376 | list_cattle = ["Wagyu", "Kobe beef", "Ohmi beef", "Hanwoo beef", "Kurobuta Pork", "Olive Sanuki Wagyu"] 377 | list_animal = list_poultry + list_cattle + list_poultry 378 | 379 | # concatenate list with `extend`, the extender list will be modified 380 | list_poultry.extend(list_cattle) 381 | print(list_poultry) 382 | # ['Chickens', 'Ducks', 'Turkeys', 'Geese', 'Quails', 'Pheasants', 'Pigeons', 'Ostriches', 'Wagyu', 'Kobe beef', 'Ohmi beef', 'Hanwoo beef', 'Kurobuta Pork', 'Olive Sanuki Wagyu'] 383 | print(list_cattle) # ["Wagyu", "Kobe beef", "Ohmi beef", "Hanwoo beef", "Kurobuta Pork", "Olive Sanuki Wagyu"] 384 | 385 | # check for existence in a list with `in` 386 | "Chickens" in list_poultry # True 387 | 388 | # examine the length with `len()` 389 | len(list_poultry) # 8 390 | 391 | # You can put multiple data types into the same list without hassle 392 | list_mixed = ["Mon", 2, "Tue", 3, "Wed", 4, "Thu", 5, "Fri", 6, "Sat", True, "Sun", False] 393 | 394 | # unpack lists into variables 395 | data = [["Blog", "Service"], ["Post 01", "Post 02"], ["Author 01", "Author 02"]] 396 | categories, posts, authors = data 397 | print(categories) # ['Blog', 'Service'] 398 | print(posts) # ['Post 01', 'Post 02'] 399 | print(authors) # ["Author 01", "Author 02"] 400 | 401 | # extended unpacking (or spreading) 402 | *rest, authors = data 403 | print(rest) # [['Blog', 'Service'], ['Post 01', 'Post 02']] 404 | print(authors) # ['Author 01', 'Author 02'] 405 | 406 | categories, *rest = data 407 | print(categories) # ['Blog', 'Service'] 408 | print(rest) # [['Post 01', 'Post 02'], ['Author 01', 'Author 02']] 409 | 410 | categories, *rest, authors = data 411 | print(categories) # ['Blog', 'Service'] 412 | print(rest) # [['Post 01', 'Post 02']] 413 | print(authors) # ['Author 01', 'Author 02'] 414 | 415 | # sorting a list is a common behavior in real-life 416 | salaries = [123.02, 3423.13, 34543, 945, 100, 8833.5] 417 | 418 | salaries.sort() # default ascending sort 419 | print(salaries) # [100, 123.02, 945, 3423.13, 8833.5, 34543] 420 | 421 | salaries.sort(reverse=True) 422 | print(salaries) # [34543, 8833.5, 3423.13, 945, 123.02, 100] 423 | 424 | names = ["John", "Corey", "Adam", "Steve", "Rick", "Thomas"] 425 | 426 | names.sort() 427 | print(names) # ['Adam', 'Corey', 'John', 'Rick', 'Steve', 'Thomas'] 428 | 429 | names.sort(reverse=True) 430 | print(names) # ['Thomas', 'Steve', 'Rick', 'John', 'Corey', 'Adam'] 431 | 432 | # check type 433 | type(list_mixed) # 434 | ``` 435 | 436 | #### Tuple 437 | 438 | Tuple is like lists but are immutable 439 | 440 | ```python 441 | user_info = ("Tuan", 35, "Halong Bay, Quang Ninh, Vietnam", True) 442 | user_info[0] # Tuan 443 | user_info[0] = "Quan" # TypeError: 'tuple' object does not support item assignment 444 | 445 | # check type 446 | # tuple of needs a comma if have elements 447 | type(("Tuan", )) # 448 | type(("Tuan", True)) # 449 | 450 | # empty tuple don't need comma 451 | type(()) # 452 | 453 | # without comma, it will never consider as tuple 454 | type(("Tuan")) # 455 | type((1)) # 456 | type((3.14)) # 457 | type((True)) # 458 | 459 | # you can do most of the list operations on tuples too 460 | len(("Tuan", True)) # 2 461 | 462 | book = ("Harry Porter", 2002, "Published") 463 | author = ("JK Rowling", 1965, "Female", "England") 464 | 465 | # joining tuples using concatenation `+` operator 466 | book_author = book + author # ('Harry Porter', 2002, 'Published', 'JK Rowling', 1965, 'Female', 'England') 467 | # or `sum()` function 468 | new_tuple_two = sum((book, author), ()) # ('Harry Porter', 2002, 'Published', 'JK Rowling', 1965, 'Female', 'England') 469 | 470 | book[:2] # ('Harry Porter', 2002) 471 | book[:-1] # ('Harry Porter',) 472 | book[::2] # ('Harry Porter', 'Published') 473 | book[2:] # ('Published',) 474 | book[1:2] # (2002,) 475 | "England" in author # True 476 | 477 | # You cannot extend a tuple directly 478 | author.extend(book) # AttributeError: 'tuple' object has no attribute 'extend' 479 | # you need to cast to list and cast back to tuple after merged 480 | author_list = list(author) 481 | book_list = list(book) 482 | 483 | author_list.extend(book_list) 484 | new_tuple = tuple(author_list) 485 | print(new_tuple) # ('JK Rowling', 1965, 'Female', 'England', 'Harry Porter', 2002, 'Published') 486 | 487 | # Unpack tuples into variables 488 | name, published_year, status = book 489 | 490 | # extended unpacking 491 | address = ((20.951826295025008, 107.01421196125972), "Halong Bay, Quang Ninh, Vietnam", 200000, "+84123456789") 492 | 493 | geopoint, *rest, phone = address 494 | print(geopoint) # (20.951826295025008, 107.01421196125972) 495 | print(rest) # ['Halong Bay, Quang Ninh, Vietnam', 200000] 496 | print(phone) # +84123456789 497 | 498 | *rest, phone = address 499 | print(rest) # [(20.951826295025008, 107.01421196125972), 'Halong Bay, Quang Ninh, Vietnam', 200000] 500 | print(phone) # +84123456789 501 | 502 | geopoint, *rest = address 503 | print(geopoint) # (20.951826295025008, 107.01421196125972) 504 | print(rest) # ['Halong Bay, Quang Ninh, Vietnam', 200000, '+84123456789'] 505 | 506 | # tuple are created by default if you leave out the parentheses 507 | name, age = "Tuan", 35 # tuple Tuan, 35 is unpacked into variables name, age 508 | print(name) # Tuan 509 | print(age) # 35 510 | 511 | # now look how easy it is to swap two values 512 | age, name = name, age 513 | print(name) # 35 514 | print(age) # Tuan 515 | ``` 516 | 517 | #### Dictionary 518 | 519 | Dictionary store mappings from keys to values 520 | 521 | ```python 522 | empty_dict = {} 523 | languages = {"en": "English", "vi": "Vietnamese"} 524 | 525 | # keys for dictionaries have to be immutable types to ensure key can be 526 | # converted to a constant hash value for quick look-ups. 527 | # immutable types include ints, float, string, tuples 528 | valid_dict = {100: "Very good", 90: "Good", 70: "Acceptable", 60: "Under average"} 529 | valid_dict = {3.14: "Pi"} 530 | valid_dict = {"cached": "total: 10, ranked 100"} 531 | valid_dict = {("Tuan", 35): [10000, 3.141592654]} 532 | 533 | invalid_dict = {[100, 200]: "salary"} # TypeError: unhashable type: 'list' 534 | invalid_dict = {{"en": "English"}: "salary"} # TypeError: unhashable type: 'list' 535 | 536 | # look up values with [] 537 | leaderboard = {1: ["Tuan", "Simon"], 2: ["Duong", "Chien"], 3: ["Truong"], 4: ["Son"], 5: ["Phuong"]} 538 | print(leaderboard[1]) # ['Tuan', 'Simon'] 539 | print(leaderboard[3]) # ['Truong'] 540 | 541 | # get all keys as an iterable with "keys()". We need to wrap the call in list() to turn it into a list 542 | # Note: 543 | # - For python version < 3.7: dictionary key ordering is not guaranteed 544 | # - For python version >= 3.7: dictionary items maintain the order at which they are inserted into the dictionary 545 | values = list(leaderboard.values()) 546 | # [['Tuan', 'Simon'], ['Duong', 'Chien'], ['Truong'], ['Son'], ['Phuong']] 547 | 548 | # check for existence of keys in a dictionary with `in` 549 | languages = { 550 | "en": "English", 551 | "vi": "Vietnamese" 552 | } 553 | "en" in languages # True 554 | 555 | # looking up for non-existing key is a KeyError 556 | languages["de"] # KeyError: 'de' 557 | 558 | # use `get()` method to avoid the KeyError 559 | languages.get("vi") # Vietnamese 560 | languages.get("de") # None 561 | 562 | # `get()` method support default value when the value is missing 563 | languages.get("de", "German") # German 564 | 565 | # `setdefault()` inserts into a dictionary only if the given key is absent 566 | languages.setdefault("de", "German") 567 | languages.setdefault("en", "France") 568 | # {'en': 'English', 'vi': 'Vietnamese', 'de': 'German'} 569 | 570 | # adding to a dictionary 571 | languages.update({"zh": "Chinese"}) # {'en': 'English', 'vi': 'Vietnamese', 'de': 'German', 'zh': 'Chinese'} 572 | languages.update({"en": "France"}) # {'en': 'France', 'vi': 'Vietnamese', 'de': 'German', 'zh': 'Chinese'} 573 | languages["en"] = "English" # {'en': 'English', 'vi': 'Vietnamese', 'de': 'German', 'zh': 'Chinese'} 574 | 575 | # remove keys from a dictionary with del, raise KeyError if key is absent 576 | del languages["zh"] 577 | del languages["absent_key"] # KeyError: 'absent_key' 578 | 579 | # from python 3.5 you can also use the additional unpacking options 580 | languages = {"en": "English", **{"zh": "Chinese"}} # {'en': 'English', 'zh': 'Chinese'} 581 | 582 | # check type 583 | print(type(languages)) # 584 | ``` 585 | 586 | #### Set 587 | 588 | Set is an unordered collection of unique and immutable elements. 589 | 590 | Set is commonly used for membership testing, eliminating duplicate entries, and performing mathematical set operation like `union`, `intersection`, and `difference`. 591 | 592 | ```python 593 | # init an empty set 594 | empty_set = set() 595 | 596 | # init a set from a list 597 | list_duplicated_values = [1, 1, 2, 2, 3, 3] 598 | unique_values = set(list_duplicated_values) # {1, 2, 3} 599 | 600 | # init a set with a bunch of values 601 | some_set = {1, 1, 2, 3, 2, 5} # {1, 2, 3, 5} 602 | 603 | days_of_week = {"Mon", "Tue"} # {'Tue', 'Mon'} 604 | 605 | # Note elements of a set have to be immutable. 606 | # Immutable types include ints, floats, strings, tuples, frozensets. 607 | # other data types will raise error TypeError: unhashable type 608 | new_set = {[1, 2]} # TypeError: unhashable type: 'list' 609 | new_set = {{"foo": "bar"}} # TypeError: unhashable type: 'dict' 610 | 611 | # tuple 612 | new_set = {("Wed", "Tue")} # {('Wed', 'Tue')} 613 | # string 614 | new_set = {"Wed", "Tue"} # {'Wed', 'Tue'} 615 | # int 616 | new_set = {1, 2} # {1, 2} 617 | # float 618 | new_set = {3.14, 1,234} # {3.14, 234, 1} 619 | # frozenset 620 | new_set = {frozenset([1, 2, 3])} # {frozenset({1, 2, 3})} 621 | 622 | mixed_set = {1, 2.2, "three", (4, 5), frozenset([6, 7])} 623 | 624 | # add elements, duplicate entries will automatically eliminated 625 | days_of_week.add("Wed") # {'Wed', 'Tue', 'Mon'} 626 | days_of_week.add("Wed") # {'Wed', 'Tue', 'Mon'} 627 | 628 | # remove elements using `remove()` or `discard()` methods. 629 | # `remove()` will raise KeyError if remove an absent element 630 | # `discard()` will never raise error 631 | days_of_week.remove("Wed") #{'Tue', 'Mon'} 632 | days_of_week.remove("Wed_asjdhas") # KeyError: 'Wed_asjdhas' 633 | 634 | days_of_week.discard("Tue") #{'Mon'} 635 | days_of_week.discard("Wed_asjdhas") # Nothing happen 636 | 637 | # union is the way to merge 2 sets and also remove the duplicates 638 | # use `union()` method or `|` operator 639 | days_of_week_one = {"Mon", "Tue", "Web"} 640 | days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} 641 | days_of_week = days_of_week_one.union(days_of_week_two) # {'Web', 'Sat', 'Mon', 'Sun', 'Fri', 'Tue', 'Thu'} 642 | 643 | days_of_week = days_of_week_one | days_of_week_two # {'Web', 'Sat', 'Mon', 'Sun', 'Fri', 'Tue', 'Thu'} 644 | 645 | # intersection is the way to find elements that found in both sets 646 | # use `intersection()` or `&` operator 647 | days_of_week_one = {"Mon", "Tue", "Web"} 648 | days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} 649 | 650 | days_appeared_in_both_sets = days_of_week_one.intersection(days_of_week_two) # {'Web'} 651 | days_appeared_in_both_sets = days_of_week_one & days_of_week_two # {'Web'} 652 | 653 | # difference is the way to find elements that appear in the first set but not in the second 654 | # use `difference()` method or `-` operator 655 | days_of_week_one = {"Mon", "Tue", "Web"} 656 | days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} 657 | 658 | days_difference_from_set_two = days_of_week_one.difference(days_of_week_two) # {'Tue', 'Mon'} 659 | days_difference_from_set_two = days_of_week_one - days_of_week_two # {'Tue', 'Mon'} 660 | 661 | # symmetric difference between two sets contains elements that are in either of the sets but not in both. 662 | # use `symmetric_difference()` or `^` operator 663 | days_of_week_one = {"Mon", "Tue", "Web"} 664 | days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} 665 | 666 | symmetric_difference = days_of_week_one.symmetric_difference(days_of_week_two) # {'Fri', 'Mon', 'Sat', 'Tue', 'Thu', 'Sun'} 667 | symmetric_difference = days_of_week_one ^ days_of_week_two # {'Fri', 'Mon', 'Sat', 'Tue', 'Thu', 'Sun'} 668 | 669 | # a set is a subset of another if all elements of the first set are in the second 670 | # use `issubset()` or `<=` operator 671 | days_of_week = {2, 3, 4, 5, 6, 7, 8} 672 | days_of_month = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30} 673 | 674 | days_of_week.issubset(days_of_month) # True 675 | days_of_week <= days_of_month # True 676 | 677 | # a set is a superset of another if it contains all elements of the second set. 678 | # use `issuperset()` or `>=` operator 679 | days_of_month.issuperset(days_of_week) 680 | days_of_month >= days_of_week 681 | 682 | # Make a one layer deep copy using the `copy()` method 683 | days_of_week_copy = days_of_week.copy() 684 | 685 | # remove all elements from a set using the `clear()` method 686 | days_of_week.clear() # set() 687 | 688 | # add multiple elements to a set using the `update()` method 689 | days_of_week = {2, 3, 4, 5, 6, 7, 8} 690 | days_of_week.update([1, 9, 10, 11]) # {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} 691 | 692 | # check for existence in a set with `in` 693 | 2 in days_of_week # True 694 | 695 | # a frozen set is an immutable version of a set. 696 | # elements cannot be added or removed after creation. 697 | # using `frozenset()` method 698 | new_set = {"Mon", "Tue"} 699 | new_frozenset = frozenset(new_set) 700 | 701 | days_of_week = frozenset(["Mon", "Tue"]) 702 | days_of_week.clean() # AttributeError: 'frozenset' object has no attribute 'clean' 703 | days_of_week.update(["Wed"]) # AttributeError: 'frozenset' object has no attribute 'update' 704 | days_of_week.add("Wed") # AttributeError 705 | days_of_week.remove("Wed") # AttributeError 706 | ``` 707 | 708 | #### Enum (Enumeration) 709 | 710 | Enum is a symbolic name for a set of values. Enum is immutable which means some case you might want to use Enum as Constants (Python have no Constant) 711 | 712 | Enum are used to create readable names for a collection of related constants and make the code more readable and maintainable. Python's enum module provides the `Enum` class for creating enuimarations. 713 | 714 | ```python 715 | # define an Enum TransactionState 716 | from enum import Enum 717 | 718 | class TransactionState(Enum): 719 | PENDING = 0 720 | PROCESSING = 1 721 | COMPLETED = 2 722 | FAILED = 3 723 | REFUNDED = 4 724 | 725 | print(TransactionState.COMPLETED.name) # COMPLETED 726 | print(TransactionState.COMPLETED.value) # 4 727 | 728 | # using Enum in Conditional statement 729 | if transaction_result == TransactionState.COMPLETED: 730 | print("Transaction completed") 731 | elif transaction_result == TransactionState.PENDING: 732 | print("Transaction pending") 733 | else: 734 | print("Transaction in other state") 735 | 736 | # loop over Enum 737 | for state in TransactionState: 738 | print(state) 739 | 740 | class Theme(Enum): 741 | DARK = "dark" 742 | LIGHT = "light" 743 | 744 | # auto assign value use `auto()` from enum module 745 | from enum import Enum, auto 746 | 747 | class Theme(Enum): 748 | DARK = auto() 749 | LIGHT = auto() 750 | 751 | for theme in Theme: 752 | print(theme.value) # 1, 2 753 | 754 | # output: will be auto-assigned starting from 1 755 | # 1 756 | # 2 757 | 758 | # enum with method 759 | from enum import Enum, auto 760 | 761 | class Theme(Enum): 762 | DARK = auto() 763 | LIGHT = auto() 764 | 765 | def is_light(self): 766 | return self in {Theme.LIGHT} 767 | 768 | print(Theme.LIGHT.is_light()) # True 769 | print(Theme.DARK.is_light()) # False 770 | 771 | # enum is immutable, which means you cannot reassign value to Enum 772 | Theme.LIGHT.value = 123 # AttributeError: cannot set attribute 'value' 773 | ``` 774 | 775 | ### Variables 776 | 777 | Variable in Python is a name that refers to a value. Python is dynamically typed which mean you don't need to declare the variable type explicitly. 778 | 779 | ```python 780 | # variable identifier must follow these rules 781 | # - must start with a letter [a-z A-Z] or an underscore `_` 782 | # - can be followed by letter, digits (0-9) or underscore `_` 783 | # - case-sensitive (e.g myVar and myvar are different variables) 784 | # - cannot be a reversed keyword (e.g if, while, for, break, continue, pass) 785 | 786 | ######################## 787 | # implicit declaration 788 | ######################## 789 | # int 790 | age = 35 791 | # float 792 | pi = 3.14 793 | # string 794 | name = "Tuan" 795 | # boolean 796 | sold_out = True 797 | # tuple 798 | person = ("Tuan", 35, "Male", "Halong Bay, Quang Ninh, Vietnam") 799 | # list 800 | stables = ["Gothenburg, Sweden", "Paris, France", "Hanoi, Vietnam"] 801 | # dict 802 | languages = {"en": "English", "vi": "Vietnam"} 803 | # set 804 | product_ids = {"sku_01", "sku_02", "sku_01", "sku_03"} 805 | 806 | ######################## 807 | # explicit declaration 808 | ######################## 809 | # int 810 | age: int = 35 811 | # float 812 | pi: float = 3.14 813 | # str 814 | name: str = "Tuan" 815 | # bool 816 | sold_out: bool = True 817 | 818 | # use `id()` function to find the variable address on memory 819 | print(id(age)) # 4357660072 820 | print(id(pi)) # 4342525072 821 | print(id(name)) # 4344113520 822 | print(id(sold_out)) # 4356703696 823 | 824 | ######################## 825 | # variable scope 826 | # - local scope: variable declared inside a function are local to that function 827 | # - enclosing scope: variable in the local scope of enclosing function (nonlocal) 828 | # - global scope: variable declared at the top level of a 829 | # script or module, or declared global using `global` keyword 830 | # - built-in scope: variable preassigned in the built-in namespace (e.g., `len`, `range`) 831 | ######################## 832 | # global variable 833 | age = 30 834 | 835 | def outer_function(): 836 | # enclosing variable 837 | age = 50 838 | 839 | def inner_function(): 840 | # local variable 841 | age = 100 842 | 843 | # global variable 844 | global name 845 | name = "Tuan" 846 | 847 | print(f'{age = }') # 100 848 | 849 | inner_function() 850 | print(f'{age = }') # 50 851 | 852 | outer_function() 853 | print(f'{age = }') # 30 854 | print(f'{name = }') # Tuan 855 | 856 | # `global` keyword allows you to modify a global variable inside a function 857 | today = "Monday" 858 | 859 | def modify_global(): 860 | global today 861 | today = "Tuesday" 862 | print(f'{today = }') # today = 'Tuesday' 863 | 864 | modify_global() 865 | print(f'{today = }') # today = 'Tuesday' 866 | 867 | # `nonlocal` keyword allows you to modify a variable in the enclosing (non-global) scope 868 | def outer_func(): 869 | name = "Tuan" 870 | 871 | def inner_func(): 872 | nonlocal name 873 | name = "Simon" 874 | print(f'{name = }') # name = 'Simon' 875 | 876 | inner_func() 877 | print(f'{name = }') # name = 'Simon' 878 | 879 | outer_func() 880 | 881 | ######################## 882 | # dynamic typing and type checking 883 | # Python is dynamically typed, meaning that variable types are 884 | # determined at runtime, variables can change type 885 | ######################## 886 | x = 10 887 | print(type(x)) # 888 | 889 | x = "Hello world" 890 | print(type(x)) # 891 | 892 | # dynamic typing offers flexibility, it can easily lead to runtime errors if not managed carefully (like Javascript). 893 | # to enhance type safety, you can use type hints and tools like `mypy` for static type checking. 894 | # don't forget to install mypy extension for your IDE for static type checking: https://www.mypy-lang.org 895 | # vscode extension https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker 896 | # IntelliJ IDEA, PyCharm https://plugins.jetbrains.com/plugin/11086-mypy 897 | def greeting(name: str) -> str: 898 | return f'Hello {name}' 899 | print(greeting("Tuan")) # Hello Tuan 900 | print(greeting(1123)) # Mypy raise thise: Argument 1 to "greeting" has incompatible type "int"; expected "str" 901 | 902 | # recommended to use pipx over pip: https://pipx.pypa.io/stable 903 | # `pip install mypy` or `pipx install mypy` 904 | # and run: `mypy your_file.py` 905 | 906 | from typing import Tuple, List, Dict, Set 907 | 908 | Person = Tuple[str, int, str, str] 909 | person: Person = ("Tuan", 35, "Male", "Halong Bay, Quang Ninh, Vietnam") 910 | print(person) 911 | 912 | names: List[str] = ["Tuan", "Simon", "Duong", "Son"] 913 | print(names) 914 | 915 | scores: Dict[str, int] = {"Tuan": 10, "Simon": 9, "Duong": 8, "Son": 7} 916 | print(scores) 917 | 918 | days_of_week: Set[str] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} 919 | print(days_of_week) 920 | 921 | ######################## 922 | # constant 923 | # while Python doesn't have a built-in constant type 924 | # by convention, constant are written in uppercase letters and are typically defined 925 | # at the module level 926 | ######################## 927 | PI = 3.141592654 928 | MAX_CONNECTIONS = 500 929 | LOGS_LEVEL = "debug" 930 | LOGS_SIZE = 5 * 1000 ** 2 #MB 931 | 932 | # immutable types includes: int, float, str, tulple, frozenset 933 | # mutable types inclide: list, dict, set 934 | 935 | # declare multiple variables at once by adding commas 936 | name, age, address = "Tuan", 35, "Halong Bay, Quang Ninh, Vietnam" 937 | ``` 938 | 939 | ### Control Flow & Iterables 940 | 941 | #### Conditional statements 942 | 943 | Conditional statements allow you to execute different blocks of code based on certain conditions. 944 | 945 | ```python 946 | # if 947 | is_raining = True 948 | if is_raining: 949 | print("It's raining") 950 | 951 | # if-else 952 | humidity = 88 953 | if humidity > 80: 954 | print("Possibility to rain") 955 | else: 956 | print("No chance to rain") 957 | 958 | # if-elif-else 959 | if humidity >= 90: 960 | print("Surely rain") 961 | elif humidity >= 80: 962 | print("Possibility to rain") 963 | else: 964 | print("No chance to rain") 965 | 966 | # switch-case 967 | # Python does not have a built-in switch-case statement like other languages. 968 | # However you can archive similar functionality using different approaches. 969 | # 1. use if-elif-else statement 970 | # 2. use dict 971 | # 3. use lambda function in dict 972 | # 4. use match-case (python 3.10+) 973 | 974 | # solution 1: if-elif-else 975 | def switch_case(value): 976 | if value == 1: 977 | return "case 01" 978 | elif value == 2: 979 | return "case 02" 980 | else: 981 | return "default case" 982 | 983 | switch_case(1) 984 | switch_case(2) 985 | switch_case("Hi") 986 | 987 | # solution 2: dict 988 | def case_1(): 989 | return "case 01" 990 | def case_2(): 991 | return "case 02" 992 | def case_default(): 993 | return "default case" 994 | switch = { 995 | 1: case_1, 996 | 2: case_2 997 | } 998 | def switch_case(value): 999 | return switch.get(value, case_default)() 1000 | 1001 | switch_case(1) 1002 | switch_case(2) 1003 | switch_case("Hi") 1004 | 1005 | # solution 3: use lambda function in dict 1006 | switch = { 1007 | 1: lambda: "case 01", 1008 | 2: lambda: "case 02", 1009 | } 1010 | def switch_case(value): 1011 | return switch.get(value, lambda: "default case")() 1012 | 1013 | switch_case(1) 1014 | switch_case(2) 1015 | switch_case("Hi") 1016 | 1017 | # solution 4: match-case (python 3.10+) 1018 | def switch_case(value): 1019 | match value: 1020 | case 1: 1021 | return "Case 1" 1022 | case 2: 1023 | return "Case 2" 1024 | case 3 | 4: 1025 | return "Case 3 or 4" 1026 | case _: 1027 | return "Default case" 1028 | 1029 | print(switch_case(1)) # Case 1 1030 | print(switch_case(3)) # Case 3 or 4 1031 | print(switch_case(4)) # Case 3 or 4 1032 | print(switch_case(5)) # Default case 1033 | 1034 | ``` 1035 | 1036 | #### Loop statements 1037 | 1038 | Loop statements allow you to repeat a block of code multiple times 1039 | 1040 | ```python 1041 | # for: Iterates over a sequence (such as a list, tuple, dictionary, set, or string) and executes a block of code for each item. 1042 | fruits = ["Apple", "Peach", "Pear", "Banana", "Kiwi"] 1043 | for fruit in fruits: 1044 | print(fruit) 1045 | 1046 | leaderboard = {1: "Tuan", 2: "Simon", 3: "Duong"} 1047 | for key in leaderboard.keys(): 1048 | print(f'{key}: {leaderboard[key]}') 1049 | # 1: Tuan 1050 | # 2: Simon 1051 | # 3: Duong 1052 | 1053 | unique_numbers = {1, 2, 3, 4, 5, 1, 3, 5} 1054 | for value in unique_numbers: 1055 | print(f'{value}') 1056 | # 1 1057 | # 2 1058 | # 3 1059 | # 4 1060 | # 5 1061 | 1062 | # use `enumerate` method to create an iterator that produces tuples containing an index and the corresponding element 1063 | # from the iterable so you can access index and element 1064 | for index, value in enumerate(unique_numbers): 1065 | print(f'{index}: {value}') 1066 | # 0: 1 1067 | # 1: 2 1068 | # 2: 3 1069 | # 3: 4 1070 | # 4: 5 1071 | 1072 | # `range()` returns an iterable of numbers from 0 up to (but excluding) the given number 1073 | for i in range(4): 1074 | print(i) # 0 1 2 3 1075 | 1076 | for i in range(5, 10): 1077 | print(i) # 5 6 7 8 9 1078 | 1079 | for i in range(5, 10, 2): 1080 | print(i) # 5 7 9 1081 | 1082 | # loop over a list to retrieve both the index and the value of each list item 1083 | # use `enumerate` method 1084 | for index, value in enumerate(["dog", "cat", "mouse"]): 1085 | print(f'{index}: {value}') 1086 | # 0: dog 1087 | # 1: cat 1088 | # 2: mouse 1089 | 1090 | # while repeats a block of code as long as a specified condition is true. 1091 | count = 0 1092 | while count < 5: # continue if this condition still True 1093 | print(count) # 0 1 2 3 4 1094 | count += 1 # shorthand for count = count + 1 1095 | 1096 | # Python offers a fundamental abstraction called the Iterable. 1097 | # An iterable is an object that can be treated as a sequence. 1098 | # The object returned by the range function is an iterable 1099 | languages = {"en": "English", "vi": "Vietnam"} 1100 | languages_iterator = languages.keys() # dict_keys(['en', 'vi']) 1101 | 1102 | # we can loop over it 1103 | for langKey in languages_iterator: 1104 | print(langKey) 1105 | 1106 | # however we cannot address elements by index, will raise TypeError 1107 | languages_iterator["en"] # TypeError: 'dict_keys' object is not subscriptable 1108 | 1109 | # an iterable is an object that knows how to create an iterable 1110 | languages_iterator = iter(languages) 1111 | 1112 | # our iterable is an object that can remember the state as we traverse through 1113 | # it. We get the next object with `next()` 1114 | next(languages_iterator) # en 1115 | next(languages_iterator) # vi 1116 | 1117 | # after the iterable has returned all of its data, it raises a StopIteration exception 1118 | next(languages_iterator) # StopIteration 1119 | 1120 | # we can also loop over iterable, infact `for` does it implicitly 1121 | for lang in iter(languages): 1122 | print(lang) # en, vi 1123 | 1124 | # we can grab all the elements of an iterable or iterator by call of `list()` 1125 | days_of_week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 1126 | days_iterator = iter(days_of_week) 1127 | list(days_iterator) # ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 1128 | list(days_iterator) # [] because state is saved 1129 | 1130 | # create custom iterator 1131 | # to create an object/class as an iterator, you have to implement 1132 | # the method `__iter__()` and `__next__()` 1133 | # `__iter__()` method acts similar, you can do operations but must always return the iterator object itself 1134 | # `__next__()` method also allows you to do operations, and must return the next item in the sequence 1135 | class MyNumbers: 1136 | def __iter__(self): 1137 | self.a = 1 1138 | return self 1139 | 1140 | def __next__(self): 1141 | if self.a <= 3: 1142 | x = self.a 1143 | self.a += 1 1144 | return x 1145 | else: 1146 | raise StopIteration 1147 | 1148 | my_class = MyNumbers() 1149 | my_iterator = iter(my_class) 1150 | 1151 | print(next(my_iterator)) # 1 1152 | print(next(my_iterator)) # 2 1153 | print(next(my_iterator)) # 3 1154 | print(next(my_iterator)) # raise StopIteration 1155 | ``` 1156 | 1157 | #### Control Statements within Loops 1158 | 1159 | ```python 1160 | # break: exits the nearest enclosing loop immediately 1161 | for value in [1, 2, 3, 4]: 1162 | if value == 3: 1163 | break 1164 | print(value) # 1, 2 1165 | 1166 | count = 0 1167 | while True: 1168 | count += 1 1169 | print(count) # 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 1170 | if count == 10: 1171 | break 1172 | 1173 | # continue: skip the rest of the code inside the current loop iteration and proceeds to the next iteration 1174 | # does continue work with while??? 1175 | for i in range(10): 1176 | if i <= 5: 1177 | print(i) # 0, 1, 2, 3, 4, 5 1178 | continue 1179 | break 1180 | 1181 | # pass: does nothing and is used as a placeholder for future code 1182 | # Empty code is not allowed in loops, function definitions, class definitions, or in if statements. 1183 | for i in range(10): 1184 | pass # Todo 1185 | 1186 | if paid_successfully: 1187 | pass # Todo 1188 | 1189 | while valid: 1190 | pass # Todo 1191 | 1192 | def paid(amount: int, currency: str) -> bool: 1193 | pass # Todo 1194 | return True 1195 | ``` 1196 | 1197 | #### Functions 1198 | 1199 | A function is a block of code which only runs when it is called 1200 | 1201 | ```python 1202 | def no_args_func(): 1203 | print("Hi") 1204 | 1205 | # invoke function 1206 | no_args_func() 1207 | 1208 | def args_func(name, age): 1209 | print(f'{name}, {age}') 1210 | 1211 | # invoke function with args 1212 | args_func("Tuan", 35) 1213 | # you can also send arguments with the key = value syntax to make it easier to follow 1214 | args_func(name = "Tuan", age = 35) 1215 | 1216 | # if you want to passing only positional-only arguments place `, /` at the end of arguments list 1217 | # means need to use positional arguments only when invoking function 1218 | def args_func(name, age, /): 1219 | print(f'{name}: {age}') 1220 | 1221 | args_func("Tuan", 35) # Tuan: 35 1222 | args_func(name = "Tuan", age = 35) # TypeError: args_func() takes 0 positional arguments but 2 were given 1223 | 1224 | # The opposite of positional arguments is keyword-only arguments 1225 | # place `* ,` before arguments 1226 | # means need to use keyword arguments only when invoking function 1227 | def args_func(*, name, age): 1228 | print(f'{name}: {age}') 1229 | 1230 | args_func("Tuan", 35) # TypeError: args_func() takes 0 positional arguments but 2 were given 1231 | args_func(name = "Tuan", age = 35) # Tuan: 35 1232 | 1233 | # you can even make it more complex by combining positional-only with keyword-only arguments 1234 | # I don't prefer to make the complex thing but just a case you want to known 1235 | def args_func(name: str, /, age: int, *, address: str) -> None: 1236 | print(f'{name}, {age}, {address}') 1237 | 1238 | # positional-only `/` always in front of keyword-only `*` arguments 1239 | args_func('John', 30, address='New York') # Valid 1240 | 1241 | # invalid `name` keyword which should have value only, and `address` which should be here 1242 | args_func(name = 'John', age = 30, 'New York') 1243 | 1244 | # defind a function explicitly 1245 | def args_func(name: str, age: int) -> str: 1246 | return f'{name}, {age}' 1247 | 1248 | args_func("Tuan", 35) 1249 | args_func(name = "Tuan", age = 35) 1250 | 1251 | # you can also set default value for arguments by assign `= value` 1252 | # default argument must always go after non-default argument 1253 | def args_func(value: int, decimal: bool = True) -> str: 1254 | return f'{value}, {decimal}' 1255 | 1256 | # this function will raise error: SyntaxError: parameter without a default follows parameter with a default 1257 | def args_func(value: int = 100, decimal: bool) -> str: 1258 | return f'{value}, {decimal}' 1259 | 1260 | args_func(1011011) 1261 | args_func(1011011, True) 1262 | args_func(value = 1011011, decimal = True) 1263 | 1264 | # use arbitrary keyword arguments `**args` 1265 | # if you don't know how many arguments that will be passed into your function 1266 | # add `**` before the parameter name in the function definition 1267 | def console_log(**logs): 1268 | print(logs) 1269 | 1270 | # {'name': 'John', 'age': 30, 'city': 'New York'} 1271 | console_log(name="John", age=30, city="New York") 1272 | 1273 | # {'company': 'SpaceX', 'avg_salary': 5000} 1274 | console_log(company = "SpaceX", avg_salary = 5000) 1275 | 1276 | # Arbitrary argument must comes after non-arbitrary argument 1277 | def console_log(level = 'debug', **logs) -> None: 1278 | print(f'{level}: {logs}') 1279 | 1280 | # debug: {'name': 'John', 'age': 30, 'city': 'New York'} 1281 | console_log(name="John", age=30, city="New York") 1282 | 1283 | # warning: {'name': 'John', 'age': 30, 'city': 'New York'} 1284 | console_log('warning',name="John", age=30, city="New York") 1285 | 1286 | # error: {'name': 'John', 'age': 30, 'city': 'New York'} 1287 | console_log(level = 'error',name="John", age=30, city="New York") 1288 | 1289 | # debug: {'company': 'SpaceX', 'avg_salary': 5000} 1290 | console_log(company = "SpaceX", avg_salary = 5000) 1291 | 1292 | def console_log(message: str, level = 'debug', **logs) -> None: 1293 | print(f'{level}: {message} ({logs})') 1294 | 1295 | # debug: Something goes wrong ({'name': 'John', 'age': 30, 'city': 'New York'}) 1296 | console_log(message = "Something goes wrong", name="John", age=30, city="New York") 1297 | 1298 | # debug: warning ({'name': 'John', 'age': 30, 'city': 'New York'}) 1299 | console_log('warning',name="John", age=30, city="New York") 1300 | 1301 | # error: Something goes wrong ({'name': 'John', 'age': 30, 'city': 'New York'}) 1302 | console_log(level = 'error', message = "Something goes wrong", name="John", age=30, city="New York") 1303 | 1304 | # debug: Something goes wrong ({'company': 'SpaceX', 'avg_salary': 5000}) 1305 | console_log("Something goes wrong", company = "SpaceX", avg_salary = 5000) 1306 | 1307 | # if you try to but arbitrary argument in front of non-arbitrary arguments 1308 | # it will raise SyntaxError: arguments cannot follow var-keyword argument 1309 | def console_log(**logs, level = 'debug'): # SyntaxError 1310 | print(f'{level}: {logs}') 1311 | 1312 | # as I mentioned from above, we use `pass` keyword for placeholder 1313 | def shipping(address: str) -> bool: 1314 | pass # Todo 1315 | return True 1316 | 1317 | shipping("Halong Bay, Quang Ninh, Vietnam") 1318 | shipping(address = "Halong Bay, Quang Ninh, Vietnam") 1319 | ``` 1320 | 1321 | #### Decorators 1322 | 1323 | Decorators are very powerful and useful tool in Python since it allows programmers to modify the behavior of a function or class. 1324 | 1325 | Decorators allow us to wrap another function in order to extend the behaviour of the wrapped function without permanently modifying it. But before deep into decorators let us understand some concepts that will come in handy in learning the decorators. 1326 | 1327 | **First Class Object** 1328 | 1329 | In Python, functions are **[first class object](https://www.geeksforgeeks.org/first-class-functions-python/)** which means that functions in Python can be used or passed as arguments 1330 | 1331 | Properties of first class functions: 1332 | 1333 | - A function is an instance of the Object type 1334 | - You can store the function in a variable 1335 | - You can pass the function as a parameter to another function 1336 | - You can return the function from a function 1337 | - You can store them in data structures such as hash tables, lists ... 1338 | 1339 | Consider the below examples for better understanding 1340 | 1341 | Example 01: Treating the functions as objects 1342 | 1343 | ```python 1344 | def shout(text: str) -> str: 1345 | return text.upper() 1346 | 1347 | # we assign function shout to a variable. This will not invoke the function instead it 1348 | # takes the function object referenced by a shout abd creates a second name pointing to it `yell` 1349 | yell = shout 1350 | 1351 | print(yell('Hello')) # HELLO 1352 | ``` 1353 | 1354 | Example 02: Passing the function as an argument 1355 | 1356 | ```python 1357 | from typing import Callable 1358 | 1359 | def shout(text: str) -> str: 1360 | return text.upper() 1361 | 1362 | def whisper(text: str) -> str: 1363 | return text.lower() 1364 | 1365 | def greet(func: Callable[[str], str]) -> None: 1366 | # storing the function in a variable 1367 | greeting = func('Hi, I am created by a function passed as an argument') 1368 | print(greeting) 1369 | 1370 | greet(shout) # HI, I AM CREATED BY A FUNCTION PASSED AS AN ARGUMENT 1371 | greet(whisper) # hi, i am created by a function passed as an argument 1372 | ``` 1373 | 1374 | In the above example, the greet function takes another function as a parameter (shout and whisper in this case). The function passed as an argument is then called inside the function greet. 1375 | 1376 | Example 03: write your first decorator 1377 | 1378 | ```python 1379 | def logger(func): 1380 | def wrapper(*args, **kwargs): 1381 | print(f"Calling {func.__name__}: {func.__code__.co_argcount}") 1382 | func(*args, **kwargs) 1383 | return wrapper 1384 | 1385 | def another_func(x: int, y: int) -> int: 1386 | return x + y 1387 | 1388 | # we can pass func as a parameter as usual but it's doesn't convenient 1389 | another_func = logger(another_func) 1390 | another_func(1, 2) # Calling another_func: 2 1391 | 1392 | # we can use decorator like this 1393 | # convenient and easy to read 1394 | @logger 1395 | def my_func(x: int, y: int) -> int: 1396 | return x + y 1397 | 1398 | my_func(1, 2) # Calling my_func: 2 1399 | ``` 1400 | 1401 | Example 4: make decorator function return data 1402 | 1403 | ```python 1404 | from functools import wraps 1405 | 1406 | def start_end_decorator(func): 1407 | # wraps ensures docstring, function name, arguments list, etc. are all copied 1408 | # to the wrapped function - instead of being replaced with wrapper's info 1409 | @wraps(func) 1410 | def wrapper(*args, **kwargs): 1411 | print('start') 1412 | result = func(*args, **kwargs) 1413 | print('end') 1414 | return result 1415 | return wrapper 1416 | 1417 | @start_end_decorator 1418 | def hello(name: str): 1419 | return f'Hello {name}' 1420 | 1421 | print(hello("Tuan")) 1422 | ``` 1423 | 1424 | **Chaining Decorator** means decorating a function with multiple decorators 1425 | 1426 | ```python 1427 | from functools import wraps 1428 | 1429 | def logger(func): 1430 | @wraps(func) 1431 | def wrapper(*args, **kwargs): 1432 | print('logger start') 1433 | result = func(*args, **kwargs) 1434 | print('logger end') 1435 | return result 1436 | return wrapper 1437 | 1438 | def acl(func): 1439 | @wraps(func) 1440 | def wrapper(*args, **kwargs): 1441 | print('check permission, valid, continue') 1442 | result = func(*args, **kwargs) 1443 | print('after check permission') 1444 | return result 1445 | return wrapper 1446 | 1447 | @logger 1448 | @acl 1449 | def add_transaction(amount: float, reason: str): 1450 | print(f'{amount =}, {reason =}') 1451 | return "random_transaction_id" 1452 | 1453 | add_transaction(120.123, "Test") 1454 | 1455 | # logger start 1456 | # check permission, valid, continue 1457 | # amount =120.123, reason ='Test' 1458 | # after check permission 1459 | # logger end 1460 | ``` 1461 | 1462 | **Decorator and classes** 1463 | 1464 | Example 01: inject decorators function to class methods 1465 | 1466 | ```python 1467 | # tracking.py 1468 | from functools import wraps 1469 | 1470 | def tracking(func): 1471 | 1472 | @wraps(func) 1473 | def wrapper(self, *args, **kwargs): 1474 | # call the original func 1475 | result = func(self, *args, **kwargs) 1476 | print(f'[{func.__name__}]: items in cart of "{self.user_id}": {len(self.items)}') 1477 | return result 1478 | return wrapper 1479 | 1480 | # cart_item.py 1481 | class CartItem: 1482 | def __init__(self, product_id: str, quantity: int) -> None: 1483 | self.product_id = product_id 1484 | self.quantity = quantity 1485 | 1486 | # shopping_cart.py 1487 | from cart_item import CartItem 1488 | from typing import List 1489 | from utils.tracking import tracking 1490 | 1491 | class ShoppingCart: 1492 | def __init__(self, user_id: str) -> None: 1493 | self.user_id = user_id 1494 | self.items: List[CartItem] = [] 1495 | 1496 | @tracking 1497 | def add_item(self, item: CartItem) -> None: 1498 | self.items.append(item) 1499 | print(f'Added item {item.product_id} to cart of user {self.user_id}') 1500 | 1501 | @tracking 1502 | def remove_item(self, item: CartItem) -> None: 1503 | self.items.remove(item) 1504 | print(f'Removed item {item.product_id} from cart of user {self.user_id}') 1505 | 1506 | @tracking 1507 | def checkout(self): 1508 | print(f'Checking out {len(self.items)}') 1509 | self.items.clear() 1510 | 1511 | # main.py 1512 | from cart_item import CartItem 1513 | from shopping_cart import ShoppingCart 1514 | 1515 | cart = ShoppingCart("user_01") 1516 | cart_item_1 = CartItem("sku_01", 2) 1517 | cart_item_2 = CartItem("sku_02", 5) 1518 | cart.add_item(cart_item_1) 1519 | cart.add_item(cart_item_2) 1520 | cart.remove_item(cart_item_1) 1521 | cart.checkout() 1522 | 1523 | # Added item sku_01 to cart of user user_01 1524 | # [add_item]: items in cart of "user_01": 1 1525 | # Added item sku_02 to cart of user user_01 1526 | # [add_item]: items in cart of "user_01": 2 1527 | # Removed item sku_01 from cart of user user_01 1528 | # [remove_item]: items in cart of "user_01": 1 1529 | # Checking out 1 1530 | # [checkout]: items in cart of "user_01": 0 1531 | ``` 1532 | 1533 | Example 02: use decorator to manipulate function result 1534 | 1535 | ```python 1536 | # product.py 1537 | class Product: 1538 | """ 1539 | Product class 1540 | Use multiple lines comment to annotate the class 1541 | """ 1542 | def __init__(self, id: str, name:str, price: float) -> None: 1543 | """ 1544 | init an instance 1545 | 1546 | Parameters: 1547 | id: str 1548 | name: str 1549 | price: float 1550 | 1551 | Returns: 1552 | None 1553 | """ 1554 | self.id = id 1555 | self.name = name 1556 | self.price = price 1557 | 1558 | # cart_item.py 1559 | class CartItem: 1560 | def __init__(self, product_id: str, quantity: int) -> None: 1561 | self.product_id = product_id 1562 | self.quantity = quantity 1563 | 1564 | # utils.py 1565 | from functools import wraps 1566 | from cart_item import CartItem 1567 | from product import Product 1568 | from typing import Callable, TypeVar 1569 | 1570 | T = TypeVar("T", bound=Callable[[Product], Product]) 1571 | 1572 | # decorator function receive CartItem and return Product 1573 | def extract_product(func: T) -> Callable[[CartItem], Product]: 1574 | @wraps(func) 1575 | def wrapper(cart_item: CartItem, *args, **kwargs) -> Product: 1576 | if not hasattr(cart_item, "product_id"): 1577 | raise ValueError("cart_item must have product_id attribute") 1578 | if not hasattr(cart_item, "quantity"): 1579 | raise ValueError("cart_item must have quantity attribute") 1580 | product = Product(cart_item.product_id, "fake_name", 0.0) 1581 | return func(product, *args, **kwargs) 1582 | return wrapper 1583 | 1584 | # main.py 1585 | from cart_item import CartItem 1586 | from product import Product 1587 | from utils import extract_product 1588 | 1589 | cart = ShoppingCart("user_01") 1590 | cart_item_1 = CartItem("sku_01", 2) 1591 | cart_item_2 = CartItem("sku_02", 5) 1592 | 1593 | # return product object with decorator function 1594 | @extract_product 1595 | def process_checkout(product: Product): 1596 | print(f"Checking out product {product.id}") 1597 | 1598 | # passing cart_item object 1599 | process_checkout(cart_item_1) # Checking out product sku_01 1600 | process_checkout(cart_item_2) # Checking out product sku_02 1601 | ``` 1602 | 1603 | #### Generators 1604 | 1605 | Generators are defined using the `yield` keyword. When a generator function is called, it returns a generator object, but the function's code is not actually run until you iterate over the generator (e.g for loop). 1606 | 1607 | Each time `yield` is encountered, the function's state is saved, and the yielded value is returned. The function can then be resumed from where it left off when the next value is requested. 1608 | 1609 | ```python 1610 | def simple_generator(): 1611 | print('first yield') 1612 | yield 1 1613 | print('second yield') 1614 | yield 2 1615 | print('third yield') 1616 | yield 3 1617 | 1618 | # using generator 1619 | gen = simple_generator() 1620 | 1621 | print(f'received {next(gen)}') 1622 | print(f'received {next(gen)}') 1623 | print(f'received {next(gen)}') 1624 | print(f'received {next(gen)}') 1625 | ``` 1626 | 1627 | Explain example from above: 1628 | 1629 | 1. first call `next(gen)` 1630 | * print first yield 1631 | * encountered `yield 1`, pausese and return 1 to the caller 1632 | * the state of the generator is saved right after `yield 1` 1633 | 2. second call `next(gen)` 1634 | * the generator resume from where it pauseed, after `yield 1` 1635 | * print second yield 1636 | * encountered `yield 2`, pausese and return 2 to the caller 1637 | * the state of the generator is saved right after `yield 2` 1638 | 3. third call `next(gen)` 1639 | * the generator resume from where it pauseed, after `yield 2` 1640 | * print third yield 1641 | * encountered `yield 3`, pausese and return 2 to the caller 1642 | * the state of the generator is saved right after `yield 3` 1643 | 4. fourth call `next(gen)` 1644 | * there is nothing left in the generator so it will raise StopIteration exception 1645 | 1646 | **Key points about yield** 1647 | 1648 | * State preservation: Unlike a regular function call, when you call a generator, the state of the function is preserved between **yield** statements, allowing the function to continue where it left off. 1649 | * Lazy evaluation: The generator does not compute all its values at once. Instead, it generates each value on-the-fly when requested, which can save memory 1650 | * Return vs Yield: While return exist a function completely, yield pause the function, allow it to resume later 1651 | 1652 | Generators are memory-efficient because they only load the data needed to process the next value in the iterable. This allows them to perform operations on otherwise prohibitively large value rangs. 1653 | 1654 | Note: `range` replaces `xrange` in Python 3 1655 | 1656 | ```python 1657 | def fibonacci_generator(limit: int): 1658 | a, b = 0, 1 1659 | while a < limit: 1660 | yield a 1661 | a, b = b, a + 1 1662 | 1663 | for number in fibonacci_generator(10): 1664 | print(number) 1665 | ``` 1666 | 1667 | Benefit of generators 1668 | 1669 | * Memory Efficiency: Generators are memory-efficient since they produre items once at a time and do not store the entire sequence in memory 1670 | * Lazy Evaluation: Generators allow for lazy evalation, meaning values are computed as needed, which is useful when working with large datasets or infinite sequences. 1671 | * Simplified Code: Generators can simplify code by eliminating the need for complex iterator logic 1672 | 1673 | ```python 1674 | # reading large file with generator 1675 | def read_large_file(path: str): 1676 | with open(path, 'r') as file: 1677 | for line in file: 1678 | yield line.strip() 1679 | 1680 | for contact in read_large_file('contacts.txt'): 1681 | print(contact) 1682 | ``` 1683 | 1684 | #### Lambda/anonymous functions 1685 | 1686 | An anonymous function in Python is a function without a name. It can be immediately invoked or stored in a variable. 1687 | 1688 | Anonymous functions in Python are also known as lambda functions. 1689 | 1690 | ```python 1691 | # syntax 1692 | lambda parameter(s) : expression 1693 | 1694 | price = 1.99 1695 | 1696 | # note that lambda has no name, we assign lambda function to variable revenue 1697 | # so that we can easily invoke the function through the variable. 1698 | revenue = lambda quantity: quantity * price 1699 | 1700 | revenue(10) # 19.9 1701 | 1702 | # Lambda functions can take any number of arguments: 1703 | sum = lambda a, b: a + b 1704 | sum(2, 3) 1705 | 1706 | # lamda function does not support type annotation 1707 | sum = lambda a: int, b: int: a + b # SyntaxError: invalid syntax 1708 | sum(2, 3) 1709 | 1710 | # you can invoke lambda function directly like this 1711 | (lambda a,b : a + b)(2, 5) #7 1712 | 1713 | # lambda function always return data without `return` keyword 1714 | # it will return None by default if you don't provide any data 1715 | print((lambda name : print(f'Hi {name}'))("Tuan")) # Hi Tuan, None 1716 | 1717 | # when to use `lambda` over `function`? 1718 | # you want to use function just once, especially useful when working with `map, reduce, filter` 1719 | # you need a short function which never consider to reuse 1720 | # you don't want to write explicit function 1721 | numbers = [1, 3, 5, 7, 9] 1722 | double_result = map(lambda value: value + value, numbers) 1723 | 1724 | print(list(double_result)) 1725 | # [2, 6, 10, 14, 18] 1726 | ``` 1727 | 1728 | #### Closures functions 1729 | 1730 | A closure is a function object that remembers values in enclosing scopes even if they are not present in memory. Closures can capture an carry some variables from the outer function to the inner function. 1731 | 1732 | This can be useful for creating function factories or function decorators. 1733 | 1734 | How closures works? 1735 | 1736 | 1. Nested Function 1737 | A function defined inside another function 1738 | 2. Free variables 1739 | Variables from the outer scope that the inner function can reference 1740 | 3. Returning the Inner Function 1741 | The outer function returns the inner function, which captures the state of the outer function's variables 1742 | 1743 | ```python 1744 | def outer_function(name: str): 1745 | def inner_function(): 1746 | print(f'Hello {name}') 1747 | return inner_function 1748 | 1749 | # create a closure 1750 | closure = outer_function('Tuan') 1751 | closure() 1752 | 1753 | # another example 1754 | def outer_function(): 1755 | count: int = 0 1756 | def inner_function(): 1757 | nonlocal count 1758 | count += 1 1759 | print(count) 1760 | return inner_function 1761 | 1762 | # create a closure 1763 | closure = outer_function() 1764 | closure() # 1 1765 | closure() # 2 1766 | closure() # 3 1767 | ``` 1768 | 1769 | Closures are useful for creating functions with some preset configurations. 1770 | 1771 | ```python 1772 | def make_multiplier(multiplier: int): 1773 | def multiplier_func(input: int) -> int: 1774 | return input * multiplier 1775 | return multiplier_func 1776 | 1777 | # create multiplier functions 1778 | double = make_multiplier(2) 1779 | triple = make_multiplier(3) 1780 | quad = make_multiplier(4) 1781 | 1782 | print(double(5)) # 10 1783 | print(double(8)) # 16 1784 | 1785 | print(triple(5)) # 15 1786 | print(triple(8)) # 24 1787 | 1788 | print(quad(5)) # 20 1789 | print(quad(8)) # 32 1790 | ``` 1791 | 1792 | Key points about closures 1793 | 1794 | * State Retention: Closures retain the state of the variables in the outer function's scope 1795 | * Encapsulation: Closures can encapsulate functionality and data together 1796 | * Function Factories: Closures are ofent used to craete functions with specific behaviors preset 1797 | 1798 | Avoiding common pitfalls 1799 | 1800 | 1. Mutable variable: be cautious with mutable variables in closures as they can lead to unexpected behaviors 1801 | 2. Late binding: Python uses late binding for closures, meaning the values of variables are looked up when the inner function is called. 1802 | This can cause issues if the variable values change after the closure is created. You can avoid this by using default arguments: 1803 | 1804 | ```python 1805 | def outer_func(numbers): 1806 | return [lambda x, n = n: x + n for n in numbers] 1807 | 1808 | closures = outer_func([1, 2, 3]) 1809 | print([func(2) for func in closures]) # [3, 4, 5] 1810 | ``` 1811 | 1812 | Real world examples of closures 1813 | 1814 | ```python 1815 | from enum import Enum 1816 | 1817 | class LogLevel(Enum): 1818 | DEBUG = "DEBUG" 1819 | INFO = "INFO" 1820 | ERROR = "ERROR" 1821 | 1822 | def create_logger(level: LogLevel): 1823 | def logger(message: str): 1824 | print(f"[{level.value}] {message}") 1825 | return logger 1826 | 1827 | info_logger = create_logger(LogLevel.INFO) 1828 | error_logger = create_logger(LogLevel.ERROR) 1829 | 1830 | info_logger('Something goes wrong') # [INFO] Something goes wrong 1831 | info_logger('Eiusmod mollit proident mollit laboris duis qui ut sint.') # [INFO] Eiusmod mollit proident mollit laboris duis qui ut sint. 1832 | 1833 | error_logger('Something goes wrong') # [ERROR] Something goes wrong 1834 | ``` 1835 | 1836 | Simple caching mechanism to store previously computed values 1837 | 1838 | ```python 1839 | def cache_func(func): 1840 | cache = {} 1841 | def wrapper(n): 1842 | if n not in cache: 1843 | cache[n] = func(n) 1844 | return cache[n] 1845 | return wrapper 1846 | 1847 | @cache_func 1848 | def fibonacci(n): 1849 | if n < 2: 1850 | return n 1851 | return fibonacci(n - 1) + fibonacci(n - 2) 1852 | 1853 | print(fibonacci(10)) # 55 1854 | print(fibonacci(23)) # 28657 1855 | ``` 1856 | 1857 | ### Modules 1858 | 1859 | A Python module is a file containing Python code. It can define functions, classes and include runnable code. 1860 | 1861 | Modules allow you to organize your code into manageable parts and reuse it across different programs. 1862 | 1863 | #### Creating a module 1864 | 1865 | Simply create a file with a `.py` extension. 1866 | 1867 | ```python 1868 | # transaction.py 1869 | 1870 | # variable in modules 1871 | mock = { 1872 | "tranId": "random_id", 1873 | "amount": 0.0, 1874 | "tax": 0.0, 1875 | "address": "123 Sub St", 1876 | } 1877 | 1878 | def add(amount: int, tax: float, address: str) -> str: 1879 | return f'tranId: random_id, {amount =}, {tax =}, {address =}' 1880 | 1881 | def update(transId: str, amount: int, tax: float, address: str) -> str: 1882 | return f'updated. tranId: {transId}, {amount =}, {tax =}, {address =}' 1883 | ``` 1884 | 1885 | #### Using a module 1886 | 1887 | To use a module, import it into your script using the `import` statement 1888 | 1889 | ```python 1890 | import transaction 1891 | 1892 | print(transaction.mock) # {'tranId': 'random_id', 'amount': 0.0, 'tax': 0.0, 'address': '123 Sub St'} 1893 | print(transaction.add(10, 20.0, "nowhere")) 1894 | print(transaction.update("011010", 10, 20.0, "nowhere")) 1895 | ``` 1896 | 1897 | #### Importing specific items 1898 | 1899 | You can import specific functions or classes from a module, separated by commas 1900 | 1901 | ```python 1902 | from transaction import add, update 1903 | 1904 | print(add(10, 20.0, "nowhere")) 1905 | print(update("011010", 10, 20.0, "nowhere")) 1906 | ``` 1907 | 1908 | You can also give an imported item an alias 1909 | 1910 | ```python 1911 | from transaction import update as modify 1912 | 1913 | print(modify("011010", 10, 20.0, "nowhere")) 1914 | ``` 1915 | 1916 | #### The `__name__` and `__main__` 1917 | 1918 | In Python, the special variable `__name__` is used to determine if a module is being run as the main program or if it has been imported into another module 1919 | 1920 | ```python 1921 | # greeting.py 1922 | 1923 | def greet(name: str) -> None: 1924 | print(f'Hello {name}') 1925 | 1926 | if __name__ == "__main__": 1927 | greet("Main") 1928 | 1929 | # When you execute greeting.py module, it will print Hello Main 1930 | # However if you import greeting.py from another script, the code inside the 1931 | # `if __name__ == "__main__":` block does not execute 1932 | ``` 1933 | 1934 | #### Find you which functions and attributes are defined in a module 1935 | 1936 | ```python 1937 | # greeting.py 1938 | age = 10 1939 | def greet(name: str) -> None: 1940 | print(f'Hello {name}') 1941 | 1942 | if __name__ == "__main__": 1943 | greet("Main") 1944 | 1945 | # main.py 1946 | import greeting 1947 | 1948 | dir(greeting) 1949 | # ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'age', 'greet'] 1950 | ``` 1951 | 1952 | #### Module priority 1953 | 1954 | Python prefer local module over modules come from different places (included Python built-in modules) 1955 | 1956 | If you have `math.py` module in the same place with your current script 1957 | 1958 | It will be loaded instead of the built-in Python module 1959 | 1960 | ```python 1961 | # math.py 1962 | 1963 | def add(a: int, b: int) -> int: 1964 | return a + b 1965 | 1966 | # main.py 1967 | import math # this is math.py because it have higher priority 1968 | 1969 | print(math.add(1, 2)) 1970 | ``` 1971 | 1972 | #### Built-in modules 1973 | 1974 | There are several built-in modules in Python, which you can import whenever you like, you can find them in here: https://docs.python.org/3/py-modindex.html 1975 | 1976 | ```python 1977 | import platform 1978 | import math 1979 | import io 1980 | 1981 | print(platform.system()) 1982 | print(math.sqrt(12)) 1983 | print(io.open("contacts.txt").readline()) 1984 | ``` 1985 | 1986 | ### Classes 1987 | 1988 | Python is an Object Oriented Programming language. 1989 | 1990 | Almost everything in Python is an object with its properties and methods. 1991 | 1992 | A class is like an object constructor or a blueprint for creating objects. 1993 | 1994 | #### Creating a class 1995 | 1996 | To create a class, use `class` keyword 1997 | 1998 | ```python 1999 | # order.py 2000 | class Order: 2001 | 2002 | # A class attribute. It is shared by all instances of this class 2003 | TAG: str = "ORDER" 2004 | 2005 | # basic initializer, this is called when this class is instantiated. 2006 | # methods (or objects or attributes) like: __init__, __str__, __repr__ etc... are called 2007 | # special methods (sometime called dunder methods) 2008 | def __init__(self, id): 2009 | # assign the argument to the instance's id attribute 2010 | self.id = id 2011 | # the leading underscore indicate the `status` property 2012 | # is intended to be used internally 2013 | # do not rely on this to be enforced: it's a hint to other devs 2014 | self._status = 0 2015 | 2016 | # `__repr__` method is intended to provide a "representation" of the object 2017 | # that is useful for debugging and development purposes. 2018 | def __repr__(self): 2019 | return f'{self.id = }, {self.amount = }, {self.status = }' 2020 | 2021 | # `__str__` method, on the other hand, is intended to provide a "string" 2022 | # representation of the object that is more user-friendly and suitable 2023 | # for display to end users 2024 | def __str__(self): 2025 | return f"Order {self.id}, amount: {self.amount}, status: {self.status}" 2026 | 2027 | # an instance method 2028 | # all method take `self` as the first argument 2029 | # `self` parameter is a reference to the current instance of the class, 2030 | # and is used to access variables that belongs to the class 2031 | def place(self, amount:float, address: str) -> None: 2032 | self.amount = amount 2033 | print(f'Placed order {self.id}, {amount}, {address}') 2034 | 2035 | # a class method is shared among all instances 2036 | # they are called with the calling class as the first argument 2037 | @classmethod 2038 | def get_id(cls): 2039 | return cls.id 2040 | 2041 | # a static method is called without a class or instance reference 2042 | @staticmethod 2043 | def payment_method(): 2044 | return "Paypal" 2045 | 2046 | # property is just like a getter 2047 | # it turns the method status() into a read-only attribute of the same name 2048 | # there's no need to write trivial getters and setters in Python 2049 | @property 2050 | def status(self): 2051 | return self._status 2052 | 2053 | # this allows the @property to be set 2054 | # `status` is the @property that we define from above 2055 | @status.setter 2056 | def status(self, status): 2057 | self._status = status 2058 | 2059 | # this allows the property to be deleted 2060 | @status.deleter 2061 | def status(self): 2062 | del self._status 2063 | 2064 | # when a Python interpreter reads a source file it executes all its code. 2065 | # this __name__ check makes sure this code block is only executed when 2066 | # this module is the main program 2067 | if __name__ == "__main__": 2068 | print("Hello from main") 2069 | 2070 | # main.py 2071 | from order import Order 2072 | 2073 | first_order = Order("000001") 2074 | 2075 | # access class attribute 2076 | first_order.TAG # ORDER 2077 | 2078 | # modify class attribute 2079 | first_order.TAG = "USER" 2080 | 2081 | # reassign value of object property 2082 | first_order.amount = 200.123 2083 | 2084 | # get object property 2085 | print(first_order.amount) 2086 | # invoke property (getter) 2087 | print(first_order.status) 2088 | # assigning properter (setter) 2089 | first_order.status = 1 2090 | 2091 | # delete property from object 2092 | del first_order.amount 2093 | del first_order.status 2094 | 2095 | # delete object 2096 | del first_order 2097 | 2098 | # invoke class method 2099 | first_order.place(10.0, "123 Main St") # Placed order 000001, 10.0, 123 Main St 2100 | first_order.get_id() # 000000 2101 | 2102 | # invoke static method via instance 2103 | print(first_order.payment_method()) # Paypal 2104 | 2105 | # invoke static method via class name 2106 | print(Order.payment_method()) # Paypal 2107 | ``` 2108 | 2109 | #### Static attributes 2110 | 2111 | Static attributes also known as **class attributes**, in Python **can be modified** because they are simply variables with the class object rather than with instances of the class. 2112 | 2113 | In Python, variables do not have inherent immutability, their mutability is determinded by the type of the object they refer to. Static attributes and modifiable both through the class itself and through its instance 2114 | 2115 | In Python, immutability is not enforced on **attributes** or **variables**. Instead, it's a characteristic of the data types: 2116 | 2117 | * **Immutable types**: int, float, str, tuple etc... 2118 | * **Mutable types**: list, dict, set, etc... 2119 | 2120 | When a static attribute refers to a mutable object, changes to the object will be reflected wherever the reference is accessed. If the static attribute itself is reassigned, it affects instance because they all refer to the same class attribute. 2121 | 2122 | To ensure static attribute are immutable we have one possible way: 2123 | 2124 | 1. Convention: Use naming conventions to indicate that an attribute should not be modified (e.g STATIC_ATTRIBUTE, TAG, UPPERCASE_VARIABLE) 2125 | 2126 | #### Inheritance 2127 | 2128 | Inheritance allows us to define a class that inherits all the methods and properties from another class. 2129 | 2130 | **Parent class** is the class being inherited from, also called base class 2131 | 2132 | **Child class** is the class that inherites from **Parent class**, also called derived class 2133 | 2134 | ```python 2135 | # Parent class 2136 | class Person: 2137 | def __init__(self, *, first_name: str, last_name: str): 2138 | self.first_name = first_name 2139 | self.last_name = last_name 2140 | 2141 | def __str__(self): 2142 | return f'{self.first_name} {self.last_name}' 2143 | 2144 | def say_my_name(self): 2145 | print(f'Hi {self.first_name} {self.last_name}') 2146 | 2147 | tuan = Person(first_name = "Tuan", last_name = "Nguyen") 2148 | tuan.say_my_name() # Hi Tuan Nguyen 2149 | 2150 | # Child class 2151 | class Student(Person): 2152 | def __init__(self, *, first_name: str, last_name: str, class_name: str): 2153 | # call parent's `__init__` function to keep inheritance 2154 | Person.__init__(self, first_name = first_name, last_name = last_name) 2155 | # or use `super()` function which have the same result but look nicer 2156 | super().__init__(first_name = first_name, last_name = last_name) 2157 | 2158 | # child class property 2159 | self.class_name = class_name 2160 | 2161 | @property 2162 | def name(self): 2163 | return f'{self.first_name} {self.last_name}' 2164 | 2165 | @name.setter 2166 | def name(self, name: str): 2167 | arr = name.split(" ") 2168 | first_name = arr[0] if len(arr) >= 1 else "default_fname" 2169 | last_name = arr[1] if len(arr) >= 2 else "default_lname" 2170 | self.first_name = first_name 2171 | self.last_name = last_name 2172 | 2173 | def welcome(self): 2174 | print(f'Welcome {self.name} to the class {self.class_name}') 2175 | 2176 | def __str__(self): 2177 | return f'[{self.class_name}]: {self.first_name} {self.last_name}' 2178 | 2179 | student_tuan = Student(first_name = "Anton", last_name = "Nguyen", class_name = "A3") 2180 | # invoke method from parent class 2181 | student_tuan.say_my_name() # Hi Anton Nguyen 2182 | student_tuan.welcome() # Welcome Anton Nguyen to the class A3 2183 | student_tuan.name = "Kim Phuong" 2184 | student_tuan.welcome() # Welcome Kim Phuong to the class A3 2185 | ``` 2186 | 2187 | #### Access Modifiers 2188 | 2189 | Python supports a form of access control for class properties and methods, although it does so in a more informal way compared to some other languages like Java or C++. 2190 | 2191 | Python use naming conventions to indicate the intended level of access: 2192 | 2193 | 1. **Public**: Public attributes and methods are accessible from anywhere. By default, all attributes and methods are public. 2194 | ```python 2195 | class MyClass: 2196 | def __init__(self): 2197 | self.public_var = "I am public" 2198 | def public_method(self): 2199 | print("this is a public method") 2200 | obj = MyClass() 2201 | print(obj.public_var) # I am public 2202 | obj.public_method() # this is a public method 2203 | ``` 2204 | 2. **Protected**: Protected attributes and methods are indicated by a single underscore prefix `_`. These are intended to be accessible within the class and its subclasses but not from outside the class. 2205 | ```python 2206 | class MyClass: 2207 | def __init__(self): 2208 | self._protected_var = "I am protected" 2209 | def _protected_method(self): 2210 | print("this is a protected method") 2211 | 2212 | class SubClass(MyClass): 2213 | def access_protected(self): 2214 | print(self._protected_var) # I am protected 2215 | self._protected_method() # this is a protected method 2216 | 2217 | obj = SubClass() 2218 | obj.access_protected() 2219 | ``` 2220 | 3. **Private**: Private attributes and methods are indicated by a double underscore prefix `__`. This triggers name mangling, where the attribute name is modified to include the class name, making it harder (but not impossible) to access from outside the class. 2221 | ```python 2222 | # example 01 2223 | class MyClass: 2224 | def __init__(self): 2225 | self.__private_var = "I am private" 2226 | def __private_method(self): 2227 | print("this is a private method") 2228 | 2229 | class SubClass(MyClass): 2230 | def access_protected(self): 2231 | print(self.__private_var) # raise AttributeError 2232 | self.__private_method() # raise AttributeError 2233 | 2234 | obj = SubClass() 2235 | obj.access_protected() 2236 | 2237 | # example 02 2238 | # Python doesn't block access to private data, it just leaves for the wisdom of the programmer, 2239 | # not to write any code that access it from outside the class. 2240 | # You can still access the private attributes/methods by Python's name mangling technique 2241 | # but will breaks the encapsulation principle so not recommend to do this in real-life 2242 | class SubClass(MyClass): 2243 | def access_protected(self): 2244 | print(self._MyClass__private_var) # I am protected 2245 | self._MyClass__private_method() # this is a protected method 2246 | 2247 | obj = SubClass() 2248 | obj.access_protected() 2249 | ``` 2250 | 2251 | #### Abstract class 2252 | 2253 | An abstract class can be considered a blueprint for other classes. It allows you to create a set of methods that must be created within any child classes built from the abstract class. 2254 | 2255 | A class that contains one or more abstract methods is called an **abstract class**. An abstract method is a method that has a delcaration but does not have an implementation. 2256 | 2257 | We use abstract class while we are designing large functional units or when we want to provide a common interface for different implementations of a component. 2258 | 2259 | ##### Abstract base classes 2260 | 2261 | By defining an abstract base class, you can define a common **Application Program Interface** (**API**) for a set of subclasses. 2262 | 2263 | By default, Python does not provide abstract classes. Python comes with a module that provides the base for defining **Abstract Base Classes** (**ABC**) and that module name **ABC**. 2264 | 2265 | * Example 01 2266 | 2267 | ```python 2268 | from abc import ABC, abstractmethod 2269 | 2270 | class Polygon(ABC): 2271 | # abstract method will be force to be implement in subclasses 2272 | @abstractmethod 2273 | def noofsides(self): 2274 | pass 2275 | 2276 | class Triangle(Polygon): 2277 | # must concrete implement abstract method 2278 | def noofsides(self): 2279 | print("I have 3 sides") 2280 | 2281 | class Pentagon(Polygon): 2282 | # must concrete implement abstract method 2283 | def noofsides(self): 2284 | print("I have 5 sides") 2285 | 2286 | class Hexagon(Polygon): 2287 | # must concrete implement abstract method 2288 | def noofsides(self): 2289 | print("I have 6 sides") 2290 | 2291 | triangle = Triangle() 2292 | triangle.noofsides() 2293 | 2294 | pentagon = Pentagon() 2295 | pentagon.noofsides() 2296 | 2297 | hexagon = Hexagon() 2298 | hexagon.noofsides() 2299 | ``` 2300 | 2301 | * Example 02 2302 | 2303 | ```python 2304 | from abc import ABC, abstractmethod 2305 | 2306 | class Record(ABC): 2307 | @abstractmethod 2308 | def __str__(self) -> str: 2309 | pass 2310 | 2311 | @abstractmethod 2312 | def create(self) -> bool: 2313 | pass 2314 | 2315 | @abstractmethod 2316 | def update(self, *args) -> bool: 2317 | pass 2318 | 2319 | @abstractmethod 2320 | def delete(self) -> bool: 2321 | pass 2322 | 2323 | @staticmethod 2324 | @abstractmethod 2325 | def TAG(): 2326 | pass 2327 | 2328 | class UserRecord(Record): 2329 | def __init__(self, name: str, bod: str, avatar: str) -> None: 2330 | self.name = name 2331 | self.bod = bod 2332 | self.avatar = avatar 2333 | 2334 | 2335 | def __str__(self) -> str: 2336 | return f"[{UserRecord.TAG()}]: {self.name} {self.bod} {self.avatar}" 2337 | 2338 | def create(self) -> bool: 2339 | # execute sql command to create user 2340 | return True 2341 | 2342 | def update(self, *args) -> bool: 2343 | # execute sql command to update user 2344 | return True 2345 | 2346 | def delete(self) -> bool: 2347 | # execute sql command to delete user 2348 | return True 2349 | 2350 | @staticmethod 2351 | def TAG(): 2352 | return "USER" 2353 | 2354 | class ProductRecord(Record): 2355 | def __init__(self, name: str, price: float, description: str) -> None: 2356 | self.name = name 2357 | self.price = price 2358 | self.description = description 2359 | 2360 | def __str__(self) -> str: 2361 | return f"[{ProductRecord.TAG()}] {self.name} {self.price} {self.description}" 2362 | 2363 | def create(self) -> bool: 2364 | # execute sql command to create product 2365 | return True 2366 | 2367 | def update(self, *args) -> bool: 2368 | # execute sql command to update product 2369 | return True 2370 | 2371 | def delete(self) -> bool: 2372 | # execute sql command to delete product 2373 | return True 2374 | 2375 | @staticmethod 2376 | def TAG(): 2377 | return "PRODUCT" 2378 | 2379 | user_record = UserRecord("John", "1990-01-01", "https://example.com/avatar.jpg") 2380 | print(user_record) # [USER]: John 1990-01-01 https://example.com/avatar.jpg 2381 | user_record.create() 2382 | user_record.update("Johny", "1991-01-01", "https://example.com/avatar2.jpg") 2383 | user_record.delete() 2384 | print(UserRecord.TAG()) # USER 2385 | 2386 | product_record = ProductRecord("iPhone", 1000, "A new phone") 2387 | print(product_record) # [PRODUCT] iPhone 1000 A new phone 2388 | product_record.create() 2389 | product_record.delete() 2390 | print(ProductRecord.TAG()) # PRODUCT 2391 | ``` 2392 | 2393 | ### Polymorphism 2394 | 2395 | Polymorphism means **many forms**, and in programming it refers to methods/functions/operators with the same name that can be executed on many object or classes. 2396 | 2397 | #### Function polymorphism 2398 | 2399 | `len()` is one of many function polymorphism in Python that accept many kind of data 2400 | 2401 | ```python 2402 | # str 2403 | len("Tuan") # 4 2404 | 2405 | # tuple 2406 | len(("apple", "banana", "cherry")) # 3 2407 | 2408 | # list 2409 | len(["apple", "banana", "cherry"]) # 3 2410 | 2411 | # dict 2412 | len({"foo": "bar"}) # 1 2413 | ``` 2414 | 2415 | #### Class polymorphism 2416 | 2417 | Polymorphism is often used in Class methods, where we can have multiple classes with the same method name. 2418 | 2419 | ```python 2420 | # polymorphism method `eat()` appear to all classes 2421 | 2422 | class Dog: 2423 | def __init__(self, name: str): 2424 | self.name = name 2425 | 2426 | def eat(self): 2427 | print('Dog is eating') 2428 | 2429 | class Cat: 2430 | def __init__(self, name: str, age: int): 2431 | self.name = name 2432 | self.age = age 2433 | 2434 | def eat(self): 2435 | print('Cat is eating') 2436 | 2437 | dog = Dog("Grant") 2438 | dog.eat() # Dog is eating 2439 | 2440 | cat = Cat("Banana", 3) 2441 | cat.eat() # Cat is eating 2442 | ``` 2443 | 2444 | #### Inheritance Class Polymorphism 2445 | 2446 | Yes, we could do that 2447 | 2448 | ```python 2449 | class Animal: 2450 | def __init__(self, name: str): 2451 | self.name = name 2452 | 2453 | def eat(self): 2454 | print('Animal is eating') 2455 | 2456 | class Dog(Animal): 2457 | def __init__(self, name: str, age: int): 2458 | super().__init__(name) 2459 | self.age = age 2460 | 2461 | def eat(self): 2462 | print('Dog is eating') 2463 | 2464 | class Cat(Animal): 2465 | def __init__(self, name: str, age: int): 2466 | super().__init__(name) 2467 | self.age = age 2468 | 2469 | def eat(self): 2470 | print('Cat is eating') 2471 | 2472 | dog = Dog("Grant", 5) 2473 | dog.eat() # Dog is eating 2474 | 2475 | cat = Cat("Banana", 3) 2476 | cat.eat() # Cat is eating 2477 | ``` 2478 | 2479 | #### Data classes 2480 | 2481 | [This module](https://docs.python.org/3/library/dataclasses.html) provides a decorator and functions for automatically adding generated special methods such ash `__init__` and `__repr()__` to user-defined classes. 2482 | 2483 | ```python 2484 | from dataclasses import dataclass, field 2485 | 2486 | @dataclass 2487 | class InventoryItem: 2488 | """Class for keeping track of an item in inventory""" 2489 | name: str 2490 | # if the default value of a field is specified by a call to `field()`, then the class attribute 2491 | # for this field will be replaced by the specified `default` value. 2492 | # if `default` is not provided, the the class attribute will be deleted 2493 | unit_price: float = field(repr = False, default = 0.0) 2494 | quantity_on_hand: int = 0 2495 | source: str = field(repr = True, default = "global") 2496 | 2497 | @property 2498 | def total_cost(self) -> float: 2499 | return self.unit_price * self.quantity_on_hand 2500 | 2501 | inventory_product_one = InventoryItem("Product name 01", 1.99, 1000) 2502 | print(inventory_product_one) # InventoryItem(name='Product name 01', quantity_on_hand=1000, source='global') 2503 | print(inventory_product_one.total_cost) # 1990.0 2504 | inventory_product_two = InventoryItem("Product name 01", 1.99, 1000) 2505 | 2506 | if inventory_product_one == inventory_product_two: # True 2507 | print("2 product are the same") 2508 | ``` 2509 | 2510 | So we don't need to add an `__init__()` method like this 2511 | 2512 | ```python 2513 | def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0): 2514 | self.name = name 2515 | self.unit_price = unit_price 2516 | self.quantity_on_hand = quantity_on_hand 2517 | ``` 2518 | 2519 | ### Exception handling 2520 | 2521 | Exception handling in Python is a mechanism that allows you to handle errors or exceptional situation in your code gracefully, instead of letting the entire program crash. Python provides a robust and flexible way to handle exceptions using the `try`, `except`, `else` and `finally` blocks. 2522 | 2523 | #### Basic structure of exception handling 2524 | 2525 | The basic structure looks like this 2526 | 2527 | ```python 2528 | try: 2529 | result = 1 + non_exist_variable 2530 | except NameError as e: 2531 | print(e) 2532 | else: 2533 | print("another error") 2534 | finally: 2535 | print("finally - cleanup operation") 2536 | ``` 2537 | 2538 | **Explanation** 2539 | 2540 | 1. try Block: 2541 | * The code that yoy want to execute and that might raise an exception in placed inside the try block 2542 | 2. except Block: 2543 | * If an excetion occurs in the `try` block, the code in the except block is executed. You can catch specific exceptions by specifying the exception type (e.g `except NameError`) 2544 | * The `as e` part allows you to capture the exception object and access its message or other attributes 2545 | 3. else Block: 2546 | * The `else` block is optional and is executed only if no exception was raised in the `try` block 2547 | 4. finally Block: 2548 | * The `finally` block is also optional and is executed regardless of whether an exception occurded or not. This is typically used for cleanup operations like closing files or releasing resources, closing database connection. 2549 | 2550 | #### Handling specific exception 2551 | 2552 | **Here's an example where we handle different types of exception** 2553 | 2554 | ```python 2555 | def divide_number(a, b): 2556 | try: 2557 | result = a / b 2558 | except ZeroDivisionError as e: 2559 | print(f"Error: {e} - Division by zero is not allowed") 2560 | except TypeError as e: 2561 | print(f"Error: {e} - Only numbers are allowed") 2562 | else: 2563 | print(f"Result: {result}") 2564 | finally: 2565 | print("Execution of the divide_number function is completed") 2566 | 2567 | divide_number(10, 0) 2568 | # Error: division by zero - Division by zero is not allowed 2569 | # Execution of the divide_number function is completed 2570 | 2571 | divide_number(10, "a") 2572 | # Error: unsupported operand type(s) for /: 'int' and 'str' - Only numbers are allowed 2573 | # Execution of the divide_number function is completed 2574 | 2575 | divide_number(10, 2) 2576 | # Result: 5.0 2577 | # Execution of the divide_number function is completed 2578 | ``` 2579 | 2580 | **Handle multiple exceptions** 2581 | 2582 | ```python 2583 | try: 2584 | risky_operation() 2585 | except (ValueError, TypeError) as e: 2586 | print(f"Error: {e}") 2587 | ``` 2588 | 2589 | #### Raising exceptions 2590 | 2591 | Sometime you might want to raise an exception manually using the `raise` keyword 2592 | 2593 | ```python 2594 | def check_age(age): 2595 | if age < 0: 2596 | raise ValueError("Age cannot be negative") 2597 | return f"Age is {age}" 2598 | 2599 | try: 2600 | check_age(-1) 2601 | except ValueError as e: 2602 | print(f"Exception '{e}'") 2603 | 2604 | # Exception 'Age cannot be negative' 2605 | ``` 2606 | 2607 | #### Custom exceptions 2608 | 2609 | You can define your own exceptions by creating a new class that inherits from Python's built-in **Exception** class 2610 | 2611 | ```python 2612 | class InsufficientBalanceError(Exception): 2613 | pass 2614 | 2615 | def checkout(total: float, balance: float) -> bool: 2616 | if balance < 0: 2617 | raise InsufficientBalanceError("Negative balance is not allowed") 2618 | if balance < total: 2619 | raise InsufficientBalanceError("Your balance is not enough for checkout") 2620 | return balance >= total 2621 | 2622 | try: 2623 | checkout(29.9, 10) 2624 | except InsufficientBalanceError as e: 2625 | print(f'Error: {e}') 2626 | 2627 | # Error: Your balance is not enough for checkout 2628 | ``` 2629 | 2630 | #### Several built-in Python exceptions that can be raised when an error occurs during the execution of a program 2631 | 2632 | * SyntaxError: raised when the interpreter encouters a syntax error in the code, such as a misspelled keyword, a missing colon, or an unbalanced parenthesis. 2633 | * TypeError: raised when an operation or function is applied to an object of the wrong type, such as adding a string to an integer. 2634 | * NameError: raised when a variable or function name is not found in the current scope 2635 | * IndexError: raised when an index is out of range for a list, tuple, or other sequence types 2636 | * KeyError: raised when a key is not found in a dictionary 2637 | * ValueError: raised when a function or method is called with an invalid argument or input, such as trying to convert a string to an integer when the string does not represent a valid integer 2638 | * AttributeError: raised when an attribute or method is not found on an object, such as trying to access a non-existent attribute of a class instance 2639 | * IOError: raised when an I/O operation, such as reading or writing a file, fails due to an input/output error 2640 | * ZeroDivisionError: raised when an attempt is made to divide a number by zero 2641 | * ImportError: raised when an import statement fails to find or load a module 2642 | 2643 | ### Some useful built-in functions in Python 2644 | 2645 | #### filter() 2646 | 2647 | The `filter()` method filters the given sequence with the help of a function that tests each element in the sequence (sequence included: `sets, lists, tuple and containers of any iterators) to be true or not 2648 | 2649 | `filter()` will return an iterator that is already filtered 2650 | 2651 | ```python 2652 | # filter with custom function 2653 | def filter_high_salary(salary: float) -> bool: 2654 | return salary > 30_000_000.0 2655 | 2656 | # filter a list 2657 | list_salary = [10_000_000.0, 15_000_000.0, 25_000_000.0, 50_000_000.0, 120_000_000.0] 2658 | 2659 | high_salary = filter(filter_high_salary, list_salary) 2660 | 2661 | for salary in high_salary: 2662 | print(f'high salary: {salary}') 2663 | # high salary: 50000000.0 2664 | # high salary: 120000000.0 2665 | 2666 | # filter with lambda 2667 | high_salary = filter(lambda salary: salary > 30_000_000.0, list_salary) 2668 | 2669 | # flter with lambda and custom function 2670 | high_salary = filter(lambda salary: filter_high_salary(salary), list_salary) 2671 | 2672 | # filter a tuple 2673 | person = ("Tuan Nguyen", 35, "Male", "Halong Bay, Quang Ninh, Vietnam") 2674 | 2675 | filtered_int_only = filter(lambda x: isinstance(x, int), person) 2676 | 2677 | print(list(filtered_int_only)) # [35] 2678 | 2679 | # filter a set 2680 | points = (88, 58, 67, 88, 45, 100, 95, 40, 20, 78, 78) 2681 | good_points = filter(lambda point: point >= 80, points) 2682 | print(list(good_points)) # [88, 88, 100, 95] 2683 | 2684 | # filter an iterable 2685 | from typing import Dict, Iterator, Callable 2686 | 2687 | # Define the type for the student data 2688 | StudentInfo = Dict[str, Dict[str, int | str]] 2689 | 2690 | students: StudentInfo = { 2691 | "STD001": { 2692 | "name": "Tuan Nguyen", 2693 | "age": 20 2694 | }, 2695 | "STD002": { 2696 | "name": "Anton", 2697 | "age": 18 2698 | }, 2699 | "STD003": { 2700 | "name": "Jimmy", 2701 | "age": 22 2702 | } 2703 | } 2704 | 2705 | student_iterators = iter(students) 2706 | 2707 | def filter_by_age(key: str) -> bool: 2708 | age = students[key]["age"] 2709 | if isinstance(age, int): 2710 | return age > 20 2711 | return False 2712 | 2713 | filtered_students: Iterator[str] = filter(filter_by_age, student_iterators) 2714 | 2715 | # Convert the filtered students to a dictionary 2716 | filtered_students_dict: StudentInfo = {key: students[key] for key in filtered_students} 2717 | 2718 | # Print the filtered students 2719 | print(filtered_students_dict) 2720 | ``` 2721 | 2722 | ### Multiprocessing 2723 | 2724 | Multiprocessing refers to the ability of a system to support more than one processor at the same time. Application in a multiprocessing system are broken to smaller routines that run independently. The operating system allocates these threads to the processors improving performance of the system. 2725 | 2726 | ![single process](./docs/multiprocessing/chef.png) 2727 | 2728 | ![multi processes](./docs/multiprocessing/multi-chef.png) 2729 | 2730 | [Read more](./docs/multiprocessing/README.md) 2731 | -------------------------------------------------------------------------------- /docs/multiprocessing/README.md: -------------------------------------------------------------------------------- 1 | # Multiprocessing 2 | 3 | ## Theory 4 | 5 | ### What is multiprocessing? 6 | 7 | - Multiprocessing refers to the ability of a system to support more than one processor at the same time. Application in a multiprocessing system are broken to smaller routines that run independently. The operating system allocates these threads to the processors improving performance of the system. 8 | 9 | ### Why multiprocessing? 10 | 11 | - Consider a computer system with a single processor. If it is assigned several processes at the same time, it will have to interrupt each task and switch briefly to another, to keep all the processes going. This situation is just like a chef workin in a kitchen alone. He has to do several tasks like baking, stirring kneading dough, etc. So the gist is that: The more tasks you must do at once, the more difficult it gets to keep track of them all, and keeping the timing right becomes more of a challenge. This is where the concept of mutiprocessing arise! 12 | 13 | ![single-task](./chef.png) 14 | 15 | ### A multiprocessing system can have 16 | 17 | 1. multiprocessor: ie a computer with more than one CPU (some server computer support more than one CPU such dual Xeon CPU) 18 | 2. multi-core processor: ie a single computing component with two or more independant actual processing units (called cores, a Xeon CPU can have 28 to 36 cores per CPU) 19 | 20 | Here the CPU can easily executes several tasks at once, with each task using it own processor. It is just like the chef in last situation being assisted by his assistants. Now, they can divide the tasks among themself and chef doesn't need to switch between his tasks. 21 | 22 | ![multi-tasks](./multi-chef.png) 23 | 24 | ## Example 25 | 26 | ### Multiprocessing in Python 27 | 28 | In Python the [mutiprocessing](https://docs.python.org/3/library/multiprocessing.html) module includes a very simple and intuitive API for dividing work between multiple processes. Let us consider a simple example using `multiprocessing` module. 29 | 30 | #### Example 01 31 | 32 | ```python 33 | import multiprocessing 34 | import time 35 | 36 | 37 | def print_cube(num: float) -> None: 38 | """ 39 | function to print cube of given num 40 | 41 | Parameters: 42 | - num: float 43 | 44 | Returns: 45 | - None 46 | """ 47 | 48 | print("Simulate some work for cube") 49 | time.sleep(10) 50 | print(f"Cube: {num * num * num}") 51 | 52 | 53 | def print_square(num: float) -> None: 54 | """ 55 | function to print square of given num 56 | 57 | Parameters: 58 | - num: float 59 | 60 | Returns: 61 | - None 62 | """ 63 | 64 | print("Simulate some work for square") 65 | time.sleep(15) 66 | print(f"Square: {num * num}") 67 | 68 | 69 | if __name__ == "__main__": 70 | # creating processes 71 | p1 = multiprocessing.Process(target=print_square, args=(10,)) 72 | p2 = multiprocessing.Process(target=print_cube, args=(13,)) 73 | 74 | # starting process 1 75 | p1.start() 76 | 77 | # starting process 2 78 | p2.start() 79 | 80 | # wait until process 1 is finished 81 | p1.join() 82 | 83 | # wait until process 2 is finished 84 | p2.join() 85 | 86 | # both processes finished 87 | print("Done") 88 | 89 | # Result 90 | # 91 | # Simulate some work for cube 92 | # Simulate some work for square 93 | # Cube: 2197 94 | # Square: 100 95 | # Done 96 | ``` 97 | 98 | Explain the example from above: 99 | 100 | * To import the `multiprocessing` module, do: `import multiprocessing` 101 | * To create a process, we create an object of Process class, it take following arguments: 102 | * target: the function to be executed by process 103 | * args: the arguments to be passed to the target function, we passing a tuple of parameters 104 | ```python 105 | p1 = multiprocessing.Process(target=method_name_01, args=(param1, param2)) 106 | p2 = multiprocessing.Process(target=method_name_02, args=(param1,)) 107 | ``` 108 | * to start a process, we use `start` method of `Process` class: `p1.start()` 109 | * once the processes start, the current program also keeps on executing, in order to stop execution of current program until a process is complete, we use `join` method 110 | ```python 111 | p1.join() 112 | p2.join() 113 | ``` 114 | 115 | #### Example 02 116 | 117 | Consider another program to understand the concept of different processes running on the same Python script. In this example, we print the ID of the processes running the target function 118 | 119 | ```python 120 | import multiprocessing 121 | import time 122 | import os 123 | 124 | 125 | def worker01() -> None: 126 | print(f"Simulate some work for worker01. {os.getpid()}") 127 | time.sleep(10) 128 | print("Done worker01") 129 | 130 | 131 | def worker02() -> None: 132 | print(f"Simulate some work for worker02. {os.getpid()}") 133 | time.sleep(10) 134 | print("Done worker02") 135 | 136 | 137 | if __name__ == "__main__": 138 | 139 | print(f"ID of main process: {os.getpid()}") 140 | 141 | # creating processes 142 | p1 = multiprocessing.Process(target=worker01) 143 | p2 = multiprocessing.Process(target=worker02) 144 | 145 | # starting process 1 146 | p1.start() 147 | 148 | # starting process 2 149 | p2.start() 150 | 151 | print(f"ID of process p1: {p1.pid}") 152 | print(f"ID of process p2: {p2.pid}") 153 | 154 | print(f"Process p1 alive: {p1.is_alive()}") 155 | print(f"Process p2 alive: {p2.is_alive()}") 156 | 157 | # wait until process 1 is finished 158 | p1.join() 159 | 160 | # wait until process 2 is finished 161 | p2.join() 162 | 163 | # both processes finished 164 | print("Done") 165 | 166 | # check processes are alive 167 | print(f"Process p1 alive: {p1.is_alive()}") 168 | print(f"Process p2 alive: {p2.is_alive()}") 169 | 170 | # Result 171 | # 172 | # ID of main process: 25392 173 | # ID of process p1: 17192 174 | # ID of process p2: 24004 175 | # Process p1 alive: True 176 | # Process p2 alive: True 177 | # Simulate some work for worker01. 17192 178 | # Simulate some work for worker02. 24004 179 | # Done worker01 180 | # Done worker02 181 | # Done 182 | # Process p1 alive: False 183 | # Process p2 alive: False 184 | ``` 185 | 186 | * The main Python script has a different process ID and multiprocessing module spawns new processes with different process IDs as we create **Process** object **p1**, **p2**. In above program, we use **os.getpid()** function to get ID of process running the current target function. Notice that it matches with the process IDs p1, p2 which we using pid attribute of Process class. 187 | * Each process runs independently and has its own memory space 188 | * As soon as the execution of target function is finished, the processes get terminated. In above program we used `is_alive()` method of **Process** class to check if a process is still active or not. 189 | 190 | Consider the diagram below to understand how new processes are different from main Python script 191 | 192 | ![multiprocessing](./multiprocessing.png) 193 | 194 | ## The 4 Essential Parts of Multiprocessing in Python 195 | 196 | Multiprocessing in Python involes several key components that allow efficient parallel execution of tasks: 197 | 198 | * **Process**: The Process class is used to create and manage independent processes. Each process runs in its own memory space 199 | * **Queue**: The Queue class is a shared job queue that allows process-safe data exchange and coordination between processes. It's used for passing messages or results between process instances. 200 | * **Pipe**: Pipes provide a way to establish a communication channel between processes. They are useful for bidirectional communication between two processes. 201 | * **Lock**: Locks are used to ensure that only process is executing a certain section of code at a time. This prevents data corruption by synchronizing access to shared resources. 202 | 203 | 204 | Example 03 205 | 206 | ```python 207 | from multiprocessing import Lock, Pipe, Queue, Process 208 | from multiprocessing.connection import PipeConnection 209 | from multiprocessing.synchronize import Lock as LockType 210 | from typing import Any 211 | 212 | 213 | def queue_worker(queue: "Queue[str]", lock: LockType): 214 | with lock: 215 | print("Task executed") 216 | queue.put("Done") 217 | 218 | 219 | def pipe_worker(conn: PipeConnection): 220 | conn.send("Data from pipe worker") 221 | conn.close() 222 | 223 | 224 | def consumer(queue: "Queue[Any]", parent_conn: PipeConnection, lock: LockType): 225 | with lock: 226 | print(f"Consumer received from queue: {queue.get()}") 227 | print(f"Consumer received from pipe: {parent_conn.recv()}") 228 | 229 | 230 | if __name__ == "__main__": 231 | """ 232 | Example that emphasize 4 essential key components of multiprocessing 233 | * Process 234 | * Queue 235 | * Pipe 236 | * Lock 237 | """ 238 | # for exchange data between processes safely 239 | queue: "Queue[Any]" = Queue() 240 | 241 | # For synchronization (ensures only one process can access at a time) 242 | lock: LockType = Lock() 243 | 244 | # Pipe for two-way communications between processes 245 | parent_conn, child_conn = Pipe() 246 | 247 | # create separate processes 248 | 249 | # queue base process 250 | queue_process = Process(target=queue_worker, args=(queue, lock)) 251 | # pipe base process 252 | pipe_process = Process(target=pipe_worker, args=(child_conn,)) 253 | # consumer that receive data 254 | consumer_process = Process(target=consumer, args=(queue, parent_conn, lock)) 255 | 256 | # start processes 257 | queue_process.start() 258 | pipe_process.start() 259 | 260 | # wait for both processes to finish 261 | queue_process.join() 262 | pipe_process.join() 263 | 264 | # start the consumer process after data is available 265 | consumer_process.start() 266 | consumer_process.join() 267 | 268 | print("Done") 269 | 270 | # Result 271 | # 272 | # Task executed 273 | # Consumer received from queue: Done 274 | # Consumer received from pipe: Data from pipe worker 275 | # Done 276 | ``` 277 | 278 | Example 04 279 | 280 | ```python 281 | from multiprocessing import Queue, Pipe, Process, Lock 282 | from multiprocessing.connection import PipeConnection 283 | from multiprocessing.synchronize import Lock as LockType 284 | import time 285 | import random 286 | from typing import List, Tuple 287 | 288 | 289 | def scrape_website( 290 | url: str, 291 | queue: "Queue[Tuple[str, str]]", 292 | child_conn: PipeConnection, 293 | lock: LockType, 294 | ): 295 | print(f"Scraping {url}") 296 | scrape_time = random.randint(1, 10) 297 | time.sleep(scrape_time) 298 | data = f"Scraped data in {scrape_time} seconds" 299 | # Using lock to ensure only one process write data to share resource 300 | with lock: 301 | print(f"Scraped {url}") 302 | 303 | # send data to the main process 304 | queue.put((url, data)) 305 | 306 | # send metadata via Pipe 307 | child_conn.send((url, data)) 308 | child_conn.close() 309 | 310 | 311 | def aggregate_data( 312 | queue: "Queue[Tuple[str, str]]", 313 | parent_conn: PipeConnection, 314 | lock: LockType, 315 | websites: List[str], 316 | ) -> None: 317 | results = [] 318 | times = [] 319 | 320 | for url in websites: 321 | web, data = queue.get() 322 | results.append((web, data)) 323 | 324 | web_url, scrape_time = parent_conn.recv() 325 | times.append((web_url, scrape_time)) 326 | 327 | # safely print the aggregate data using Lock 328 | with lock: 329 | print("---Aggregated result---") 330 | for url, data in results: 331 | print(f"{url}: {data}") 332 | 333 | for url, scrape_time in times: 334 | print(f"{url}: {scrape_time} seconds") 335 | 336 | 337 | if __name__ == "__main__": 338 | websites = ["google.com", "facebook.com", "x.com", "github.com"] 339 | 340 | queue: "Queue[Tuple[str, str]]" = Queue() 341 | parent_conn, child_conn = Pipe() 342 | lock = Lock() 343 | 344 | processes = [] 345 | 346 | for url in websites: 347 | p = Process(target=scrape_website, args=(url, queue, child_conn, lock)) 348 | processes.append(p) 349 | p.start() 350 | 351 | # wait for all processes to finish 352 | for p in processes: 353 | p.join() 354 | 355 | aggregate_process = Process( 356 | target=aggregate_data, args=(queue, parent_conn, lock, websites) 357 | ) 358 | aggregate_process.start() 359 | aggregate_process.join() 360 | print("Done") 361 | 362 | # Result 363 | # 364 | # Scraping google.com 365 | # Scraping facebook.com 366 | # Scraping x.com 367 | # Scraping github.com 368 | # Scraped facebook.com 369 | # Scraped x.com 370 | # Scraped google.com 371 | # Scraped github.com 372 | # ---Aggregated result--- 373 | # facebook.com: Scraped data in 2 seconds 374 | # x.com: Scraped data in 5 seconds 375 | # google.com: Scraped data in 9 seconds 376 | # github.com: Scraped data in 10 seconds 377 | # facebook.com: Scraped data in 2 seconds seconds 378 | # x.com: Scraped data in 5 seconds seconds 379 | # google.com: Scraped data in 9 seconds seconds 380 | # github.com: Scraped data in 10 seconds seconds 381 | # Done 382 | ``` 383 | 384 | 385 | ## Advance section 386 | 387 | Multiprocessing support two types of communication channel between processes: 388 | 389 | * Queue: share data between multiple processes (slightly slow, multiple workers sending results to a single aggregator) 390 | * Pipe: share data between two processes only (fast, one process send data and another process receive) 391 | 392 | ### Queue 393 | 394 | A simple way to communicate between processes, pass messages back and forth between many processes. Any Python object can pass through a Queue. 395 | 396 | Queue acts like FIFO (First In First Out), it useful when multiple processes need to share data. The mechanism is bidirection (multiple producers, multiple consumers). 397 | 398 | Queue is slow since it need to synchronize data internally to ensure any process can access. 399 | 400 | You don't need **Lock** with **Queue** since Queue is thread-safe (process safe). It mean Queue implement internal locking mechanism to manage concurrent access by multiple processes. So Queue automatically ensure **only one** process read from or write to queue at a time. 401 | 402 | You only need **Lock** when dealing with **shared resources** like modify shared variables, write the same file, printing to console (something that not inheritantly thread-safe) 403 | 404 | ![Multiprocessing Queue diagram](./multiprocess-queue-diagram.png) 405 | 406 | Example 01: without `Lock` 407 | 408 | ```python 409 | from multiprocessing import Queue, Process 410 | from typing import List 411 | 412 | """ 413 | Queue example without Lock since it is thread-safe 414 | """ 415 | 416 | def square_list(mylist: List[int], queue: "Queue[int]"): 417 | for item in mylist: 418 | # put data type int to queue 419 | queue.put(item**2) 420 | 421 | 422 | def print_queue(queue: "Queue[int]"): 423 | while not queue.empty(): 424 | print(queue.get()) 425 | print("Queue is now empty") 426 | 427 | 428 | if __name__ == "__main__": 429 | queue: "Queue[int]" = Queue() 430 | myList = [1, 4, 5, 6, 7, 4] 431 | p1 = Process(target=square_list, args=(myList, queue)) 432 | p2 = Process(target=print_queue, args=(queue,)) 433 | 434 | # running process p1 to square the list 435 | p1.start() 436 | p1.join() 437 | 438 | # running process p2 to get queue elements to print 439 | p2.start() 440 | p2.join() 441 | 442 | print("All Done") 443 | 444 | # Result 445 | # 446 | # 1 447 | # 16 448 | # 25 449 | # 36 450 | # 49 451 | # 16 452 | # Queue is now empty 453 | # All Done 454 | ``` 455 | 456 | Example 02: with `Lock` 457 | 458 | ```python 459 | from multiprocessing import Process, Queue, Lock 460 | from multiprocessing.synchronize import Lock as LockType 461 | import time 462 | from random import randint 463 | 464 | 465 | # Worker function that sends data to the Queue 466 | def worker_with_queue(queue: "Queue[int]", lock: LockType) -> None: 467 | result = randint(1, 100) # Simulate some work by generating a random number 468 | time.sleep(randint(1, 3)) # Simulate task duration 469 | queue.put(result) # Send the result to the shared Queue 470 | 471 | # Protect console output with Lock 472 | with lock: 473 | print(f"Worker {result} sent data to the queue.") 474 | 475 | 476 | # Aggregator function that collects results from the Queue 477 | def aggregator_with_queue( 478 | queue: "Queue[int]", lock: LockType, num_workers: int 479 | ) -> None: 480 | results = [] 481 | for _ in range(num_workers): 482 | result = queue.get() # Collect results from the Queue 483 | results.append(result) 484 | 485 | # Protect console output with Lock 486 | with lock: 487 | print(f"Aggregator received {result} from the queue.") 488 | print("Final aggregated results from Queue:", results) 489 | 490 | 491 | if __name__ == "__main__": 492 | queue: "Queue[int]" = Queue() # Queue is process-safe, no Lock needed 493 | lock = Lock() # Lock for protecting console output 494 | num_workers = 3 495 | processes = [ 496 | Process(target=worker_with_queue, args=(queue, lock)) 497 | for _ in range(num_workers) 498 | ] 499 | 500 | # Start all worker processes 501 | for p in processes: 502 | p.start() 503 | 504 | # Start the aggregator to collect results 505 | aggregator = Process(target=aggregator_with_queue, args=(queue, lock, num_workers)) 506 | aggregator.start() 507 | 508 | # Wait for all processes to finish 509 | for p in processes: 510 | p.join() 511 | aggregator.join() 512 | 513 | # Result 514 | # 515 | # Worker 38 sent data to the queue. 516 | # Aggregator received 38 from the queue. 517 | # Worker 38 sent data to the queue. 518 | # Aggregator received 38 from the queue. 519 | # Worker 77 sent data to the queue. 520 | # Aggregator received 77 from the queue. 521 | # Final aggregated results from Queue: [38, 38, 77] 522 | ``` 523 | 524 | ### Pipe 525 | 526 | A pipe can have only two endpoints. Hence it is preffered over Queue when only two-way communication is required. 527 | 528 | Pipe acts like socket or direct channel between TWO processes. Useful when two process need to communicate directly, usually unindirectional but can be bidirection with two connections. 529 | 530 | Pipe is fast since it just communicate between TWO processes. 531 | 532 | ![Multiprocessing Pipe diagram](./multiprocessing-pipe-diagram.png) 533 | 534 | ```python 535 | ``` 536 | -------------------------------------------------------------------------------- /docs/multiprocessing/chef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anhtuank7c/learn-python/5104aafd2555dc395268f87c514f5f74259c34ad/docs/multiprocessing/chef.png -------------------------------------------------------------------------------- /docs/multiprocessing/example/example01.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import time 3 | 4 | 5 | def print_cube(num: float) -> None: 6 | """ 7 | function to print cube of given num 8 | 9 | Parameters: 10 | - num: float 11 | 12 | Returns: 13 | - None 14 | """ 15 | 16 | print("Simulate some work for cube") 17 | time.sleep(10) 18 | print(f"Cube: {num * num * num}") 19 | 20 | 21 | def print_square(num: float) -> None: 22 | """ 23 | function to print square of given num 24 | 25 | Parameters: 26 | - num: float 27 | 28 | Returns: 29 | - None 30 | """ 31 | 32 | print("Simulate some work for square") 33 | time.sleep(15) 34 | print(f"Square: {num * num}") 35 | 36 | 37 | if __name__ == "__main__": 38 | # creating processes 39 | p1 = multiprocessing.Process(target=print_square, args=(10,)) 40 | p2 = multiprocessing.Process(target=print_cube, args=(13,)) 41 | 42 | # starting process 1 43 | p1.start() 44 | 45 | # starting process 2 46 | p2.start() 47 | 48 | # wait until process 1 is finished 49 | p1.join() 50 | 51 | # wait until process 2 is finished 52 | # p2.join() 53 | 54 | # both processes finished 55 | print("Done") 56 | -------------------------------------------------------------------------------- /docs/multiprocessing/example/example02.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import time 3 | import os 4 | 5 | 6 | def worker01() -> None: 7 | print(f"Simulate some work for worker01. {os.getpid()}") 8 | time.sleep(10) 9 | print("Done worker01") 10 | 11 | 12 | def worker02() -> None: 13 | print(f"Simulate some work for worker02. {os.getpid()}") 14 | time.sleep(10) 15 | print("Done worker02") 16 | 17 | 18 | if __name__ == "__main__": 19 | 20 | print(f"ID of main process: {os.getpid()}") 21 | 22 | # creating processes 23 | p1 = multiprocessing.Process(target=worker01) 24 | p2 = multiprocessing.Process(target=worker02) 25 | 26 | # starting process 1 27 | p1.start() 28 | 29 | # starting process 2 30 | p2.start() 31 | 32 | print(f"ID of process p1: {p1.pid}") 33 | print(f"ID of process p2: {p2.pid}") 34 | 35 | print(f"Process p1 alive: {p1.is_alive()}") 36 | print(f"Process p2 alive: {p2.is_alive()}") 37 | 38 | # wait until process 1 is finished 39 | p1.join() 40 | 41 | # wait until process 2 is finished 42 | p2.join() 43 | 44 | # both processes finished 45 | print("Done") 46 | 47 | # check processes are alive 48 | print(f"Process p1 alive: {p1.is_alive()}") 49 | print(f"Process p2 alive: {p2.is_alive()}") 50 | -------------------------------------------------------------------------------- /docs/multiprocessing/example/example03.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Lock, Pipe, Queue, Process 2 | from multiprocessing.connection import PipeConnection 3 | from multiprocessing.synchronize import Lock as LockType 4 | from typing import Any 5 | 6 | 7 | def queue_worker(queue: "Queue[str]", lock: LockType): 8 | with lock: 9 | print("Task executed") 10 | queue.put("Done") 11 | 12 | 13 | def pipe_worker(conn: PipeConnection): 14 | conn.send("Data from pipe worker") 15 | conn.close() 16 | 17 | 18 | def consumer(queue: "Queue[Any]", parent_conn: PipeConnection, lock: LockType): 19 | with lock: 20 | print(f"Consumer received from queue: {queue.get()}") 21 | print(f"Consumer received from pipe: {parent_conn.recv()}") 22 | 23 | 24 | if __name__ == "__main__": 25 | # for exchange data between processes safely 26 | queue: "Queue[Any]" = Queue() 27 | 28 | # For synchronization (ensures only one process can access at a time) 29 | lock: LockType = Lock() 30 | 31 | # Pipe for two-way communications between processes 32 | parent_conn, child_conn = Pipe() 33 | 34 | # create separate processes 35 | 36 | # queue base process 37 | queue_process = Process(target=queue_worker, args=(queue, lock)) 38 | # pipe base process 39 | pipe_process = Process(target=pipe_worker, args=(child_conn,)) 40 | # consumer that receive data 41 | consumer_process = Process(target=consumer, args=(queue, parent_conn, lock)) 42 | 43 | # start processes 44 | queue_process.start() 45 | pipe_process.start() 46 | 47 | # wait for both processes to finish 48 | queue_process.join() 49 | pipe_process.join() 50 | 51 | # start the consumer process after data is available 52 | consumer_process.start() 53 | consumer_process.join() 54 | 55 | print("Done") 56 | -------------------------------------------------------------------------------- /docs/multiprocessing/example/example04.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Queue, Pipe, Process, Lock 2 | from multiprocessing.connection import PipeConnection 3 | from multiprocessing.synchronize import Lock as LockType 4 | import time 5 | import random 6 | from typing import List, Tuple 7 | 8 | 9 | def scrape_website( 10 | url: str, 11 | queue: "Queue[Tuple[str, str]]", 12 | child_conn: PipeConnection, 13 | lock: LockType, 14 | ): 15 | print(f"Scraping {url}") 16 | scrape_time = random.randint(1, 10) 17 | time.sleep(scrape_time) 18 | data = f"Scraped data in {scrape_time} seconds" 19 | # Using lock to ensure only one process write data to share resource 20 | with lock: 21 | print(f"Scraped {url}") 22 | 23 | # send data to the main process 24 | queue.put((url, data)) 25 | 26 | # send metadata via Pipe 27 | child_conn.send((url, data)) 28 | child_conn.close() 29 | 30 | 31 | def aggregate_data( 32 | queue: "Queue[Tuple[str, str]]", 33 | parent_conn: PipeConnection, 34 | lock: LockType, 35 | websites: List[str], 36 | ) -> None: 37 | results = [] 38 | times = [] 39 | 40 | for url in websites: 41 | web, data = queue.get() 42 | results.append((web, data)) 43 | 44 | web_url, scrape_time = parent_conn.recv() 45 | times.append((web_url, scrape_time)) 46 | 47 | # safely print the aggregate data using Lock 48 | with lock: 49 | print("---Aggregated result---") 50 | for url, data in results: 51 | print(f"{url}: {data}") 52 | 53 | for url, scrape_time in times: 54 | print(f"{url}: {scrape_time} seconds") 55 | 56 | 57 | if __name__ == "__main__": 58 | websites = ["google.com", "facebook.com", "x.com", "github.com"] 59 | 60 | queue: "Queue[Tuple[str, str]]" = Queue() 61 | parent_conn, child_conn = Pipe() 62 | lock = Lock() 63 | 64 | processes = [] 65 | 66 | for url in websites: 67 | p = Process(target=scrape_website, args=(url, queue, child_conn, lock)) 68 | processes.append(p) 69 | p.start() 70 | 71 | # wait for all processes to finish 72 | for p in processes: 73 | p.join() 74 | 75 | aggregate_process = Process( 76 | target=aggregate_data, args=(queue, parent_conn, lock, websites) 77 | ) 78 | aggregate_process.start() 79 | aggregate_process.join() 80 | print("Done") 81 | -------------------------------------------------------------------------------- /docs/multiprocessing/example/pipe_example.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pipe, Process 2 | from multiprocessing.connection import PipeConnection 3 | 4 | 5 | def pipe_worker(conn: PipeConnection): 6 | conn.send("Data from pipe worker") 7 | conn.close() 8 | 9 | 10 | if __name__ == "__main__": 11 | print("Start") 12 | parent_conn, child_conn = Pipe() 13 | pipe_process = Process(target=pipe_worker, args=(child_conn,)) 14 | pipe_process.start() 15 | pipe_process.join() 16 | print(parent_conn.recv()) 17 | print("Done") 18 | -------------------------------------------------------------------------------- /docs/multiprocessing/example/queue_example.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Queue, Process 2 | from typing import List 3 | 4 | 5 | def square_list(mylist: List[int], queue: "Queue[int]"): 6 | for item in mylist: 7 | queue.put(item**2) 8 | 9 | 10 | def print_queue(queue: "Queue[int]"): 11 | while not queue.empty(): 12 | print(queue.get()) 13 | print("Queue is now empty") 14 | 15 | 16 | if __name__ == "__main__": 17 | queue: "Queue[int]" = Queue() 18 | myList = [1, 4, 5, 6, 7, 4] 19 | p1 = Process(target=square_list, args=(myList, queue)) 20 | p2 = Process(target=print_queue, args=(queue,)) 21 | 22 | # running process p1 to square the list 23 | p1.start() 24 | p1.join() 25 | 26 | # running process p2 to get queue elements to print 27 | p2.start() 28 | p2.join() 29 | 30 | print("All Done") 31 | -------------------------------------------------------------------------------- /docs/multiprocessing/example/queue_example_lock.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process, Queue, Lock 2 | from multiprocessing.synchronize import Lock as LockType 3 | import time 4 | from random import randint 5 | 6 | 7 | # Worker function that sends data to the Queue 8 | def worker_with_queue(queue: "Queue[int]", lock: LockType) -> None: 9 | result = randint(1, 100) # Simulate some work by generating a random number 10 | time.sleep(randint(1, 3)) # Simulate task duration 11 | queue.put(result) # Send the result to the shared Queue 12 | 13 | # Protect console output with Lock 14 | with lock: 15 | print(f"Worker {result} sent data to the queue.") 16 | 17 | 18 | # Aggregator function that collects results from the Queue 19 | def aggregator_with_queue( 20 | queue: "Queue[int]", lock: LockType, num_workers: int 21 | ) -> None: 22 | results = [] 23 | for _ in range(num_workers): 24 | result = queue.get() # Collect results from the Queue 25 | results.append(result) 26 | 27 | # Protect console output with Lock 28 | with lock: 29 | print(f"Aggregator received {result} from the queue.") 30 | print("Final aggregated results from Queue:", results) 31 | 32 | 33 | if __name__ == "__main__": 34 | queue: "Queue[int]" = Queue() # Queue is process-safe, no Lock needed 35 | lock = Lock() # Lock for protecting console output 36 | num_workers = 3 37 | processes = [ 38 | Process(target=worker_with_queue, args=(queue, lock)) 39 | for _ in range(num_workers) 40 | ] 41 | 42 | # Start all worker processes 43 | for p in processes: 44 | p.start() 45 | 46 | # Start the aggregator to collect results 47 | aggregator = Process(target=aggregator_with_queue, args=(queue, lock, num_workers)) 48 | aggregator.start() 49 | 50 | # Wait for all processes to finish 51 | for p in processes: 52 | p.join() 53 | aggregator.join() 54 | -------------------------------------------------------------------------------- /docs/multiprocessing/multi-chef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anhtuank7c/learn-python/5104aafd2555dc395268f87c514f5f74259c34ad/docs/multiprocessing/multi-chef.png -------------------------------------------------------------------------------- /docs/multiprocessing/multiprocess-queue-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anhtuank7c/learn-python/5104aafd2555dc395268f87c514f5f74259c34ad/docs/multiprocessing/multiprocess-queue-diagram.png -------------------------------------------------------------------------------- /docs/multiprocessing/multiprocessing-pipe-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anhtuank7c/learn-python/5104aafd2555dc395268f87c514f5f74259c34ad/docs/multiprocessing/multiprocessing-pipe-diagram.png -------------------------------------------------------------------------------- /docs/multiprocessing/multiprocessing-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anhtuank7c/learn-python/5104aafd2555dc395268f87c514f5f74259c34ad/docs/multiprocessing/multiprocessing-queue.png -------------------------------------------------------------------------------- /docs/multiprocessing/multiprocessing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anhtuank7c/learn-python/5104aafd2555dc395268f87c514f5f74259c34ad/docs/multiprocessing/multiprocessing.png --------------------------------------------------------------------------------