I use this repo to track all the lessons I learned about python
- https://www.geeksforgeeks.org/python-programming-language-tutorial
- https://www.w3schools.com/python/default.asp
- https://learnxinyminutes.com/docs/python/
- https://docs.python.org/3/tutorial/index.html
- https://www.youtube.com/watch?v=eWRfhZUzrAc&t=386s
# Single line comment """ Multiple line comment This kind of comment very helpful in comment in class and method Scroll to Class section to read more """ def func_name(name: str, age: int) -> None: """ func_name which print a string Parameters: name: str age: int Returns: None """ print(f'Hello {name}, you are {age} age')
There are 2 kind of numbers in Python: integer, float
# integer # -2, -1, 0, 1, 2, 3, 4, 5 tax = 20 # you can use underscore to make readable number salary = 100_000_000_000 # 1000000000 print(f"{salary: _}") # 100_000_000_000 print(f"{salary: ,}") # 100,000,000,000 # float # -1.25, -1.0, --0.5, 0.0, 0.5, 1.0, 1.25 buy_price = 1.68 # you can use underscore to make readable number income = 88_000_000.123_456 # 88000000.123456 print(f"{income: _.2f}") # 88_000_000.12 print(f"{income: ,.6f}") # 88,000,000.123456 # you can declare multiple variables on a single line, separated by commas sell_price, sold_amount = 1.99, 2123 co = buy_price * sold_amount revenue = sold_amount * sell_price profit_before_tax = (sell_price - buy_price) * sold_amount profit_after_tax = profit_before_tax - (profit_before_tax * tax / 100) print(str.format("CO: ${0:,.2f}", co)) print(str.format("Revenue: ${0:,.2f}", revenue)) print(str.format("Profit before tax: ${0:,.2f}", profit_before_tax)) print(str.format("Profit after tax: ${0:,.2f}", profit_after_tax)) # CO: 3,566ドル.64 # Revenue: 4,224ドル.77 # Profit before tax: 658ドル.13 # Profit after tax: 526ドル.50 latitude = 20.577180620745487 longitude = 106.04136437153396 print(f"Geopoint is {latitude}, {longitude}") # Geopoint is 20.577180620745487, 106.04136437153396 # classic division / always return a float print(6 / 2) # 3.0 print(9 / 5) # 1.8 # floor division // discards the fractional part print(6 // 3) # 2 print(6 // 4) # 1 # % operator returns the remainder of the division print(6 % 3) # 0 print(6 % 4) # 2 # use ** to calculate the powers print(4 ** 4) # 256 print(f'4 ** 4 = {4 ** 4}') # 4 ** 4 = 256 print(f'{4 ** 4 = }') # 4 ** 4 = 256 print(2 ** 7) # 128 print(f'2 ** 7 = {2 ** 7}') # 2 ** 7 = 128 print(f'{2 ** 7 = }') # 2 ** 7 = 128 # casting data using `int(), float()` methods int("123") # <class 'int'> float("123.123") # <class 'float'> # check type type(12) # <class 'int'> type(3.14) # <class 'float'>
address = 'Halong Bay, Quang Ninh, Vietnam' name = "Tuan" # find lenght of a string len(name) # 4 # string can be treated like a list characters # character in position 0 print(name[0]) # T # last character print(name[-1]) # n # second last character print(name[-2]) # a # character from position 0 (included) to 2 (excluded) print(name[0:2]) # Tu # character from position 1 (included) to 3 (excluded) print(name[1:3]) # ua # character from beginning to 2 (excluded) print(name[:2]) # Tu # character from 2 (exclude 2) to the end print(name[2:]) # an # Python string cannot be changed, they are immutable, therefore assigning to an indexed position in the string result in an error name[0] = "H" # error # Traceback (most recent call last): # File "/home/runner/RockPaperScissors/main.py", line 2, in <module> # name[0] = "H" # TypeError: 'str' object does not support item assignment # concat string greeting_one = "Hello " + name greeting_two = str.format("Hello {0}", name) concat_string = f"Hello {name}" print(greeting_one) print(greeting_two) print(concat_string) # Hello Tuan # Hello Tuan # Hello Tuan # string methods greeting_three = str.format("Hello {0}", name.upper()) print(greeting_three) print(greeting_three.split(" ")) # Hello TUAN # ['Hello', 'TUAN'] # pad string name = 'TUAN' print(f'{name:_<10}') # TUAN______ print(f'{name:_>10}') # ______TUAN print(f'{name:_^10}') # ___TUAN___ # you need to cast data to string before concat age = 30 greeting_f_our = "Hello " + name + ", age: " + str(age) # but you can use str.format to concat other data type into string greeting_five = str.format("Hello {0}, age: {1}", name, age) # split line print("Multiple\nline\n\tstring") # Multiple # line # string # trim whitespace from lead and tail " this is a string with spacing ".strip() # this is a string with spacing # casting data using `str()` method str(123) # <class 'str'> str(123.123) # <class 'str'> str("123") # <class 'str'> # check type type("Hello world") # <class 'str'>
It have 2 value: True/False
exceeded_quota = True published = False # negative with not new_value = not exceeded_quota # False # Boolean Operators True & True # True True and True # True False & True # False False and True # False # True/False is 1 and 0 but with different keywords True + True # 2 True + False # 1 True * 8 # 8 False - 5 # -5 True - 5 # -4 # comparision operators look at the numerical value of True and False 0 == False # True 1 == False # False 1 == True # True 0 == True # False 2 > True # True 2 >= True # True 2 != True # True # None, 0, and empty strings/list/dics/tuples/sets all evaluate to False # All other values are True print(bool(None)) # False print(bool(0)) # False print(bool("")) # False print(bool([])) # False print(bool({})) # False print(bool(())) # False print(bool(set())) # False # int print(bool(5)) # True # string print(bool("tuan")) # True # list print(bool(["tuan", "nguyen"])) # True # dics print(bool({"foo": "bar"})) # True # tuple print(bool((1, 2, 3, 4, 5))) # True # set print(bool({120, 120, 10, 30})) # True # equality is == 1 == True # True 1 == 1 # True 0 == True # False # inequality is != 1 != True # False 1 != False # True 1 != 0 # True # More comparision 1 < 10 # True 1 > 10 # False 1 <= 10 # True 1 >= 10 # False # Seeing whether a value is in a range 1 < 2 and 2 < 3 # True 2 < 3 and 3 < 2 # False # Chaining make this look nicer 1 < 2 < 3 # True 2 < 3 < 2 # False # is vs == # is check if 2 variables refer to the same object # == check if the objects pointed to the same value list_one = [1, 2, 3, 4] list_two = list_one list_two is list_one # True, list_one and list_two point to the same object list_two == list_one # True, list_one and list_two are equal because they have the same value list_three = [1, 2, 3, 4] list_three is list_one # False, list_three is not point to the same object as list_one list_three == list_one # True, list_three and list_one have the same value # check type type(True) # <class 'bool'>
None is an object
None # None # Don't use the equality `==` to compare objects to None # Use `is` instead. `is` check for equality of object identity "etc" is None # False None is None # True # check type type(None) # <class 'NoneType'>
List store sequences
days_of_week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 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] # append element days_of_week.append("Lazy") append_result = days_of_month.append(31) # None 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] # remove from the end with pop pop_result = days_of_month.pop() # 31 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] # access a list like you would any array days_of_month[0] # 1 # look at the last element days_of_month[-1] # 30 # looking out of bounds is an IndexError days_of_month[50] # IndexError: list index out of range # You can look at ranges with slice syntax. # the start index is included, the end index is not # (It's a closed/open range for you mathy types.) days_of_month[1:3] # return list from 1 (excluded) to 3 (included) => [2, 3] days_of_month[28:] # return list from 28 (excluded) to the end => [29, 30] days_of_month[:3] # return list from beginning to index 3 (excluded) => [1, 2, 3] days_of_month[::3] # return list selecting elements with a step size of 3 => [1, 4, 7, 10, 13, 16, 19, 22, 25, 28] 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] # make one layer deep copy using slices days_of_month_copy = days_of_month[:] # remove arbitrary elements from a list with `del` 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] # remove first occurrence of a value, raise ValueError if absent list_with_duplicate_value = [1, 2, 3, 4, 5, 4, 3, 8] list_with_duplicate_value.remove(4) # [1, 2, 3, 5, 4, 3, 8] list_with_duplicate_value.remove(100) # ValueError: list.remove(x): x not in list # insert an element at a specific index list_with_duplicate_value.insert(2, 100) # [1, 2, 100, 3, 4, 5, 4, 3, 8] # get the index of the first item found matching the argument, raise ValueError if absent list_item = ["Apple", "Banana", "Peach", "Guava", "Avocado", "Peach"] list_item.index("Peach") # 3 list_item.index("Peachy") # ValueError: 'Peachy' is not in list # use `+` to add multiple list to make a new list, original list will not being modified list_poultry = ["Chickens", "Ducks", "Turkeys", "Geese", "Quails", "Pheasants", "Pigeons", "Ostriches"] list_cattle = ["Wagyu", "Kobe beef", "Ohmi beef", "Hanwoo beef", "Kurobuta Pork", "Olive Sanuki Wagyu"] list_animal = list_poultry + list_cattle + list_poultry # concatenate list with `extend`, the extender list will be modified list_poultry.extend(list_cattle) print(list_poultry) # ['Chickens', 'Ducks', 'Turkeys', 'Geese', 'Quails', 'Pheasants', 'Pigeons', 'Ostriches', 'Wagyu', 'Kobe beef', 'Ohmi beef', 'Hanwoo beef', 'Kurobuta Pork', 'Olive Sanuki Wagyu'] print(list_cattle) # ["Wagyu", "Kobe beef", "Ohmi beef", "Hanwoo beef", "Kurobuta Pork", "Olive Sanuki Wagyu"] # check for existence in a list with `in` "Chickens" in list_poultry # True # examine the length with `len()` len(list_poultry) # 8 # You can put multiple data types into the same list without hassle list_mixed = ["Mon", 2, "Tue", 3, "Wed", 4, "Thu", 5, "Fri", 6, "Sat", True, "Sun", False] # unpack lists into variables data = [["Blog", "Service"], ["Post 01", "Post 02"], ["Author 01", "Author 02"]] categories, posts, authors = data print(categories) # ['Blog', 'Service'] print(posts) # ['Post 01', 'Post 02'] print(authors) # ["Author 01", "Author 02"] # extended unpacking (or spreading) *rest, authors = data print(rest) # [['Blog', 'Service'], ['Post 01', 'Post 02']] print(authors) # ['Author 01', 'Author 02'] categories, *rest = data print(categories) # ['Blog', 'Service'] print(rest) # [['Post 01', 'Post 02'], ['Author 01', 'Author 02']] categories, *rest, authors = data print(categories) # ['Blog', 'Service'] print(rest) # [['Post 01', 'Post 02']] print(authors) # ['Author 01', 'Author 02'] # sorting a list is a common behavior in real-life salaries = [123.02, 3423.13, 34543, 945, 100, 8833.5] salaries.sort() # default ascending sort print(salaries) # [100, 123.02, 945, 3423.13, 8833.5, 34543] salaries.sort(reverse=True) print(salaries) # [34543, 8833.5, 3423.13, 945, 123.02, 100] names = ["John", "Corey", "Adam", "Steve", "Rick", "Thomas"] names.sort() print(names) # ['Adam', 'Corey', 'John', 'Rick', 'Steve', 'Thomas'] names.sort(reverse=True) print(names) # ['Thomas', 'Steve', 'Rick', 'John', 'Corey', 'Adam'] # check type type(list_mixed) # <class 'list'>
Tuple is like lists but are immutable
user_info = ("Tuan", 35, "Halong Bay, Quang Ninh, Vietnam", True) user_info[0] # Tuan user_info[0] = "Quan" # TypeError: 'tuple' object does not support item assignment # check type # tuple of needs a comma if have elements type(("Tuan", )) # <class 'tuple'> type(("Tuan", True)) # <class 'tuple'> # empty tuple don't need comma type(()) # <class 'tuple'> # without comma, it will never consider as tuple type(("Tuan")) # <class 'str'> type((1)) # <class 'int'> type((3.14)) # <class 'float'> type((True)) # <class 'bool'> # you can do most of the list operations on tuples too len(("Tuan", True)) # 2 book = ("Harry Porter", 2002, "Published") author = ("JK Rowling", 1965, "Female", "England") # joining tuples using concatenation `+` operator book_author = book + author # ('Harry Porter', 2002, 'Published', 'JK Rowling', 1965, 'Female', 'England') # or `sum()` function new_tuple_two = sum((book, author), ()) # ('Harry Porter', 2002, 'Published', 'JK Rowling', 1965, 'Female', 'England') book[:2] # ('Harry Porter', 2002) book[:-1] # ('Harry Porter',) book[::2] # ('Harry Porter', 'Published') book[2:] # ('Published',) book[1:2] # (2002,) "England" in author # True # You cannot extend a tuple directly author.extend(book) # AttributeError: 'tuple' object has no attribute 'extend' # you need to cast to list and cast back to tuple after merged author_list = list(author) book_list = list(book) author_list.extend(book_list) new_tuple = tuple(author_list) print(new_tuple) # ('JK Rowling', 1965, 'Female', 'England', 'Harry Porter', 2002, 'Published') # Unpack tuples into variables name, published_year, status = book # extended unpacking address = ((20.951826295025008, 107.01421196125972), "Halong Bay, Quang Ninh, Vietnam", 200000, "+84123456789") geopoint, *rest, phone = address print(geopoint) # (20.951826295025008, 107.01421196125972) print(rest) # ['Halong Bay, Quang Ninh, Vietnam', 200000] print(phone) # +84123456789 *rest, phone = address print(rest) # [(20.951826295025008, 107.01421196125972), 'Halong Bay, Quang Ninh, Vietnam', 200000] print(phone) # +84123456789 geopoint, *rest = address print(geopoint) # (20.951826295025008, 107.01421196125972) print(rest) # ['Halong Bay, Quang Ninh, Vietnam', 200000, '+84123456789'] # tuple are created by default if you leave out the parentheses name, age = "Tuan", 35 # tuple Tuan, 35 is unpacked into variables name, age print(name) # Tuan print(age) # 35 # now look how easy it is to swap two values age, name = name, age print(name) # 35 print(age) # Tuan
Dictionary store mappings from keys to values
empty_dict = {} languages = {"en": "English", "vi": "Vietnamese"} # keys for dictionaries have to be immutable types to ensure key can be # converted to a constant hash value for quick look-ups. # immutable types include ints, float, string, tuples valid_dict = {100: "Very good", 90: "Good", 70: "Acceptable", 60: "Under average"} valid_dict = {3.14: "Pi"} valid_dict = {"cached": "total: 10, ranked 100"} valid_dict = {("Tuan", 35): [10000, 3.141592654]} invalid_dict = {[100, 200]: "salary"} # TypeError: unhashable type: 'list' invalid_dict = {{"en": "English"}: "salary"} # TypeError: unhashable type: 'list' # look up values with [] leaderboard = {1: ["Tuan", "Simon"], 2: ["Duong", "Chien"], 3: ["Truong"], 4: ["Son"], 5: ["Phuong"]} print(leaderboard[1]) # ['Tuan', 'Simon'] print(leaderboard[3]) # ['Truong'] # get all keys as an iterable with "keys()". We need to wrap the call in list() to turn it into a list # Note: # - For python version < 3.7: dictionary key ordering is not guaranteed # - For python version >= 3.7: dictionary items maintain the order at which they are inserted into the dictionary values = list(leaderboard.values()) # [['Tuan', 'Simon'], ['Duong', 'Chien'], ['Truong'], ['Son'], ['Phuong']] # check for existence of keys in a dictionary with `in` languages = { "en": "English", "vi": "Vietnamese" } "en" in languages # True # looking up for non-existing key is a KeyError languages["de"] # KeyError: 'de' # use `get()` method to avoid the KeyError languages.get("vi") # Vietnamese languages.get("de") # None # `get()` method support default value when the value is missing languages.get("de", "German") # German # `setdefault()` inserts into a dictionary only if the given key is absent languages.setdefault("de", "German") languages.setdefault("en", "France") # {'en': 'English', 'vi': 'Vietnamese', 'de': 'German'} # adding to a dictionary languages.update({"zh": "Chinese"}) # {'en': 'English', 'vi': 'Vietnamese', 'de': 'German', 'zh': 'Chinese'} languages.update({"en": "France"}) # {'en': 'France', 'vi': 'Vietnamese', 'de': 'German', 'zh': 'Chinese'} languages["en"] = "English" # {'en': 'English', 'vi': 'Vietnamese', 'de': 'German', 'zh': 'Chinese'} # remove keys from a dictionary with del, raise KeyError if key is absent del languages["zh"] del languages["absent_key"] # KeyError: 'absent_key' # from python 3.5 you can also use the additional unpacking options languages = {"en": "English", **{"zh": "Chinese"}} # {'en': 'English', 'zh': 'Chinese'} # check type print(type(languages)) # <class 'dict'>
Set is an unordered collection of unique and immutable elements.
Set is commonly used for membership testing, eliminating duplicate entries, and performing mathematical set operation like union, intersection, and difference.
# init an empty set empty_set = set() # init a set from a list list_duplicated_values = [1, 1, 2, 2, 3, 3] unique_values = set(list_duplicated_values) # {1, 2, 3} # init a set with a bunch of values some_set = {1, 1, 2, 3, 2, 5} # {1, 2, 3, 5} days_of_week = {"Mon", "Tue"} # {'Tue', 'Mon'} # Note elements of a set have to be immutable. # Immutable types include ints, floats, strings, tuples, frozensets. # other data types will raise error TypeError: unhashable type new_set = {[1, 2]} # TypeError: unhashable type: 'list' new_set = {{"foo": "bar"}} # TypeError: unhashable type: 'dict' # tuple new_set = {("Wed", "Tue")} # {('Wed', 'Tue')} # string new_set = {"Wed", "Tue"} # {'Wed', 'Tue'} # int new_set = {1, 2} # {1, 2} # float new_set = {3.14, 1,234} # {3.14, 234, 1} # frozenset new_set = {frozenset([1, 2, 3])} # {frozenset({1, 2, 3})} mixed_set = {1, 2.2, "three", (4, 5), frozenset([6, 7])} # add elements, duplicate entries will automatically eliminated days_of_week.add("Wed") # {'Wed', 'Tue', 'Mon'} days_of_week.add("Wed") # {'Wed', 'Tue', 'Mon'} # remove elements using `remove()` or `discard()` methods. # `remove()` will raise KeyError if remove an absent element # `discard()` will never raise error days_of_week.remove("Wed") #{'Tue', 'Mon'} days_of_week.remove("Wed_asjdhas") # KeyError: 'Wed_asjdhas' days_of_week.discard("Tue") #{'Mon'} days_of_week.discard("Wed_asjdhas") # Nothing happen # union is the way to merge 2 sets and also remove the duplicates # use `union()` method or `|` operator days_of_week_one = {"Mon", "Tue", "Web"} days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} days_of_week = days_of_week_one.union(days_of_week_two) # {'Web', 'Sat', 'Mon', 'Sun', 'Fri', 'Tue', 'Thu'} days_of_week = days_of_week_one | days_of_week_two # {'Web', 'Sat', 'Mon', 'Sun', 'Fri', 'Tue', 'Thu'} # intersection is the way to find elements that found in both sets # use `intersection()` or `&` operator days_of_week_one = {"Mon", "Tue", "Web"} days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} days_appeared_in_both_sets = days_of_week_one.intersection(days_of_week_two) # {'Web'} days_appeared_in_both_sets = days_of_week_one & days_of_week_two # {'Web'} # difference is the way to find elements that appear in the first set but not in the second # use `difference()` method or `-` operator days_of_week_one = {"Mon", "Tue", "Web"} days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} days_difference_from_set_two = days_of_week_one.difference(days_of_week_two) # {'Tue', 'Mon'} days_difference_from_set_two = days_of_week_one - days_of_week_two # {'Tue', 'Mon'} # symmetric difference between two sets contains elements that are in either of the sets but not in both. # use `symmetric_difference()` or `^` operator days_of_week_one = {"Mon", "Tue", "Web"} days_of_week_two = {"Web", "Thu", "Fri", "Sat", "Sun"} symmetric_difference = days_of_week_one.symmetric_difference(days_of_week_two) # {'Fri', 'Mon', 'Sat', 'Tue', 'Thu', 'Sun'} symmetric_difference = days_of_week_one ^ days_of_week_two # {'Fri', 'Mon', 'Sat', 'Tue', 'Thu', 'Sun'} # a set is a subset of another if all elements of the first set are in the second # use `issubset()` or `<=` operator days_of_week = {2, 3, 4, 5, 6, 7, 8} 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} days_of_week.issubset(days_of_month) # True days_of_week <= days_of_month # True # a set is a superset of another if it contains all elements of the second set. # use `issuperset()` or `>=` operator days_of_month.issuperset(days_of_week) days_of_month >= days_of_week # Make a one layer deep copy using the `copy()` method days_of_week_copy = days_of_week.copy() # remove all elements from a set using the `clear()` method days_of_week.clear() # set() # add multiple elements to a set using the `update()` method days_of_week = {2, 3, 4, 5, 6, 7, 8} days_of_week.update([1, 9, 10, 11]) # {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} # check for existence in a set with `in` 2 in days_of_week # True # a frozen set is an immutable version of a set. # elements cannot be added or removed after creation. # using `frozenset()` method new_set = {"Mon", "Tue"} new_frozenset = frozenset(new_set) days_of_week = frozenset(["Mon", "Tue"]) days_of_week.clean() # AttributeError: 'frozenset' object has no attribute 'clean' days_of_week.update(["Wed"]) # AttributeError: 'frozenset' object has no attribute 'update' days_of_week.add("Wed") # AttributeError days_of_week.remove("Wed") # AttributeError
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)
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.
# define an Enum TransactionState from enum import Enum class TransactionState(Enum): PENDING = 0 PROCESSING = 1 COMPLETED = 2 FAILED = 3 REFUNDED = 4 print(TransactionState.COMPLETED.name) # COMPLETED print(TransactionState.COMPLETED.value) # 4 # using Enum in Conditional statement if transaction_result == TransactionState.COMPLETED: print("Transaction completed") elif transaction_result == TransactionState.PENDING: print("Transaction pending") else: print("Transaction in other state") # loop over Enum for state in TransactionState: print(state) class Theme(Enum): DARK = "dark" LIGHT = "light" # auto assign value use `auto()` from enum module from enum import Enum, auto class Theme(Enum): DARK = auto() LIGHT = auto() for theme in Theme: print(theme.value) # 1, 2 # output: will be auto-assigned starting from 1 # 1 # 2 # enum with method from enum import Enum, auto class Theme(Enum): DARK = auto() LIGHT = auto() def is_light(self): return self in {Theme.LIGHT} print(Theme.LIGHT.is_light()) # True print(Theme.DARK.is_light()) # False # enum is immutable, which means you cannot reassign value to Enum Theme.LIGHT.value = 123 # AttributeError: <enum 'Enum'> cannot set attribute 'value'
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.
# variable identifier must follow these rules # - must start with a letter [a-z A-Z] or an underscore `_` # - can be followed by letter, digits (0-9) or underscore `_` # - case-sensitive (e.g myVar and myvar are different variables) # - cannot be a reversed keyword (e.g if, while, for, break, continue, pass) ######################## # implicit declaration ######################## # int age = 35 # float pi = 3.14 # string name = "Tuan" # boolean sold_out = True # tuple person = ("Tuan", 35, "Male", "Halong Bay, Quang Ninh, Vietnam") # list stables = ["Gothenburg, Sweden", "Paris, France", "Hanoi, Vietnam"] # dict languages = {"en": "English", "vi": "Vietnam"} # set product_ids = {"sku_01", "sku_02", "sku_01", "sku_03"} ######################## # explicit declaration ######################## # int age: int = 35 # float pi: float = 3.14 # str name: str = "Tuan" # bool sold_out: bool = True # use `id()` function to find the variable address on memory print(id(age)) # 4357660072 print(id(pi)) # 4342525072 print(id(name)) # 4344113520 print(id(sold_out)) # 4356703696 ######################## # variable scope # - local scope: variable declared inside a function are local to that function # - enclosing scope: variable in the local scope of enclosing function (nonlocal) # - global scope: variable declared at the top level of a # script or module, or declared global using `global` keyword # - built-in scope: variable preassigned in the built-in namespace (e.g., `len`, `range`) ######################## # global variable age = 30 def outer_function(): # enclosing variable age = 50 def inner_function(): # local variable age = 100 # global variable global name name = "Tuan" print(f'{age = }') # 100 inner_function() print(f'{age = }') # 50 outer_function() print(f'{age = }') # 30 print(f'{name = }') # Tuan # `global` keyword allows you to modify a global variable inside a function today = "Monday" def modify_global(): global today today = "Tuesday" print(f'{today = }') # today = 'Tuesday' modify_global() print(f'{today = }') # today = 'Tuesday' # `nonlocal` keyword allows you to modify a variable in the enclosing (non-global) scope def outer_func(): name = "Tuan" def inner_func(): nonlocal name name = "Simon" print(f'{name = }') # name = 'Simon' inner_func() print(f'{name = }') # name = 'Simon' outer_func() ######################## # dynamic typing and type checking # Python is dynamically typed, meaning that variable types are # determined at runtime, variables can change type ######################## x = 10 print(type(x)) # <class 'int'> x = "Hello world" print(type(x)) # <class 'str'> # dynamic typing offers flexibility, it can easily lead to runtime errors if not managed carefully (like Javascript). # to enhance type safety, you can use type hints and tools like `mypy` for static type checking. # don't forget to install mypy extension for your IDE for static type checking: https://www.mypy-lang.org # vscode extension https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker # IntelliJ IDEA, PyCharm https://plugins.jetbrains.com/plugin/11086-mypy def greeting(name: str) -> str: return f'Hello {name}' print(greeting("Tuan")) # Hello Tuan print(greeting(1123)) # Mypy raise thise: Argument 1 to "greeting" has incompatible type "int"; expected "str" # recommended to use pipx over pip: https://pipx.pypa.io/stable # `pip install mypy` or `pipx install mypy` # and run: `mypy your_file.py` from typing import Tuple, List, Dict, Set Person = Tuple[str, int, str, str] person: Person = ("Tuan", 35, "Male", "Halong Bay, Quang Ninh, Vietnam") print(person) names: List[str] = ["Tuan", "Simon", "Duong", "Son"] print(names) scores: Dict[str, int] = {"Tuan": 10, "Simon": 9, "Duong": 8, "Son": 7} print(scores) days_of_week: Set[str] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} print(days_of_week) ######################## # constant # while Python doesn't have a built-in constant type # by convention, constant are written in uppercase letters and are typically defined # at the module level ######################## PI = 3.141592654 MAX_CONNECTIONS = 500 LOGS_LEVEL = "debug" LOGS_SIZE = 5 * 1000 ** 2 #MB # immutable types includes: int, float, str, tulple, frozenset # mutable types inclide: list, dict, set # declare multiple variables at once by adding commas name, age, address = "Tuan", 35, "Halong Bay, Quang Ninh, Vietnam"
Conditional statements allow you to execute different blocks of code based on certain conditions.
# if is_raining = True if is_raining: print("It's raining") # if-else humidity = 88 if humidity > 80: print("Possibility to rain") else: print("No chance to rain") # if-elif-else if humidity >= 90: print("Surely rain") elif humidity >= 80: print("Possibility to rain") else: print("No chance to rain") # switch-case # Python does not have a built-in switch-case statement like other languages. # However you can archive similar functionality using different approaches. # 1. use if-elif-else statement # 2. use dict # 3. use lambda function in dict # 4. use match-case (python 3.10+) # solution 1: if-elif-else def switch_case(value): if value == 1: return "case 01" elif value == 2: return "case 02" else: return "default case" switch_case(1) switch_case(2) switch_case("Hi") # solution 2: dict def case_1(): return "case 01" def case_2(): return "case 02" def case_default(): return "default case" switch = { 1: case_1, 2: case_2 } def switch_case(value): return switch.get(value, case_default)() switch_case(1) switch_case(2) switch_case("Hi") # solution 3: use lambda function in dict switch = { 1: lambda: "case 01", 2: lambda: "case 02", } def switch_case(value): return switch.get(value, lambda: "default case")() switch_case(1) switch_case(2) switch_case("Hi") # solution 4: match-case (python 3.10+) def switch_case(value): match value: case 1: return "Case 1" case 2: return "Case 2" case 3 | 4: return "Case 3 or 4" case _: return "Default case" print(switch_case(1)) # Case 1 print(switch_case(3)) # Case 3 or 4 print(switch_case(4)) # Case 3 or 4 print(switch_case(5)) # Default case
Loop statements allow you to repeat a block of code multiple times
# for: Iterates over a sequence (such as a list, tuple, dictionary, set, or string) and executes a block of code for each item. fruits = ["Apple", "Peach", "Pear", "Banana", "Kiwi"] for fruit in fruits: print(fruit) leaderboard = {1: "Tuan", 2: "Simon", 3: "Duong"} for key in leaderboard.keys(): print(f'{key}: {leaderboard[key]}') # 1: Tuan # 2: Simon # 3: Duong unique_numbers = {1, 2, 3, 4, 5, 1, 3, 5} for value in unique_numbers: print(f'{value}') # 1 # 2 # 3 # 4 # 5 # use `enumerate` method to create an iterator that produces tuples containing an index and the corresponding element # from the iterable so you can access index and element for index, value in enumerate(unique_numbers): print(f'{index}: {value}') # 0: 1 # 1: 2 # 2: 3 # 3: 4 # 4: 5 # `range()` returns an iterable of numbers from 0 up to (but excluding) the given number for i in range(4): print(i) # 0 1 2 3 for i in range(5, 10): print(i) # 5 6 7 8 9 for i in range(5, 10, 2): print(i) # 5 7 9 # loop over a list to retrieve both the index and the value of each list item # use `enumerate` method for index, value in enumerate(["dog", "cat", "mouse"]): print(f'{index}: {value}') # 0: dog # 1: cat # 2: mouse # while repeats a block of code as long as a specified condition is true. count = 0 while count < 5: # continue if this condition still True print(count) # 0 1 2 3 4 count += 1 # shorthand for count = count + 1 # Python offers a fundamental abstraction called the Iterable. # An iterable is an object that can be treated as a sequence. # The object returned by the range function is an iterable languages = {"en": "English", "vi": "Vietnam"} languages_iterator = languages.keys() # dict_keys(['en', 'vi']) # we can loop over it for langKey in languages_iterator: print(langKey) # however we cannot address elements by index, will raise TypeError languages_iterator["en"] # TypeError: 'dict_keys' object is not subscriptable # an iterable is an object that knows how to create an iterable languages_iterator = iter(languages) # our iterable is an object that can remember the state as we traverse through # it. We get the next object with `next()` next(languages_iterator) # en next(languages_iterator) # vi # after the iterable has returned all of its data, it raises a StopIteration exception next(languages_iterator) # StopIteration # we can also loop over iterable, infact `for` does it implicitly for lang in iter(languages): print(lang) # en, vi # we can grab all the elements of an iterable or iterator by call of `list()` days_of_week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] days_iterator = iter(days_of_week) list(days_iterator) # ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] list(days_iterator) # [] because state is saved # create custom iterator # to create an object/class as an iterator, you have to implement # the method `__iter__()` and `__next__()` # `__iter__()` method acts similar, you can do operations but must always return the iterator object itself # `__next__()` method also allows you to do operations, and must return the next item in the sequence class MyNumbers: def __iter__(self): self.a = 1 return self def __next__(self): if self.a <= 3: x = self.a self.a += 1 return x else: raise StopIteration my_class = MyNumbers() my_iterator = iter(my_class) print(next(my_iterator)) # 1 print(next(my_iterator)) # 2 print(next(my_iterator)) # 3 print(next(my_iterator)) # raise StopIteration
# break: exits the nearest enclosing loop immediately for value in [1, 2, 3, 4]: if value == 3: break print(value) # 1, 2 count = 0 while True: count += 1 print(count) # 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 if count == 10: break # continue: skip the rest of the code inside the current loop iteration and proceeds to the next iteration # does continue work with while??? for i in range(10): if i <= 5: print(i) # 0, 1, 2, 3, 4, 5 continue break # pass: does nothing and is used as a placeholder for future code # Empty code is not allowed in loops, function definitions, class definitions, or in if statements. for i in range(10): pass # Todo if paid_successfully: pass # Todo while valid: pass # Todo def paid(amount: int, currency: str) -> bool: pass # Todo return True
A function is a block of code which only runs when it is called
def no_args_func(): print("Hi") # invoke function no_args_func() def args_func(name, age): print(f'{name}, {age}') # invoke function with args args_func("Tuan", 35) # you can also send arguments with the key = value syntax to make it easier to follow args_func(name = "Tuan", age = 35) # if you want to passing only positional-only arguments place `, /` at the end of arguments list # means need to use positional arguments only when invoking function def args_func(name, age, /): print(f'{name}: {age}') args_func("Tuan", 35) # Tuan: 35 args_func(name = "Tuan", age = 35) # TypeError: args_func() takes 0 positional arguments but 2 were given # The opposite of positional arguments is keyword-only arguments # place `* ,` before arguments # means need to use keyword arguments only when invoking function def args_func(*, name, age): print(f'{name}: {age}') args_func("Tuan", 35) # TypeError: args_func() takes 0 positional arguments but 2 were given args_func(name = "Tuan", age = 35) # Tuan: 35 # you can even make it more complex by combining positional-only with keyword-only arguments # I don't prefer to make the complex thing but just a case you want to known def args_func(name: str, /, age: int, *, address: str) -> None: print(f'{name}, {age}, {address}') # positional-only `/` always in front of keyword-only `*` arguments args_func('John', 30, address='New York') # Valid # invalid `name` keyword which should have value only, and `address` which should be here args_func(name = 'John', age = 30, 'New York') # defind a function explicitly def args_func(name: str, age: int) -> str: return f'{name}, {age}' args_func("Tuan", 35) args_func(name = "Tuan", age = 35) # you can also set default value for arguments by assign `= value` # default argument must always go after non-default argument def args_func(value: int, decimal: bool = True) -> str: return f'{value}, {decimal}' # this function will raise error: SyntaxError: parameter without a default follows parameter with a default def args_func(value: int = 100, decimal: bool) -> str: return f'{value}, {decimal}' args_func(1011011) args_func(1011011, True) args_func(value = 1011011, decimal = True) # use arbitrary keyword arguments `**args` # if you don't know how many arguments that will be passed into your function # add `**` before the parameter name in the function definition def console_log(**logs): print(logs) # {'name': 'John', 'age': 30, 'city': 'New York'} console_log(name="John", age=30, city="New York") # {'company': 'SpaceX', 'avg_salary': 5000} console_log(company = "SpaceX", avg_salary = 5000) # Arbitrary argument must comes after non-arbitrary argument def console_log(level = 'debug', **logs) -> None: print(f'{level}: {logs}') # debug: {'name': 'John', 'age': 30, 'city': 'New York'} console_log(name="John", age=30, city="New York") # warning: {'name': 'John', 'age': 30, 'city': 'New York'} console_log('warning',name="John", age=30, city="New York") # error: {'name': 'John', 'age': 30, 'city': 'New York'} console_log(level = 'error',name="John", age=30, city="New York") # debug: {'company': 'SpaceX', 'avg_salary': 5000} console_log(company = "SpaceX", avg_salary = 5000) def console_log(message: str, level = 'debug', **logs) -> None: print(f'{level}: {message} ({logs})') # debug: Something goes wrong ({'name': 'John', 'age': 30, 'city': 'New York'}) console_log(message = "Something goes wrong", name="John", age=30, city="New York") # debug: warning ({'name': 'John', 'age': 30, 'city': 'New York'}) console_log('warning',name="John", age=30, city="New York") # error: Something goes wrong ({'name': 'John', 'age': 30, 'city': 'New York'}) console_log(level = 'error', message = "Something goes wrong", name="John", age=30, city="New York") # debug: Something goes wrong ({'company': 'SpaceX', 'avg_salary': 5000}) console_log("Something goes wrong", company = "SpaceX", avg_salary = 5000) # if you try to but arbitrary argument in front of non-arbitrary arguments # it will raise SyntaxError: arguments cannot follow var-keyword argument def console_log(**logs, level = 'debug'): # SyntaxError print(f'{level}: {logs}') # as I mentioned from above, we use `pass` keyword for placeholder def shipping(address: str) -> bool: pass # Todo return True shipping("Halong Bay, Quang Ninh, Vietnam") shipping(address = "Halong Bay, Quang Ninh, Vietnam")
Decorators are very powerful and useful tool in Python since it allows programmers to modify the behavior of a function or class.
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.
First Class Object
In Python, functions are first class object which means that functions in Python can be used or passed as arguments
Properties of first class functions:
- A function is an instance of the Object type
- You can store the function in a variable
- You can pass the function as a parameter to another function
- You can return the function from a function
- You can store them in data structures such as hash tables, lists ...
Consider the below examples for better understanding
Example 01: Treating the functions as objects
def shout(text: str) -> str: return text.upper() # we assign function shout to a variable. This will not invoke the function instead it # takes the function object referenced by a shout abd creates a second name pointing to it `yell` yell = shout print(yell('Hello')) # HELLO
Example 02: Passing the function as an argument
from typing import Callable def shout(text: str) -> str: return text.upper() def whisper(text: str) -> str: return text.lower() def greet(func: Callable[[str], str]) -> None: # storing the function in a variable greeting = func('Hi, I am created by a function passed as an argument') print(greeting) greet(shout) # HI, I AM CREATED BY A FUNCTION PASSED AS AN ARGUMENT greet(whisper) # hi, i am created by a function passed as an argument
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.
Example 03: write your first decorator
def logger(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}: {func.__code__.co_argcount}") func(*args, **kwargs) return wrapper def another_func(x: int, y: int) -> int: return x + y # we can pass func as a parameter as usual but it's doesn't convenient another_func = logger(another_func) another_func(1, 2) # Calling another_func: 2 # we can use decorator like this # convenient and easy to read @logger def my_func(x: int, y: int) -> int: return x + y my_func(1, 2) # Calling my_func: 2
Example 4: make decorator function return data
from functools import wraps def start_end_decorator(func): # wraps ensures docstring, function name, arguments list, etc. are all copied # to the wrapped function - instead of being replaced with wrapper's info @wraps(func) def wrapper(*args, **kwargs): print('start') result = func(*args, **kwargs) print('end') return result return wrapper @start_end_decorator def hello(name: str): return f'Hello {name}' print(hello("Tuan"))
Chaining Decorator means decorating a function with multiple decorators
from functools import wraps def logger(func): @wraps(func) def wrapper(*args, **kwargs): print('logger start') result = func(*args, **kwargs) print('logger end') return result return wrapper def acl(func): @wraps(func) def wrapper(*args, **kwargs): print('check permission, valid, continue') result = func(*args, **kwargs) print('after check permission') return result return wrapper @logger @acl def add_transaction(amount: float, reason: str): print(f'{amount =}, {reason =}') return "random_transaction_id" add_transaction(120.123, "Test") # logger start # check permission, valid, continue # amount =120.123, reason ='Test' # after check permission # logger end
Decorator and classes
Example 01: inject decorators function to class methods
# tracking.py from functools import wraps def tracking(func): @wraps(func) def wrapper(self, *args, **kwargs): # call the original func result = func(self, *args, **kwargs) print(f'[{func.__name__}]: items in cart of "{self.user_id}": {len(self.items)}') return result return wrapper # cart_item.py class CartItem: def __init__(self, product_id: str, quantity: int) -> None: self.product_id = product_id self.quantity = quantity # shopping_cart.py from cart_item import CartItem from typing import List from utils.tracking import tracking class ShoppingCart: def __init__(self, user_id: str) -> None: self.user_id = user_id self.items: List[CartItem] = [] @tracking def add_item(self, item: CartItem) -> None: self.items.append(item) print(f'Added item {item.product_id} to cart of user {self.user_id}') @tracking def remove_item(self, item: CartItem) -> None: self.items.remove(item) print(f'Removed item {item.product_id} from cart of user {self.user_id}') @tracking def checkout(self): print(f'Checking out {len(self.items)}') self.items.clear() # main.py from cart_item import CartItem from shopping_cart import ShoppingCart cart = ShoppingCart("user_01") cart_item_1 = CartItem("sku_01", 2) cart_item_2 = CartItem("sku_02", 5) cart.add_item(cart_item_1) cart.add_item(cart_item_2) cart.remove_item(cart_item_1) cart.checkout() # Added item sku_01 to cart of user user_01 # [add_item]: items in cart of "user_01": 1 # Added item sku_02 to cart of user user_01 # [add_item]: items in cart of "user_01": 2 # Removed item sku_01 from cart of user user_01 # [remove_item]: items in cart of "user_01": 1 # Checking out 1 # [checkout]: items in cart of "user_01": 0
Example 02: use decorator to manipulate function result
# product.py class Product: """ Product class Use multiple lines comment to annotate the class """ def __init__(self, id: str, name:str, price: float) -> None: """ init an instance Parameters: id: str name: str price: float Returns: None """ self.id = id self.name = name self.price = price # cart_item.py class CartItem: def __init__(self, product_id: str, quantity: int) -> None: self.product_id = product_id self.quantity = quantity # utils.py from functools import wraps from cart_item import CartItem from product import Product from typing import Callable, TypeVar T = TypeVar("T", bound=Callable[[Product], Product]) # decorator function receive CartItem and return Product def extract_product(func: T) -> Callable[[CartItem], Product]: @wraps(func) def wrapper(cart_item: CartItem, *args, **kwargs) -> Product: if not hasattr(cart_item, "product_id"): raise ValueError("cart_item must have product_id attribute") if not hasattr(cart_item, "quantity"): raise ValueError("cart_item must have quantity attribute") product = Product(cart_item.product_id, "fake_name", 0.0) return func(product, *args, **kwargs) return wrapper # main.py from cart_item import CartItem from product import Product from utils import extract_product cart = ShoppingCart("user_01") cart_item_1 = CartItem("sku_01", 2) cart_item_2 = CartItem("sku_02", 5) # return product object with decorator function @extract_product def process_checkout(product: Product): print(f"Checking out product {product.id}") # passing cart_item object process_checkout(cart_item_1) # Checking out product sku_01 process_checkout(cart_item_2) # Checking out product sku_02
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).
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.
def simple_generator(): print('first yield') yield 1 print('second yield') yield 2 print('third yield') yield 3 # using generator gen = simple_generator() print(f'received {next(gen)}') print(f'received {next(gen)}') print(f'received {next(gen)}') print(f'received {next(gen)}')
Explain example from above:
- first call
next(gen)
- print first yield
- encountered
yield 1, pausese and return 1 to the caller - the state of the generator is saved right after
yield 1
- second call
next(gen)
- the generator resume from where it pauseed, after
yield 1 - print second yield
- encountered
yield 2, pausese and return 2 to the caller - the state of the generator is saved right after
yield 2
- third call
next(gen)
- the generator resume from where it pauseed, after
yield 2 - print third yield
- encountered
yield 3, pausese and return 2 to the caller - the state of the generator is saved right after
yield 3
- fourth call
next(gen)
- there is nothing left in the generator so it will raise StopIteration exception
Key points about yield
- 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.
- 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
- Return vs Yield: While return exist a function completely, yield pause the function, allow it to resume later
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.
Note: range replaces xrange in Python 3
def fibonacci_generator(limit: int): a, b = 0, 1 while a < limit: yield a a, b = b, a + 1 for number in fibonacci_generator(10): print(number)
Benefit of generators
- Memory Efficiency: Generators are memory-efficient since they produre items once at a time and do not store the entire sequence in memory
- Lazy Evaluation: Generators allow for lazy evalation, meaning values are computed as needed, which is useful when working with large datasets or infinite sequences.
- Simplified Code: Generators can simplify code by eliminating the need for complex iterator logic
# reading large file with generator def read_large_file(path: str): with open(path, 'r') as file: for line in file: yield line.strip() for contact in read_large_file('contacts.txt'): print(contact)
An anonymous function in Python is a function without a name. It can be immediately invoked or stored in a variable.
Anonymous functions in Python are also known as lambda functions.
# syntax lambda parameter(s) : expression price = 1.99 # note that lambda has no name, we assign lambda function to variable revenue # so that we can easily invoke the function through the variable. revenue = lambda quantity: quantity * price revenue(10) # 19.9 # Lambda functions can take any number of arguments: sum = lambda a, b: a + b sum(2, 3) # lamda function does not support type annotation sum = lambda a: int, b: int: a + b # SyntaxError: invalid syntax sum(2, 3) # you can invoke lambda function directly like this (lambda a,b : a + b)(2, 5) #7 # lambda function always return data without `return` keyword # it will return None by default if you don't provide any data print((lambda name : print(f'Hi {name}'))("Tuan")) # Hi Tuan, None # when to use `lambda` over `function`? # you want to use function just once, especially useful when working with `map, reduce, filter` # you need a short function which never consider to reuse # you don't want to write explicit function numbers = [1, 3, 5, 7, 9] double_result = map(lambda value: value + value, numbers) print(list(double_result)) # [2, 6, 10, 14, 18]
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.
This can be useful for creating function factories or function decorators.
How closures works?
- Nested Function A function defined inside another function
- Free variables Variables from the outer scope that the inner function can reference
- Returning the Inner Function The outer function returns the inner function, which captures the state of the outer function's variables
def outer_function(name: str): def inner_function(): print(f'Hello {name}') return inner_function # create a closure closure = outer_function('Tuan') closure() # another example def outer_function(): count: int = 0 def inner_function(): nonlocal count count += 1 print(count) return inner_function # create a closure closure = outer_function() closure() # 1 closure() # 2 closure() # 3
Closures are useful for creating functions with some preset configurations.
def make_multiplier(multiplier: int): def multiplier_func(input: int) -> int: return input * multiplier return multiplier_func # create multiplier functions double = make_multiplier(2) triple = make_multiplier(3) quad = make_multiplier(4) print(double(5)) # 10 print(double(8)) # 16 print(triple(5)) # 15 print(triple(8)) # 24 print(quad(5)) # 20 print(quad(8)) # 32
Key points about closures
- State Retention: Closures retain the state of the variables in the outer function's scope
- Encapsulation: Closures can encapsulate functionality and data together
- Function Factories: Closures are ofent used to craete functions with specific behaviors preset
Avoiding common pitfalls
- Mutable variable: be cautious with mutable variables in closures as they can lead to unexpected behaviors
- Late binding: Python uses late binding for closures, meaning the values of variables are looked up when the inner function is called. This can cause issues if the variable values change after the closure is created. You can avoid this by using default arguments:
def outer_func(numbers): return [lambda x, n = n: x + n for n in numbers] closures = outer_func([1, 2, 3]) print([func(2) for func in closures]) # [3, 4, 5]
Real world examples of closures
from enum import Enum class LogLevel(Enum): DEBUG = "DEBUG" INFO = "INFO" ERROR = "ERROR" def create_logger(level: LogLevel): def logger(message: str): print(f"[{level.value}] {message}") return logger info_logger = create_logger(LogLevel.INFO) error_logger = create_logger(LogLevel.ERROR) info_logger('Something goes wrong') # [INFO] Something goes wrong info_logger('Eiusmod mollit proident mollit laboris duis qui ut sint.') # [INFO] Eiusmod mollit proident mollit laboris duis qui ut sint. error_logger('Something goes wrong') # [ERROR] Something goes wrong
Simple caching mechanism to store previously computed values
def cache_func(func): cache = {} def wrapper(n): if n not in cache: cache[n] = func(n) return cache[n] return wrapper @cache_func def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(10)) # 55 print(fibonacci(23)) # 28657
A Python module is a file containing Python code. It can define functions, classes and include runnable code.
Modules allow you to organize your code into manageable parts and reuse it across different programs.
Simply create a file with a .py extension.
# transaction.py # variable in modules mock = { "tranId": "random_id", "amount": 0.0, "tax": 0.0, "address": "123 Sub St", } def add(amount: int, tax: float, address: str) -> str: return f'tranId: random_id, {amount =}, {tax =}, {address =}' def update(transId: str, amount: int, tax: float, address: str) -> str: return f'updated. tranId: {transId}, {amount =}, {tax =}, {address =}'
To use a module, import it into your script using the import statement
import transaction print(transaction.mock) # {'tranId': 'random_id', 'amount': 0.0, 'tax': 0.0, 'address': '123 Sub St'} print(transaction.add(10, 20.0, "nowhere")) print(transaction.update("011010", 10, 20.0, "nowhere"))
You can import specific functions or classes from a module, separated by commas
from transaction import add, update print(add(10, 20.0, "nowhere")) print(update("011010", 10, 20.0, "nowhere"))
You can also give an imported item an alias
from transaction import update as modify print(modify("011010", 10, 20.0, "nowhere"))
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
# greeting.py def greet(name: str) -> None: print(f'Hello {name}') if __name__ == "__main__": greet("Main") # When you execute greeting.py module, it will print Hello Main # However if you import greeting.py from another script, the code inside the # `if __name__ == "__main__":` block does not execute
# greeting.py age = 10 def greet(name: str) -> None: print(f'Hello {name}') if __name__ == "__main__": greet("Main") # main.py import greeting dir(greeting) # ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'age', 'greet']
Python prefer local module over modules come from different places (included Python built-in modules)
If you have math.py module in the same place with your current script
It will be loaded instead of the built-in Python module
# math.py def add(a: int, b: int) -> int: return a + b # main.py import math # this is math.py because it have higher priority print(math.add(1, 2))
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
import platform import math import io print(platform.system()) print(math.sqrt(12)) print(io.open("contacts.txt").readline())
Python is an Object Oriented Programming language.
Almost everything in Python is an object with its properties and methods.
A class is like an object constructor or a blueprint for creating objects.
To create a class, use class keyword
# order.py class Order: # A class attribute. It is shared by all instances of this class TAG: str = "ORDER" # basic initializer, this is called when this class is instantiated. # methods (or objects or attributes) like: __init__, __str__, __repr__ etc... are called # special methods (sometime called dunder methods) def __init__(self, id): # assign the argument to the instance's id attribute self.id = id # the leading underscore indicate the `status` property # is intended to be used internally # do not rely on this to be enforced: it's a hint to other devs self._status = 0 # `__repr__` method is intended to provide a "representation" of the object # that is useful for debugging and development purposes. def __repr__(self): return f'{self.id = }, {self.amount = }, {self.status = }' # `__str__` method, on the other hand, is intended to provide a "string" # representation of the object that is more user-friendly and suitable # for display to end users def __str__(self): return f"Order {self.id}, amount: {self.amount}, status: {self.status}" # an instance method # all method take `self` as the first argument # `self` parameter is a reference to the current instance of the class, # and is used to access variables that belongs to the class def place(self, amount:float, address: str) -> None: self.amount = amount print(f'Placed order {self.id}, {amount}, {address}') # a class method is shared among all instances # they are called with the calling class as the first argument @classmethod def get_id(cls): return cls.id # a static method is called without a class or instance reference @staticmethod def payment_method(): return "Paypal" # property is just like a getter # it turns the method status() into a read-only attribute of the same name # there's no need to write trivial getters and setters in Python @property def status(self): return self._status # this allows the @property to be set # `status` is the @property that we define from above @status.setter def status(self, status): self._status = status # this allows the property to be deleted @status.deleter def status(self): del self._status # when a Python interpreter reads a source file it executes all its code. # this __name__ check makes sure this code block is only executed when # this module is the main program if __name__ == "__main__": print("Hello from main") # main.py from order import Order first_order = Order("000001") # access class attribute first_order.TAG # ORDER # modify class attribute first_order.TAG = "USER" # reassign value of object property first_order.amount = 200.123 # get object property print(first_order.amount) # invoke property (getter) print(first_order.status) # assigning properter (setter) first_order.status = 1 # delete property from object del first_order.amount del first_order.status # delete object del first_order # invoke class method first_order.place(10.0, "123 Main St") # Placed order 000001, 10.0, 123 Main St first_order.get_id() # 000000 # invoke static method via instance print(first_order.payment_method()) # Paypal # invoke static method via class name print(Order.payment_method()) # Paypal
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.
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
In Python, immutability is not enforced on attributes or variables. Instead, it's a characteristic of the data types:
- Immutable types: int, float, str, tuple etc...
- Mutable types: list, dict, set, etc...
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.
To ensure static attribute are immutable we have one possible way:
- Convention: Use naming conventions to indicate that an attribute should not be modified (e.g STATIC_ATTRIBUTE, TAG, UPPERCASE_VARIABLE)
Inheritance allows us to define a class that inherits all the methods and properties from another class.
Parent class is the class being inherited from, also called base class
Child class is the class that inherites from Parent class, also called derived class
# Parent class class Person: def __init__(self, *, first_name: str, last_name: str): self.first_name = first_name self.last_name = last_name def __str__(self): return f'{self.first_name} {self.last_name}' def say_my_name(self): print(f'Hi {self.first_name} {self.last_name}') tuan = Person(first_name = "Tuan", last_name = "Nguyen") tuan.say_my_name() # Hi Tuan Nguyen # Child class class Student(Person): def __init__(self, *, first_name: str, last_name: str, class_name: str): # call parent's `__init__` function to keep inheritance Person.__init__(self, first_name = first_name, last_name = last_name) # or use `super()` function which have the same result but look nicer super().__init__(first_name = first_name, last_name = last_name) # child class property self.class_name = class_name @property def name(self): return f'{self.first_name} {self.last_name}' @name.setter def name(self, name: str): arr = name.split(" ") first_name = arr[0] if len(arr) >= 1 else "default_fname" last_name = arr[1] if len(arr) >= 2 else "default_lname" self.first_name = first_name self.last_name = last_name def welcome(self): print(f'Welcome {self.name} to the class {self.class_name}') def __str__(self): return f'[{self.class_name}]: {self.first_name} {self.last_name}' student_tuan = Student(first_name = "Anton", last_name = "Nguyen", class_name = "A3") # invoke method from parent class student_tuan.say_my_name() # Hi Anton Nguyen student_tuan.welcome() # Welcome Anton Nguyen to the class A3 student_tuan.name = "Kim Phuong" student_tuan.welcome() # Welcome Kim Phuong to the class A3
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++.
Python use naming conventions to indicate the intended level of access:
- Public: Public attributes and methods are accessible from anywhere. By default, all attributes and methods are public.
class MyClass: def __init__(self): self.public_var = "I am public" def public_method(self): print("this is a public method") obj = MyClass() print(obj.public_var) # I am public obj.public_method() # this is a public method
- 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.class MyClass: def __init__(self): self._protected_var = "I am protected" def _protected_method(self): print("this is a protected method") class SubClass(MyClass): def access_protected(self): print(self._protected_var) # I am protected self._protected_method() # this is a protected method obj = SubClass() obj.access_protected()
- 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.# example 01 class MyClass: def __init__(self): self.__private_var = "I am private" def __private_method(self): print("this is a private method") class SubClass(MyClass): def access_protected(self): print(self.__private_var) # raise AttributeError self.__private_method() # raise AttributeError obj = SubClass() obj.access_protected() # example 02 # Python doesn't block access to private data, it just leaves for the wisdom of the programmer, # not to write any code that access it from outside the class. # You can still access the private attributes/methods by Python's name mangling technique # but will breaks the encapsulation principle so not recommend to do this in real-life class SubClass(MyClass): def access_protected(self): print(self._MyClass__private_var) # I am protected self._MyClass__private_method() # this is a protected method obj = SubClass() obj.access_protected()
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.
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.
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.
By defining an abstract base class, you can define a common Application Program Interface (API) for a set of subclasses.
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.
-
Example 01
from abc import ABC, abstractmethod class Polygon(ABC): # abstract method will be force to be implement in subclasses @abstractmethod def noofsides(self): pass class Triangle(Polygon): # must concrete implement abstract method def noofsides(self): print("I have 3 sides") class Pentagon(Polygon): # must concrete implement abstract method def noofsides(self): print("I have 5 sides") class Hexagon(Polygon): # must concrete implement abstract method def noofsides(self): print("I have 6 sides") triangle = Triangle() triangle.noofsides() pentagon = Pentagon() pentagon.noofsides() hexagon = Hexagon() hexagon.noofsides()
-
Example 02
from abc import ABC, abstractmethod class Record(ABC): @abstractmethod def __str__(self) -> str: pass @abstractmethod def create(self) -> bool: pass @abstractmethod def update(self, *args) -> bool: pass @abstractmethod def delete(self) -> bool: pass @staticmethod @abstractmethod def TAG(): pass class UserRecord(Record): def __init__(self, name: str, bod: str, avatar: str) -> None: self.name = name self.bod = bod self.avatar = avatar def __str__(self) -> str: return f"[{UserRecord.TAG()}]: {self.name} {self.bod} {self.avatar}" def create(self) -> bool: # execute sql command to create user return True def update(self, *args) -> bool: # execute sql command to update user return True def delete(self) -> bool: # execute sql command to delete user return True @staticmethod def TAG(): return "USER" class ProductRecord(Record): def __init__(self, name: str, price: float, description: str) -> None: self.name = name self.price = price self.description = description def __str__(self) -> str: return f"[{ProductRecord.TAG()}] {self.name} {self.price} {self.description}" def create(self) -> bool: # execute sql command to create product return True def update(self, *args) -> bool: # execute sql command to update product return True def delete(self) -> bool: # execute sql command to delete product return True @staticmethod def TAG(): return "PRODUCT" user_record = UserRecord("John", "1990-01-01", "https://example.com/avatar.jpg") print(user_record) # [USER]: John 1990年01月01日 https://example.com/avatar.jpg user_record.create() user_record.update("Johny", "1991-01-01", "https://example.com/avatar2.jpg") user_record.delete() print(UserRecord.TAG()) # USER product_record = ProductRecord("iPhone", 1000, "A new phone") print(product_record) # [PRODUCT] iPhone 1000 A new phone product_record.create() product_record.delete() print(ProductRecord.TAG()) # PRODUCT
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.
len() is one of many function polymorphism in Python that accept many kind of data
# str len("Tuan") # 4 # tuple len(("apple", "banana", "cherry")) # 3 # list len(["apple", "banana", "cherry"]) # 3 # dict len({"foo": "bar"}) # 1
Polymorphism is often used in Class methods, where we can have multiple classes with the same method name.
# polymorphism method `eat()` appear to all classes class Dog: def __init__(self, name: str): self.name = name def eat(self): print('Dog is eating') class Cat: def __init__(self, name: str, age: int): self.name = name self.age = age def eat(self): print('Cat is eating') dog = Dog("Grant") dog.eat() # Dog is eating cat = Cat("Banana", 3) cat.eat() # Cat is eating
Yes, we could do that
class Animal: def __init__(self, name: str): self.name = name def eat(self): print('Animal is eating') class Dog(Animal): def __init__(self, name: str, age: int): super().__init__(name) self.age = age def eat(self): print('Dog is eating') class Cat(Animal): def __init__(self, name: str, age: int): super().__init__(name) self.age = age def eat(self): print('Cat is eating') dog = Dog("Grant", 5) dog.eat() # Dog is eating cat = Cat("Banana", 3) cat.eat() # Cat is eating
This module provides a decorator and functions for automatically adding generated special methods such ash __init__ and __repr()__ to user-defined classes.
from dataclasses import dataclass, field @dataclass class InventoryItem: """Class for keeping track of an item in inventory""" name: str # if the default value of a field is specified by a call to `field()`, then the class attribute # for this field will be replaced by the specified `default` value. # if `default` is not provided, the the class attribute will be deleted unit_price: float = field(repr = False, default = 0.0) quantity_on_hand: int = 0 source: str = field(repr = True, default = "global") @property def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand inventory_product_one = InventoryItem("Product name 01", 1.99, 1000) print(inventory_product_one) # InventoryItem(name='Product name 01', quantity_on_hand=1000, source='global') print(inventory_product_one.total_cost) # 1990.0 inventory_product_two = InventoryItem("Product name 01", 1.99, 1000) if inventory_product_one == inventory_product_two: # True print("2 product are the same")
So we don't need to add an __init__() method like this
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0): self.name = name self.unit_price = unit_price self.quantity_on_hand = quantity_on_hand
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.
The basic structure looks like this
try: result = 1 + non_exist_variable except NameError as e: print(e) else: print("another error") finally: print("finally - cleanup operation")
Explanation
- try Block:
- The code that yoy want to execute and that might raise an exception in placed inside the try block
- except Block:
- If an excetion occurs in the
tryblock, the code in the except block is executed. You can catch specific exceptions by specifying the exception type (e.gexcept NameError) - The
as epart allows you to capture the exception object and access its message or other attributes
- else Block:
- The
elseblock is optional and is executed only if no exception was raised in thetryblock
- finally Block:
- The
finallyblock 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.
Here's an example where we handle different types of exception
def divide_number(a, b): try: result = a / b except ZeroDivisionError as e: print(f"Error: {e} - Division by zero is not allowed") except TypeError as e: print(f"Error: {e} - Only numbers are allowed") else: print(f"Result: {result}") finally: print("Execution of the divide_number function is completed") divide_number(10, 0) # Error: division by zero - Division by zero is not allowed # Execution of the divide_number function is completed divide_number(10, "a") # Error: unsupported operand type(s) for /: 'int' and 'str' - Only numbers are allowed # Execution of the divide_number function is completed divide_number(10, 2) # Result: 5.0 # Execution of the divide_number function is completed
Handle multiple exceptions
try: risky_operation() except (ValueError, TypeError) as e: print(f"Error: {e}")
Sometime you might want to raise an exception manually using the raise keyword
def check_age(age): if age < 0: raise ValueError("Age cannot be negative") return f"Age is {age}" try: check_age(-1) except ValueError as e: print(f"Exception '{e}'") # Exception 'Age cannot be negative'
You can define your own exceptions by creating a new class that inherits from Python's built-in Exception class
class InsufficientBalanceError(Exception): pass def checkout(total: float, balance: float) -> bool: if balance < 0: raise InsufficientBalanceError("Negative balance is not allowed") if balance < total: raise InsufficientBalanceError("Your balance is not enough for checkout") return balance >= total try: checkout(29.9, 10) except InsufficientBalanceError as e: print(f'Error: {e}') # Error: Your balance is not enough for checkout
Several built-in Python exceptions that can be raised when an error occurs during the execution of a program
- SyntaxError: raised when the interpreter encouters a syntax error in the code, such as a misspelled keyword, a missing colon, or an unbalanced parenthesis.
- TypeError: raised when an operation or function is applied to an object of the wrong type, such as adding a string to an integer.
- NameError: raised when a variable or function name is not found in the current scope
- IndexError: raised when an index is out of range for a list, tuple, or other sequence types
- KeyError: raised when a key is not found in a dictionary
- 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
- 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
- IOError: raised when an I/O operation, such as reading or writing a file, fails due to an input/output error
- ZeroDivisionError: raised when an attempt is made to divide a number by zero
- ImportError: raised when an import statement fails to find or load a module
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
filter() will return an iterator that is already filtered
# filter with custom function def filter_high_salary(salary: float) -> bool: return salary > 30_000_000.0 # filter a list list_salary = [10_000_000.0, 15_000_000.0, 25_000_000.0, 50_000_000.0, 120_000_000.0] high_salary = filter(filter_high_salary, list_salary) for salary in high_salary: print(f'high salary: {salary}') # high salary: 50000000.0 # high salary: 120000000.0 # filter with lambda high_salary = filter(lambda salary: salary > 30_000_000.0, list_salary) # flter with lambda and custom function high_salary = filter(lambda salary: filter_high_salary(salary), list_salary) # filter a tuple person = ("Tuan Nguyen", 35, "Male", "Halong Bay, Quang Ninh, Vietnam") filtered_int_only = filter(lambda x: isinstance(x, int), person) print(list(filtered_int_only)) # [35] # filter a set points = (88, 58, 67, 88, 45, 100, 95, 40, 20, 78, 78) good_points = filter(lambda point: point >= 80, points) print(list(good_points)) # [88, 88, 100, 95] # filter an iterable from typing import Dict, Iterator, Callable # Define the type for the student data StudentInfo = Dict[str, Dict[str, int | str]] students: StudentInfo = { "STD001": { "name": "Tuan Nguyen", "age": 20 }, "STD002": { "name": "Anton", "age": 18 }, "STD003": { "name": "Jimmy", "age": 22 } } student_iterators = iter(students) def filter_by_age(key: str) -> bool: age = students[key]["age"] if isinstance(age, int): return age > 20 return False filtered_students: Iterator[str] = filter(filter_by_age, student_iterators) # Convert the filtered students to a dictionary filtered_students_dict: StudentInfo = {key: students[key] for key in filtered_students} # Print the filtered students print(filtered_students_dict)
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.