From 4a339e40599703013c3a9e28d6420a625f808b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Gr=C4=99dowski?= Date: Fri, 6 Sep 2024 08:21:33 +0200 Subject: [PATCH] Add optimization and essentials --- .presentations/rust_python/plugin.js | 1 - .presentations/rust_python/rust_python.md | 1 + .vscode/settings.json | 11 ++- .../01_modules/00_module_as_file/__init__.py | 0 .../01_modules/00_module_as_file/common.py | 5 + .../01_modules/00_module_as_file/main.py | 8 ++ .../01_module_as_directory/__init__.py | 0 .../01_module_as_directory/common/__init__.py | 4 + .../common/_subtract.py | 2 + .../01_module_as_directory/common/add.py | 45 +++++++++ .../01_modules/01_module_as_directory/main.py | 11 +++ src/essentials/02_names/main.py | 87 +++++++++++++++++ src/essentials/03_more_of_names/__init__.py | 7 ++ src/essentials/03_more_of_names/bad.py | 22 +++++ src/essentials/03_more_of_names/good.py | 5 + .../04_global_variables/00_data.csv | 6 ++ .../04_global_variables/__init__.py | 0 .../04_global_variables/__main__.py | 28 ++++++ .../04_global_variables/divisions.py | 31 +++++++ src/essentials/04_global_variables/reader.py | 16 ++++ .../04_global_variables/settings.py | 15 +++ src/optimization/__init__.py | 0 src/optimization/palindrome.py | 88 ++++++++++++++++++ src/optimization/sum_of_squares.py | 93 +++++++++++++++++++ 24 files changed, 484 insertions(+), 2 deletions(-) delete mode 100644 .presentations/rust_python/plugin.js create mode 100644 src/essentials/01_modules/00_module_as_file/__init__.py create mode 100644 src/essentials/01_modules/00_module_as_file/common.py create mode 100644 src/essentials/01_modules/00_module_as_file/main.py create mode 100644 src/essentials/01_modules/01_module_as_directory/__init__.py create mode 100644 src/essentials/01_modules/01_module_as_directory/common/__init__.py create mode 100644 src/essentials/01_modules/01_module_as_directory/common/_subtract.py create mode 100644 src/essentials/01_modules/01_module_as_directory/common/add.py create mode 100644 src/essentials/01_modules/01_module_as_directory/main.py create mode 100644 src/essentials/02_names/main.py create mode 100644 src/essentials/03_more_of_names/__init__.py create mode 100644 src/essentials/03_more_of_names/bad.py create mode 100644 src/essentials/03_more_of_names/good.py create mode 100644 src/essentials/04_global_variables/00_data.csv create mode 100644 src/essentials/04_global_variables/__init__.py create mode 100644 src/essentials/04_global_variables/__main__.py create mode 100644 src/essentials/04_global_variables/divisions.py create mode 100644 src/essentials/04_global_variables/reader.py create mode 100644 src/essentials/04_global_variables/settings.py create mode 100644 src/optimization/__init__.py create mode 100644 src/optimization/palindrome.py create mode 100644 src/optimization/sum_of_squares.py diff --git a/.presentations/rust_python/plugin.js b/.presentations/rust_python/plugin.js deleted file mode 100644 index 0967ef4..0000000 --- a/.presentations/rust_python/plugin.js +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/.presentations/rust_python/rust_python.md b/.presentations/rust_python/rust_python.md index f60d43f..cae68a9 100644 --- a/.presentations/rust_python/rust_python.md +++ b/.presentations/rust_python/rust_python.md @@ -30,6 +30,7 @@ revealOptions: + ## How Rust is speeding up Python (bindings and tooling) "Rust and Python: A Match Made in Heaven\*" diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f843d1..b197524 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,14 @@ "python.testing.pytestEnabled": true, "python.linting.enabled": false, "ruff.organizeImports": true, - "ruff.fixAll": true + "ruff.fixAll": true, + "ruff.lint.args": [ + "--config", + "pyproject.toml" + ], + "ruff.lint.enable": false, + "ruff.format.args": [ + "--config", + "pyproject.toml" + ] } diff --git a/src/essentials/01_modules/00_module_as_file/__init__.py b/src/essentials/01_modules/00_module_as_file/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/essentials/01_modules/00_module_as_file/common.py b/src/essentials/01_modules/00_module_as_file/common.py new file mode 100644 index 0000000..5fe9e5e --- /dev/null +++ b/src/essentials/01_modules/00_module_as_file/common.py @@ -0,0 +1,5 @@ +def add_two_numbers(a, b): + return a + b + +def subtract_two_numbers(a, b): + return a - b diff --git a/src/essentials/01_modules/00_module_as_file/main.py b/src/essentials/01_modules/00_module_as_file/main.py new file mode 100644 index 0000000..6fce426 --- /dev/null +++ b/src/essentials/01_modules/00_module_as_file/main.py @@ -0,0 +1,8 @@ +from .common import add_two_numbers +from .common import subtract_two_numbers + +if __name__ == '__main__': + a = 1 + b = 2 + print(f"Add {a} and {b}: {add_two_numbers(a, b)}") + print(f"Subtract {a} and {b}: {subtract_two_numbers(a, b)}") diff --git a/src/essentials/01_modules/01_module_as_directory/__init__.py b/src/essentials/01_modules/01_module_as_directory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/essentials/01_modules/01_module_as_directory/common/__init__.py b/src/essentials/01_modules/01_module_as_directory/common/__init__.py new file mode 100644 index 0000000..94e6427 --- /dev/null +++ b/src/essentials/01_modules/01_module_as_directory/common/__init__.py @@ -0,0 +1,4 @@ +from ._add import add_two_numbers # noqa: F401 +from ._subtract import subtract_two_numbers # noqa: F401 + + diff --git a/src/essentials/01_modules/01_module_as_directory/common/_subtract.py b/src/essentials/01_modules/01_module_as_directory/common/_subtract.py new file mode 100644 index 0000000..d147cba --- /dev/null +++ b/src/essentials/01_modules/01_module_as_directory/common/_subtract.py @@ -0,0 +1,2 @@ +def subtract_two_numbers(a, b): + return a - b diff --git a/src/essentials/01_modules/01_module_as_directory/common/add.py b/src/essentials/01_modules/01_module_as_directory/common/add.py new file mode 100644 index 0000000..877ae5d --- /dev/null +++ b/src/essentials/01_modules/01_module_as_directory/common/add.py @@ -0,0 +1,45 @@ +def _add(a, b): + return a + b + +def _check_if_positive(a): + ... + +def add_two_numbers(a, b): + _check_if_positive(a) + return _add(a, b) + + +class Calculator: + class _Adder: + def add(self, a, b): + return a + b + class Subtracter: + def subtract(self, a, b): + return a - b + + class Config: + ... + +Calculator._Adder() +Calculator.Config() + + +quarter = 0.25 + +plot_size_multiplier = [0.5, 0.25, 0.25] + +plot_size_multiplier[0] + +plot_size_multiplier[1] + +standard_plot_width = 1000 + +plot_2_size = standard_plot_width * plot_size_multiplier[1] +plot_2_size = 1000 * 0.25 +plot_2_size = 250 + +# + +time_for_timeout = 60 * 60 * 24 # 24 hours + +timeout(86400) diff --git a/src/essentials/01_modules/01_module_as_directory/main.py b/src/essentials/01_modules/01_module_as_directory/main.py new file mode 100644 index 0000000..5b7e130 --- /dev/null +++ b/src/essentials/01_modules/01_module_as_directory/main.py @@ -0,0 +1,11 @@ +from .common import add_two_numbers +from .common import subtract_two_numbers + + +from .common import add_two_numbers + +if __name__ == '__main__': + a = 1 + b = 2 + print(f"Add {a} and {b}: {add_two_numbers(a, b)}") + print(f"Subtract {a} and {b}: {subtract_two_numbers(a, b)}") diff --git a/src/essentials/02_names/main.py b/src/essentials/02_names/main.py new file mode 100644 index 0000000..a1a4b35 --- /dev/null +++ b/src/essentials/02_names/main.py @@ -0,0 +1,87 @@ +# 1. Non-descriptive function names +def func(): + pass + +# 2. Overly abbreviated function names +def calc(): + pass + +# 3. Misleading function names +def get_data(): + pass # But it actually updates the database + +# 4. CamelCase function names (not recommended in Python) +def getData(): + pass + +def set_a_number(): + pass + +# 1. Non-descriptive class names +class MyClass: + pass + +# 2. Overly abbreviated class names +class C: + pass + +# 3. Misleading class names +class Manager: + pass # But it actually represents a single employee + +# 4. CamelCase class names (not recommended in Python) +class MyFirstClass: + pass + + +# 1. Single-letter variable names +x = 10 + +diameter = 10 + +# 2. Overly abbreviated variable names +temp = 25 + +# 3. Misleading variable names +num = 'John' # When it should represent an integer + +data = {} + +# 4. Using reserved keywords as variable names +for_ = 5 # 'for' is a reserved keyword in Python + +type_ = 123 + +__a = 1 + +## Other bad examples +flag = False # It's not clear what 'flag' is indicating. +use_database = True # It's indicating action, not a state. +red = True +x = 172800 # It's not clear what 'x' represents. + +## Good examples +should_use_database = True +is_red = True +two_days = 60 * 60 * 24 * 2 # 2 days in seconds +timeout = two_days + + +class PositiveCalculator: + + + def _validate_number(self, number): + if number < 0: + raise ValueError("Number must be positive") + + def add(self, a, b): + self._validate_number(a) + self._validate_number(b) + return a + b + + def subtract(self, a, b): + return a - b + +# 1.2.3 +# 2.0.0 + diff --git a/src/essentials/03_more_of_names/__init__.py b/src/essentials/03_more_of_names/__init__.py new file mode 100644 index 0000000..8020aba --- /dev/null +++ b/src/essentials/03_more_of_names/__init__.py @@ -0,0 +1,7 @@ +flag = ... +use_database = ... +red = ... +x = ... + + +should_use_database diff --git a/src/essentials/03_more_of_names/bad.py b/src/essentials/03_more_of_names/bad.py new file mode 100644 index 0000000..6f45ff2 --- /dev/null +++ b/src/essentials/03_more_of_names/bad.py @@ -0,0 +1,22 @@ +## Other bad examples +flag = False # It's not clear what 'flag' is indicating. +use_database = True # It's indicating action, not a state. +red = True +x = 172800 # It's not clear what 'x' represents. + + + +def use_database(): + database.use() + pass + +def run_a_car(): + pass + +def should_run_a_car(): + +# use_database = True + +def func(callable): + ... + diff --git a/src/essentials/03_more_of_names/good.py b/src/essentials/03_more_of_names/good.py new file mode 100644 index 0000000..5f148ca --- /dev/null +++ b/src/essentials/03_more_of_names/good.py @@ -0,0 +1,5 @@ +## Good examples +should_use_database = True +is_red = True +two_days = 60 * 60 * 24 * 2 # 2 days in seconds +timeout = two_days diff --git a/src/essentials/04_global_variables/00_data.csv b/src/essentials/04_global_variables/00_data.csv new file mode 100644 index 0000000..3afb6a1 --- /dev/null +++ b/src/essentials/04_global_variables/00_data.csv @@ -0,0 +1,6 @@ +Country,Capital,CountryCode +Poland,Warsaw,PL +Denmark,Copenhagen,DK +Malaysia,Kuala Lumpur,MY +Germany,Berlin,DE +Spain,Madrid,ES diff --git a/src/essentials/04_global_variables/__init__.py b/src/essentials/04_global_variables/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/essentials/04_global_variables/__main__.py b/src/essentials/04_global_variables/__main__.py new file mode 100644 index 0000000..19383f1 --- /dev/null +++ b/src/essentials/04_global_variables/__main__.py @@ -0,0 +1,28 @@ +import pathlib +import typing + +from reader import read_csv +from divisions import get_divisions_for_country, print_divisions +from settings import settings + + + +def main(): + settings.print_app_title() + + path_to_file = str(pathlib.Path(__file__).parent / "00_data.csv") + data = read_csv(path_to_file) + for row in data: + country_code = row.get("CountryCode") + if (country_code is None) or country_code in settings.skip_country_codes: + continue + divisions = get_divisions_for_country(country_code) + print_divisions( + country_name=row['Country'], + capital=row['Capital'], + divisions=divisions + ) + + +if __name__ == '__main__': + main() diff --git a/src/essentials/04_global_variables/divisions.py b/src/essentials/04_global_variables/divisions.py new file mode 100644 index 0000000..27dd2d3 --- /dev/null +++ b/src/essentials/04_global_variables/divisions.py @@ -0,0 +1,31 @@ +import argparse +import re +import httpx +import typing + +def _get_url_for_country(country_code: str): + return f"https://rawcdn.githack.com/kamikazechaser/administrative-divisions-db/master/api/{country_code}.json" + +def get_divisions_for_country(country_code: str): + return httpx.get(_get_url_for_country(country_code)).json() + +def print_divisions(*, country_name: str, divisions: typing.List[str], capital: typing.Union[str, None] = None): + print("====================================") + capital_part = f" ({capital})" if capital else "" + print(f"Divisions for {country_name}{capital_part}:") + for division in divisions: + print(f"- {division}") + print("====================================") + +argparser = argparse.ArgumentParser() + +argparser.add_argument("--country", help="Country code to get divisions for", default="PL") +argparser.add_argument("--pretty", help="Pretty print the output", action="store_true") +args = argparser.parse_args() + +divisions = get_divisions_for_country(args.country) + +if args.pretty: + print_divisions(country_name=args.country, divisions=divisions) +else: + print(divisions) diff --git a/src/essentials/04_global_variables/reader.py b/src/essentials/04_global_variables/reader.py new file mode 100644 index 0000000..b2b06f5 --- /dev/null +++ b/src/essentials/04_global_variables/reader.py @@ -0,0 +1,16 @@ +import pathlib +import csv + +def read_csv(path_to_file: str): + with open(path_to_file, mode='r') as file: + reader = csv.DictReader(file) + data = list(reader) + + return data + + +path_to_file = str(pathlib.Path(__file__).parent / "00_data.csv") +print("====================================") +print(read_csv(path_to_file)) +print("====================================") + diff --git a/src/essentials/04_global_variables/settings.py b/src/essentials/04_global_variables/settings.py new file mode 100644 index 0000000..8bd1de8 --- /dev/null +++ b/src/essentials/04_global_variables/settings.py @@ -0,0 +1,15 @@ +import dataclasses + + +@dataclasses.dataclass +class _AppSettings: + title: str + should_shout: bool = False + skip_country_codes: list = dataclasses.field(default_factory=list) + + def print_app_title(self): + title = self.title.upper() if self.should_shout else self.title + print(title) + + +settings = _AppSettings(title="Divisions Finder", skip_country_codes=["DE"]) diff --git a/src/optimization/__init__.py b/src/optimization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/optimization/palindrome.py b/src/optimization/palindrome.py new file mode 100644 index 0000000..f32c156 --- /dev/null +++ b/src/optimization/palindrome.py @@ -0,0 +1,88 @@ +""" +"We should forget about small efficiencies, say about 97% of the time: premature +optimization is the root of all evil. Yet we should not pass up our opportunities +in that critical 3%" + +https://en.wikipedia.org/wiki/Program_optimization + +Premature optimization can lead to several issues: + +- Increased Complexity: Introducing optimizations too early can make the code more complex and harder to understand, maintain, and debug. +- Wasted Resources: Time and effort spent on optimizing parts of the code that do not significantly impact performance could be better used elsewhere. +- Reduced Flexibility: Early optimizations can create rigid structures that are difficult to modify or extend, limiting the ability to adapt to future requirements or improvements. +""" + +""" +A palindrome is a word, number, phrase, or other sequence of symbols that reads the +same backwards as forwards, such as madam or racecar. + +https://en.wikipedia.org/wiki/Palindrome +""" + + +def is_palindrome__simple(s): + """ + When `is_palindrome__simple` is good enough? + - when we want to check not *a lot* of strings + - when the strings are not *too long*, I mean 1_000_000 + - when after profiling we see that the function is not a bottleneck and other parts of the code + are taking *much* (100x) more time + """ + return s == s[::-1] + + +def is_palindrome__optimized_v1(s): + """ + When `is_palindrome__optimized` is required? + - when we want to check *a lot* of strings + - when the strings are *too long*, I mean 1_000_000_000 + - when after profiling we see that the function is a bottleneck and other parts of the code + """ + n = len(s) + for i in range(n // 2): + if s[i] != s[n - i - 1]: + return False + return True + + +def is_palindrome__optimized_v2(string): + """ + When `is_palindrome__optimized` is required? + - when we want to check *a lot* of strings + - when the strings are *too long*, I mean 1_000_000_000 + - when after profiling we see that the function is a bottleneck and other parts of the code + """ + length = len(string) + for i in range(length // 2): + if string[i] != string[length - i - 1]: + return False + return True + + +# def measure_them(string_formula: str, number: int = 1_000): + +# import timeit + +# s = eval(string_formula) +# print(f"String formula: {string_formula}") + +# simple_time = timeit.timeit(lambda: is_palindrome__simple(s), number=number) +# optimized_time_v1 = timeit.timeit(lambda: is_palindrome__optimized_v1(s), number=number) +# optimized_time_v2 = timeit.timeit(lambda: is_palindrome__optimized_v2(s), number=number) + +# padding = 30 + +# print(f"> Simple:".ljust(padding), f"{simple_time} s") +# print(f"> Optimized v1:".ljust(padding), f"{optimized_time_v1} s") +# print(f"> Optimized v2:".ljust(padding), f"{optimized_time_v2} s") + +# print(f"> Simple/Optimized:".ljust(padding), f"{simple_time / optimized_time_v2:.2f}") +# print() + + +if __name__ == "__main__": + for _ in range(1000): + is_palindrome__simple(eval('"ab" * 10**6')) + # measure_them('"ab" * 10**6', number=1_000) + # measure_them('"racecar"', number=10_000) + # measure_them('"saippuakivikauppias"', number=10_000) diff --git a/src/optimization/sum_of_squares.py b/src/optimization/sum_of_squares.py new file mode 100644 index 0000000..4dc88ee --- /dev/null +++ b/src/optimization/sum_of_squares.py @@ -0,0 +1,93 @@ +import timeit + +import numpy as np + + +def sum_of_squares_v1(numbers): + result = 0 + for number in numbers: + result += number**2 + return result + + + + + + + + + + + + + + + + + + + + + + + + + +def sum_of_squares_v2(numbers): + result = 0 + for number in numbers: + # Premature optimization by using bitwise operations (unnecessary here) + result += (number << 1) * (number >> 1) + number + return result + + + + + + + + + + + + + + + + + + + + + + + + + +def sum_of_squares_v3(numbers): + # Using a more efficient built-in function for large datasets + return np.sum(np.square(numbers)) + + + + + + + + + + + + + + +if __name__ == "__main__": + + numbers = [1, 2, 3, 4, 5] + print(sum_of_squares_v1.__name__, sum_of_squares_v1(numbers)) # Output: 55 + print(sum_of_squares_v2.__name__, sum_of_squares_v2(numbers)) # Output: 55 + print(sum_of_squares_v3.__name__, sum_of_squares_v3(numbers)) # Output: 55 + + print(sum_of_squares_v1.__name__, timeit.timeit(lambda: sum_of_squares_v1(numbers * 1000), number=1_000)) + print(sum_of_squares_v2.__name__, timeit.timeit(lambda: sum_of_squares_v2(numbers * 1000), number=1_000)) + print(sum_of_squares_v3.__name__, timeit.timeit(lambda: sum_of_squares_v3(numbers * 1000), number=1_000))