├── .clang-format ├── .github └── workflows │ └── pre-commit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .style.yapf ├── CUDA ├── 1-introduction-to-CUDA.md ├── 2-CUDA-host-programming.md └── 3-CUDA-kernel-programming.md ├── MPI ├── 1-practice-pi.md ├── README.md ├── convolution │ ├── Makefile │ ├── README.md │ ├── convolution.cu │ ├── convolution.cuh │ ├── half.hpp │ ├── main.cpp │ ├── util.cpp │ └── util.h └── matmul │ ├── Makefile │ ├── README.md │ ├── main.cpp │ ├── matmul.cpp │ ├── matmul.h │ ├── run.sh │ ├── run_performance.sh │ ├── run_performance_base.sh │ ├── run_validation_npernode1.sh │ ├── run_validation_npernode2.sh │ ├── run_validation_npernode5.sh │ ├── util.cpp │ └── util.h ├── README.md ├── cpp-OOP ├── 01-introduction │ └── README.md ├── 02-method │ └── README.md ├── 03-constructor │ └── README.md ├── 04-access-specifier │ └── README.md ├── 05-encapsulation │ └── README.md ├── 06-inheritance │ └── README.md ├── 07-polymorphism │ └── README.md └── 08-template │ └── README.md ├── cpp-advanced └── cpp-rvalue-references-explained.md ├── cpp-basic ├── 01-cpp-syntax-and-variables │ └── README.md ├── 02-user-input-and-data-types │ └── README.md ├── 03-operators │ └── README.md ├── 04-strings │ └── README.md ├── 05-math-and-boolean-logic │ └── README.md ├── 06-loops-and-iteration │ └── README.md ├── 07-arrays │ └── README.md ├── 08-structures │ └── README.md ├── 09-enums │ └── README.md ├── 10-references-pointers │ └── README.md ├── 11-new-and-delete │ └── README.md ├── 12-functions │ └── README.md ├── 13-function-overloading │ └── README.md ├── 14-variable-scope │ └── README.md ├── 15-recursion │ └── README.md ├── 16-lambda-functions │ └── README.md ├── 17-errors-debugging-exceptions-validation │ └── README.md ├── 18-namespaces │ └── README.md └── README.md ├── docs ├── README.md ├── mixed-precision-training │ ├── 01-introduction-to-mixed-precision-training.md │ ├── 02-fp16-vs-bf16-formats.md │ ├── 03-training-with-mixed-precision-fp16.md │ ├── 04-mixed-precision-training-scenarios-npu.md │ └── README.md └── questions │ ├── 01-designing-python-api-for-high-performance.md │ ├── 02-minimizing-python-overhead-numeric-computations.md │ ├── 03-integrating-with-numpy-scipy.md │ ├── 04-interfacing-python-with-cpp.md │ ├── 05-challenges-calling-cpp-from-python.md │ ├── 06-handling-gil-in-cpp-backend.md │ ├── 07-cuda-vs-tensor-cores.md │ ├── 08-cuda-memory-coalescing.md │ ├── 09-warp-divergence-in-cuda.md │ ├── 10-shared-memory-in-cuda.md │ ├── 11-opencl-vs-cuda.md │ ├── 12-blas-lapack-in-python.md │ ├── 13-benchmarking-cpu-vs-gpu-kernels.md │ ├── 14-numerical-stability-in-floating-point-summation.md │ ├── 15-memory-access-patterns-cpu-performance.md │ ├── 16-why-numpy-is-faster-than-pure-python.md │ ├── 17-cupy-vs-numpy.md │ ├── 18-scipy-vs-numpy.md │ ├── 19-numba-jit-compiler-for-python.md │ ├── 20-pytorch-performance-experience.md │ ├── 21-introduction-to-jax-high-performance-python.md │ ├── 22-parallel-computing-experience-dask-mpi.md │ └── README.md ├── python-OOP ├── 01-object-oriented-programming │ ├── 01_intro_to_oop.py │ ├── 02_classes_and_instances.py │ ├── 03_instance_and_class_attributes.py │ ├── 04_encapsulation.py │ ├── 05_getters_setters.py │ ├── 06_methods_and_self.py │ ├── 07_init_method.py │ ├── 08_str_and_repr.py │ ├── 09_access_control.py │ ├── 10_destructor.py │ └── README.md ├── 02-class-vs-instance-attributes │ ├── README.md │ ├── class_method_example.py │ ├── class_method_inheritance.py │ ├── class_vs_instance.py │ ├── instance_counter.py │ ├── person_counter.py │ ├── robot_laws.py │ └── static_method_example.py ├── 03-properties-vs-getters-setters │ ├── README.md │ ├── clamped_setter.py │ ├── conditional_setattr.py │ ├── convert_to_property.py │ ├── generic_getattr_setattr.py │ ├── getter_setter_basic.py │ ├── getter_setter_use_case.py │ ├── property_alt_syntax.py │ ├── property_decorator.py │ ├── public_attribute.py │ └── robot_condition_property.py ├── 04-immutable-classes │ ├── README.md │ ├── dataclass_frozen.py │ ├── getter_only.py │ ├── namedtuple_immutable.py │ ├── property_readonly.py │ └── slots_example.py ├── 05-dataclasses │ ├── README.md │ ├── basic_dataclass.py │ ├── dataclass_in_set_dict.py │ ├── exercise_book.py │ ├── frozen_dataclass.py │ ├── traditional_class.py │ └── traditional_immutable.py ├── 06-custom-property │ ├── README.md │ ├── chatty_property.py │ └── our_property.py ├── 07-magic-methods │ ├── README.md │ ├── currency_converter.py │ └── length.py ├── 08-dynamic-data-transformation │ ├── README.md │ ├── product.py │ └── products_demo.py ├── 09-introduction-to-descriptors │ ├── 1-introduction.md │ ├── 2-descriptor-protocol.md │ ├── 3-data-non-data-descriptor.md │ ├── 4-practical-use-cases.md │ ├── 5-how-python-internally-uses-descriptors.md │ ├── 6-dynamic-descriptor-creation.md │ └── README.md ├── 10-inheritance │ ├── README.md │ ├── animal_hierarchy.py │ ├── basic_inheritance.py │ ├── doctor_robot.py │ ├── override_and_super.py │ ├── shape_hierarchy.py │ └── type_vs_isinstance.py ├── 11-multiple-inheritance │ ├── README.md │ ├── calendar_clock.py │ ├── diamond_problem.py │ ├── hybrid_car.py │ └── polymorphism.py ├── 12-multiple-inheritance-example │ ├── README.md │ ├── advanced_fighting_healing_robot.py │ ├── fighting_nurse_robot.py │ ├── fighting_robot.py │ ├── nursing_robot.py │ └── robot_base.py ├── 13-callable-instances │ ├── README.md │ ├── callable_check.py │ ├── food_supply.py │ ├── fuzzy_triangle_area.py │ ├── merge_experts.py │ ├── polynomial.py │ ├── running_average.py │ ├── temperature_converter.py │ ├── triangle_area.py │ └── visualize_lines.py ├── 14-slots-static-attributes │ ├── 01_dynamic_attributes.py │ ├── 02_builtin_no_dynamic.py │ ├── 03_slots_usage.py │ └── README.md ├── 15-python-polynomial-class │ ├── README.md │ ├── polynomial_arithmetic.py │ ├── polynomial_basic.py │ ├── polynomial_callable.py │ ├── polynomial_derivative.py │ └── polynomial_str_repr.py ├── 16-dynamic-class-creation-with-type │ ├── 01_type_basics.py │ ├── 02_class_is_instance_of_type.py │ ├── 03_manual_class_creation.py │ ├── 04_robot_vs_robot2.py │ └── README.md ├── 17-road-to-metaclasses │ ├── 01_manual_inheritance.py │ ├── 02_runtime_injection.py │ ├── 03_manager_function.py │ ├── 04_class_decorator.py │ ├── 05_realistic_class_decorator.py │ └── README.md ├── 18-metaclasses │ ├── 01_little_meta.py │ ├── 02_essential_answers.py │ ├── 03_singleton_metaclass.py │ ├── 04_singleton_inheritance.py │ ├── 05_singleton_decorator.py │ ├── 06_camelcase_metaclass.py │ ├── 07_camelcase_decorator.py │ └── README.md ├── 19-count-function-calls-with-metaclass │ ├── README.md │ ├── call_counter_decorator.py │ ├── metaclass_counter.py │ └── test_metaclass_counter.py ├── 20-abstract-base-classes │ ├── 01_non_abstract_class.py │ ├── 02_abstract_class_error.py │ ├── 03_abstract_class_correct.py │ ├── 04_abstract_with_base_impl.py │ └── README.md ├── 21-oop-purely-functional │ ├── README.md │ ├── robot_class.py │ └── robot_functional.py └── README.md ├── python-advanced ├── 01-collections │ ├── README.md │ ├── counter_example.py │ ├── defaultdict_example.py │ ├── deque_example.py │ ├── namedtuple_example.py │ └── ordereddict_example.py ├── 02-itertools │ ├── README.md │ ├── accumulate_example.py │ ├── combinations_example.py │ ├── groupby_example.py │ ├── infinite_iterators.py │ ├── permutations_example.py │ └── product_example.py ├── 03-lambda │ ├── README.md │ ├── lambda_basics.py │ ├── lambda_filter.py │ ├── lambda_higher_order.py │ ├── lambda_map.py │ ├── lambda_reduce.py │ └── lambda_sorting.py ├── 04-exception │ ├── README.md │ ├── common_exceptions.py │ ├── custom_exception.py │ ├── else_finally.py │ ├── raise_assert.py │ ├── syntax_vs_exception.py │ └── try_except.py ├── 05-logging │ ├── README.md │ ├── basic_config.py │ ├── json_logger.py │ ├── log_filters.py │ ├── log_handlers.py │ ├── log_propagation.py │ ├── log_stacktrace.py │ ├── logging_config_file │ │ ├── config_usage.py │ │ └── logging.conf │ ├── logging_levels.py │ ├── module_logging │ │ ├── helper.py │ │ └── main.py │ ├── rotating_file_handler.py │ └── timed_rotating_handler.py ├── 06-json │ ├── README.md │ ├── json_basic_decode.py │ ├── json_basic_encode.py │ ├── json_custom_decoder.py │ ├── json_custom_encoder.py │ ├── json_file_io.py │ └── json_template_encoder.py ├── 07-random-number │ ├── README.md │ ├── numpy_random.py │ ├── random_module.py │ ├── random_seed.py │ └── secrets_module.py ├── 08-decorators │ ├── README.md │ ├── basic_function_decorator.py │ ├── class_decorator.py │ ├── decorator_with_args_and_return.py │ ├── decorator_with_arguments.py │ ├── functools_wraps_preserve_metadata.py │ ├── nested_decorators.py │ └── practical_use_cases.py ├── 09-generators │ ├── README.md │ ├── generator_basics.py │ ├── generator_custom_iterable.py │ ├── generator_expression_vs_list.py │ ├── generator_fibonacci.py │ └── generator_memory_efficiency.py ├── 10-threading-multiprocessing │ ├── README.md │ ├── cpu_bound_multiprocessing.py │ ├── cpu_bound_threading.py │ ├── io_bound_threading.py │ ├── multiprocessing_basics.py │ ├── shared_memory_gil_limitations.py │ └── threading_basics.py ├── 11-function-arguments │ ├── README.md │ ├── arguments_vs_parameters.py │ ├── default_arguments.py │ ├── global_vs_local.py │ ├── keyword_only_args.py │ ├── parameter_passing.py │ ├── positional_vs_keyword.py │ ├── unpacking_arguments.py │ └── variable_length_args.py ├── 12-asterisk │ ├── README.md │ ├── args_kwargs.py │ ├── arithmetic_operations.py │ ├── keyword_only_arguments.py │ ├── merge_iterables_dicts.py │ ├── non_string_dict_merge_error.py │ ├── repeat_elements.py │ ├── unpacking_containers.py │ └── unpacking_for_function_calls.py ├── 13-copying │ ├── README.md │ ├── assignment_reference.py │ ├── custom_class_copy.py │ ├── deep_copy_nested.py │ ├── shallow_copy_flat.py │ └── shallow_copy_nested.py ├── 14-context-manager │ ├── README.md │ ├── file_with_statement.py │ ├── managed_file_class.py │ ├── managed_file_class_exception.py │ ├── managed_file_class_handled.py │ ├── managed_file_generator.py │ ├── notes.txt │ └── notes2.txt ├── 15-function-caching │ └── README.md ├── 16-virtual-environment │ └── README.md ├── 17-virtual-environment-uv │ └── README.md └── README.md └── python-basic ├── 01-list ├── README.md ├── list_basics.py ├── list_comprehension.py ├── list_copying.py ├── list_methods.py ├── list_slicing.py └── nested_lists.py ├── 02-tuple ├── README.md ├── nested_tuples.py ├── tuple_basics.py ├── tuple_methods.py ├── tuple_slicing.py ├── tuple_unpacking.py └── tuple_vs_list.py ├── 03-dictionary ├── README.md ├── dict_access.py ├── dict_basics.py ├── dict_copy_merge.py ├── dict_key_types.py ├── dict_looping.py ├── dict_modification.py └── nested_dicts.py ├── 04-set ├── README.md ├── frozenset_example.py ├── set_add_remove.py ├── set_basics.py ├── set_copy.py ├── set_operations.py ├── set_relations.py └── set_update.py ├── 05-string ├── README.md ├── string_access.py ├── string_basics.py ├── string_concat_vs_join.py ├── string_format.py ├── string_fstrings.py └── string_methods.py └── README.md /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | ColumnLimit: 120 3 | IndentWidth: 4 4 | AllowShortFunctionsOnASingleLine: Inline 5 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: Python Code Style (pre-commit) 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | pre-commit: 11 | name: Run pre-commit hooks 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.10" 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install pre-commit 26 | 27 | - name: Run pre-commit hooks 28 | run: | 29 | pre-commit run --all-files 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.log 3 | *.log.* 4 | .DS_Store 5 | __pycache__ 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pycqa/isort 3 | rev: 5.12.0 4 | hooks: 5 | - id: isort 6 | files: \.py$ 7 | args: ["--profile", "google"] 8 | 9 | - repo: https://github.com/pre-commit/mirrors-yapf 10 | rev: v0.32.0 11 | hooks: 12 | - id: yapf 13 | name: Format Python code with YAPF 14 | files: \.py$ 15 | args: ["--in-place", "--recursive"] 16 | 17 | - repo: https://github.com/pre-commit/mirrors-prettier 18 | rev: v3.0.0 19 | hooks: 20 | - id: prettier 21 | name: Format Markdown files 22 | files: \.md$ 23 | - id: prettier 24 | name: Format YAML files 25 | files: \.(yaml|yml)$ 26 | 27 | - repo: https://github.com/pre-commit/mirrors-clang-format 28 | rev: v17.0.6 29 | hooks: 30 | - id: clang-format 31 | name: Format C/C++/CUDA code 32 | files: \.(c|cpp|h|hpp|cc|cxx|cu|cuh)$ 33 | args: [--style=file] 34 | 35 | - repo: https://github.com/pre-commit/pre-commit-hooks 36 | rev: v5.0.0 37 | hooks: 38 | - id: trailing-whitespace 39 | name: Remove trailing whitespace 40 | - id: end-of-file-fixer 41 | name: Fix end-of-file newline 42 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = google 3 | column_limit = 120 4 | indent_width = 4 5 | -------------------------------------------------------------------------------- /MPI/convolution/Makefile: -------------------------------------------------------------------------------- 1 | TARGET=main 2 | OBJECTS=main.o util.o convolution.o 3 | 4 | CPPFLAGS=-std=c++14 -O3 -Wall -march=native -mavx2 -mfma -fopenmp -mno-avx512f -I/usr/local/cuda/include 5 | CUDA_CFLAGS:=$(foreach option, $(CPPFLAGS),-Xcompiler=$(option) -arch=sm_70) 6 | 7 | LDFLAGS=-L/usr/local/cuda/lib64 8 | LDLIBS=-lstdc++ -lcudart -lm -lcudnn -lmpi -lmpi_cxx -lnccl 9 | 10 | CXX=g++ 11 | CUX=/usr/local/cuda/bin/nvcc 12 | 13 | all: $(TARGET) 14 | 15 | $(TARGET): $(OBJECTS) 16 | $(CC) $(CPPFLAGS) -o $(TARGET) $(OBJECTS) $(LDFLAGS) $(LDLIBS) 17 | 18 | %.o: %.cpp 19 | $(CXX) $(CPPFLAGS) -c -o $@ $^ 20 | 21 | %.o: %.cu 22 | $(CUX) $(CUDA_CFLAGS) -c -o $@ $^ 23 | 24 | clean: 25 | rm -rf $(TARGET) $(OBJECTS) 26 | -------------------------------------------------------------------------------- /MPI/convolution/convolution.cuh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void convolution(half *_I, half *_F, float *_O, int N, int C, int H, int W, int K, int R, int S, int pad_h, int pad_w, 6 | int stride_h, int stride_w, int dilation_h, int dilation_w, int _mpi_rank, int _mpi_world_size); 7 | 8 | void convolution_initialize(int N, int C, int H, int W, int K, int R, int S, int pad_h, int pad_w, int stride_h, 9 | int stride_w, int dilation_h, int dilation_w, int _mpi_rank, int _mpi_world_size); 10 | 11 | void convolution_cleanup(half *_I, half *_F, float *_O, int N, int C, int H, int W, int K, int R, int S, int pad_h, 12 | int pad_w, int stride_h, int stride_w, int dilation_h, int dilation_w); 13 | -------------------------------------------------------------------------------- /MPI/convolution/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | void splitIntoIntervals(int N, int M, int interval_id, int &begin, int &end); 5 | 6 | double get_time(); 7 | 8 | half *alloc_tensor(int N, int C, int H, int W); 9 | 10 | float *alloc_tensor32(int N, int C, int H, int W); 11 | 12 | void rand_tensor(half *m, int N, int C, int H, int W); 13 | 14 | void zero_tensor(half *m, int N, int C, int H, int W); 15 | 16 | void zero_tensor32(float *m, int N, int C, int H, int W); 17 | 18 | void print_tensor(half *m, int N, int C, int H, int W); 19 | 20 | void print_tensor32(float *m, int N, int C, int H, int W); 21 | 22 | void check_convolution(half *I, half *F, float *O, int N, int C, int H, int W, int K, int R, int S, int pad_h, 23 | int pad_w, int stride_h, int stride_w, int dilation_h, int dilation_w); 24 | -------------------------------------------------------------------------------- /MPI/matmul/Makefile: -------------------------------------------------------------------------------- 1 | TARGET=main 2 | OBJECTS=util.o matmul.o 3 | 4 | CPPFLAGS=-std=c++11 -O3 -Wall -march=znver2 -mavx2 -mfma -fopenmp 5 | LDLIBS=-lm -lmpi -lmpi_cxx -lnuma 6 | 7 | all: $(TARGET) 8 | 9 | $(TARGET): $(OBJECTS) 10 | 11 | clean: 12 | rm -rf $(TARGET) $(OBJECTS) 13 | -------------------------------------------------------------------------------- /MPI/matmul/matmul.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void matmul(float *_A, float *_B, float *_C, int _M, int _N, int _K, int _num_threads, int _mpi_rank, 4 | int _mpi_world_size); 5 | -------------------------------------------------------------------------------- /MPI/matmul/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${NODES:=2} 4 | 5 | salloc -N $NODES --exclusive \ 6 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 7 | ./main $@ 8 | -------------------------------------------------------------------------------- /MPI/matmul/run_performance.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | salloc -N 2 --exclusive \ 4 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 5 | ./main -t 32 -n 10 8192 8192 8192 6 | 7 | salloc -N 2 --exclusive \ 8 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 9 | ./main -t 32 -n 10 4096 4096 4096 10 | 11 | salloc -N 2 --exclusive \ 12 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 13 | ./main -t 32 -n 10 2048 2048 2048 14 | 15 | salloc -N 2 --exclusive \ 16 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 17 | ./main -t 32 -n 10 1024 1024 1024 18 | 19 | salloc -N 2 --exclusive \ 20 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 21 | ./main -t 32 -n 10 5678 7891 1234 22 | 23 | salloc -N 2 --exclusive \ 24 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 25 | ./main -t 32 -n 10 7891 1234 5678 26 | 27 | salloc -N 2 --exclusive \ 28 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 29 | ./main -t 32 -n 10 1234 5678 7891 30 | -------------------------------------------------------------------------------- /MPI/matmul/run_performance_base.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | salloc -N 2 --exclusive \ 4 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 5 | ./main -t 32 -n 10 8192 8192 8192 6 | -------------------------------------------------------------------------------- /MPI/matmul/run_validation_npernode1.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | salloc -N 2 --exclusive \ 4 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 5 | ./main -v -t 32 -n 3 293 399 123 6 | 7 | salloc -N 2 --exclusive \ 8 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 9 | ./main -v -t 32 -n 3 3 699 10 10 | 11 | salloc -N 2 --exclusive \ 12 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 13 | ./main -v -t 32 -n 3 331 21 129 14 | 15 | salloc -N 2 --exclusive \ 16 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 17 | ./main -v -t 32 -n 3 2000 1 2000 18 | 19 | salloc -N 2 --exclusive \ 20 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 21 | ./main -v -t 32 -n 3 323 429 111 22 | 23 | salloc -N 2 --exclusive \ 24 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 25 | ./main -v -t 32 -n 3 1 2000 2000 26 | 27 | salloc -N 2 --exclusive \ 28 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 29 | ./main -v -t 32 -n 3 2000 2000 1 30 | 31 | salloc -N 2 --exclusive \ 32 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 33 | ./main -v -t 32 -n 3 64 64 64 34 | 35 | salloc -N 2 --exclusive \ 36 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 37 | ./main -v -t 32 -n 3 128 128 128 38 | 39 | salloc -N 2 --exclusive \ 40 | mpirun --bind-to none -mca btl ^openib -npernode 1 \ 41 | ./main -v -t 32 -n 3 256 256 256 42 | -------------------------------------------------------------------------------- /MPI/matmul/run_validation_npernode2.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | salloc -N 2 --exclusive \ 4 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 5 | ./main -v -t 32 -n 3 293 399 123 6 | 7 | salloc -N 2 --exclusive \ 8 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 9 | ./main -v -t 32 -n 3 3 699 10 10 | 11 | salloc -N 2 --exclusive \ 12 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 13 | ./main -v -t 32 -n 3 331 21 129 14 | 15 | salloc -N 2 --exclusive \ 16 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 17 | ./main -v -t 32 -n 3 2000 1 2000 18 | 19 | salloc -N 2 --exclusive \ 20 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 21 | ./main -v -t 32 -n 3 323 429 111 22 | 23 | salloc -N 2 --exclusive \ 24 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 25 | ./main -v -t 32 -n 3 1 2000 2000 26 | 27 | salloc -N 2 --exclusive \ 28 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 29 | ./main -v -t 32 -n 3 2000 2000 1 30 | 31 | salloc -N 2 --exclusive \ 32 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 33 | ./main -v -t 32 -n 3 64 64 64 34 | 35 | salloc -N 2 --exclusive \ 36 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 37 | ./main -v -t 32 -n 3 128 128 128 38 | 39 | salloc -N 2 --exclusive \ 40 | mpirun --bind-to none -mca btl ^openib -npernode 2 \ 41 | ./main -v -t 32 -n 3 256 256 256 42 | -------------------------------------------------------------------------------- /MPI/matmul/run_validation_npernode5.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | salloc -N 2 --exclusive \ 4 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 5 | ./main -v -t 32 -n 3 293 399 123 6 | 7 | salloc -N 2 --exclusive \ 8 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 9 | ./main -v -t 32 -n 3 3 699 10 10 | 11 | salloc -N 2 --exclusive \ 12 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 13 | ./main -v -t 32 -n 3 331 21 129 14 | 15 | salloc -N 2 --exclusive \ 16 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 17 | ./main -v -t 32 -n 3 2000 1 2000 18 | 19 | salloc -N 2 --exclusive \ 20 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 21 | ./main -v -t 32 -n 3 323 429 111 22 | 23 | salloc -N 2 --exclusive \ 24 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 25 | ./main -v -t 32 -n 3 1 2000 2000 26 | 27 | salloc -N 2 --exclusive \ 28 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 29 | ./main -v -t 32 -n 3 2000 2000 1 30 | 31 | salloc -N 2 --exclusive \ 32 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 33 | ./main -v -t 32 -n 3 64 64 64 34 | 35 | salloc -N 2 --exclusive \ 36 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 37 | ./main -v -t 32 -n 3 128 128 128 38 | 39 | salloc -N 2 --exclusive \ 40 | mpirun --bind-to none -mca btl ^openib -npernode 5 \ 41 | ./main -v -t 32 -n 3 256 256 256 42 | -------------------------------------------------------------------------------- /MPI/matmul/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void splitIntoIntervals(int N, int M, int interval_id, int &begin, int &end); 4 | 5 | void timer_start(int i); 6 | 7 | double timer_stop(int i); 8 | 9 | void check_matmul(float *A, float *B, float *C, int M, int N, int K); 10 | 11 | void print_mat(float *m, int R, int C); 12 | 13 | void alloc_mat(float **m, int R, int C); 14 | 15 | void rand_mat(float *m, int R, int C); 16 | 17 | void zero_mat(float *m, int R, int C); 18 | -------------------------------------------------------------------------------- /cpp-OOP/02-method/README.md: -------------------------------------------------------------------------------- 1 | # C++ OOP - Class Methods 2 | 3 | ## What are Class Methods? 4 | 5 | In C++, **methods** are functions that belong to a class. They define behaviors or actions that class objects can perform. 6 | 7 | You can define a method in two ways: 8 | 9 | 1. **Inside the class definition** 10 | 2. **Outside the class definition** (recommended for larger programs) 11 | 12 | ## 1. Method Defined Inside the Class 13 | 14 | You can define a method directly inside the class body. 15 | 16 | ### Example: 17 | 18 | ```cpp 19 | class MyClass { 20 | public: 21 | void myMethod() { 22 | cout << "Hello World!"; 23 | } 24 | }; 25 | 26 | int main() { 27 | MyClass myObj; 28 | myObj.myMethod(); // Call the method 29 | return 0; 30 | } 31 | ``` 32 | 33 | ### Explanation: 34 | 35 | - `myMethod()` is a method of the class. 36 | - We use the dot `.` operator on an object (`myObj`) to call the method. 37 | 38 | ## 2. Method Defined Outside the Class 39 | 40 | In larger projects, it is common to separate the method definition from its declaration. 41 | 42 | Use the **scope resolution operator** `::` to define the method outside the class. 43 | 44 | ### Example: 45 | 46 | ```cpp 47 | class MyClass { 48 | public: 49 | void myMethod(); // Method declaration 50 | }; 51 | 52 | // Method definition outside the class 53 | void MyClass::myMethod() { 54 | cout << "Hello World!"; 55 | } 56 | 57 | int main() { 58 | MyClass myObj; 59 | myObj.myMethod(); // Call the method 60 | return 0; 61 | } 62 | ``` 63 | 64 | ## Method with Parameters 65 | 66 | You can pass values to class methods just like you do with regular functions. 67 | 68 | ### Example: 69 | 70 | ```cpp 71 | #include 72 | using namespace std; 73 | 74 | class Car { 75 | public: 76 | int speed(int maxSpeed); // Declaration 77 | }; 78 | 79 | int Car::speed(int maxSpeed) { // Definition 80 | return maxSpeed; 81 | } 82 | 83 | int main() { 84 | Car myObj; 85 | cout << myObj.speed(200); // Output: 200 86 | return 0; 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /cpp-basic/README.md: -------------------------------------------------------------------------------- 1 | # C++ Basics Tutorial 2 | 3 | This repository contains structured folders that walk through the fundamental concepts of C++ programming. It aligns closely with the [W3Schools C++ Tutorial](https://www.w3schools.com/cpp/), providing simple examples and hands-on learning for beginners. 4 | -------------------------------------------------------------------------------- /docs/mixed-precision-training/01-introduction-to-mixed-precision-training.md: -------------------------------------------------------------------------------- 1 | # Part 1: Introduction to Mixed Precision Training 2 | 3 | ## 1. Introduction 4 | 5 | Using numerical formats with lower precision than 32-bit floating point offers several practical benefits: 6 | 7 | - **Reduced memory usage**: Lower precision formats consume less memory, allowing for training and deployment of larger models. 8 | - **Lower memory bandwidth**: Smaller data formats mean faster data transfers. 9 | - **Faster computation**: Especially on GPUs with Tensor Core support, reduced precision significantly accelerates mathematical operations. 10 | 11 | **Mixed precision training** achieves these benefits while maintaining task-specific accuracy. It works by using 16-bit floating point (FP16) for most operations and retaining 32-bit floating point (FP32) only where necessary, such as in gradient accumulation or weight updates. 12 | 13 | ## 2. What is Mixed Precision Training? 14 | 15 | Mixed precision training involves the combined use of FP16 and FP32 in a deep learning workflow. This approach provides significant computational speedup—often up to 3x—on hardware that supports FP16 computation, such as NVIDIA GPUs with Tensor Cores (starting from the Volta architecture). 16 | 17 | To enable mixed precision training: 18 | 19 | - The model must be modified to use FP16 where appropriate. 20 | - **Loss scaling** must be added to preserve small gradient values that might otherwise underflow in FP16. 21 | 22 | This technique became practically feasible with CUDA 8 and the NVIDIA Deep Learning SDK, initially on Pascal GPUs. 23 | 24 | ## 3. Why Use Lower Precision? 25 | 26 | Training deep neural networks with lower precision: 27 | 28 | - **Decreases memory consumption**: FP16 uses half the memory of FP32, allowing larger models or mini-batches. 29 | - **Reduces training time**: FP16 cuts memory access time and provides higher arithmetic throughput—up to 8x on supported GPUs. 30 | 31 | Mixed precision with proper loss scaling achieves comparable accuracy to full precision while offering computational benefits. 32 | -------------------------------------------------------------------------------- /docs/questions/06-handling-gil-in-cpp-backend.md: -------------------------------------------------------------------------------- 1 | # How do you handle Python’s Global Interpreter Lock (GIL) when using a C++ backend? 2 | 3 | ### What is the GIL? 4 | 5 | - In **CPython**, only **one thread can execute Python bytecode at a time**, due to the GIL. 6 | - This can become a **bottleneck** in multithreaded or parallel applications, especially for CPU-bound work. 7 | 8 | ### Strategy: Release the GIL in C++ Code 9 | 10 | If your C++ code: 11 | 12 | - Performs **long-running computations** 13 | - Doesn’t call any Python API during execution 14 | 15 | Then you can **safely release the GIL** to allow other Python threads to run concurrently. 16 | 17 | ### In Native C++ (Python C API): 18 | 19 | Wrap compute-intensive code like this: 20 | 21 | ```cpp 22 | #include 23 | 24 | void heavy_computation() { 25 | Py_BEGIN_ALLOW_THREADS 26 | // Your long-running C++ code here 27 | compute(); 28 | Py_END_ALLOW_THREADS 29 | } 30 | ``` 31 | 32 | > Important: **Do not call any Python C API functions inside** this block. 33 | 34 | ### In `pybind11`: 35 | 36 | Pybind11 handles this cleanly using `py::gil_scoped_release`: 37 | 38 | ```cpp 39 | #include 40 | 41 | void compute() { 42 | // Do something expensive in C++ 43 | } 44 | 45 | PYBIND11_MODULE(my_module, m) { 46 | m.def("compute", []() { 47 | pybind11::gil_scoped_release release; 48 | compute(); // GIL released during this call 49 | }); 50 | } 51 | ``` 52 | 53 | ### Other Mitigation Strategies: 54 | 55 | - For truly **parallel workloads**, you can use **multiprocessing** in Python, which avoids the GIL entirely (since each process has its own interpreter). 56 | - If your C++ code is **multi-threaded**, releasing the GIL ensures it can utilize **all CPU cores** without being blocked. 57 | 58 | ### Summary: 59 | 60 | > To handle the GIL with a C++ backend, **release it during long or parallel C++ computations** using `Py_BEGIN_ALLOW_THREADS` or `gil_scoped_release` in pybind11. This enables true parallelism while avoiding Python thread contention. 61 | -------------------------------------------------------------------------------- /docs/questions/09-warp-divergence-in-cuda.md: -------------------------------------------------------------------------------- 1 | # Describe the concept of warp divergence and its impact on GPU performance. 2 | 3 | ### What is Warp Divergence? 4 | 5 | On NVIDIA GPUs, a **warp** is a group of **32 threads** that execute **in lockstep** — meaning all threads in the warp execute the **same instruction** at the **same time**. 6 | 7 | **Warp divergence** occurs when threads in the same warp **take different execution paths** due to conditional logic (e.g., `if/else`, `switch`, loops). 8 | 9 | ### Example of Warp Divergence: 10 | 11 | ```cpp 12 | int tid = threadIdx.x; 13 | 14 | if (tid % 2 == 0) { 15 | // Half the warp executes this 16 | do_even_work(); 17 | } else { 18 | // The other half waits 19 | do_odd_work(); 20 | } 21 | ``` 22 | 23 | > Result: Threads must **serialize** — first, all even threads run while odd ones are idle, then vice versa. 24 | 25 | ### Impact on Performance: 26 | 27 | - GPU **serializes** divergent paths within a warp. 28 | - This **reduces effective parallelism** — while some threads are active, others are **idle**. 29 | - Performance degradation depends on how many divergent paths there are and how unbalanced the workload is. 30 | 31 | ### How to Minimize Warp Divergence: 32 | 33 | 1. **Avoid divergent conditionals** inside warps when possible. 34 | 35 | - Use **predication** (evaluate both branches but commit results selectively). 36 | 37 | 2. **Reorganize data** so threads in the same warp follow the same code path. 38 | 3. Replace conditionals with **bitwise ops**, **select functions**, or **lookup tables** if possible. 39 | 4. Use **warp-level primitives** like `__shfl_sync()` or `__ballot_sync()` for communication without branching. 40 | 41 | ### Summary: 42 | 43 | > **Warp divergence** happens when threads in a warp take **different execution paths**, causing them to **serialize execution** and lose parallel efficiency. Reducing divergence is key to writing high-performance CUDA code — especially in control-flow-heavy kernels. 44 | -------------------------------------------------------------------------------- /docs/questions/README.md: -------------------------------------------------------------------------------- 1 | ## Questions 2 | 3 | 1. How would you design a Python API for a high-performance library to be both user-friendly and efficient? 4 | 2. What steps can you take to minimize Python overhead in numeric computations? 5 | 3. How would you ensure your Python API integrates well with existing scientific Python libraries (NumPy/SciPy)? 6 | 4. How can you interface Python with high-performance C++ code? 7 | 5. What are the challenges of calling into C++ from Python, and how do you mitigate them? 8 | 6. How do you handle Python’s Global Interpreter Lock (GIL) when using a C++ backend? 9 | 7. Explain the difference between CUDA cores and Tensor Cores on NVIDIA GPUs. 10 | 8. How does memory coalescing work in CUDA, and why is it important? 11 | 9. Describe the concept of warp divergence and its impact on GPU performance. 12 | 10. What is the role of shared memory in CUDA kernels? 13 | 11. Have you used OpenCL, and how does it compare to CUDA? 14 | 12. Why are BLAS and LAPACK important for numerical computing in Python (e.g. NumPy/SciPy)? 15 | 13. How would you benchmark and optimize a computational kernel (e.g., a matrix operation) on CPU vs GPU? 16 | 14. Give an example of a numerical stability issue and how to address it. 17 | 15. How do memory access patterns affect performance on modern CPUs? 18 | 16. How does NumPy achieve performance that pure Python can’t? 19 | 17. What is CuPy, and how is it related to NumPy? 20 | 18. Have you used SciPy, and how does it improve on NumPy for certain tasks? 21 | 19. What is Numba and how can it speed up Python code? 22 | 20. Discuss your experience with PyTorch or TensorFlow and how they achieve high performance. 23 | 21. What is JAX and how does it enable high-performance Python computations? 24 | 22. Do you have experience with parallel computing frameworks or libraries (e.g. MPI, Dask)? 25 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/01_intro_to_oop.py: -------------------------------------------------------------------------------- 1 | x = 42 2 | print(type(x)) 3 | 4 | 5 | def add_one(n): 6 | return n + 1 7 | 8 | 9 | print(type(add_one)) 10 | 11 | import math 12 | 13 | print(type(math)) 14 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/02_classes_and_instances.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | pass 3 | 4 | 5 | x = Robot() 6 | y = Robot() 7 | y2 = y 8 | 9 | print("y == y2:", y == y2) 10 | print("y == x:", y == x) 11 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/03_instance_and_class_attributes.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | pass 3 | 4 | 5 | x = Robot() 6 | y = Robot() 7 | 8 | x.name = "Marvin" 9 | x.build_year = 1979 10 | x.brand = "Boston Dynamics" 11 | 12 | y.name = "Caliban" 13 | y.build_year = 1993 14 | 15 | Robot.brand = "Kuka" 16 | 17 | print(x.__dict__) 18 | print(y.__dict__) 19 | print(Robot.__dict__) 20 | print(x.brand) 21 | print(y.brand) 22 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/04_encapsulation.py: -------------------------------------------------------------------------------- 1 | x = [3, 6, 9] 2 | y = [45, "abc"] 3 | 4 | print(x[1]) 5 | x[1] = 99 6 | x.append(42) 7 | 8 | print("Popped from y:", y.pop()) 9 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/05_getters_setters.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name=None, build_year=None): 4 | self.name = name 5 | self.build_year = build_year 6 | 7 | def say_hi(self): 8 | print(f"Hi, I am {self.name} and I was built in {self.build_year}") 9 | 10 | def set_name(self, name): 11 | self.name = name 12 | 13 | def get_name(self): 14 | return self.name 15 | 16 | def set_build_year(self, year): 17 | self.build_year = year 18 | 19 | def get_build_year(self): 20 | return self.build_year 21 | 22 | 23 | x = Robot("Henry", 2008) 24 | y = Robot() 25 | y.set_name(x.get_name()) 26 | x.say_hi() 27 | y.say_hi() 28 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/06_methods_and_self.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name): 4 | self.name = name 5 | 6 | def say_hi(self): 7 | print(f"Hi, I am {self.name}") 8 | 9 | 10 | x = Robot("Marvin") 11 | x.say_hi() 12 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/07_init_method.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name=None): 4 | self.name = name 5 | 6 | def say_hi(self): 7 | if self.name: 8 | print(f"Hi, I am {self.name}") 9 | else: 10 | print("Hi, I am a robot without a name") 11 | 12 | 13 | x = Robot() 14 | y = Robot("Marvin") 15 | x.say_hi() 16 | y.say_hi() 17 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/08_str_and_repr.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name, build_year): 4 | self.name = name 5 | self.build_year = build_year 6 | 7 | def __repr__(self): 8 | return f"Robot('{self.name}', {self.build_year})" 9 | 10 | def __str__(self): 11 | return f"Name: {self.name}, Build Year: {self.build_year}" 12 | 13 | 14 | x = Robot("Marvin", 1979) 15 | print(repr(x)) 16 | print(str(x)) 17 | print(eval(repr(x))) 18 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/09_access_control.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name, build_year): 4 | self.__name = name 5 | self._build_year = build_year 6 | self.type = "Android" 7 | 8 | def get_name(self): 9 | return self.__name 10 | 11 | def get_build_year(self): 12 | return self._build_year 13 | 14 | 15 | x = Robot("Marvin", 1979) 16 | print(x.type) 17 | print(x._build_year) 18 | print(x.get_name()) 19 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/10_destructor.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name): 4 | self.name = name 5 | print(f"{self.name} has been created!") 6 | 7 | def __del__(self): 8 | print(f"{self.name} is being destroyed") 9 | 10 | 11 | x = Robot("Tik-Tok") 12 | y = Robot("Jenkins") 13 | z = x 14 | del x 15 | del z 16 | del y 17 | -------------------------------------------------------------------------------- /python-OOP/01-object-oriented-programming/README.md: -------------------------------------------------------------------------------- 1 | # 01 - Object-Oriented Programming (OOP) in Python 2 | 3 | Welcome to the first module on Object-Oriented Programming (OOP) in Python. This section introduces key OOP concepts and how they are implemented in Python with clear examples and exercises. 4 | 5 | ## Topics Covered 6 | 7 | | File | Topic | 8 | | ------------------------------------- | --------------------------------------------------------------------- | 9 | | `01_intro_to_oop.py` | Everything in Python is an object | 10 | | `02_classes_and_instances.py` | Defining classes and creating instances | 11 | | `03_instance_and_class_attributes.py` | Instance vs class attributes, attribute resolution | 12 | | `04_encapsulation.py` | Encapsulation via methods and Python’s built-in classes (like `list`) | 13 | | `05_getters_setters.py` | Creating and using getters and setters | 14 | | `06_methods_and_self.py` | Method definition and the role of `self` | 15 | | `07_init_method.py` | The `__init__` constructor method | 16 | | `08_str_and_repr.py` | `__str__()` vs `__repr__()` for string representation | 17 | | `09_access_control.py` | Public, protected, and private attributes | 18 | | `10_destructor.py` | The `__del__()` method and object destruction | 19 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/README.md: -------------------------------------------------------------------------------- 1 | # 02 - Class vs. Instance Attributes in Python 2 | 3 | This chapter explores the distinction between **class attributes**, **instance attributes**, and the use of **static** and **class methods** in Python's object-oriented programming model. 4 | 5 | ## Topics Covered 6 | 7 | | File | Description | 8 | | ----------------------------- | ------------------------------------------------------------------------------ | 9 | | `class_vs_instance.py` | Demonstrates difference in scope and behavior of class vs. instance attributes | 10 | | `robot_laws.py` | Stores shared robot laws using class attributes | 11 | | `instance_counter.py` | Uses class attribute to count instances created and deleted | 12 | | `static_method_example.py` | Introduces static methods and their purpose | 13 | | `class_method_example.py` | Explains how class methods differ from static and instance methods | 14 | | `class_method_inheritance.py` | Shows advantage of classmethods in inheritance scenarios | 15 | | `person_counter.py` | Real-world example of classmethod usage to track population count | 16 | 17 | ## Key Concepts 18 | 19 | ### Class Attribute 20 | 21 | - Shared across all instances. 22 | - Defined directly in the class body. 23 | 24 | ### Instance Attribute 25 | 26 | - Unique to each object. 27 | - Defined within `__init__`. 28 | 29 | ### Static Method 30 | 31 | - Doesn’t access class or instance data. 32 | - Use for utility functions tied to a class context. 33 | 34 | ### Class Method 35 | 36 | - Takes `cls` as the first parameter. 37 | - Can access class state; useful in inheritance and factory patterns. 38 | 39 | ## Learning Tip 40 | 41 | - Use class attributes for global counters or shared constants. 42 | - Use instance attributes to track data specific to each object. 43 | - Prefer class methods when functionality must vary across subclasses. 44 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/class_method_example.py: -------------------------------------------------------------------------------- 1 | """Demonstrates a class method that accesses class-level data.""" 2 | 3 | 4 | class Robot: 5 | __counter = 0 6 | 7 | def __init__(self): 8 | type(self).__counter += 1 9 | 10 | @classmethod 11 | def RobotInstances(cls): 12 | return cls, Robot.__counter 13 | 14 | 15 | if __name__ == "__main__": 16 | print(Robot.RobotInstances()) 17 | x = Robot() 18 | print(x.RobotInstances()) 19 | y = Robot() 20 | print(Robot.RobotInstances()) 21 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/class_method_inheritance.py: -------------------------------------------------------------------------------- 1 | """Compares staticmethod vs classmethod when inherited.""" 2 | 3 | 4 | class Pet: 5 | _class_info = "pet animals" 6 | 7 | @classmethod 8 | def about(cls): 9 | print("This class is about " + cls._class_info + "!") 10 | 11 | 12 | class Dog(Pet): 13 | _class_info = "man's best friends" 14 | 15 | 16 | class Cat(Pet): 17 | _class_info = "all kinds of cats" 18 | 19 | 20 | Pet.about() 21 | Dog.about() 22 | Cat.about() 23 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/class_vs_instance.py: -------------------------------------------------------------------------------- 1 | """Demonstrates how class and instance attributes behave differently.""" 2 | 3 | 4 | class A: 5 | a = "I am a class attribute!" 6 | 7 | 8 | x = A() 9 | y = A() 10 | 11 | print("x.a:", x.a) 12 | print("y.a:", y.a) 13 | print("A.a:", A.a) 14 | 15 | x.a = "This creates a new instance attribute for x!" 16 | print("After x.a change:") 17 | print("x.a:", x.a) 18 | print("y.a:", y.a) 19 | print("A.a:", A.a) 20 | 21 | A.a = "This is changing the class attribute 'a'!" 22 | print("After A.a change:") 23 | print("x.a:", x.a) 24 | print("y.a:", y.a) 25 | print("A.a:", A.a) 26 | 27 | print("x.__dict__:", x.__dict__) 28 | print("y.__dict__:", y.__dict__) 29 | print("A.__dict__:", A.__dict__) 30 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/instance_counter.py: -------------------------------------------------------------------------------- 1 | """Counts instances using a class attribute.""" 2 | 3 | 4 | class C: 5 | counter = 0 6 | 7 | def __init__(self): 8 | type(self).counter += 1 9 | 10 | def __del__(self): 11 | type(self).counter -= 1 12 | 13 | 14 | if __name__ == "__main__": 15 | x = C() 16 | print("Instances:", C.counter) 17 | y = C() 18 | print("Instances:", C.counter) 19 | del x 20 | print("Instances:", C.counter) 21 | del y 22 | print("Instances:", C.counter) 23 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/person_counter.py: -------------------------------------------------------------------------------- 1 | """Real-world classmethod example to count people.""" 2 | 3 | 4 | class Person: 5 | total_people = 0 6 | 7 | def __init__(self, name): 8 | self.name = name 9 | Person.total_people += 1 10 | 11 | def __del__(self): 12 | Person.total_people -= 1 13 | print(f"{self.name} has been removed from the count.") 14 | 15 | @classmethod 16 | def display_total_people(cls): 17 | print("Total number of people:", cls.total_people) 18 | 19 | 20 | person1 = Person("Alice") 21 | person2 = Person("Bob") 22 | person3 = Person("Charlie") 23 | del person2 # Deleting an instance does not affect the class attribute 24 | 25 | Person.display_total_people() 26 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/robot_laws.py: -------------------------------------------------------------------------------- 1 | """Robot class with class attribute to store Asimov's Three Laws of Robotics.""" 2 | 3 | 4 | class Robot: 5 | Three_Laws = ( 6 | "A robot may not injure a human being or, through inaction, allow a human being to come to harm.", 7 | "A robot must obey the orders given to it by human beings, except where such orders would conflict with the First Law.", 8 | "A robot must protect its own existence as long as such protection does not conflict with the First or Second Law.", 9 | ) 10 | 11 | 12 | for i, law in enumerate(Robot.Three_Laws, 1): 13 | print(f"{i}: {law}") 14 | -------------------------------------------------------------------------------- /python-OOP/02-class-vs-instance-attributes/static_method_example.py: -------------------------------------------------------------------------------- 1 | """Demonstrates use of a static method to query class attribute.""" 2 | 3 | 4 | class Robot: 5 | __counter = 0 6 | 7 | def __init__(self): 8 | type(self).__counter += 1 9 | 10 | @staticmethod 11 | def RobotInstances(): 12 | return Robot.__counter 13 | 14 | 15 | if __name__ == "__main__": 16 | print(Robot.RobotInstances()) 17 | x = Robot() 18 | print(x.RobotInstances()) 19 | y = Robot() 20 | print(Robot.RobotInstances()) 21 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/README.md: -------------------------------------------------------------------------------- 1 | # 03 - Properties vs. Getters and Setters in Python 2 | 3 | This chapter covers the evolution from traditional getter/setter methods to Pythonic property-based encapsulation. It highlights the simplicity and flexibility that Python properties offer, and explains when getter/setter methods may still be useful. 4 | 5 | ## Topics Covered 6 | 7 | | File | Description | 8 | | ----------------------------- | ---------------------------------------------------------------------- | 9 | | `getter_setter_basic.py` | Traditional encapsulation with explicit `get_` and `set_` methods | 10 | | `public_attribute.py` | Pythonic class design with public attributes | 11 | | `clamped_setter.py` | Uses setter validation to clamp values into a range | 12 | | `property_decorator.py` | Uses `@property` and `@x.setter` decorators for Pythonic encapsulation | 13 | | `property_alt_syntax.py` | Alternative property declaration using `property()` | 14 | | `robot_condition_property.py` | Property depending on multiple internal values | 15 | | `convert_to_property.py` | Migrating from public attribute to property without interface breaking | 16 | | `generic_getattr_setattr.py` | Uses `__getattr__` and `__setattr__` for generalized property behavior | 17 | | `conditional_setattr.py` | Advanced example with conditional validation in `__setattr__` | 18 | | `getter_setter_use_case.py` | When to prefer traditional getters/setters (e.g. additional arguments) | 19 | 20 | ## Key Takeaways 21 | 22 | - **Use properties** for Pythonic, simple, and readable interfaces. 23 | - **Use private attributes with properties** when encapsulation or validation is needed. 24 | - **Fallback to getter/setter methods** when external compatibility or additional arguments are required. 25 | - Properties preserve interface compatibility when implementation changes. 26 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/clamped_setter.py: -------------------------------------------------------------------------------- 1 | """Clamp values between 0 and 1000 using a traditional setter.""" 2 | 3 | 4 | class P: 5 | 6 | def __init__(self, x): 7 | self.set_x(x) 8 | 9 | def get_x(self): 10 | return self.__x 11 | 12 | def set_x(self, x): 13 | if x < 0: 14 | self.__x = 0 15 | elif x > 1000: 16 | self.__x = 1000 17 | else: 18 | self.__x = x 19 | 20 | 21 | p1 = P(1001) 22 | print(p1.get_x()) 23 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/conditional_setattr.py: -------------------------------------------------------------------------------- 1 | """Advanced setattr with validation logic.""" 2 | 3 | 4 | class Robot: 5 | 6 | def __init__(self, name, build_year, city): 7 | self.name = name 8 | self.build_year = build_year 9 | self.city = city 10 | 11 | def __getattr__(self, name): 12 | return self.__dict__[f"__{name}"] 13 | 14 | def __setattr__(self, name, value): 15 | if name == "name" and value in ["Henry", "Oscar"]: 16 | raise ValueError("Not a decent Robot name") 17 | elif name == "build_year" and int(value) < 2020: 18 | raise ValueError("Build Year has to be after 2019") 19 | self.__dict__[f"__{name}"] = value 20 | 21 | 22 | robot = Robot("Marvin", 2020, "TechCity") 23 | print(robot.name) 24 | print(robot.build_year) 25 | print(robot.city) 26 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/convert_to_property.py: -------------------------------------------------------------------------------- 1 | """Migrate public attribute to property without interface break.""" 2 | 3 | 4 | class OurClass: 5 | 6 | def __init__(self, a): 7 | self.OurAtt = a 8 | 9 | @property 10 | def OurAtt(self): 11 | return self.__OurAtt 12 | 13 | @OurAtt.setter 14 | def OurAtt(self, val): 15 | if val < 0: 16 | self.__OurAtt = 0 17 | elif val > 1000: 18 | self.__OurAtt = 1000 19 | else: 20 | self.__OurAtt = val 21 | 22 | 23 | x = OurClass(10) 24 | print(x.OurAtt) 25 | 26 | x.OurAtt = 2000 # This will set OurAtt to 1000 due to the setter logic 27 | print(x.OurAtt) # Output will be 1000 28 | 29 | x.OurAtt = -5 # This will set OurAtt to 0 due to the setter logic 30 | print(x.OurAtt) # Output will be 0 31 | 32 | print(x.__dict__) # This will show the internal state of the object 33 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/generic_getattr_setattr.py: -------------------------------------------------------------------------------- 1 | """Generic property behavior using __getattr__ and __setattr__.""" 2 | 3 | 4 | class Robot: 5 | 6 | def __init__(self, name, build_year, city): 7 | self.name = name 8 | self.build_year = build_year 9 | self.city = city 10 | 11 | def __getattr__(self, name): 12 | return self.__dict__[f"__{name}"] 13 | 14 | def __setattr__(self, name, value): 15 | self.__dict__[f"__{name}"] = value 16 | 17 | 18 | robot = Robot("RoboBot", 2022, "TechCity") 19 | print(robot.name) 20 | print(robot.build_year) 21 | print(robot.city) 22 | print(robot.__dict__) 23 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/getter_setter_basic.py: -------------------------------------------------------------------------------- 1 | """Traditional getter and setter example.""" 2 | 3 | 4 | class P: 5 | 6 | def __init__(self, x): 7 | self.__x = x 8 | 9 | def get_x(self): 10 | return self.__x 11 | 12 | def set_x(self, x): 13 | self.__x = x 14 | 15 | 16 | p1 = P(42) 17 | p2 = P(4711) 18 | p1.set_x(p1.get_x() + p2.get_x()) 19 | print(p1.get_x()) 20 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/getter_setter_use_case.py: -------------------------------------------------------------------------------- 1 | """Use case: getter/setter with extra logic/arguments.""" 2 | 3 | 4 | class Person: 5 | 6 | def __init__(self, name, height): 7 | self.name = name 8 | self._height = height 9 | 10 | def get_height(self): 11 | return self._height 12 | 13 | def set_height(self, value, validate=True): 14 | if validate and not (150 <= value <= 200): 15 | raise ValueError("Height must be between 150 and 200 cm.") 16 | self._height = value 17 | 18 | 19 | person = Person("Alice", 170) 20 | person.set_height(175) 21 | print(person.get_height()) 22 | 23 | try: 24 | person.set_height(210) 25 | except ValueError as e: 26 | print(e) 27 | 28 | person.set_height(210, validate=False) 29 | print(person.get_height()) 30 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/property_alt_syntax.py: -------------------------------------------------------------------------------- 1 | """Define property without decorators.""" 2 | 3 | 4 | class P: 5 | 6 | def __init__(self, x): 7 | self.__set_x(x) 8 | 9 | def __get_x(self): 10 | return self.__x 11 | 12 | def __set_x(self, x): 13 | if x < 0: 14 | self.__x = 0 15 | elif x > 1000: 16 | self.__x = 1000 17 | else: 18 | self.__x = x 19 | 20 | x = property(__get_x, __set_x) 21 | 22 | 23 | p1 = P(500) 24 | print(p1.x) 25 | 26 | p1 = P(5000) 27 | print(p1.x) 28 | 29 | p1 = P(-5000) 30 | print(p1.x) 31 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/property_decorator.py: -------------------------------------------------------------------------------- 1 | """Use property decorator for clean encapsulation.""" 2 | 3 | 4 | class P: 5 | 6 | def __init__(self, x): 7 | self.x = x 8 | 9 | @property 10 | def x(self): 11 | return self.__x 12 | 13 | @x.setter 14 | def x(self, x): 15 | if x < 0: 16 | self.__x = 0 17 | elif x > 1000: 18 | self.__x = 1000 19 | else: 20 | self.__x = x 21 | 22 | 23 | p1 = P(1001) 24 | print(p1.x) 25 | p1.x = -12 26 | print(p1.x) 27 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/public_attribute.py: -------------------------------------------------------------------------------- 1 | """Pythonic use of public attributes without getters and setters.""" 2 | 3 | 4 | class P: 5 | 6 | def __init__(self, x): 7 | self.x = x 8 | 9 | 10 | p1 = P(42) 11 | p2 = P(4711) 12 | p1.x = p1.x + p2.x 13 | print(p1.x) 14 | -------------------------------------------------------------------------------- /python-OOP/03-properties-vs-getters-setters/robot_condition_property.py: -------------------------------------------------------------------------------- 1 | """Property derived from multiple internal values.""" 2 | 3 | 4 | class Robot: 5 | 6 | def __init__(self, name, build_year, lk=0.5, lp=0.5): 7 | self.name = name 8 | self.build_year = build_year 9 | self.__potential_physical = lk 10 | self.__potential_psychic = lp 11 | 12 | @property 13 | def condition(self): 14 | s = self.__potential_physical + self.__potential_psychic 15 | if s <= -1: 16 | return "I feel miserable!" 17 | elif s <= 0: 18 | return "I feel bad!" 19 | elif s <= 0.5: 20 | return "Could be worse!" 21 | elif s <= 1: 22 | return "Seems to be okay!" 23 | else: 24 | return "Great!" 25 | 26 | 27 | x = Robot("Marvin", 1979, 0.2, 0.4) 28 | y = Robot("Caliban", 1993, -0.4, 0.3) 29 | print(x.condition) 30 | print(y.condition) 31 | -------------------------------------------------------------------------------- /python-OOP/04-immutable-classes/README.md: -------------------------------------------------------------------------------- 1 | # 04 - Creating Immutable Classes in Python 2 | 3 | This chapter introduces immutable class design in Python. Immutability means that an object’s state cannot be modified after it is created. Immutable objects are common in functional programming, and Python supports several patterns to create them. 4 | 5 | ## Why Immutability? 6 | 7 | - ✅ **Thread-safe**: No data races or lock management. 8 | - **Predictable**: The object state stays consistent. 9 | - **Cacheable**: Supports efficient reuse and memoization. 10 | - **Simpler tests**: No mutable state means fewer side effects. 11 | - **Easier debugging**: No hidden mutations to track. 12 | - **Prevents unintended changes**. 13 | - **Hashable**: Suitable for `set` and `dict` keys. 14 | - **Functional programming style**: Promotes purity and composability. 15 | 16 | ## Techniques for Creating Immutable Classes 17 | 18 | | File | Technique Description | 19 | | ------------------------- | ------------------------------------------------------------ | 20 | | `getter_only.py` | Getter-only methods, no setters | 21 | | `property_readonly.py` | Read-only properties using `@property` | 22 | | `dataclass_frozen.py` | `@dataclass(frozen=True)` to auto-generate immutable classes | 23 | | `namedtuple_immutable.py` | Using `collections.namedtuple` | 24 | | `slots_example.py` | `__slots__` to prevent dynamic attribute creation | 25 | 26 | ## Tip 27 | 28 | Prefer `@dataclass(frozen=True)` or `namedtuple` for lightweight immutable models. Use `__slots__` for memory optimization, not immutability enforcement. 29 | -------------------------------------------------------------------------------- /python-OOP/04-immutable-classes/dataclass_frozen.py: -------------------------------------------------------------------------------- 1 | """Using @dataclass(frozen=True) to create an immutable class.""" 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass(frozen=True) 7 | class ImmutableRobot: 8 | name: str 9 | brandname: str 10 | 11 | 12 | robot = ImmutableRobot("RoboX", "TechBot") 13 | print(robot.name) 14 | print(robot.brandname) 15 | 16 | try: 17 | robot.name = "RoboY" 18 | except AttributeError as e: 19 | print(e) 20 | 21 | try: 22 | robot.brandname = "NewTechBot" 23 | except AttributeError as e: 24 | print(e) 25 | -------------------------------------------------------------------------------- /python-OOP/04-immutable-classes/getter_only.py: -------------------------------------------------------------------------------- 1 | """Immutable class with getter-only methods (Java style).""" 2 | 3 | 4 | class ImmutableRobot: 5 | 6 | def __init__(self, name, brandname): 7 | self.__name = name 8 | self.__brandname = brandname 9 | 10 | def get_name(self): 11 | return self.__name 12 | 13 | def get_brandname(self): 14 | return self.__brandname 15 | 16 | 17 | robot = ImmutableRobot("RoboX", "TechBot") 18 | print(robot.get_name()) 19 | print(robot.get_brandname()) 20 | -------------------------------------------------------------------------------- /python-OOP/04-immutable-classes/namedtuple_immutable.py: -------------------------------------------------------------------------------- 1 | """Using namedtuple from collections for immutability.""" 2 | 3 | from collections import namedtuple 4 | 5 | ImmutableRobot = namedtuple("ImmutableRobot", ["name", "brandname"]) 6 | 7 | robot = ImmutableRobot("RoboX", "TechBot") 8 | print(robot.name) 9 | print(robot.brandname) 10 | 11 | try: 12 | robot.name = "RoboY" 13 | except AttributeError as e: 14 | print(e) 15 | 16 | try: 17 | robot.brandname = "NewTechBot" 18 | except AttributeError as e: 19 | print(e) 20 | -------------------------------------------------------------------------------- /python-OOP/04-immutable-classes/property_readonly.py: -------------------------------------------------------------------------------- 1 | """Immutable class using @property with no setters.""" 2 | 3 | 4 | class ImmutableRobot: 5 | 6 | def __init__(self, name, brandname): 7 | self.__name = name 8 | self.__brandname = brandname 9 | 10 | @property 11 | def name(self): 12 | return self.__name 13 | 14 | @property 15 | def brandname(self): 16 | return self.__brandname 17 | 18 | 19 | robot = ImmutableRobot("RoboX", "TechBot") 20 | print(robot.name) 21 | print(robot.brandname) 22 | 23 | try: 24 | robot.name = "RoboY" 25 | except AttributeError as e: 26 | print(e) 27 | 28 | try: 29 | robot.brandname = "NewTechBot" 30 | except AttributeError as e: 31 | print(e) 32 | -------------------------------------------------------------------------------- /python-OOP/04-immutable-classes/slots_example.py: -------------------------------------------------------------------------------- 1 | """Using __slots__ to restrict attribute creation (not full immutability).""" 2 | 3 | 4 | class ImmutableRobot: 5 | __slots__ = ("__name", "__brandname") 6 | 7 | def __init__(self, name, brandname): 8 | self.__name = name 9 | self.__brandname = brandname 10 | 11 | @property 12 | def name(self): 13 | return self.__name 14 | 15 | @property 16 | def brandname(self): 17 | return self.__brandname 18 | 19 | 20 | robot = ImmutableRobot("RoboX", "TechBot") 21 | print(robot.name) 22 | print(robot.brandname) 23 | 24 | try: 25 | robot.serial_number = 12345 26 | except AttributeError as e: 27 | print(e) 28 | -------------------------------------------------------------------------------- /python-OOP/05-dataclasses/README.md: -------------------------------------------------------------------------------- 1 | # 05 - Dataclasses in Python 2 | 3 | Dataclasses simplify the creation of classes that primarily store data. Introduced in Python 3.7, they automatically generate boilerplate methods such as `__init__`, `__repr__`, `__eq__`, and optionally `__hash__`. 4 | 5 | ## Benefits of Using Dataclasses 6 | 7 | - ✅ Less boilerplate: Auto-generates `__init__`, `__repr__`, `__eq__`, and `__hash__` 8 | - Ideal for immutable data with `frozen=True` 9 | - Works well with `typing` for better static checks 10 | - Supports default values and default factories 11 | - Inheritance and customization-friendly 12 | 13 | ## Topics Covered 14 | 15 | | File | Description | 16 | | -------------------------- | --------------------------------------------------- | 17 | | `traditional_class.py` | Regular class with manual `__init__` and `__repr__` | 18 | | `basic_dataclass.py` | Basic `@dataclass` usage | 19 | | `frozen_dataclass.py` | Immutable `@dataclass(frozen=True)` | 20 | | `traditional_immutable.py` | Manual implementation of an immutable class | 21 | | `dataclass_in_set_dict.py` | Hashable dataclasses used in sets and dictionaries | 22 | | `exercise_book.py` | Exercise solution: Book class with dataclass | 23 | 24 | ## Tip 25 | 26 | Use `frozen=True` when your data should never change. This makes your instances hashable and usable in sets and dict keys. 27 | -------------------------------------------------------------------------------- /python-OOP/05-dataclasses/basic_dataclass.py: -------------------------------------------------------------------------------- 1 | """Basic usage of @dataclass.""" 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class Robot: 8 | model: str 9 | serial_number: str 10 | manufacturer: str 11 | 12 | 13 | y = Robot("MachinaMaster MM-42", "986-42", "Quantum Automations Inc.") 14 | print(repr(y)) 15 | -------------------------------------------------------------------------------- /python-OOP/05-dataclasses/dataclass_in_set_dict.py: -------------------------------------------------------------------------------- 1 | """Using frozen dataclass in sets and dictionaries.""" 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass(frozen=True) 7 | class ImmutableRobot: 8 | name: str 9 | brandname: str 10 | 11 | 12 | robot1 = ImmutableRobot("Marvin", "NanoGuardian XR-2000") 13 | robot2 = ImmutableRobot("R2D2", "QuantumTech Sentinel-7") 14 | robot3 = ImmutableRobot("Marva", "MachinaMaster MM-42") 15 | 16 | robots = {robot1, robot2, robot3} 17 | 18 | print("The robots in the set robots:") 19 | for robo in robots: 20 | print(robo) 21 | 22 | activity = {robot1: "activated", robot2: "activated", robot3: "deactivated"} 23 | 24 | print("\nAll the activated robots:") 25 | for robo, mode in activity.items(): 26 | if mode == "activated": 27 | print(f"{robo} is activated") 28 | -------------------------------------------------------------------------------- /python-OOP/05-dataclasses/exercise_book.py: -------------------------------------------------------------------------------- 1 | """Exercise: Dataclass for book information.""" 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class Book: 8 | title: str 9 | author: str 10 | isbn: str 11 | publication_year: int 12 | genre: str 13 | 14 | 15 | book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565", 1925, "Fiction") 16 | book2 = Book("To Kill a Mockingbird", "Harper Lee", "9780061120084", 1960, "Fiction") 17 | book3 = Book("1984", "George Orwell", "9780451524935", 1949, "Science Fiction") 18 | 19 | for i, book in enumerate([book1, book2, book3], 1): 20 | print(f"Book {i}:") 21 | print(f"Title: {book.title}") 22 | print(f"Author: {book.author}") 23 | print(f"ISBN: {book.isbn}") 24 | print(f"Publication Year: {book.publication_year}") 25 | print(f"Genre: {book.genre}\n") 26 | -------------------------------------------------------------------------------- /python-OOP/05-dataclasses/frozen_dataclass.py: -------------------------------------------------------------------------------- 1 | """Immutable dataclass with frozen=True.""" 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass(frozen=True) 7 | class ImmutableRobot: 8 | name: str 9 | brandname: str 10 | 11 | 12 | x1 = ImmutableRobot("Marvin", "NanoGuardian XR-2000") 13 | x2 = ImmutableRobot("Marvin", "NanoGuardian XR-2000") 14 | print(x1 == x2) 15 | print(x1.__hash__(), x2.__hash__()) 16 | -------------------------------------------------------------------------------- /python-OOP/05-dataclasses/traditional_class.py: -------------------------------------------------------------------------------- 1 | """Traditional class with manually defined __init__ and __repr__.""" 2 | 3 | 4 | class Robot_traditional: 5 | 6 | def __init__(self, model, serial_number, manufacturer): 7 | self.model = model 8 | self.serial_number = serial_number 9 | self.manufacturer = manufacturer 10 | 11 | def __repr__(self): 12 | return (f"Robot_traditional(model='{self.model}', " 13 | f"serial_number='{self.serial_number}', " 14 | f"manufacturer='{self.manufacturer}')") 15 | 16 | 17 | x = Robot_traditional("NanoGuardian XR-2000", "234-76", "Cyber Robotics Co.") 18 | print(repr(x)) 19 | -------------------------------------------------------------------------------- /python-OOP/05-dataclasses/traditional_immutable.py: -------------------------------------------------------------------------------- 1 | """Manual implementation of immutable class with __eq__ and __hash__.""" 2 | 3 | 4 | class ImmutableRobot_traditional: 5 | 6 | def __init__(self, name: str, brandname: str): 7 | self._name = name 8 | self._brandname = brandname 9 | 10 | @property 11 | def name(self) -> str: 12 | return self._name 13 | 14 | @property 15 | def brandname(self) -> str: 16 | return self._brandname 17 | 18 | def __eq__(self, other): 19 | if not isinstance(other, ImmutableRobot_traditional): 20 | return False 21 | return self.name == other.name and self.brandname == other.brandname 22 | 23 | def __hash__(self): 24 | return hash((self.name, self.brandname)) 25 | 26 | 27 | x1 = ImmutableRobot_traditional("Marvin", "NanoGuardian XR-2000") 28 | x2 = ImmutableRobot_traditional("Marvin", "NanoGuardian XR-2000") 29 | print(x1 == x2) 30 | print(x1.__hash__(), x2.__hash__()) 31 | -------------------------------------------------------------------------------- /python-OOP/06-custom-property/README.md: -------------------------------------------------------------------------------- 1 | # 06 - Implementing a Custom Property Class 2 | 3 | In this chapter, we go deeper into how Python's built-in `property` works by reimplementing our own version. This is especially useful for gaining a better understanding of descriptors and decorators in Python. 4 | 5 | ## Why Build a Custom Property? 6 | 7 | - Understand how `@property` works under the hood 8 | - Learn about Python’s descriptor protocol 9 | - Get comfortable with decorator patterns 10 | - Useful for creating debug-friendly or logging-enabled properties 11 | 12 | ## Files Included 13 | 14 | | File | Description | 15 | | -------------------- | --------------------------------------------- | 16 | | `our_property.py` | Minimal implementation of `property` class | 17 | | `chatty_property.py` | A verbose version of `property` for debugging | 18 | 19 | ## Key Concepts 20 | 21 | - `__get__`, `__set__`, and `__delete__` implement descriptor behavior 22 | - Custom `getter`, `setter`, and `deleter` methods replicate `@property` chaining 23 | - Demonstrates how decorators return new descriptors with updated behavior 24 | -------------------------------------------------------------------------------- /python-OOP/06-custom-property/chatty_property.py: -------------------------------------------------------------------------------- 1 | """Verbose reimplementation of property using chatty_property for debugging.""" 2 | 3 | 4 | class chatty_property: 5 | 6 | def __init__(self, fget=None, fset=None, fdel=None, doc=None): 7 | self.fget = fget 8 | self.fset = fset 9 | self.fdel = fdel 10 | print("\n__init__ called with:") 11 | print(f"fget={fget}, fset={fset}, fdel={fdel}, doc={doc}") 12 | if doc is None and fget is not None: 13 | print(f"doc set to docstring of {fget.__name__} method") 14 | doc = fget.__doc__ 15 | self.__doc__ = doc 16 | 17 | def __get__(self, obj, objtype=None): 18 | if obj is None: 19 | return self 20 | if self.fget is None: 21 | raise AttributeError("unreadable attribute") 22 | return self.fget(obj) 23 | 24 | def __set__(self, obj, value): 25 | if self.fset is None: 26 | raise AttributeError("can't set attribute") 27 | self.fset(obj, value) 28 | 29 | def __delete__(self, obj): 30 | if self.fdel is None: 31 | raise AttributeError("can't delete attribute") 32 | self.fdel(obj) 33 | 34 | def getter(self, fget): 35 | return type(self)(fget, self.fset, self.fdel, self.__doc__) 36 | 37 | def setter(self, fset): 38 | print(type(self)) 39 | return type(self)(self.fget, fset, self.fdel, self.__doc__) 40 | 41 | def deleter(self, fdel): 42 | return type(self)(self.fget, self.fset, fdel, self.__doc__) 43 | 44 | 45 | class Robot: 46 | 47 | def __init__(self, city): 48 | self.city = city 49 | 50 | @chatty_property 51 | def city(self): 52 | """city attribute of Robot""" 53 | print("The Property 'city' will be returned now:") 54 | return self.__city 55 | 56 | @city.setter 57 | def city(self, city): 58 | print("'city' will be set") 59 | self.__city = city 60 | 61 | 62 | robo = Robot("Berlin") 63 | print("Current city:", robo.city) 64 | robo.city = "Munich" 65 | print("New city:", robo.city) 66 | -------------------------------------------------------------------------------- /python-OOP/06-custom-property/our_property.py: -------------------------------------------------------------------------------- 1 | """Simple custom property class (our_property) and usage demo.""" 2 | 3 | 4 | class our_property: 5 | 6 | def __init__(self, fget=None, fset=None, fdel=None, doc=None): 7 | self.fget = fget 8 | self.fset = fset 9 | self.fdel = fdel 10 | if doc is None and fget is not None: 11 | doc = fget.__doc__ 12 | self.__doc__ = doc 13 | 14 | def __get__(self, obj, objtype=None): 15 | if obj is None: 16 | return self 17 | if self.fget is None: 18 | raise AttributeError("unreadable attribute") 19 | return self.fget(obj) 20 | 21 | def __set__(self, obj, value): 22 | if self.fset is None: 23 | raise AttributeError("can't set attribute") 24 | self.fset(obj, value) 25 | 26 | def __delete__(self, obj): 27 | if self.fdel is None: 28 | raise AttributeError("can't delete attribute") 29 | self.fdel(obj) 30 | 31 | def getter(self, fget): 32 | return type(self)(fget, self.fset, self.fdel, self.__doc__) 33 | 34 | def setter(self, fset): 35 | return type(self)(self.fget, fset, self.fdel, self.__doc__) 36 | 37 | def deleter(self, fdel): 38 | return type(self)(self.fget, self.fset, fdel, self.__doc__) 39 | 40 | 41 | class Robot: 42 | 43 | def __init__(self, city): 44 | self.city = city 45 | 46 | @our_property 47 | def city(self): 48 | print("The Property 'city' will be returned now:") 49 | return self.__city 50 | 51 | @city.setter 52 | def city(self, city): 53 | print("'city' will be set") 54 | self.__city = city 55 | 56 | 57 | print("Instantiating Robot and setting 'city' to 'Berlin'") 58 | robo = Robot("Berlin") 59 | print("The value is:", robo.city) 60 | 61 | print("Our robot moves now to Frankfurt:") 62 | robo.city = "Frankfurt" 63 | print("The value is:", robo.city) 64 | -------------------------------------------------------------------------------- /python-OOP/07-magic-methods/README.md: -------------------------------------------------------------------------------- 1 | # 07 - Magic Methods 2 | 3 | This chapter introduces Python's "magic methods" or "dunder methods" (double underscore methods), such as `__init__`, `__add__`, `__repr__`, `__str__`, and more. 4 | 5 | Magic methods enable operator overloading and customization of standard behavior. For example, you can define how objects of your class respond to `+`, `==`, `str()`, `repr()` and even function calls using `__call__`. 6 | 7 | ## Highlights 8 | 9 | - `__init__`: Constructor called on object creation. 10 | - `__str__`: Returns user-friendly string representation. 11 | - `__repr__`: Returns unambiguous string (used by developers). 12 | - `__add__`, `__radd__`: Overload `+` and reverse `+`. 13 | - `__iadd__`: Overload `+=`. 14 | - `__mul__`, `__rmul__`, `__imul__`: Overload multiplication. 15 | - `__call__`: Make an object callable like a function. 16 | 17 | ## Examples 18 | 19 | ### Length class 20 | 21 | Supports addition of different units: 22 | 23 | ```python 24 | from length import Length as L 25 | print(L(2.56,"m") + L(3,"yd") + L(7.8,"in") + L(7.03,"cm")) 26 | ``` 27 | 28 | ### Currency class 29 | 30 | Supports addition and multiplication of currency values: 31 | 32 | ```python 33 | from currency_converter import Ccy 34 | 35 | x = Ccy(10.00, "EUR") 36 | y = Ccy(10.00, "GBP") 37 | print(x + y) # currency-aware addition 38 | print(2 * x + y * 0.9) # scalar multiplication 39 | ``` 40 | -------------------------------------------------------------------------------- /python-OOP/07-magic-methods/length.py: -------------------------------------------------------------------------------- 1 | """ 2 | Length class with support for unit conversion and operator overloading. 3 | Supports addition, in-place addition, and reverse addition. 4 | """ 5 | 6 | 7 | class Length: 8 | __metric = {"mm": 0.001, "cm": 0.01, "m": 1, "km": 1000, "in": 0.0254, "ft": 0.3048, "yd": 0.9144, "mi": 1609.344} 9 | 10 | def __init__(self, value, unit="m"): 11 | self.value = value 12 | self.unit = unit 13 | 14 | def Converse2Metres(self): 15 | return self.value * Length.__metric[self.unit] 16 | 17 | def __add__(self, other): 18 | if isinstance(other, (int, float)): 19 | l = self.Converse2Metres() + other 20 | else: 21 | l = self.Converse2Metres() + other.Converse2Metres() 22 | return Length(l / Length.__metric[self.unit], self.unit) 23 | 24 | def __radd__(self, other): 25 | return self.__add__(other) 26 | 27 | def __iadd__(self, other): 28 | if isinstance(other, (int, float)): 29 | l = self.Converse2Metres() + other 30 | else: 31 | l = self.Converse2Metres() + other.Converse2Metres() 32 | self.value = l / Length.__metric[self.unit] 33 | return self 34 | 35 | def __str__(self): 36 | return str(round(self.Converse2Metres(), 5)) 37 | 38 | def __repr__(self): 39 | return f"Length({self.value}, '{self.unit}')" 40 | 41 | 42 | if __name__ == "__main__": 43 | x = Length(4) 44 | print(x) 45 | y = eval(repr(x)) 46 | z = Length(4.5, "yd") + Length(1) 47 | z += Length(2, "m") 48 | print(repr(z)) 49 | print(z) 50 | -------------------------------------------------------------------------------- /python-OOP/08-dynamic-data-transformation/product.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class Product: 5 | conversion_rates = {"USD": 1, "EUR": 0.92, "CHF": 0.90, "GBP": 0.79} 6 | 7 | def __init__(self, name, price, shipping_cost, currency="USD"): 8 | self.name = name 9 | self._price = price 10 | self._shipping_cost = shipping_cost 11 | self.currency = currency 12 | self._used_currency = currency 13 | 14 | def set_currency(self, new_currency, adapt_data=False): 15 | if self.currency != new_currency: 16 | self.currency = new_currency 17 | if adapt_data: 18 | self._price = self.price 19 | self._shipping_cost = self.shipping_cost 20 | self._used_currency = new_currency 21 | 22 | @property 23 | def price(self): 24 | return self._convert_currency(self._price) 25 | 26 | @property 27 | def shipping_cost(self): 28 | return self._convert_currency(self._shipping_cost) 29 | 30 | def _convert_currency(self, amount): 31 | factor = Product.conversion_rates[self.currency] / Product.conversion_rates[self._used_currency] 32 | return round(amount * factor, 2) 33 | 34 | def __str__(self): 35 | return f"{self.name:35s} {self.price:7.2f} {self.shipping_cost:6.2f}" 36 | 37 | def show_saved_data(self): 38 | print( 39 | f"Saved Data: self.name='{self.name}', self.currency='{self.currency}', self._used_currency='{self._used_currency}' self._price={self._price}, self._shipping_cost={self._shipping_cost}" 40 | ) 41 | 42 | 43 | class Products: 44 | 45 | def __init__(self): 46 | self.product_list = [] 47 | 48 | def add_product(self, product): 49 | self.product_list.append(product) 50 | 51 | def view_products(self, currency="USD", adapt_data=False): 52 | print(f"{'Product name':39s} Price Shipping") 53 | for product in self.product_list: 54 | product.set_currency(currency, adapt_data) 55 | print(product) 56 | 57 | def view_products_as_saved_data(self): 58 | for product in self.product_list: 59 | product.show_saved_data() 60 | -------------------------------------------------------------------------------- /python-OOP/08-dynamic-data-transformation/products_demo.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from product import Product 4 | from product import Products 5 | 6 | fantasy_names = [ 7 | "Elixir of Eternal Youth", 8 | "Dragonfire Sword", 9 | "Phoenix Feather Wand", 10 | "Mermaid's Tears Necklace", 11 | "Elven Cloak of Invisibility", 12 | "Potion of Flying", 13 | "Amulet of Wisdom", 14 | "Crystal Ball of Fortune", 15 | "Enchanted Mirror", 16 | "Unicorn Horn Ring", 17 | ] 18 | 19 | currencies = list(Product.conversion_rates.keys()) 20 | products = Products() 21 | 22 | # Add randomized products 23 | for name in fantasy_names: 24 | price = random.uniform(70, 1000) 25 | products.add_product(Product(name, price, price * 0.03, currency=random.choice(currencies))) 26 | 27 | # View products in USD 28 | print("\nViewing products in USD:") 29 | products.view_products(currency="USD") 30 | 31 | # View products in CHF without adapting data 32 | print("\nViewing products in CHF (no adaptation):") 33 | products.view_products(currency="CHF") 34 | 35 | # Show saved internal state 36 | print("\nSaved internal data view:") 37 | products.view_products_as_saved_data() 38 | 39 | # View products in CHF with data adaptation 40 | print("\nViewing products in CHF (with adapt_data=True):") 41 | products.view_products(currency="CHF", adapt_data=True) 42 | 43 | # View final saved data 44 | print("\nFinal saved internal state (after adaptation):") 45 | products.view_products_as_saved_data() 46 | -------------------------------------------------------------------------------- /python-OOP/09-introduction-to-descriptors/README.md: -------------------------------------------------------------------------------- 1 | # Python Descriptors Tutorial 2 | 3 | Welcome to the **Python Descriptors Tutorial**. This multi-part guide walks you through the fundamentals and advanced usage of Python’s powerful descriptor protocol—one of the core features behind `@property`, `@classmethod`, `@staticmethod`, and more. 4 | 5 | This tutorial is organized into six clearly structured sections: 6 | 7 | ## Table of Contents 8 | 9 | | Part | Title | Description | 10 | | -------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------- | 11 | | [1](./1-introduction.md) | **Introduction** | Overview of descriptors and how they integrate with Python's attribute lookup chain | 12 | | [2](./2-descriptor-protocol.md) | **Descriptor Protocol** | Explanation of `__get__`, `__set__`, and `__delete__`, with a simple custom descriptor example | 13 | | [3](./3-data-non-data-descriptor.md) | **Data vs Non-Data Descriptors** | Difference between data and non-data descriptors, and how lookup precedence works | 14 | | [4](./4-practical-use-cases.md) | **Practical Use Cases** | Real-world applications: validation, caching, computed attributes, and type checking | 15 | | [5](./5-how-python-internally-uses-descriptors.md) | **How Python Internally Uses Descriptors** | How built-in features like `@property`, methods, and `super()` rely on descriptors | 16 | | [6](./6-dynamic-descriptor-creation.md) | **Dynamic Descriptors & Best Practices** | How to generate descriptors at runtime and best practices for maintainability | 17 | -------------------------------------------------------------------------------- /python-OOP/10-inheritance/README.md: -------------------------------------------------------------------------------- 1 | # 10. Inheritance 2 | 3 | Inheritance is a key feature of object-oriented programming that allows one class (child/subclass) to inherit attributes and methods from another class (parent/superclass). It supports code reuse and models real-world hierarchical relationships. 4 | 5 | ## Key Concepts 6 | 7 | - **Single Inheritance**: A class inherits from one parent class. 8 | - **Method Overriding**: A subclass redefines a method from its superclass. 9 | - **`super()` Function**: Calls a method from the parent class. 10 | - **`isinstance()` vs `type()`**: Use `isinstance()` to check inheritance chains. 11 | - **Overwriting vs Overriding vs Overloading**: Explained with examples. 12 | 13 | ## Examples 14 | 15 | | File | Description | 16 | | ----------------------- | ------------------------------------------------------------ | 17 | | `basic_inheritance.py` | Shows simple inheritance of methods. | 18 | | `override_and_super.py` | Demonstrates method overriding and use of `super()`. | 19 | | `doctor_robot.py` | Applies inheritance in a real-world example (healing robot). | 20 | | `type_vs_isinstance.py` | Highlights difference between `type()` and `isinstance()`. | 21 | | `animal_hierarchy.py` | Exercise 1: Inheritance in an animal class hierarchy. | 22 | | `shape_hierarchy.py` | Exercise 2: Inheritance in geometric shape classes. | 23 | 24 | ## Exercises 25 | 26 | 1. Build a class hierarchy of animals (Mammal, Bird, Reptile). 27 | 2. Create a shape system (Circle, Rectangle, Triangle) using inheritance. 28 | 29 | Run the Python files individually to see behavior and output. 30 | -------------------------------------------------------------------------------- /python-OOP/10-inheritance/animal_hierarchy.py: -------------------------------------------------------------------------------- 1 | class Animal: 2 | 3 | def __init__(self, name, age, sound): 4 | self.name = name 5 | self.age = age 6 | self.sound = sound 7 | 8 | def make_sound(self): 9 | print(f"{self.name} says: {self.sound}") 10 | 11 | 12 | class Mammal(Animal): 13 | 14 | def __init__(self, name, age, sound, fur_color, number_of_legs): 15 | super().__init__(name, age, sound) 16 | self.fur_color = fur_color 17 | self.number_of_legs = number_of_legs 18 | 19 | def give_birth(self, name): 20 | return Mammal(name, 0, self.sound, self.fur_color, self.number_of_legs) 21 | 22 | def nurse_young(self): 23 | print(f"{self.name} nurses its young.") 24 | 25 | 26 | class Bird(Animal): 27 | 28 | def __init__(self, name, age, sound, wingspan): 29 | super().__init__(name, age, sound) 30 | self.wingspan = wingspan 31 | 32 | def fly(self): 33 | print(f"{self.name} flies with a wingspan of {self.wingspan}.") 34 | 35 | 36 | class Reptile(Animal): 37 | 38 | def __init__(self, name, age, sound, scale_color): 39 | super().__init__(name, age, sound) 40 | self.scale_color = scale_color 41 | 42 | def crawl(self): 43 | print(f"{self.name} crawls with {self.scale_color} scales.") 44 | 45 | 46 | dog = Mammal("Molly", 5, "Woof", "Brown", 4) 47 | eagle = Bird("Eagle", 3, "Screech", "Large") 48 | turtle = Reptile("Turtle", 10, "Hiss", "Green") 49 | 50 | dog.make_sound() 51 | dog.give_birth("Charlie").make_sound() 52 | eagle.fly() 53 | turtle.crawl() 54 | -------------------------------------------------------------------------------- /python-OOP/10-inheritance/basic_inheritance.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name): 4 | self.name = name 5 | 6 | def say_hi(self): 7 | print(f"Hi, I am {self.name}") 8 | 9 | 10 | class PhysicianRobot(Robot): 11 | pass 12 | 13 | 14 | x = Robot("Marvin") 15 | y = PhysicianRobot("James") 16 | 17 | x.say_hi() 18 | y.say_hi() 19 | -------------------------------------------------------------------------------- /python-OOP/10-inheritance/doctor_robot.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class Robot: 5 | 6 | def __init__(self, name): 7 | self.name = name 8 | self.health_level = random.random() 9 | 10 | def say_hi(self): 11 | print(f"Hi, I am {self.name}") 12 | 13 | def needs_a_doctor(self): 14 | return self.health_level < 0.8 15 | 16 | 17 | class PhysicianRobot(Robot): 18 | 19 | def say_hi(self): 20 | print("Everything will be okay!") 21 | print(f"{self.name} takes care of you!") 22 | 23 | def heal(self, robo): 24 | robo.health_level = random.uniform(robo.health_level, 1) 25 | print(f"{robo.name} has been healed by {self.name}!") 26 | 27 | 28 | doc = PhysicianRobot("Dr. Frankenstein") 29 | 30 | for i in range(5): 31 | patient = Robot(f"Marvin{i}") 32 | if patient.needs_a_doctor(): 33 | print(f"{patient.name} before: {patient.health_level:.2f}") 34 | doc.heal(patient) 35 | print(f"{patient.name} after: {patient.health_level:.2f}") 36 | -------------------------------------------------------------------------------- /python-OOP/10-inheritance/override_and_super.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name): 4 | self.name = name 5 | 6 | def say_hi(self): 7 | print(f"Hi, I am {self.name}") 8 | 9 | 10 | class PhysicianRobot(Robot): 11 | 12 | def say_hi(self): 13 | super().say_hi() 14 | print("and I am a physician!") 15 | 16 | 17 | doc = PhysicianRobot("Dr. Frankenstein") 18 | doc.say_hi() 19 | -------------------------------------------------------------------------------- /python-OOP/10-inheritance/shape_hierarchy.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | class Shape: 5 | 6 | def __init__(self, color): 7 | self.color = color 8 | 9 | def calculate_area(self): 10 | pass 11 | 12 | 13 | class Circle(Shape): 14 | 15 | def __init__(self, color, radius): 16 | super().__init__(color) 17 | self.radius = radius 18 | 19 | def calculate_area(self): 20 | return math.pi * self.radius**2 21 | 22 | 23 | class Rectangle(Shape): 24 | 25 | def __init__(self, color, width, height): 26 | super().__init__(color) 27 | self.width = width 28 | self.height = height 29 | 30 | def calculate_area(self): 31 | return self.width * self.height 32 | 33 | 34 | class Triangle(Shape): 35 | 36 | def __init__(self, color, base, height): 37 | super().__init__(color) 38 | self.base = base 39 | self.height = height 40 | 41 | def calculate_area(self): 42 | return 0.5 * self.base * self.height 43 | 44 | 45 | circle = Circle("Red", 5) 46 | rectangle = Rectangle("Blue", 4, 6) 47 | triangle = Triangle("Green", 3, 4) 48 | 49 | print("Circle Area:", circle.calculate_area()) 50 | print("Rectangle Area:", rectangle.calculate_area()) 51 | print("Triangle Area:", triangle.calculate_area()) 52 | -------------------------------------------------------------------------------- /python-OOP/10-inheritance/type_vs_isinstance.py: -------------------------------------------------------------------------------- 1 | class A: 2 | pass 3 | 4 | 5 | class B(A): 6 | pass 7 | 8 | 9 | class C(B): 10 | pass 11 | 12 | 13 | x = C() 14 | 15 | print(isinstance(x, A)) # True 16 | print(type(x) == A) # False 17 | -------------------------------------------------------------------------------- /python-OOP/11-multiple-inheritance/README.md: -------------------------------------------------------------------------------- 1 | # 11. Multiple Inheritance 2 | 3 | This lesson covers **multiple inheritance** in Python, where a class can inherit from more than one parent class. Python supports this with a well-designed Method Resolution Order (MRO) mechanism. 4 | 5 | ## Topics Covered 6 | 7 | - What is multiple inheritance? 8 | - The diamond problem 9 | - Using `super()` and MRO 10 | - CalendarClock example combining `Clock` and `Calendar` 11 | - HybridCar example 12 | - Polymorphism demonstration 13 | 14 | ## Learning Objectives 15 | 16 | - Understand how multiple inheritance works in Python. 17 | - Learn about ambiguity in inheritance (diamond problem). 18 | - Apply `super()` to resolve method conflicts cleanly. 19 | - Combine behavior from multiple classes effectively. 20 | -------------------------------------------------------------------------------- /python-OOP/11-multiple-inheritance/diamond_problem.py: -------------------------------------------------------------------------------- 1 | class A: 2 | 3 | def m(self): 4 | print("m of A") 5 | 6 | 7 | class B(A): 8 | 9 | def m(self): 10 | print("m of B") 11 | super().m() 12 | 13 | 14 | class C(A): 15 | 16 | def m(self): 17 | print("m of C") 18 | super().m() 19 | 20 | 21 | class D(B, C): 22 | 23 | def m(self): 24 | print("m of D") 25 | super().m() 26 | 27 | 28 | x = D() 29 | x.m() 30 | 31 | print("MRO of D:", [cls.__name__ for cls in D.mro()]) 32 | print(D.__mro__) 33 | -------------------------------------------------------------------------------- /python-OOP/11-multiple-inheritance/hybrid_car.py: -------------------------------------------------------------------------------- 1 | class ElectricVehicle: 2 | 3 | def __init__(self, battery_capacity, charging_time): 4 | self.battery_capacity = battery_capacity 5 | self.charging_time = charging_time 6 | 7 | def charge_battery(self): 8 | print("Charging battery...") 9 | 10 | 11 | class GasolineVehicle: 12 | 13 | def __init__(self, fuel_tank_capacity, fuel_efficiency): 14 | self.fuel_tank_capacity = fuel_tank_capacity 15 | self.fuel_efficiency = fuel_efficiency 16 | 17 | def refuel(self): 18 | print("Refueling...") 19 | 20 | 21 | class HybridCar(ElectricVehicle, GasolineVehicle): 22 | 23 | def __init__(self, battery_capacity, charging_time, fuel_tank_capacity, fuel_efficiency): 24 | ElectricVehicle.__init__(self, battery_capacity, charging_time) 25 | GasolineVehicle.__init__(self, fuel_tank_capacity, fuel_efficiency) 26 | 27 | def drive(self): 28 | print("Driving with electric and gasoline power.") 29 | 30 | 31 | car = HybridCar(60, 4, 40, 30) 32 | car.charge_battery() 33 | car.refuel() 34 | car.drive() 35 | -------------------------------------------------------------------------------- /python-OOP/11-multiple-inheritance/polymorphism.py: -------------------------------------------------------------------------------- 1 | def f(x, y): 2 | print("values:", x, y) 3 | 4 | 5 | f(42, 43) 6 | f(42, 43.7) 7 | f(42.3, 43) 8 | f([1, 2], {3, 4}) 9 | f("hello", ("tuple", 123)) 10 | -------------------------------------------------------------------------------- /python-OOP/12-multiple-inheritance-example/README.md: -------------------------------------------------------------------------------- 1 | # 12. Multiple Inheritance in Python 2 | 3 | This section of our Python OOP tutorial focuses on **Multiple Inheritance**, building on our understanding from single inheritance. We use an extended example involving different types of robots to explore key OOP principles in Python, including: 4 | 5 | - Inheritance and class hierarchies 6 | - Overriding methods 7 | - Type-dependent instantiation (`type(self)`) 8 | - Use of `super()` and `**kwargs` for cooperative multiple inheritance 9 | 10 | ## Structure 11 | 12 | ### Concepts Covered: 13 | 14 | - Base class: `Robot` 15 | - Derived classes: `NursingRobot`, `FightingRobot` 16 | - Multiple inheritance: `FightingNurseRobot` 17 | - Flexible constructors with `**kwargs`: `FightingHealingRobot` 18 | 19 | ### How to Run 20 | 21 | Each `.py` file contains standalone runnable code. You can execute them directly: 22 | 23 | ```bash 24 | python robot_base.py 25 | python nursing_robot.py 26 | python fighting_robot.py 27 | python fighting_nurse_robot.py 28 | python advanced_fighting_healing_robot.py 29 | ``` 30 | -------------------------------------------------------------------------------- /python-OOP/12-multiple-inheritance-example/advanced_fighting_healing_robot.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class Robot: 5 | 6 | def __init__(self, name, health_level, **kwargs): 7 | self.name = name 8 | self.health_level = health_level 9 | 10 | 11 | class HealingRobot(Robot): 12 | 13 | def __init__(self, healing_power, **kwargs): 14 | super().__init__(**kwargs) 15 | self.healing_power = healing_power 16 | 17 | def heal(self, robo): 18 | robo.health_level = random.uniform(robo.health_level, 1) 19 | print(f"{robo.name} has been healed by {self.name}!") 20 | 21 | 22 | class FightingRobot(Robot): 23 | 24 | def __init__(self, fighting_power=1, **kwargs): 25 | super().__init__(**kwargs) 26 | self.fighting_power = fighting_power 27 | 28 | def attack(self, robo): 29 | robo.health_level = random.uniform(0, robo.health_level) 30 | print(f"{robo.name} has been attacked by {self.name}!") 31 | 32 | 33 | class FightingHealingRobot(HealingRobot, FightingRobot): 34 | 35 | def __init__(self, name, health_level, healing_power, fighting_power, mode="healing", **kw): 36 | self.mode = mode 37 | super().__init__(name=name, 38 | health_level=health_level, 39 | healing_power=healing_power, 40 | fighting_power=fighting_power, 41 | **kw) 42 | 43 | def say_hi(self): 44 | if self.mode == "fighting": 45 | FightingRobot.say_hi(self) 46 | elif self.mode == "healing": 47 | HealingRobot.say_hi(self) 48 | else: 49 | Robot.say_hi(self) 50 | 51 | 52 | # Run this as demo 53 | if __name__ == "__main__": 54 | x = FightingHealingRobot(name="Rambo", health_level=0.9, fighting_power=0.7, healing_power=0.9) 55 | print(x.__dict__) 56 | -------------------------------------------------------------------------------- /python-OOP/12-multiple-inheritance-example/fighting_nurse_robot.py: -------------------------------------------------------------------------------- 1 | from fighting_robot import FightingRobot 2 | from nursing_robot import NursingRobot 3 | 4 | 5 | class FightingNurseRobot(NursingRobot, FightingRobot): 6 | 7 | def __init__(self, name, mode="nursing"): 8 | super().__init__(name) 9 | self.mode = mode 10 | 11 | def say_hi(self): 12 | if self.mode == "fighting": 13 | FightingRobot.say_hi(self) 14 | elif self.mode == "nursing": 15 | NursingRobot.say_hi(self) 16 | else: 17 | super().say_hi() 18 | -------------------------------------------------------------------------------- /python-OOP/12-multiple-inheritance-example/fighting_robot.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from robot_base import Robot 4 | 5 | 6 | class FightingRobot(Robot): 7 | __maximum_damage = 0.2 8 | 9 | def __init__(self, name="Hubert", fighting_power=None): 10 | super().__init__(name) 11 | max_dam = FightingRobot.__maximum_damage 12 | self.fighting_power = fighting_power or random.uniform(max_dam, 1) 13 | 14 | def say_hi(self): 15 | print(f"I am the terrible ... {self.name}") 16 | 17 | def attack(self, other): 18 | other.health_level *= self.fighting_power 19 | if isinstance(other, FightingRobot): 20 | self.health_level *= other.fighting_power 21 | -------------------------------------------------------------------------------- /python-OOP/12-multiple-inheritance-example/nursing_robot.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from robot_base import Robot 4 | 5 | 6 | class NursingRobot(Robot): 7 | 8 | def __init__(self, name="Hubert", healing_power=None): 9 | super().__init__(name) 10 | self.healing_power = healing_power or random.uniform(0.8, 1) 11 | 12 | def say_hi(self): 13 | print(f"Well, well, everything will be fine ... {self.name} takes care of you!") 14 | 15 | def say_hi_to_doc(self): 16 | Robot.say_hi(self) 17 | 18 | def heal(self, robo): 19 | if robo.health_level > self.healing_power: 20 | print(f"{self.name} not strong enough to heal {robo.name}") 21 | else: 22 | robo.health_level = random.uniform(robo.health_level, self.healing_power) 23 | print(f"{robo.name} has been healed by {self.name}!") 24 | -------------------------------------------------------------------------------- /python-OOP/12-multiple-inheritance-example/robot_base.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class Robot: 5 | __illegal_names = {"Henry", "Oscar"} 6 | __crucial_health_level = 0.6 7 | 8 | def __init__(self, name): 9 | self.name = name 10 | self.health_level = random.random() 11 | 12 | @property 13 | def name(self): 14 | return self.__name 15 | 16 | @name.setter 17 | def name(self, name): 18 | self.__name = "Marvin" if name in Robot.__illegal_names else name 19 | 20 | def __str__(self): 21 | return self.name + ", Robot" 22 | 23 | def __add__(self, other): 24 | first = self.name.split("-")[0] 25 | second = other.name.split("-")[0] 26 | return type(self)(f"{first}-{second}") 27 | 28 | def needs_a_nurse(self): 29 | return self.health_level < Robot.__crucial_health_level 30 | 31 | def say_hi(self): 32 | print(f"Hi, I am {self.name}") 33 | print(f"My health level is: {self.health_level}") 34 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/README.md: -------------------------------------------------------------------------------- 1 | # Callable Instances of Classes in Python 2 | 3 | This tutorial demonstrates how to make instances of custom Python classes behave like functions using the `__call__` method. 4 | 5 | ## Topics Covered 6 | 7 | - What is a callable? 8 | - Using `__call__` to make instances callable 9 | - Practical examples: 10 | - Food supply generator 11 | - Triangle area calculator 12 | - Fuzzy logic with triangle area 13 | - Merging expert predictions 14 | - Polynomial functions 15 | - Running average tracker 16 | - Temperature converter 17 | - Visualization of callable lines 18 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/callable_check.py: -------------------------------------------------------------------------------- 1 | def the_answer(question): 2 | return 42 3 | 4 | 5 | print("the_answer: ", callable(the_answer)) 6 | print("int is callable:", callable(int)) 7 | print("list is callable:", callable(list)) 8 | print("dict is callable:", callable(dict)) 9 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/food_supply.py: -------------------------------------------------------------------------------- 1 | class FoodSupply: 2 | 3 | def __init__(self, *ingredients): 4 | self.ingredients = ingredients 5 | 6 | def __call__(self): 7 | return " ".join(self.ingredients) + " plus delicious spam!" 8 | 9 | 10 | f = FoodSupply("fish", "rice") 11 | print(f()) 12 | 13 | g = FoodSupply("vegetables") 14 | print(g()) 15 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/fuzzy_triangle_area.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class FuzzyTriangleArea: 5 | 6 | def __init__(self, p=0.8, v=0.1): 7 | self.p, self.v = p, v 8 | 9 | def __call__(self, a, b, c): 10 | p = (a + b + c) / 2 11 | result = (p * (p - a) * (p - b) * (p - c))**0.5 12 | if random.random() <= self.p: 13 | return result 14 | return random.uniform(result - self.v, result + self.v) 15 | 16 | 17 | area1 = FuzzyTriangleArea() 18 | area2 = FuzzyTriangleArea(0.5, 0.2) 19 | 20 | for _ in range(5): 21 | print(f"{area1(3, 4, 5):.3f}, {area2(3, 4, 5):.3f}") 22 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/merge_experts.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from random import uniform 3 | 4 | from fuzzy_triangle_area import FuzzyTriangleArea 5 | 6 | 7 | class MergeExperts: 8 | 9 | def __init__(self, mode, *experts): 10 | self.mode, self.experts = mode, experts 11 | 12 | def __call__(self, a, b, c): 13 | results = [exp(a, b, c) for exp in self.experts] 14 | if self.mode == "vote": 15 | return Counter(results).most_common(1)[0][0] 16 | elif self.mode == "mean": 17 | return sum(results) / len(results) 18 | 19 | 20 | rvalues = [(uniform(0.7, 0.9), uniform(0.05, 0.2)) for _ in range(20)] 21 | experts = [FuzzyTriangleArea(p, v) for p, v in rvalues] 22 | merger1 = MergeExperts("vote", *experts) 23 | merger2 = MergeExperts("mean", *experts) 24 | 25 | print("Vote:", merger1(3, 4, 5)) 26 | print("Mean:", merger2(3, 4, 5)) 27 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/polynomial.py: -------------------------------------------------------------------------------- 1 | class Polynomial: 2 | 3 | def __init__(self, *coefficients): 4 | self.coefficients = coefficients[::-1] 5 | 6 | def __call__(self, x): 7 | return sum(coeff * x**i for i, coeff in enumerate(self.coefficients)) 8 | 9 | 10 | p1 = Polynomial(42) 11 | p2 = Polynomial(0.75, 2) 12 | p3 = Polynomial(1, -0.5, 0.75, 2) 13 | 14 | for i in range(1, 10): 15 | print(i, p1(i), p2(i), p3(i)) 16 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/running_average.py: -------------------------------------------------------------------------------- 1 | class RunningAverage: 2 | 3 | def __init__(self): 4 | self.numbers = [] 5 | 6 | def add_number(self, number): 7 | self.numbers.append(number) 8 | 9 | def __call__(self): 10 | return sum(self.numbers) / len(self.numbers) if self.numbers else 0 11 | 12 | def reset(self): 13 | self.numbers = [] 14 | 15 | 16 | average = RunningAverage() 17 | print("Init average:", average()) 18 | for x in [3, 5, 12, 9, 1]: 19 | average.add_number(x) 20 | print("Average:", average()) 21 | average.reset() 22 | for x in [3.1, 19.8, 3]: 23 | average.add_number(x) 24 | print("Average:", average()) 25 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/temperature_converter.py: -------------------------------------------------------------------------------- 1 | class TemperatureConverter: 2 | 3 | def __init__(self, temperature, unit="C"): 4 | self.temperature = temperature 5 | self.unit = unit 6 | 7 | @property 8 | def unit(self): 9 | return self.__unit 10 | 11 | @unit.setter 12 | def unit(self, unit): 13 | if unit.upper() in {"C", "F"}: 14 | self.__unit = unit 15 | else: 16 | raise ValueError("Should be 'C' or 'F'") 17 | 18 | def convert(self): 19 | new_unit = "F" if self.unit == "C" else "C" 20 | return self._convert_to_unit(new_unit) 21 | 22 | def __call__(self): 23 | return self.temperature 24 | 25 | def change_unit(self, new_unit): 26 | new_unit = new_unit.upper() 27 | if new_unit != self.unit: 28 | self.temperature = self._convert_to_unit(new_unit) 29 | self.unit = new_unit 30 | 31 | def _convert_to_unit(self, target_unit): 32 | if target_unit == "C": 33 | return (self.temperature - 32) * 5 / 9 34 | return self.temperature * 9 / 5 + 32 35 | 36 | 37 | converter = TemperatureConverter(25, "C") 38 | print("Temp:", converter()) 39 | print("To F:", converter.convert()) 40 | converter.change_unit("F") 41 | print("New temp:", converter()) 42 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/triangle_area.py: -------------------------------------------------------------------------------- 1 | class TriangleArea: 2 | 3 | def __call__(self, a, b, c): 4 | p = (a + b + c) / 2 5 | return (p * (p - a) * (p - b) * (p - c))**0.5 6 | 7 | 8 | area = TriangleArea() 9 | print(area(3, 4, 5)) # Output: 6.0 10 | -------------------------------------------------------------------------------- /python-OOP/13-callable-instances/visualize_lines.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | 5 | class StraightLines: 6 | 7 | def __init__(self, m, c): 8 | self.slope = m 9 | self.y_intercept = c 10 | 11 | def __call__(self, x): 12 | return self.slope * x + self.y_intercept 13 | 14 | 15 | lines = [StraightLines(1, 0), StraightLines(0.5, 3), StraightLines(-1.4, 1.6)] 16 | 17 | X = np.linspace(-5, 5, 100) 18 | for idx, line in enumerate(lines): 19 | Y = np.vectorize(line)(X) 20 | plt.plot(X, Y, label=f"line{idx}") 21 | plt.title("Some straight lines") 22 | plt.xlabel("x") 23 | plt.ylabel("y") 24 | plt.legend() 25 | plt.grid() 26 | plt.show() 27 | -------------------------------------------------------------------------------- /python-OOP/14-slots-static-attributes/01_dynamic_attributes.py: -------------------------------------------------------------------------------- 1 | class A: 2 | pass 3 | 4 | 5 | a = A() 6 | a.x = 66 7 | a.y = "dynamically created attribute" 8 | 9 | print("Attributes in a:", a.__dict__) 10 | -------------------------------------------------------------------------------- /python-OOP/14-slots-static-attributes/02_builtin_no_dynamic.py: -------------------------------------------------------------------------------- 1 | x = 42 2 | try: 3 | x.a = "not possible" 4 | except AttributeError as e: 5 | print("Error when adding attribute to int:", e) 6 | 7 | lst = [34, 999, 1001] 8 | try: 9 | lst.a = "forget it" 10 | except AttributeError as e: 11 | print("Error when adding attribute to list:", e) 12 | -------------------------------------------------------------------------------- /python-OOP/14-slots-static-attributes/03_slots_usage.py: -------------------------------------------------------------------------------- 1 | class S: 2 | __slots__ = ["val"] 3 | 4 | def __init__(self, v): 5 | self.val = v 6 | 7 | 8 | x = S(42) 9 | print("x.val =", x.val) 10 | 11 | try: 12 | x.new = "not allowed" 13 | except AttributeError as e: 14 | print("Error when assigning new attribute to S:", e) 15 | -------------------------------------------------------------------------------- /python-OOP/14-slots-static-attributes/README.md: -------------------------------------------------------------------------------- 1 | # 14. Slots: Avoiding Dynamically Created Attributes 2 | 3 | This tutorial demonstrates how to use `__slots__` in Python to prevent dynamically adding attributes to objects, and to reduce memory usage for classes that are instantiated many times. 4 | 5 | ## Why Use `__slots__`? 6 | 7 | By default, Python uses a `__dict__` to store instance attributes, allowing dynamic attribute creation. However, this flexibility comes with a memory cost. When creating many instances, this cost becomes significant. 8 | 9 | Using `__slots__`, we define a static structure that: 10 | 11 | - Saves memory by preventing dynamic attribute creation. 12 | - Improves performance when creating large numbers of objects. 13 | 14 | ## Example: Without `__slots__` 15 | 16 | ```python 17 | class A: 18 | pass 19 | 20 | a = A() 21 | a.x = 66 22 | a.y = "dynamically created attribute" 23 | 24 | print(a.__dict__) # {'x': 66, 'y': 'dynamically created attribute'} 25 | ``` 26 | -------------------------------------------------------------------------------- /python-OOP/15-python-polynomial-class/README.md: -------------------------------------------------------------------------------- 1 | # 15. Polynomial Class in Python 2 | 3 | This tutorial explores how to implement a `Polynomial` class in Python that supports: 4 | 5 | - Initialization from coefficients 6 | - Evaluation as callable objects 7 | - Addition and subtraction of polynomials 8 | - Derivatives of polynomials 9 | - Plotting polynomial graphs using `matplotlib` 10 | -------------------------------------------------------------------------------- /python-OOP/15-python-polynomial-class/polynomial_arithmetic.py: -------------------------------------------------------------------------------- 1 | from itertools import zip_longest 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | 6 | 7 | class Polynomial: 8 | 9 | def __init__(self, *coefficients): 10 | self.coefficients = list(coefficients) 11 | 12 | def __call__(self, x): 13 | res = 0 14 | for coeff in self.coefficients: 15 | res = res * x + coeff 16 | return res 17 | 18 | def __add__(self, other): 19 | c1 = self.coefficients[::-1] 20 | c2 = other.coefficients[::-1] 21 | result = [sum(t) for t in zip_longest(c1, c2, fillvalue=0)] 22 | return Polynomial(*result[::-1]) 23 | 24 | def __sub__(self, other): 25 | c1 = self.coefficients[::-1] 26 | c2 = other.coefficients[::-1] 27 | result = [t1 - t2 for t1, t2 in zip_longest(c1, c2, fillvalue=0)] 28 | return Polynomial(*result[::-1]) 29 | 30 | 31 | p1 = Polynomial(4, 0, -4, 3, 0) 32 | p2 = Polynomial(-0.8, 2.3, 0.5, 1, 0.2) 33 | 34 | p_sum = p1 + p2 35 | p_diff = p1 - p2 36 | 37 | X = np.linspace(-3, 3, 100) 38 | plt.plot(X, p1(X), label="p1") 39 | plt.plot(X, p2(X), label="p2") 40 | plt.plot(X, p_sum(X), label="p1 + p2") 41 | plt.plot(X, p_diff(X), label="p1 - p2") 42 | plt.legend() 43 | plt.grid() 44 | plt.show() 45 | -------------------------------------------------------------------------------- /python-OOP/15-python-polynomial-class/polynomial_basic.py: -------------------------------------------------------------------------------- 1 | class Polynomial: 2 | 3 | def __init__(self, *coefficients): 4 | self.coefficients = list(coefficients) 5 | 6 | def __repr__(self): 7 | return "Polynomial" + str(tuple(self.coefficients)) 8 | 9 | 10 | p = Polynomial(1, 0, -4, 3, 0) 11 | print(p) 12 | 13 | p2 = eval(repr(p)) 14 | print(p2) 15 | -------------------------------------------------------------------------------- /python-OOP/15-python-polynomial-class/polynomial_callable.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | 5 | class Polynomial: 6 | 7 | def __init__(self, *coefficients): 8 | self.coefficients = list(coefficients) 9 | 10 | def __call__(self, x): 11 | res = 0 12 | for coeff in self.coefficients: 13 | res = res * x + coeff 14 | return res 15 | 16 | 17 | p = Polynomial(3, 0, -5, 2, 1) 18 | X = np.linspace(-1.5, 1.5, 50) 19 | F = p(X) 20 | plt.plot(X, F) 21 | plt.title("Polynomial Callable Example") 22 | plt.grid() 23 | plt.show() 24 | -------------------------------------------------------------------------------- /python-OOP/15-python-polynomial-class/polynomial_derivative.py: -------------------------------------------------------------------------------- 1 | from itertools import zip_longest 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | 6 | 7 | class Polynomial: 8 | 9 | def __init__(self, *coefficients): 10 | self.coefficients = list(coefficients) 11 | 12 | def __call__(self, x): 13 | res = 0 14 | for coeff in self.coefficients: 15 | res = res * x + coeff 16 | return res 17 | 18 | def derivative(self): 19 | n = len(self.coefficients) 20 | derived = [self.coefficients[i] * (n - i - 1) for i in range(n - 1)] 21 | return Polynomial(*derived) 22 | 23 | 24 | p = Polynomial(-0.8, 2.3, 0.5, 1, 0.2) 25 | p_der = p.derivative() 26 | 27 | X = np.linspace(-2, 3, 100) 28 | plt.plot(X, p(X), label="Polynomial") 29 | plt.plot(X, p_der(X), label="Derivative") 30 | plt.legend() 31 | plt.grid() 32 | plt.show() 33 | -------------------------------------------------------------------------------- /python-OOP/15-python-polynomial-class/polynomial_str_repr.py: -------------------------------------------------------------------------------- 1 | class Polynomial: 2 | 3 | def __init__(self, *coefficients): 4 | self.coefficients = list(coefficients) 5 | 6 | def __repr__(self): 7 | return "Polynomial" + str(tuple(self.coefficients)) 8 | 9 | def __str__(self): 10 | 11 | def x_expr(degree): 12 | return "" if degree == 0 else "x" if degree == 1 else f"x^{degree}" 13 | 14 | degree = len(self.coefficients) - 1 15 | res = "" 16 | for i, coeff in enumerate(self.coefficients): 17 | if coeff == 0: 18 | continue 19 | sign = "+" if coeff > 0 else "-" 20 | coeff_abs = abs(coeff) 21 | coeff_str = "" if coeff_abs == 1 and i != degree else str(coeff_abs) 22 | term = f"{sign}{coeff_str}{x_expr(degree - i)}" 23 | res += term 24 | 25 | return res.lstrip("+") 26 | 27 | 28 | polys = [ 29 | Polynomial(1, 0, -4, 3, 0), 30 | Polynomial(2, 0), 31 | Polynomial(4, 1, -1), 32 | Polynomial(3, 0, -5, 2, 7), 33 | Polynomial(-42), 34 | ] 35 | 36 | for i, poly in enumerate(polys): 37 | print(f"$p_{i} = {str(poly)}$") 38 | -------------------------------------------------------------------------------- /python-OOP/16-dynamic-class-creation-with-type/01_type_basics.py: -------------------------------------------------------------------------------- 1 | x = [4, 5, 9] 2 | y = "Hello" 3 | 4 | print("Type of x:", type(x)) 5 | print("Type of y:", type(y)) 6 | -------------------------------------------------------------------------------- /python-OOP/16-dynamic-class-creation-with-type/02_class_is_instance_of_type.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | pass 3 | 4 | 5 | print("Type of class:", type(MyClass)) # 6 | print("Type of instance:", type(MyClass())) # 7 | print("Is MyClass an instance of type?", isinstance(MyClass, type)) # True 8 | -------------------------------------------------------------------------------- /python-OOP/16-dynamic-class-creation-with-type/03_manual_class_creation.py: -------------------------------------------------------------------------------- 1 | # Creating class A dynamically 2 | A = type("A", (), {}) 3 | x = A() 4 | 5 | print("Type of x:", type(x)) # 6 | print("Class name:", x.__class__.__name__) 7 | -------------------------------------------------------------------------------- /python-OOP/16-dynamic-class-creation-with-type/04_robot_vs_robot2.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | counter = 0 3 | 4 | def __init__(self, name): 5 | self.name = name 6 | 7 | def sayHello(self): 8 | return "Hi, I am " + self.name 9 | 10 | 11 | def Rob_init(self, name): 12 | self.name = name 13 | 14 | 15 | Robot2 = type("Robot2", (), {"counter": 0, "__init__": Rob_init, "sayHello": lambda self: "Hi, I am " + self.name}) 16 | 17 | x = Robot2("Marvin") 18 | y = Robot("Marvin") 19 | 20 | print("Robot2:", x.name, "|", x.sayHello()) 21 | print("Robot :", y.name, "|", y.sayHello()) 22 | print("x.__dict__:", x.__dict__) 23 | print("y.__dict__:", y.__dict__) 24 | -------------------------------------------------------------------------------- /python-OOP/16-dynamic-class-creation-with-type/README.md: -------------------------------------------------------------------------------- 1 | # 16. Dynamically Creating Classes with `type` 2 | 3 | In this lesson, we explore how Python dynamically creates classes using the built-in `type()` function and how class definitions work behind the scenes. 4 | 5 | ## Overview 6 | 7 | In Python, **everything is an object**, including classes themselves. This tutorial provides a deeper insight into how class creation works internally using the `type` constructor, and how it relates to metaprogramming. 8 | 9 | Key Concepts Covered: 10 | 11 | - `type(obj)` returns the class of an object. 12 | - `type(class)` returns `type` because classes themselves are instances of `type`. 13 | - Classes can be created dynamically by calling `type()` with 3 arguments. 14 | - The usual `class` keyword is syntactic sugar for a call to `type`. 15 | 16 | ## Learning Goals 17 | 18 | - Understand how class and `type` are related. 19 | - Create classes dynamically using `type(name, bases, dict)`. 20 | - See how Python processes class definitions internally. 21 | - Compare traditional and dynamic class definitions. 22 | 23 | ## File Guide 24 | 25 | ### `01_type_basics.py` 26 | 27 | Demonstrates how `type()` behaves on objects and classes. 28 | 29 | ### `02_class_is_instance_of_type.py` 30 | 31 | Reveals how user-defined classes are instances of `type`. 32 | 33 | ### `03_manual_class_creation.py` 34 | 35 | Shows how to use `type()` to create a class dynamically. 36 | 37 | ### `04_robot_vs_robot2.py` 38 | 39 | Compares a traditional `Robot` class with a dynamically generated `Robot2` class using `type`. 40 | -------------------------------------------------------------------------------- /python-OOP/17-road-to-metaclasses/01_manual_inheritance.py: -------------------------------------------------------------------------------- 1 | class Answers: 2 | 3 | def the_answer(self, *args): 4 | return 42 5 | 6 | 7 | class Philosopher1(Answers): 8 | pass 9 | 10 | 11 | class Philosopher2(Answers): 12 | pass 13 | 14 | 15 | plato = Philosopher1() 16 | aristotle = Philosopher2() 17 | 18 | print(plato.the_answer()) # 42 19 | print(aristotle.the_answer()) # 42 20 | -------------------------------------------------------------------------------- /python-OOP/17-road-to-metaclasses/02_runtime_injection.py: -------------------------------------------------------------------------------- 1 | reply = input("Do you need the answer? (y/n): ") 2 | required = reply.lower() in ("y", "yes") 3 | 4 | 5 | def the_answer(self, *args): 6 | return 42 7 | 8 | 9 | class Philosopher1: 10 | pass 11 | 12 | 13 | class Philosopher2: 14 | pass 15 | 16 | 17 | class Philosopher3: 18 | pass 19 | 20 | 21 | if required: 22 | Philosopher1.the_answer = the_answer 23 | Philosopher2.the_answer = the_answer 24 | Philosopher3.the_answer = the_answer 25 | 26 | plato = Philosopher1() 27 | aristotle = Philosopher2() 28 | 29 | if required: 30 | print(plato.the_answer()) 31 | print(aristotle.the_answer()) 32 | else: 33 | print("The silence of the philosophers") 34 | -------------------------------------------------------------------------------- /python-OOP/17-road-to-metaclasses/03_manager_function.py: -------------------------------------------------------------------------------- 1 | reply = input("Do you need the answer? (y/n): ") 2 | required = reply.lower() in ("y", "yes") 3 | 4 | 5 | def the_answer(self, *args): 6 | return 42 7 | 8 | 9 | def augment_answer(cls): 10 | if required: 11 | cls.the_answer = the_answer 12 | return cls 13 | 14 | 15 | class Philosopher1: 16 | pass 17 | 18 | 19 | augment_answer(Philosopher1) 20 | 21 | 22 | class Philosopher2: 23 | pass 24 | 25 | 26 | augment_answer(Philosopher2) 27 | 28 | plato = Philosopher1() 29 | aristotle = Philosopher2() 30 | 31 | if required: 32 | print(plato.the_answer()) 33 | print(aristotle.the_answer()) 34 | else: 35 | print("The silence of the philosophers") 36 | -------------------------------------------------------------------------------- /python-OOP/17-road-to-metaclasses/04_class_decorator.py: -------------------------------------------------------------------------------- 1 | reply = input("Do you need the answer? (y/n): ") 2 | required = reply.lower() in ("y", "yes") 3 | 4 | 5 | def the_answer(self, *args): 6 | return 42 7 | 8 | 9 | def augment_answer(cls): 10 | if required: 11 | cls.the_answer = the_answer 12 | return cls 13 | 14 | 15 | @augment_answer 16 | class Philosopher1: 17 | pass 18 | 19 | 20 | @augment_answer 21 | class Philosopher2: 22 | pass 23 | 24 | 25 | plato = Philosopher1() 26 | aristotle = Philosopher2() 27 | 28 | if required: 29 | print(plato.the_answer()) 30 | print(aristotle.the_answer()) 31 | else: 32 | print("The silence of the philosophers") 33 | -------------------------------------------------------------------------------- /python-OOP/17-road-to-metaclasses/05_realistic_class_decorator.py: -------------------------------------------------------------------------------- 1 | def add_class_variable(variable_name, value): 2 | 3 | def decorator(cls): 4 | setattr(cls, variable_name, value) 5 | return cls 6 | 7 | return decorator 8 | 9 | 10 | @add_class_variable("city", "Erlangen") 11 | class MyClass: 12 | 13 | def __init__(self, value): 14 | self.value = value 15 | 16 | 17 | obj = MyClass(10) 18 | print(obj.city) # Output: Erlangen 19 | -------------------------------------------------------------------------------- /python-OOP/17-road-to-metaclasses/README.md: -------------------------------------------------------------------------------- 1 | # 17. Road to Metaclasses 2 | 3 | This tutorial explores the concepts and practices that pave the way to understanding Python metaclasses. While not directly implementing metaclasses, we build a foundation by exploring **class decoration**, **runtime augmentation**, and **dynamic behavior injection** into classes. 4 | 5 | ## Contents 6 | 7 | ### 1. Manual Inheritance 8 | 9 | Demonstrates a naive implementation of duplicated logic across multiple classes and how inheritance solves it using a base class. 10 | 11 | ### 2. Runtime Method Injection 12 | 13 | Introduces the concept of dynamically injecting a method into classes based on a runtime condition. 14 | 15 | ### 3. Manager Function 16 | 17 | Encapsulates augmentation logic inside a function to reduce repetition and error-prone logic. 18 | 19 | ### 4. Class Decorator 20 | 21 | Demonstrates how decorators can cleanly and automatically apply changes to class definitions at runtime. 22 | 23 | ### 5. Realistic Class Decorator 24 | 25 | Shows how to create parameterized class decorators that dynamically add class-level attributes. 26 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/01_little_meta.py: -------------------------------------------------------------------------------- 1 | class LittleMeta(type): 2 | 3 | def __new__(cls, clsname, superclasses, attributedict): 4 | print("clsname:", clsname) 5 | print("superclasses:", superclasses) 6 | print("attributedict:", attributedict) 7 | return type.__new__(cls, clsname, superclasses, attributedict) 8 | 9 | 10 | class Foo: 11 | pass 12 | 13 | 14 | class A(Foo, metaclass=LittleMeta): 15 | pass 16 | 17 | 18 | a = A() 19 | print(a) 20 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/02_essential_answers.py: -------------------------------------------------------------------------------- 1 | x = input("Do you need the answer? (y/n): ").strip().lower() 2 | required = x in ("y", "yes") 3 | 4 | 5 | def the_answer(self, *args): 6 | return 42 7 | 8 | 9 | class EssentialAnswers(type): 10 | 11 | def __init__(cls, clsname, superclasses, attributedict): 12 | if required: 13 | cls.the_answer = the_answer 14 | 15 | 16 | class Philosopher1(metaclass=EssentialAnswers): 17 | pass 18 | 19 | 20 | kant = Philosopher1() 21 | try: 22 | print(kant.the_answer()) 23 | except AttributeError: 24 | print("The method the_answer is not implemented") 25 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/03_singleton_metaclass.py: -------------------------------------------------------------------------------- 1 | class Singleton(type): 2 | _instances = {} 3 | 4 | def __call__(cls, *args, **kwargs): 5 | if cls not in cls._instances: 6 | cls._instances[cls] = super().__call__(*args, **kwargs) 7 | return cls._instances[cls] 8 | 9 | 10 | class SingletonClass(metaclass=Singleton): 11 | pass 12 | 13 | 14 | class RegularClass: 15 | pass 16 | 17 | 18 | x = SingletonClass() 19 | y = SingletonClass() 20 | print(x == y) # True 21 | 22 | x = RegularClass() 23 | y = RegularClass() 24 | print(x == y) # False 25 | 26 | print("All Singleton Instances:", Singleton._instances) 27 | print("SingletonClass Instances:", SingletonClass._instances) 28 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/04_singleton_inheritance.py: -------------------------------------------------------------------------------- 1 | class Singleton: 2 | _instance = None 3 | 4 | def __new__(cls, *args, **kwargs): 5 | if not cls._instance: 6 | cls._instance = super().__new__(cls, *args, **kwargs) 7 | return cls._instance 8 | 9 | 10 | class SingletonClass(Singleton): 11 | pass 12 | 13 | 14 | class RegularClass: 15 | pass 16 | 17 | 18 | x = SingletonClass() 19 | y = SingletonClass() 20 | print(x == y) # True 21 | 22 | x = RegularClass() 23 | y = RegularClass() 24 | print(x == y) # False 25 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/05_singleton_decorator.py: -------------------------------------------------------------------------------- 1 | def singleton(cls): 2 | instances = {} 3 | 4 | def get_instance(*args, **kwargs): 5 | if cls not in instances: 6 | instances[cls] = cls(*args, **kwargs) 7 | return instances[cls] 8 | 9 | return get_instance 10 | 11 | 12 | @singleton 13 | class SingletonClass: 14 | 15 | def __init__(self, data): 16 | self.data = data 17 | 18 | 19 | singleton_instance_1 = SingletonClass("Instance 1") 20 | singleton_instance_2 = SingletonClass("Instance 2") 21 | 22 | print(singleton_instance_1 is singleton_instance_2) # True 23 | print(singleton_instance_1.data) # Instance 1 24 | print(singleton_instance_2.data) # Instance 1 25 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/06_camelcase_metaclass.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class CamelCaseToUnderscoreMeta(type): 5 | 6 | def __new__(cls, name, bases, dct): 7 | modified_items = {} 8 | for key, value in dct.items(): 9 | if callable(value) and re.match(r"^[a-z]+(?:[A-Z][a-z]*)*$", key): 10 | underscore_name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", key).lower() 11 | modified_items[underscore_name] = value 12 | else: 13 | modified_items[key] = value 14 | return super().__new__(cls, name, bases, modified_items) 15 | 16 | 17 | class CamelCaseClass(metaclass=CamelCaseToUnderscoreMeta): 18 | 19 | def processData(self): 20 | print("Processing data...") 21 | 22 | def transformData(self): 23 | print("Transforming data...") 24 | 25 | def processOutputData(self): 26 | print("Processing output data...") 27 | 28 | 29 | camel_case_instance = CamelCaseClass() 30 | camel_case_instance.process_data() 31 | camel_case_instance.transform_data() 32 | camel_case_instance.process_output_data() 33 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/07_camelcase_decorator.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | # Class decorator to convert camelCase method names to snake_case 5 | def camel_case_to_underscore(cls): 6 | modified_items = {} 7 | 8 | for key, value in cls.__dict__.items(): 9 | if callable(value) and re.match(r"^[a-z]+(?:[A-Z][a-z]*)+$", key): 10 | # Convert camelCase to snake_case 11 | underscore_name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", key).lower() 12 | modified_items[underscore_name] = value 13 | else: 14 | modified_items[key] = value 15 | 16 | return type(cls.__name__, cls.__bases__, modified_items) 17 | 18 | 19 | @camel_case_to_underscore 20 | class CamelCaseClass: 21 | 22 | def processData(self): 23 | print("Processing data...") 24 | 25 | def transformData(self): 26 | print("Transforming data...") 27 | 28 | def processOutputData(self): 29 | print("Processing output data...") 30 | 31 | 32 | # Creating an instance and calling snake_case methods 33 | camel_case_instance = CamelCaseClass() 34 | camel_case_instance.process_data() 35 | camel_case_instance.transform_data() 36 | camel_case_instance.process_output_data() 37 | -------------------------------------------------------------------------------- /python-OOP/18-metaclasses/README.md: -------------------------------------------------------------------------------- 1 | # 18. Metaclasses in Python 2 | 3 | ## Introduction 4 | 5 | A **metaclass** is a class whose instances are classes. It allows you to control the behavior of classes just as classes control the behavior of instances. Python supports metaclasses through the `type` metaclass, and also allows you to define custom ones. 6 | 7 | ## Why Use Metaclasses? 8 | 9 | Some practical uses of metaclasses include: 10 | 11 | - Logging and profiling 12 | - Interface checking 13 | - Auto-registering classes 14 | - Injecting methods/properties 15 | - Enforcing naming conventions 16 | - Singleton pattern 17 | - Auto-synchronization and more 18 | 19 | ## Hierarchy Recap 20 | 21 | - Instances are created from **Classes** 22 | - Classes are created from **Metaclasses** 23 | 24 | ## Key Concepts 25 | 26 | - The default metaclass in Python is `type` 27 | - You can override `__new__` and/or `__init__` in a metaclass to modify class creation 28 | - You use `metaclass=YourMetaClass` in a class definition to hook it to a custom metaclass 29 | 30 | ## Example Files 31 | 32 | | File | Description | 33 | | ----------------------------- | --------------------------------------------------------------- | 34 | | `01_little_meta.py` | Shows how to hook into class creation using a minimal metaclass | 35 | | `02_essential_answers.py` | Injects method into classes conditionally using a metaclass | 36 | | `03_singleton_metaclass.py` | Implements Singleton pattern using a metaclass | 37 | | `04_singleton_inheritance.py` | Singleton pattern without metaclasses using inheritance | 38 | | `05_singleton_decorator.py` | Singleton pattern using a class decorator | 39 | | `06_camelcase_metaclass.py` | Converts CamelCase methods to snake_case using metaclass | 40 | | `07_camelcase_decorator.py` | Same behavior as above, but using a decorator | 41 | 42 | Run each script to see the metaclass magic in action! 43 | -------------------------------------------------------------------------------- /python-OOP/19-count-function-calls-with-metaclass/README.md: -------------------------------------------------------------------------------- 1 | # 19. Count Function Calls with a Metaclass 2 | 3 | This tutorial demonstrates how to use a **Python metaclass** to automatically decorate class methods so that the number of times each method is called is counted. 4 | 5 | ## What You'll Learn 6 | 7 | - What a metaclass is and how it works. 8 | - How to use metaclasses to modify class behavior. 9 | - How to apply a decorator to all methods in a class. 10 | - A simple profiling use case: method call counting. 11 | 12 | ## Files 13 | 14 | | File | Description | 15 | | --------------------------- | ------------------------------------------------------------------------- | 16 | | `call_counter_decorator.py` | A standalone version of the call counter decorator. | 17 | | `metaclass_counter.py` | Defines a metaclass that decorates class methods to count function calls. | 18 | | `test_metaclass_counter.py` | Example usage and test of the metaclass-based function call counter. | 19 | 20 | ## Run 21 | 22 | ```bash 23 | python test_metaclass_counter.py 24 | ``` 25 | -------------------------------------------------------------------------------- /python-OOP/19-count-function-calls-with-metaclass/call_counter_decorator.py: -------------------------------------------------------------------------------- 1 | def call_counter(func): 2 | 3 | def helper(*args, **kwargs): 4 | helper.calls += 1 5 | return func(*args, **kwargs) 6 | 7 | helper.calls = 0 8 | helper.__name__ = func.__name__ 9 | return helper 10 | 11 | 12 | # Manual usage example 13 | if __name__ == "__main__": 14 | 15 | def greet(): 16 | print("Hello") 17 | 18 | greet = call_counter(greet) 19 | print(greet.calls) # Output: 0 20 | greet() 21 | greet() 22 | print(greet.calls) # Output: 2 23 | -------------------------------------------------------------------------------- /python-OOP/19-count-function-calls-with-metaclass/metaclass_counter.py: -------------------------------------------------------------------------------- 1 | class FuncCallCounter(type): 2 | """A Metaclass which decorates all methods of the 3 | subclass using call_counter as the decorator. 4 | """ 5 | 6 | @staticmethod 7 | def call_counter(func): 8 | 9 | def helper(*args, **kwargs): 10 | helper.calls += 1 11 | return func(*args, **kwargs) 12 | 13 | helper.calls = 0 14 | helper.__name__ = func.__name__ 15 | return helper 16 | 17 | def __new__(cls, clsname, superclasses, attributedict): 18 | for attr in attributedict: 19 | if callable(attributedict[attr]) and not attr.startswith("__"): 20 | attributedict[attr] = cls.call_counter(attributedict[attr]) 21 | return super().__new__(cls, clsname, superclasses, attributedict) 22 | -------------------------------------------------------------------------------- /python-OOP/19-count-function-calls-with-metaclass/test_metaclass_counter.py: -------------------------------------------------------------------------------- 1 | from metaclass_counter import FuncCallCounter 2 | 3 | 4 | class A(metaclass=FuncCallCounter): 5 | 6 | def foo(self): 7 | pass 8 | 9 | def bar(self): 10 | pass 11 | 12 | 13 | if __name__ == "__main__": 14 | a = A() 15 | print(a.foo.calls, a.bar.calls) # 0 0 16 | a.foo() 17 | print(a.foo.calls, a.bar.calls) # 1 0 18 | a.foo() 19 | a.bar() 20 | print(a.foo.calls, a.bar.calls) # 2 1 21 | -------------------------------------------------------------------------------- /python-OOP/20-abstract-base-classes/01_non_abstract_class.py: -------------------------------------------------------------------------------- 1 | # This is NOT an abstract base class. It doesn't enforce anything. 2 | 3 | 4 | class AbstractClass: 5 | 6 | def do_something(self): 7 | pass 8 | 9 | 10 | class B(AbstractClass): 11 | pass 12 | 13 | 14 | a = AbstractClass() # Works 15 | b = B() # Also works 16 | print("Instance created without enforcing method implementation.") 17 | -------------------------------------------------------------------------------- /python-OOP/20-abstract-base-classes/02_abstract_class_error.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from abc import abstractmethod 3 | 4 | 5 | class AbstractClassExample(ABC): 6 | 7 | def __init__(self, value): 8 | self.value = value 9 | 10 | @abstractmethod 11 | def do_something(self): 12 | pass 13 | 14 | 15 | class DoAdd42(AbstractClassExample): 16 | pass 17 | 18 | 19 | # This will raise TypeError: Can't instantiate abstract class 20 | x = DoAdd42(4) 21 | -------------------------------------------------------------------------------- /python-OOP/20-abstract-base-classes/03_abstract_class_correct.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from abc import abstractmethod 3 | 4 | 5 | class AbstractClassExample(ABC): 6 | 7 | def __init__(self, value): 8 | self.value = value 9 | 10 | @abstractmethod 11 | def do_something(self): 12 | pass 13 | 14 | 15 | class DoAdd42(AbstractClassExample): 16 | 17 | def do_something(self): 18 | return self.value + 42 19 | 20 | 21 | class DoMul42(AbstractClassExample): 22 | 23 | def do_something(self): 24 | return self.value * 42 25 | 26 | 27 | x = DoAdd42(10) 28 | y = DoMul42(10) 29 | 30 | print(x.do_something()) # 52 31 | print(y.do_something()) # 420 32 | -------------------------------------------------------------------------------- /python-OOP/20-abstract-base-classes/04_abstract_with_base_impl.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from abc import abstractmethod 3 | 4 | 5 | class AbstractClassExample(ABC): 6 | 7 | @abstractmethod 8 | def do_something(self): 9 | print("Some implementation!") 10 | 11 | 12 | class AnotherSubclass(AbstractClassExample): 13 | 14 | def do_something(self): 15 | super().do_something() 16 | print("The enrichment from AnotherSubclass") 17 | 18 | 19 | x = AnotherSubclass() 20 | x.do_something() 21 | 22 | # Output: 23 | # Some implementation! 24 | # The enrichment from AnotherSubclass 25 | -------------------------------------------------------------------------------- /python-OOP/20-abstract-base-classes/README.md: -------------------------------------------------------------------------------- 1 | # 20. The 'ABC' of Abstract Base Classes 2 | 3 | This tutorial teaches how to use **Abstract Base Classes (ABCs)** in Python using the `abc` module. Abstract classes help enforce method definitions across subclasses and are useful in interface design and consistent APIs. 4 | 5 | ## What You'll Learn 6 | 7 | - What abstract classes are and why they're useful. 8 | - How to use the `abc` module and `@abstractmethod` decorator. 9 | - How Python prevents instantiation of abstract classes. 10 | - How abstract methods can still have base implementations. 11 | 12 | ## File Overview 13 | 14 | | File | Description | 15 | | ------------------------------- | ----------------------------------------------------------------------- | 16 | | `01_non_abstract_class.py` | Demonstrates a normal base class with no enforcement. | 17 | | `02_abstract_class_error.py` | Shows how instantiation fails when abstract methods aren't implemented. | 18 | | `03_abstract_class_correct.py` | Shows correct implementation of all abstract methods. | 19 | | `04_abstract_with_base_impl.py` | Demonstrates base implementation using `super()`. | 20 | 21 | ## Run Examples 22 | 23 | ```bash 24 | python 01_non_abstract_class.py 25 | python 02_abstract_class_error.py 26 | python 03_abstract_class_correct.py 27 | python 04_abstract_with_base_impl.py 28 | ``` 29 | -------------------------------------------------------------------------------- /python-OOP/21-oop-purely-functional/README.md: -------------------------------------------------------------------------------- 1 | # 21. OOP Purely Functional 2 | 3 | This chapter bridges Object-Oriented Programming (OOP) and Functional Programming (FP) by demonstrating how functional techniques can emulate OOP behavior such as encapsulation, state management, and interface methods. 4 | 5 | ## Concepts Covered 6 | 7 | - Encapsulation using closures 8 | - Local state via `nonlocal` variables 9 | - Getter and Setter design patterns in functional style 10 | - Comparison between functional and class-based OOP 11 | 12 | ## Functional Robot 13 | 14 | The `robot_functional.py` file defines a `Robot` function that uses closures to mimic private variables and methods in OOP. 15 | 16 | ## Class-Based Robot 17 | 18 | The `robot_class.py` file shows a traditional class-based approach for comparison. 19 | 20 | ## Comparison 21 | 22 | Both styles provide: 23 | 24 | - Controlled access to internal state 25 | - Independent instances with encapsulated data 26 | - Method-like interfaces 27 | 28 | ## Run Examples 29 | 30 | ```bash 31 | python robot_functional.py 32 | python robot_class.py 33 | ``` 34 | -------------------------------------------------------------------------------- /python-OOP/21-oop-purely-functional/robot_class.py: -------------------------------------------------------------------------------- 1 | class Robot: 2 | 3 | def __init__(self, name, city): 4 | self._name = name 5 | self._city = city 6 | 7 | def get_name(self): 8 | return self._name 9 | 10 | def set_name(self, new_name): 11 | self._name = new_name 12 | 13 | def get_city(self): 14 | return self._city 15 | 16 | def set_city(self, new_city): 17 | self._city = new_city 18 | 19 | 20 | # Create robot objects 21 | marvin = Robot("Marvin", "New York") 22 | r2d2 = Robot("R2D2", "Tatooine") 23 | 24 | print(marvin.get_name(), marvin.get_city()) 25 | marvin.set_name("Marvin 2.0") 26 | print(marvin.get_name(), marvin.get_city()) 27 | 28 | print(r2d2.get_name(), r2d2.get_city()) 29 | r2d2.set_city("Naboo") 30 | print(r2d2.get_name(), r2d2.get_city()) 31 | -------------------------------------------------------------------------------- /python-OOP/21-oop-purely-functional/robot_functional.py: -------------------------------------------------------------------------------- 1 | def Robot(name, city): 2 | 3 | def self(): 4 | return None 5 | 6 | def get_name(): 7 | return name 8 | 9 | def set_name(new_name): 10 | nonlocal name 11 | name = new_name 12 | 13 | def get_city(): 14 | return city 15 | 16 | def set_city(new_city): 17 | nonlocal city 18 | city = new_city 19 | 20 | self.get_name = get_name 21 | self.set_name = set_name 22 | self.get_city = get_city 23 | self.set_city = set_city 24 | 25 | return self 26 | 27 | 28 | # Create robot objects 29 | marvin = Robot("Marvin", "New York") 30 | r2d2 = Robot("R2D2", "Tatooine") 31 | 32 | print(marvin.get_name(), marvin.get_city()) 33 | marvin.set_name("Marvin 2.0") 34 | print(marvin.get_name(), marvin.get_city()) 35 | 36 | print(r2d2.get_name(), r2d2.get_city()) 37 | r2d2.set_city("Naboo") 38 | print(r2d2.get_name(), r2d2.get_city()) 39 | -------------------------------------------------------------------------------- /python-advanced/01-collections/README.md: -------------------------------------------------------------------------------- 1 | # Python `collections` Module 2 | 3 | The `collections` module provides high-performance container datatypes as alternatives to Python’s general-purpose built-in containers. 4 | 5 | ## Key Tools Covered 6 | 7 | - `Counter` – Counting elements and frequencies 8 | - `namedtuple` – Lightweight, immutable, named object tuples 9 | - `OrderedDict` – Dictionary that preserves insertion order 10 | - `defaultdict` – Dictionary with default factory values 11 | - `deque` – Double-ended queue with fast appends and pops 12 | 13 | ## Files 14 | 15 | - `counter_example.py` 16 | - `namedtuple_example.py` 17 | - `ordereddict_example.py` 18 | - `defaultdict_example.py` 19 | - `deque_example.py` 20 | 21 | ## Run Examples 22 | 23 | ```bash 24 | python counter_example.py 25 | ``` 26 | -------------------------------------------------------------------------------- /python-advanced/01-collections/counter_example.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | a = "aaaaabbbbcccdde" 4 | my_counter = Counter(a) 5 | print(my_counter) 6 | print(my_counter.items()) 7 | print(my_counter.keys()) 8 | print(my_counter.values()) 9 | 10 | my_list = [0, 1, 0, 1, 2, 1, 1, 3, 2, 3, 2, 4] 11 | my_counter = Counter(my_list) 12 | print(my_counter) 13 | 14 | print("Most common:", my_counter.most_common(1)) 15 | print("All elements:", list(my_counter.elements())) 16 | -------------------------------------------------------------------------------- /python-advanced/01-collections/defaultdict_example.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | d = defaultdict(int) 4 | d["yellow"] = 1 5 | d["blue"] = 2 6 | print(d.items()) 7 | print("Missing key returns:", d["green"]) 8 | 9 | d = defaultdict(list) 10 | s = [("yellow", 1), ("blue", 2), ("yellow", 3), ("blue", 4), ("red", 5)] 11 | for k, v in s: 12 | d[k].append(v) 13 | 14 | print(d.items()) 15 | print("Accessing missing list:", d["green"]) 16 | -------------------------------------------------------------------------------- /python-advanced/01-collections/deque_example.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | d = deque() 4 | d.append("a") 5 | d.append("b") 6 | print(d) 7 | 8 | d.appendleft("c") 9 | print(d) 10 | 11 | print("Popped:", d.pop()) 12 | print("Popleft:", d.popleft()) 13 | print("After pops:", d) 14 | 15 | d.clear() 16 | print("Cleared:", d) 17 | 18 | d = deque(["a", "b", "c", "d"]) 19 | d.extend(["e", "f", "g"]) 20 | d.extendleft(["h", "i", "j"]) 21 | print("Extended both ends:", d) 22 | 23 | print("Count 'h':", d.count("h")) 24 | 25 | d.rotate(1) 26 | print("Rotate right:", d) 27 | 28 | d.rotate(-2) 29 | print("Rotate left:", d) 30 | -------------------------------------------------------------------------------- /python-advanced/01-collections/namedtuple_example.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Point = namedtuple("Point", "x, y") 4 | pt = Point(1, -4) 5 | print(pt) 6 | print(pt._fields) 7 | print(type(pt)) 8 | print(pt.x, pt.y) 9 | 10 | Person = namedtuple("Person", "name, age") 11 | friend = Person(name="Tom", age=25) 12 | print(friend.name, friend.age) 13 | -------------------------------------------------------------------------------- /python-advanced/01-collections/ordereddict_example.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | ordinary_dict = {} 4 | ordinary_dict["a"] = 1 5 | ordinary_dict["b"] = 2 6 | ordinary_dict["c"] = 3 7 | ordinary_dict["d"] = 4 8 | ordinary_dict["e"] = 5 9 | print("Ordinary dict:", ordinary_dict) 10 | 11 | ordered_dict = OrderedDict() 12 | ordered_dict["a"] = 1 13 | ordered_dict["b"] = 2 14 | ordered_dict["c"] = 3 15 | ordered_dict["d"] = 4 16 | ordered_dict["e"] = 5 17 | print("OrderedDict:", ordered_dict) 18 | 19 | for k, v in ordered_dict.items(): 20 | print(k, v) 21 | -------------------------------------------------------------------------------- /python-advanced/02-itertools/README.md: -------------------------------------------------------------------------------- 1 | # Python `itertools` Module 2 | 3 | `itertools` is a standard library module that provides fast, memory-efficient tools for working with iterators. 4 | 5 | Official docs: https://docs.python.org/3/library/itertools.html 6 | 7 | ## Files 8 | 9 | - `product_example.py` – Cartesian products 10 | - `permutations_example.py` – All possible orderings 11 | - `combinations_example.py` – Subsets of elements 12 | - `accumulate_example.py` – Running totals or operations 13 | - `groupby_example.py` – Grouping elements by a key 14 | - `infinite_iterators.py` – Infinite patterns (e.g., `count`, `cycle`, `repeat`) 15 | 16 | ## Run Examples 17 | 18 | ```bash 19 | python product_example.py 20 | ``` 21 | -------------------------------------------------------------------------------- /python-advanced/02-itertools/accumulate_example.py: -------------------------------------------------------------------------------- 1 | from itertools import accumulate 2 | import operator 3 | 4 | print(list(accumulate([1, 2, 3, 4]))) 5 | 6 | print(list(accumulate([1, 2, 3, 4], func=operator.mul))) 7 | 8 | print(list(accumulate([1, 5, 2, 6, 3, 4], func=max))) 9 | -------------------------------------------------------------------------------- /python-advanced/02-itertools/combinations_example.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations 2 | from itertools import combinations_with_replacement 3 | 4 | comb = combinations([1, 2, 3, 4], 2) 5 | print(list(comb)) 6 | 7 | comb_wr = combinations_with_replacement([1, 2, 3, 4], 2) 8 | print(list(comb_wr)) 9 | -------------------------------------------------------------------------------- /python-advanced/02-itertools/groupby_example.py: -------------------------------------------------------------------------------- 1 | from itertools import groupby 2 | 3 | 4 | def smaller_than_3(x): 5 | return x < 3 6 | 7 | 8 | group_obj = groupby([1, 2, 3, 4], key=smaller_than_3) 9 | for key, group in group_obj: 10 | print(key, list(group)) 11 | 12 | group_obj = groupby(["hi", "nice", "hello", "cool"], key=lambda x: "i" in x) 13 | for key, group in group_obj: 14 | print(key, list(group)) 15 | 16 | persons = [ 17 | { 18 | "name": "Tim", 19 | "age": 25 20 | }, 21 | { 22 | "name": "Dan", 23 | "age": 25 24 | }, 25 | { 26 | "name": "Lisa", 27 | "age": 27 28 | }, 29 | { 30 | "name": "Claire", 31 | "age": 28 32 | }, 33 | ] 34 | 35 | for key, group in groupby(persons, key=lambda x: x["age"]): 36 | print(key, list(group)) 37 | -------------------------------------------------------------------------------- /python-advanced/02-itertools/infinite_iterators.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | from itertools import cycle 3 | from itertools import repeat 4 | 5 | # count 6 | for i in count(10): 7 | print(i) 8 | if i >= 13: 9 | break 10 | 11 | print("") 12 | 13 | # cycle 14 | sum = 0 15 | for i in cycle([1, 2, 3]): 16 | print(i) 17 | sum += i 18 | if sum >= 12: 19 | break 20 | 21 | print("") 22 | 23 | # repeat 24 | for i in repeat("A", 3): 25 | print(i) 26 | -------------------------------------------------------------------------------- /python-advanced/02-itertools/permutations_example.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations 2 | 3 | perm = permutations([1, 2, 3]) 4 | print(list(perm)) 5 | 6 | perm = permutations([1, 2, 3], 2) 7 | print(list(perm)) 8 | -------------------------------------------------------------------------------- /python-advanced/02-itertools/product_example.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | 3 | # Cartesian product 4 | prod = product([1, 2], [3, 4]) 5 | print(list(prod)) 6 | 7 | # Repeating product 8 | prod = product([1, 2], [3], repeat=2) 9 | print(list(prod)) 10 | -------------------------------------------------------------------------------- /python-advanced/03-lambda/README.md: -------------------------------------------------------------------------------- 1 | # ⚡ Python Lambda Functions 2 | 3 | Lambda functions are small anonymous functions defined using the `lambda` keyword. They are most often used as arguments to higher-order functions such as `map()`, `filter()`, and `reduce()`. 4 | 5 | ## Files 6 | 7 | - `lambda_basics.py` – Syntax and basic usage 8 | - `lambda_higher_order.py` – Lambda inside other functions 9 | - `lambda_sorting.py` – Custom sorting using lambda as `key` 10 | - `lambda_map.py` – Transform elements with `map()` 11 | - `lambda_filter.py` – Filter elements with `filter()` 12 | - `lambda_reduce.py` – Reduce elements to a single result with `reduce()` 13 | 14 | ## Run Examples 15 | 16 | ```bash 17 | python lambda_basics.py 18 | ``` 19 | -------------------------------------------------------------------------------- /python-advanced/03-lambda/lambda_basics.py: -------------------------------------------------------------------------------- 1 | # Lambda that adds 10 2 | f = lambda x: x + 10 3 | print(f(5), f(100)) 4 | 5 | # Lambda that multiplies two numbers 6 | f = lambda x, y: x * y 7 | print(f(2, 10), f(7, 5)) 8 | -------------------------------------------------------------------------------- /python-advanced/03-lambda/lambda_filter.py: -------------------------------------------------------------------------------- 1 | a = [1, 2, 3, 4, 5, 6, 7, 8] 2 | b = list(filter(lambda x: x % 2 == 0, a)) 3 | c = [x for x in a if x % 2 == 0] 4 | 5 | print("Filter result:", b) 6 | print("List comp result:", c) 7 | -------------------------------------------------------------------------------- /python-advanced/03-lambda/lambda_higher_order.py: -------------------------------------------------------------------------------- 1 | def myfunc(n): 2 | return lambda x: x * n 3 | 4 | 5 | doubler = myfunc(2) 6 | tripler = myfunc(3) 7 | 8 | print(doubler(6)) 9 | print(tripler(6)) 10 | -------------------------------------------------------------------------------- /python-advanced/03-lambda/lambda_map.py: -------------------------------------------------------------------------------- 1 | a = [1, 2, 3, 4, 5, 6] 2 | b = list(map(lambda x: x * 2, a)) 3 | c = [x * 2 for x in a] 4 | 5 | print("Map result:", b) 6 | print("List comp result:", c) 7 | -------------------------------------------------------------------------------- /python-advanced/03-lambda/lambda_reduce.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | a = [1, 2, 3, 4] 4 | 5 | product_a = reduce(lambda x, y: x * y, a) 6 | sum_a = reduce(lambda x, y: x + y, a) 7 | 8 | print("Product:", product_a) 9 | print("Sum:", sum_a) 10 | -------------------------------------------------------------------------------- /python-advanced/03-lambda/lambda_sorting.py: -------------------------------------------------------------------------------- 1 | points2D = [(1, 9), (4, 1), (5, -3), (10, 2)] 2 | sorted_by_y = sorted(points2D, key=lambda x: x[1]) 3 | print("Sorted by Y:", sorted_by_y) 4 | 5 | mylist = [-1, -4, -2, -3, 1, 2, 3, 4] 6 | sorted_by_abs = sorted(mylist, key=lambda x: abs(x)) 7 | print("Sorted by abs:", sorted_by_abs) 8 | -------------------------------------------------------------------------------- /python-advanced/04-exception/README.md: -------------------------------------------------------------------------------- 1 | # Python Exceptions 2 | 3 | This module covers how Python handles errors and exceptions, including how to raise, catch, and define custom exceptions. 4 | 5 | ## Files 6 | 7 | - `syntax_vs_exception.py` – Syntax errors vs runtime exceptions 8 | - `raise_assert.py` – Raising exceptions and using assertions 9 | - `try_except.py` – Exception handling with try-except blocks 10 | - `else_finally.py` – Using `else` and `finally` with try-except 11 | - `common_exceptions.py` – Examples of common built-in exceptions 12 | - `custom_exception.py` – Defining and raising custom exceptions 13 | 14 | Reference: https://docs.python.org/3/library/exceptions.html 15 | 16 | ## Run Examples 17 | 18 | ```bash 19 | python try_except.py 20 | ``` 21 | -------------------------------------------------------------------------------- /python-advanced/04-exception/common_exceptions.py: -------------------------------------------------------------------------------- 1 | # Uncomment each block individually to test 2 | 3 | # ImportError 4 | # import nonexistingmodule 5 | 6 | # NameError 7 | # print(undefined_var) 8 | 9 | # FileNotFoundError 10 | # with open("nonexistingfile.txt") as f: 11 | # data = f.read() 12 | 13 | # ValueError 14 | # a = [1, 2] 15 | # a.remove(3) 16 | 17 | # TypeError 18 | # a = 5 + "10" 19 | 20 | # IndexError 21 | # a = [0, 1, 2] 22 | # print(a[5]) 23 | 24 | # KeyError 25 | # my_dict = {"name": "Max"} 26 | # print(my_dict["age"]) 27 | -------------------------------------------------------------------------------- /python-advanced/04-exception/custom_exception.py: -------------------------------------------------------------------------------- 1 | class ValueTooHighError(Exception): 2 | pass 3 | 4 | 5 | class ValueTooLowError(Exception): 6 | 7 | def __init__(self, message, value): 8 | self.message = message 9 | self.value = value 10 | 11 | 12 | def test_value(a): 13 | if a > 1000: 14 | raise ValueTooHighError("Value is too high.") 15 | if a < 5: 16 | raise ValueTooLowError("Value is too low.", a) 17 | return a 18 | 19 | 20 | try: 21 | test_value(1) 22 | except ValueTooHighError as e: 23 | print(e) 24 | except ValueTooLowError as e: 25 | print(e.message, "The value is:", e.value) 26 | -------------------------------------------------------------------------------- /python-advanced/04-exception/else_finally.py: -------------------------------------------------------------------------------- 1 | try: 2 | a = 5 / 1 3 | except ZeroDivisionError: 4 | print("Division failed") 5 | else: 6 | print("Everything is OK") 7 | finally: 8 | print("Cleaning up...") 9 | -------------------------------------------------------------------------------- /python-advanced/04-exception/raise_assert.py: -------------------------------------------------------------------------------- 1 | x = -5 2 | 3 | try: 4 | if x < 0: 5 | raise Exception("x should not be negative.") 6 | except Exception as e: 7 | print("Raised:", e) 8 | 9 | try: 10 | assert x >= 0, "x is not positive." 11 | except AssertionError as e: 12 | print("Assert failed:", e) 13 | -------------------------------------------------------------------------------- /python-advanced/04-exception/syntax_vs_exception.py: -------------------------------------------------------------------------------- 1 | # SyntaxError example (won't run at all if uncommented) 2 | # a = 5 print(a) 3 | 4 | # TypeError (runtime exception) 5 | try: 6 | a = 5 + "10" 7 | except TypeError as e: 8 | print("Caught TypeError:", e) 9 | -------------------------------------------------------------------------------- /python-advanced/04-exception/try_except.py: -------------------------------------------------------------------------------- 1 | # Generic catch 2 | try: 3 | a = 5 / 0 4 | except: 5 | print("Some error occurred.") 6 | 7 | # Catch specific exception 8 | try: 9 | a = 5 / 0 10 | except ZeroDivisionError as e: 11 | print("Handled ZeroDivisionError:", e) 12 | 13 | # Multiple errors 14 | try: 15 | a = 5 / 1 16 | b = a + "10" 17 | except ZeroDivisionError as e: 18 | print("ZeroDivisionError:", e) 19 | except TypeError as e: 20 | print("TypeError:", e) 21 | -------------------------------------------------------------------------------- /python-advanced/05-logging/README.md: -------------------------------------------------------------------------------- 1 | # Python Logging 2 | 3 | This module introduces Python’s built-in `logging` module and best practices for structured, efficient, and extensible logging systems. 4 | 5 | ## Files 6 | 7 | - `logging_levels.py` – Basic log levels 8 | - `basic_config.py` – Custom formatting and file logging 9 | - `module_logging/` – Logging across modules with `__name__` 10 | - `log_propagation.py` – Control log propagation 11 | - `log_handlers.py` – Stream/File handlers with formatters 12 | - `log_filters.py` – Filtering log records 13 | - `logging_config_file/` – Using config file to control logging 14 | - `log_stacktrace.py` – Capturing full exception stack trace 15 | - `rotating_file_handler.py` – Size-based log rotation 16 | - `timed_rotating_handler.py` – Time-based log rotation 17 | - `json_logger.py` – Logging structured messages in JSON 18 | 19 | Official Docs: 20 | 21 | - https://docs.python.org/3/library/logging.html 22 | - https://github.com/madzak/python-json-logger 23 | -------------------------------------------------------------------------------- /python-advanced/05-logging/basic_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(level=logging.DEBUG, 4 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 5 | datefmt="%m/%d/%Y %H:%M:%S") 6 | # logging.basicConfig(filename="app.log") → log to file instead 7 | 8 | logging.debug("Debug message") 9 | -------------------------------------------------------------------------------- /python-advanced/05-logging/json_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pythonjsonlogger import jsonlogger 4 | 5 | logger = logging.getLogger() 6 | logHandler = logging.StreamHandler() 7 | formatter = jsonlogger.JsonFormatter() 8 | 9 | logHandler.setFormatter(formatter) 10 | logger.addHandler(logHandler) 11 | 12 | logger.info("App started", extra={"user": "shawnguyen", "env": "prod"}) 13 | -------------------------------------------------------------------------------- /python-advanced/05-logging/log_filters.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class InfoFilter(logging.Filter): 5 | 6 | def filter(self, record): 7 | return record.levelno == logging.INFO 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | logger.setLevel(logging.DEBUG) 12 | 13 | handler = logging.StreamHandler() 14 | handler.addFilter(InfoFilter()) 15 | logger.addHandler(handler) 16 | 17 | logger.info("This will appear") 18 | logger.warning("This won't") 19 | -------------------------------------------------------------------------------- /python-advanced/05-logging/log_handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | stream_handler = logging.StreamHandler() 6 | file_handler = logging.FileHandler("file.log") 7 | 8 | stream_handler.setLevel(logging.WARNING) 9 | file_handler.setLevel(logging.ERROR) 10 | 11 | stream_format = logging.Formatter("%(name)s - %(levelname)s - %(message)s") 12 | file_format = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 13 | 14 | stream_handler.setFormatter(stream_format) 15 | file_handler.setFormatter(file_format) 16 | 17 | logger.addHandler(stream_handler) 18 | logger.addHandler(file_handler) 19 | 20 | logger.warning("Stream warning") 21 | logger.error("Logged to stream + file") 22 | -------------------------------------------------------------------------------- /python-advanced/05-logging/log_propagation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | logger.propagate = False 5 | logger.info("This won't be shown if no handler is attached") 6 | -------------------------------------------------------------------------------- /python-advanced/05-logging/log_stacktrace.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | 4 | try: 5 | a = [1, 2, 3] 6 | value = a[3] 7 | except IndexError as e: 8 | logging.error("Exception occurred", exc_info=True) 9 | logging.error("Traceback:\n%s", traceback.format_exc()) 10 | -------------------------------------------------------------------------------- /python-advanced/05-logging/logging_config_file/config_usage.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | 3 | logging.config.fileConfig("logging.conf") 4 | logger = logging.getLogger("simpleExample") 5 | 6 | logger.debug("debug message") 7 | logger.info("info message") 8 | -------------------------------------------------------------------------------- /python-advanced/05-logging/logging_config_file/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,simpleExample 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [logger_simpleExample] 15 | level=DEBUG 16 | handlers=consoleHandler 17 | qualname=simpleExample 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=DEBUG 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 28 | -------------------------------------------------------------------------------- /python-advanced/05-logging/logging_levels.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.debug("This is a debug message") 4 | logging.info("This is an info message") 5 | logging.warning("This is a warning message") 6 | logging.error("This is an error message") 7 | logging.critical("This is a critical message") 8 | -------------------------------------------------------------------------------- /python-advanced/05-logging/module_logging/helper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | logger.info("HELLO from helper") 5 | -------------------------------------------------------------------------------- /python-advanced/05-logging/module_logging/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(level=logging.INFO, format="%(name)s - %(levelname)s - %(message)s") 4 | 5 | import helper 6 | -------------------------------------------------------------------------------- /python-advanced/05-logging/rotating_file_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import RotatingFileHandler 3 | 4 | logger = logging.getLogger(__name__) 5 | logger.setLevel(logging.INFO) 6 | 7 | handler = RotatingFileHandler("app.log", maxBytes=2000, backupCount=5) 8 | logger.addHandler(handler) 9 | 10 | for _ in range(10000): 11 | logger.info("Hello, world!") 12 | -------------------------------------------------------------------------------- /python-advanced/05-logging/timed_rotating_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import TimedRotatingFileHandler 3 | import time 4 | 5 | logger = logging.getLogger(__name__) 6 | logger.setLevel(logging.INFO) 7 | 8 | handler = TimedRotatingFileHandler("timed_test.log", when="m", interval=1, backupCount=5) 9 | logger.addHandler(handler) 10 | 11 | for i in range(6): 12 | logger.info("Hello, world!") 13 | time.sleep(1) 14 | -------------------------------------------------------------------------------- /python-advanced/06-json/README.md: -------------------------------------------------------------------------------- 1 | # Python JSON Handling 2 | 3 | This module covers how to serialize and deserialize data using Python's built-in `json` module. 4 | 5 | ## Files 6 | 7 | - `json_basic_encode.py` – Convert Python objects to JSON strings 8 | - `json_basic_decode.py` – Convert JSON strings to Python objects 9 | - `json_file_io.py` – Read/write JSON from/to files 10 | - `json_custom_encoder.py` – Encode custom objects using a function or custom encoder class 11 | - `json_custom_decoder.py` – Decode JSON to custom Python objects using `object_hook` 12 | - `json_template_encoder.py` – Generic encoding/decoding for any class using metadata 13 | - `sample_files/person.json` – Sample JSON file used in file-based examples 14 | 15 | ## Run Examples 16 | 17 | ```bash 18 | python json_basic_encode.py 19 | ``` 20 | 21 | Docs: [https://docs.python.org/3/library/json.html](https://docs.python.org/3/library/json.html) 22 | -------------------------------------------------------------------------------- /python-advanced/06-json/json_basic_decode.py: -------------------------------------------------------------------------------- 1 | """Convert JSON strings into Python objects using json.loads().""" 2 | 3 | import json 4 | 5 | person_json = """ 6 | { 7 | "age": 30, 8 | "city": "New York", 9 | "hasChildren": false, 10 | "name": "John", 11 | "titles": ["engineer", "programmer"] 12 | } 13 | """ 14 | 15 | person = json.loads(person_json) 16 | print(person) 17 | -------------------------------------------------------------------------------- /python-advanced/06-json/json_basic_encode.py: -------------------------------------------------------------------------------- 1 | """Convert Python objects to JSON strings using json.dumps().""" 2 | 3 | import json 4 | 5 | person = {"name": "John", "age": 30, "city": "New York", "hasChildren": False, "titles": ["engineer", "programmer"]} 6 | 7 | person_json = json.dumps(person) 8 | person_json2 = json.dumps(person, indent=4, separators=("; ", "= "), sort_keys=True) 9 | 10 | print(person_json) 11 | print(person_json2) 12 | -------------------------------------------------------------------------------- /python-advanced/06-json/json_custom_decoder.py: -------------------------------------------------------------------------------- 1 | """Decode JSON back into custom objects using object_hook.""" 2 | 3 | import json 4 | 5 | 6 | def decode_complex(dct): 7 | if "complex" in dct: 8 | return complex(dct["real"], dct["imag"]) 9 | return dct 10 | 11 | 12 | zJSON = '{"complex": true, "real": 5.0, "imag": 9.0}' 13 | z = json.loads(zJSON, object_hook=decode_complex) 14 | print(type(z)) 15 | print(z) 16 | -------------------------------------------------------------------------------- /python-advanced/06-json/json_custom_encoder.py: -------------------------------------------------------------------------------- 1 | """Encode custom Python objects (e.g. complex numbers) with a function or JSONEncoder.""" 2 | 3 | import json 4 | from json import JSONEncoder 5 | 6 | 7 | def encode_complex(z): 8 | if isinstance(z, complex): 9 | return {"complex": True, "real": z.real, "imag": z.imag} 10 | raise TypeError(f"Not serializable: {type(z)}") 11 | 12 | 13 | class ComplexEncoder(JSONEncoder): 14 | 15 | def default(self, o): 16 | if isinstance(o, complex): 17 | return {"complex": True, "real": o.real, "imag": o.imag} 18 | return super().default(o) 19 | 20 | 21 | z = 5 + 9j 22 | try: 23 | print(json.dumps(z)) 24 | except TypeError as e: 25 | print(f"Error: {e}") 26 | print(json.dumps(z, default=encode_complex)) 27 | print(json.dumps(z, cls=ComplexEncoder)) 28 | -------------------------------------------------------------------------------- /python-advanced/06-json/json_file_io.py: -------------------------------------------------------------------------------- 1 | """Read/write JSON to/from files using json.dump and json.load.""" 2 | 3 | import json 4 | import os 5 | 6 | person = {"name": "John", "age": 30, "city": "New York", "hasChildren": False, "titles": ["engineer", "programmer"]} 7 | 8 | # Ensure the directory exists 9 | os.makedirs("sample_files", exist_ok=True) 10 | 11 | with open("sample_files/person.json", "w") as f: 12 | json.dump(person, f, indent=2) 13 | 14 | with open("sample_files/person.json", "r") as f: 15 | data = json.load(f) 16 | print(data) 17 | -------------------------------------------------------------------------------- /python-advanced/06-json/json_template_encoder.py: -------------------------------------------------------------------------------- 1 | """Generic encoder/decoder for any class with __init__ using __module__ and __class__.""" 2 | 3 | import json 4 | 5 | 6 | class User: 7 | 8 | def __init__(self, name, age, active, balance, friends): 9 | self.name = name 10 | self.age = age 11 | self.active = active 12 | self.balance = balance 13 | self.friends = friends 14 | 15 | 16 | class Player: 17 | 18 | def __init__(self, name, nickname, level): 19 | self.name = name 20 | self.nickname = nickname 21 | self.level = level 22 | 23 | 24 | def encode_obj(obj): 25 | obj_dict = {"__class__": obj.__class__.__name__, "__module__": obj.__module__} 26 | obj_dict.update(obj.__dict__) 27 | return obj_dict 28 | 29 | 30 | def decode_dct(dct): 31 | if "__class__" in dct: 32 | class_name = dct.pop("__class__") 33 | module_name = dct.pop("__module__") 34 | module = __import__(module_name) 35 | class_ = getattr(module, class_name) 36 | return class_(**dct) 37 | return dct 38 | 39 | 40 | user = User("John", 28, True, 20.7, ["Jane", "Tom"]) 41 | user_json = json.dumps(user, default=encode_obj, indent=4) 42 | print(user_json) 43 | user_decoded = json.loads(user_json, object_hook=decode_dct) 44 | print(type(user_decoded)) 45 | 46 | player = Player("Max", "max1234", 5) 47 | player_json = json.dumps(player, default=encode_obj, indent=4) 48 | print(player_json) 49 | player_decoded = json.loads(player_json, object_hook=decode_dct) 50 | print(type(player_decoded)) 51 | -------------------------------------------------------------------------------- /python-advanced/07-random-number/README.md: -------------------------------------------------------------------------------- 1 | # Random Number Generation in Python 2 | 3 | This module explores different ways to generate random numbers in Python: 4 | 5 | - `random` module for general-purpose PRNG 6 | - `secrets` module for cryptographically secure numbers 7 | - NumPy’s random system for scientific computing 8 | 9 | ## Files 10 | 11 | - `random_module.py` – Common random operations: float, int, choice, shuffle, etc. 12 | - `random_seed.py` – Make PRNG reproducible using `random.seed()` 13 | - `secrets_module.py` – Secure token and secret generation 14 | - `numpy_random.py` – Random number generation using NumPy 15 | 16 | ## Run Examples 17 | 18 | ```bash 19 | python random_module.py 20 | ``` 21 | 22 | Official Docs: 23 | 24 | - [https://docs.python.org/3/library/random.html](https://docs.python.org/3/library/random.html) 25 | - [https://docs.python.org/3/library/secrets.html](https://docs.python.org/3/library/secrets.html) 26 | - [https://numpy.org/doc/stable/reference/random/index.html](https://numpy.org/doc/stable/reference/random/index.html) 27 | -------------------------------------------------------------------------------- /python-advanced/07-random-number/numpy_random.py: -------------------------------------------------------------------------------- 1 | """Use numpy.random for scientific-grade random generation.""" 2 | 3 | import numpy as np 4 | 5 | np.random.seed(1) 6 | print(np.random.rand(3)) # Array of 3 floats 7 | 8 | np.random.seed(1) 9 | print(np.random.rand(3)) # Reproducible 10 | 11 | print(np.random.randint(0, 10, (5, 3))) # 5x3 int array 12 | 13 | print(np.random.randn(5)) # Standard normal 14 | 15 | arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) 16 | np.random.shuffle(arr) # Shuffle rows 17 | print(arr) 18 | -------------------------------------------------------------------------------- /python-advanced/07-random-number/random_module.py: -------------------------------------------------------------------------------- 1 | """Common random operations using the `random` module.""" 2 | 3 | import random 4 | 5 | print(random.random()) # Float [0,1) 6 | print(random.uniform(1, 10)) # Float [1,10] 7 | print(random.randint(1, 10)) # Int [1,10] 8 | print(random.randrange(1, 10)) # Int [1,10) 9 | print(random.normalvariate(0, 1)) # Normal dist float 10 | 11 | print(random.choice("ABCDEFGHI")) # Random element 12 | print(random.sample("ABCDEFGHI", 3)) # k unique elements 13 | print(random.choices("ABCDEFGHI", k=3)) # k with replacement 14 | 15 | lst = list("ABCDEFGHI") 16 | random.shuffle(lst) # In-place shuffle 17 | print(lst) 18 | -------------------------------------------------------------------------------- /python-advanced/07-random-number/random_seed.py: -------------------------------------------------------------------------------- 1 | """Demonstrates deterministic results using random.seed().""" 2 | 3 | import random 4 | 5 | 6 | def demo(seed): 7 | print(f"\nSeeding with {seed}...\n") 8 | random.seed(seed) 9 | print(random.random()) 10 | print(random.uniform(1, 10)) 11 | print(random.choice("ABCDEFGHI")) 12 | 13 | 14 | demo(1) 15 | demo(42) 16 | demo(1) 17 | demo(42) 18 | -------------------------------------------------------------------------------- /python-advanced/07-random-number/secrets_module.py: -------------------------------------------------------------------------------- 1 | """Generate cryptographically secure values using secrets module.""" 2 | 3 | import secrets 4 | 5 | print(secrets.randbelow(10)) # Integer [0,10) 6 | print(secrets.randbits(5)) # 5 random bits 7 | print(secrets.choice("ABCDEFGHI")) # Secure choice 8 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/README.md: -------------------------------------------------------------------------------- 1 | # Python Decorators 2 | 3 | Decorators provide a powerful way to extend and modify function or method behavior without modifying the actual code. 4 | 5 | ## Topics Covered 6 | 7 | - Basic decorators 8 | - Handling function arguments and return values 9 | - Preserving metadata using `functools.wraps` 10 | - Passing arguments to decorators 11 | - Stacking multiple decorators 12 | - Stateful class-based decorators 13 | - Practical examples: timing, debugging, caching, validation 14 | 15 | ## Files 16 | 17 | | File | Description | 18 | | -------------------------------------- | --------------------------------------------------- | 19 | | `basic_function_decorator.py` | Intro to decorators with simple wrapper logic | 20 | | `decorator_with_args_and_return.py` | Handle arguments and return values | 21 | | `functools_wraps_preserve_metadata.py` | Use `functools.wraps` to preserve function identity | 22 | | `decorator_with_arguments.py` | Create decorators that accept arguments | 23 | | `nested_decorators.py` | Combine multiple decorators on a function | 24 | | `class_decorator.py` | Use a class as a decorator with state tracking | 25 | | `practical_use_cases.py` | Real-world decorator examples (timing, logging) | 26 | 27 | ## Run Example 28 | 29 | ```bash 30 | python basic_function_decorator.py 31 | ``` 32 | 33 | Docs: https://realpython.com/primer-on-python-decorators/ 34 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/basic_function_decorator.py: -------------------------------------------------------------------------------- 1 | """Basic decorator example that adds behavior before and after a function.""" 2 | 3 | 4 | def start_end_decorator(func): 5 | 6 | def wrapper(): 7 | print("Start") 8 | func() 9 | print("End") 10 | 11 | return wrapper 12 | 13 | 14 | @start_end_decorator 15 | def greet(): 16 | print("Hello!") 17 | 18 | 19 | greet() 20 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/class_decorator.py: -------------------------------------------------------------------------------- 1 | """Class-based decorator that tracks how many times a function is called.""" 2 | 3 | import functools 4 | 5 | 6 | class CountCalls: 7 | 8 | def __init__(self, func): 9 | functools.update_wrapper(self, func) 10 | self.func = func 11 | self.num_calls = 0 12 | 13 | def __call__(self, *args, **kwargs): 14 | self.num_calls += 1 15 | print(f"Call {self.num_calls} to {self.func.__name__}") 16 | return self.func(*args, **kwargs) 17 | 18 | 19 | @CountCalls 20 | def say_hello(): 21 | print("Hello!") 22 | 23 | 24 | say_hello() 25 | say_hello() 26 | say_hello() 27 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/decorator_with_args_and_return.py: -------------------------------------------------------------------------------- 1 | """Decorator that supports arbitrary arguments and preserves return value.""" 2 | 3 | 4 | def start_end_decorator(func): 5 | 6 | def wrapper(*args, **kwargs): 7 | print("Start") 8 | result = func(*args, **kwargs) 9 | print("End") 10 | return result 11 | 12 | return wrapper 13 | 14 | 15 | @start_end_decorator 16 | def add_5(x): 17 | return x + 5 18 | 19 | 20 | print(add_5(10)) 21 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/decorator_with_arguments.py: -------------------------------------------------------------------------------- 1 | """Decorator factory that accepts arguments (e.g., repeat a function multiple times).""" 2 | 3 | import functools 4 | 5 | 6 | def repeat(n): 7 | 8 | def decorator_repeat(func): 9 | 10 | @functools.wraps(func) 11 | def wrapper(*args, **kwargs): 12 | for _ in range(n): 13 | result = func(*args, **kwargs) 14 | return result 15 | 16 | return wrapper 17 | 18 | return decorator_repeat 19 | 20 | 21 | @repeat(3) 22 | def greet(name): 23 | print(f"Hello, {name}!") 24 | 25 | 26 | greet("Alex") 27 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/functools_wraps_preserve_metadata.py: -------------------------------------------------------------------------------- 1 | """Decorator that preserves function metadata using functools.wraps.""" 2 | 3 | import functools 4 | 5 | 6 | def start_end_decorator(func): 7 | 8 | @functools.wraps(func) 9 | def wrapper(*args, **kwargs): 10 | print("Start") 11 | result = func(*args, **kwargs) 12 | print("End") 13 | return result 14 | 15 | return wrapper 16 | 17 | 18 | @start_end_decorator 19 | def add_5(x): 20 | """Adds 5 to the input.""" 21 | return x + 5 22 | 23 | 24 | print(add_5(10)) 25 | print(add_5.__name__) 26 | print(add_5.__doc__) 27 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/nested_decorators.py: -------------------------------------------------------------------------------- 1 | """Example of using multiple decorators (stacked execution order).""" 2 | 3 | import functools 4 | 5 | 6 | def debug(func): 7 | 8 | @functools.wraps(func) 9 | def wrapper(*args, **kwargs): 10 | print(f"Calling {func.__name__} with {args}, {kwargs}") 11 | result = func(*args, **kwargs) 12 | print(f"{func.__name__} returned {result}") 13 | return result 14 | 15 | return wrapper 16 | 17 | 18 | def start_end(func): 19 | 20 | @functools.wraps(func) 21 | def wrapper(*args, **kwargs): 22 | print("Start") 23 | result = func(*args, **kwargs) 24 | print("End") 25 | return result 26 | 27 | return wrapper 28 | 29 | 30 | @debug 31 | @start_end 32 | def say_hello(name): 33 | print(f"Hello, {name}!") 34 | return f"Greeted {name}" 35 | 36 | 37 | say_hello("Alex") 38 | -------------------------------------------------------------------------------- /python-advanced/08-decorators/practical_use_cases.py: -------------------------------------------------------------------------------- 1 | """Real-world use cases for decorators: timing, debugging, validation, etc.""" 2 | 3 | import functools 4 | import time 5 | 6 | 7 | def timer(func): 8 | 9 | @functools.wraps(func) 10 | def wrapper(*args, **kwargs): 11 | start = time.time() 12 | result = func(*args, **kwargs) 13 | end = time.time() 14 | print(f"{func.__name__} took {end - start:.4f}s") 15 | return result 16 | 17 | return wrapper 18 | 19 | 20 | @timer 21 | def slow_add(x, y): 22 | time.sleep(1) 23 | return x + y 24 | 25 | 26 | print(slow_add(3, 7)) 27 | -------------------------------------------------------------------------------- /python-advanced/09-generators/README.md: -------------------------------------------------------------------------------- 1 | # Generators in Python 2 | 3 | Generators are functions that return an iterator and allow for **lazy evaluation**, meaning values are produced one at a time only as needed. This makes them ideal for working with large data streams or infinite sequences. 4 | 5 | ## Core Concepts 6 | 7 | - `yield` pauses and resumes function execution 8 | - `next()` fetches the next value 9 | - Generators are memory-efficient 10 | - Generator expressions are like lazy list comprehensions 11 | - Behind the scenes: `__iter__` and `__next__` protocol 12 | 13 | ## Files 14 | 15 | | File | Description | 16 | | --------------------------------- | ------------------------------------------------------ | 17 | | `generator_basics.py` | Yielding values and `next()` | 18 | | `generator_memory_efficiency.py` | Compare memory usage between list vs generator | 19 | | `generator_fibonacci.py` | Example: Fibonacci generator | 20 | | `generator_expression_vs_list.py` | Memory difference: generator expression vs list comp | 21 | | `generator_custom_iterable.py` | Custom generator class using `__iter__` and `__next__` | 22 | 23 | ## Run Example 24 | 25 | ```bash 26 | python generator_basics.py 27 | ``` 28 | 29 | Docs: https://docs.python.org/3/library/stdtypes.html#generator-types 30 | -------------------------------------------------------------------------------- /python-advanced/09-generators/generator_basics.py: -------------------------------------------------------------------------------- 1 | """Demonstrates basic generator behavior using `yield` and `next()`.""" 2 | 3 | 4 | def countdown(num): 5 | print("Starting") 6 | while num > 0: 7 | yield num 8 | num -= 1 9 | 10 | 11 | gen = countdown(3) 12 | 13 | print(next(gen)) # Starts and yields 3 14 | print(next(gen)) # Yields 2 15 | print(next(gen)) # Yields 1 16 | # print(next(gen)) # Uncomment to see StopIteration 17 | 18 | print("\nUsing for loop:") 19 | for value in countdown(3): 20 | print(value) 21 | 22 | print("Sum:", sum(countdown(3))) 23 | print("Sorted:", sorted(countdown(3))) 24 | -------------------------------------------------------------------------------- /python-advanced/09-generators/generator_custom_iterable.py: -------------------------------------------------------------------------------- 1 | """Manually implement an iterable class mimicking a generator.""" 2 | 3 | 4 | class FirstN: 5 | 6 | def __init__(self, n): 7 | self.n = n 8 | self.num = 0 9 | 10 | def __iter__(self): 11 | return self 12 | 13 | def __next__(self): 14 | if self.num < self.n: 15 | current = self.num 16 | self.num += 1 17 | return current 18 | raise StopIteration() 19 | 20 | 21 | firstn = FirstN(1_000_000) 22 | print("Sum using custom iterable:", sum(firstn)) 23 | -------------------------------------------------------------------------------- /python-advanced/09-generators/generator_expression_vs_list.py: -------------------------------------------------------------------------------- 1 | """Compare memory size between generator expressions and list comprehensions.""" 2 | 3 | import sys 4 | 5 | gen_expr = (i for i in range(1000) if i % 2 == 0) 6 | list_comp = [i for i in range(1000) if i % 2 == 0] 7 | 8 | print("Generator expression size:", sys.getsizeof(gen_expr)) 9 | print("List comprehension size:", sys.getsizeof(list_comp)) 10 | -------------------------------------------------------------------------------- /python-advanced/09-generators/generator_fibonacci.py: -------------------------------------------------------------------------------- 1 | """Yield Fibonacci numbers lazily with a generator.""" 2 | 3 | 4 | def fibonacci(limit): 5 | a, b = 0, 1 6 | while b < limit: 7 | yield b 8 | a, b = b, a + b 9 | 10 | 11 | print("Fibonacci < 30:", list(fibonacci(30))) 12 | -------------------------------------------------------------------------------- /python-advanced/09-generators/generator_memory_efficiency.py: -------------------------------------------------------------------------------- 1 | """Compare memory usage between list and generator for large ranges.""" 2 | 3 | import sys 4 | 5 | 6 | # List version 7 | def firstn_list(n): 8 | num, nums = 0, [] 9 | while num < n: 10 | nums.append(num) 11 | num += 1 12 | return nums 13 | 14 | 15 | print("List sum:", sum(firstn_list(1_000_000))) 16 | print("List size:", sys.getsizeof(firstn_list(1_000_000)), "bytes") 17 | 18 | 19 | # Generator version 20 | def firstn_gen(n): 21 | num = 0 22 | while num < n: 23 | yield num 24 | num += 1 25 | 26 | 27 | print("Generator sum:", sum(firstn_gen(1_000_000))) 28 | print("Generator size:", sys.getsizeof(firstn_gen(1_000_000)), "bytes") 29 | -------------------------------------------------------------------------------- /python-advanced/10-threading-multiprocessing/cpu_bound_multiprocessing.py: -------------------------------------------------------------------------------- 1 | """Demonstrates multiprocessing on a CPU-bound task. 2 | This approach achieves parallel execution and bypasses the GIL. 3 | """ 4 | 5 | from multiprocessing import cpu_count 6 | from multiprocessing import Process 7 | 8 | 9 | def compute_squares(): 10 | for _ in range(1_000_000): 11 | _ = 42**2 12 | 13 | 14 | if __name__ == "__main__": 15 | processes = [] 16 | for _ in range(cpu_count()): 17 | process = Process(target=compute_squares) 18 | processes.append(process) 19 | process.start() 20 | 21 | for process in processes: 22 | process.join() 23 | -------------------------------------------------------------------------------- /python-advanced/10-threading-multiprocessing/cpu_bound_threading.py: -------------------------------------------------------------------------------- 1 | """Demonstrates threading on a CPU-bound task. 2 | Threading is ineffective here due to the Global Interpreter Lock (GIL). 3 | """ 4 | 5 | from threading import Thread 6 | 7 | 8 | def compute_squares(): 9 | for _ in range(100_000_000): 10 | _ = 42**2 11 | print("Finished computing squares.") 12 | 13 | 14 | if __name__ == "__main__": 15 | threads = [] 16 | for _ in range(4): 17 | thread = Thread(target=compute_squares) 18 | threads.append(thread) 19 | thread.start() 20 | 21 | for thread in threads: 22 | thread.join() 23 | -------------------------------------------------------------------------------- /python-advanced/10-threading-multiprocessing/io_bound_threading.py: -------------------------------------------------------------------------------- 1 | """Demonstrates threading used for I/O-bound tasks. 2 | Threads are ideal for overlapping wait times, e.g., network or file I/O. 3 | """ 4 | 5 | from threading import Thread 6 | import time 7 | 8 | 9 | def simulated_io_task(name): 10 | print(f"{name} started I/O operation") 11 | time.sleep(2) 12 | print(f"{name} completed I/O operation") 13 | 14 | 15 | if __name__ == "__main__": 16 | threads = [] 17 | for i in range(5): 18 | thread = Thread(target=simulated_io_task, args=(f"Thread-{i}",)) 19 | threads.append(thread) 20 | thread.start() 21 | 22 | for thread in threads: 23 | thread.join() 24 | -------------------------------------------------------------------------------- /python-advanced/10-threading-multiprocessing/shared_memory_gil_limitations.py: -------------------------------------------------------------------------------- 1 | """Demonstrates shared memory and the limitations of the GIL in multi-threaded Python. 2 | All threads increment the same counter; expected result is not guaranteed due to race conditions. 3 | """ 4 | 5 | import dis 6 | import threading 7 | 8 | counter = 0 9 | 10 | 11 | def f(): 12 | global counter 13 | counter += 1 14 | 15 | 16 | def increment(): 17 | global counter 18 | for _ in range(1_000_000): 19 | counter += 1 20 | 21 | 22 | if __name__ == "__main__": 23 | # To confirm the race potential, inspect the bytecode of the function 24 | dis.dis(f) 25 | threads = [] 26 | for _ in range(2): 27 | thread = threading.Thread(target=increment) 28 | threads.append(thread) 29 | thread.start() 30 | 31 | for thread in threads: 32 | thread.join() 33 | 34 | print("Expected counter = 2_000_000") 35 | print("Actual counter =", counter) 36 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/README.md: -------------------------------------------------------------------------------- 1 | # Python Function Arguments 2 | 3 | This module explores all aspects of function arguments in Python: 4 | 5 | - 🧾 Difference between arguments and parameters 6 | - Positional and keyword arguments 7 | - Default arguments 8 | - Variable-length arguments (`*args`, `**kwargs`) 9 | - Unpacking containers into arguments 10 | - 🌍 Local vs. global variables 11 | - Parameter passing: value vs. reference semantics 12 | 13 | Each example is separated into focused `.py` files. 14 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/arguments_vs_parameters.py: -------------------------------------------------------------------------------- 1 | """Difference between arguments and parameters.""" 2 | 3 | 4 | def print_name(name): # name is the parameter 5 | print(name) 6 | 7 | 8 | print_name("Alex") # 'Alex' is the argument 9 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/default_arguments.py: -------------------------------------------------------------------------------- 1 | """Default arguments in functions.""" 2 | 3 | 4 | def foo(a, b, c, d=4): 5 | print(a, b, c, d) 6 | 7 | 8 | foo(1, 2, 3) 9 | foo(1, b=2, c=3, d=100) 10 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/global_vs_local.py: -------------------------------------------------------------------------------- 1 | """Demonstrating global vs. local variables.""" 2 | 3 | 4 | def foo1(): 5 | x = number 6 | print("number in function:", x) 7 | 8 | 9 | def foo2(): 10 | global number 11 | number = 3 12 | 13 | 14 | def foo3(): 15 | number = 3 # local 16 | 17 | 18 | number = 0 19 | foo1() 20 | print("number before foo2():", number) 21 | foo2() 22 | print("number after foo2():", number) 23 | 24 | print("number before foo3():", number) 25 | foo3() 26 | print("number after foo3():", number) 27 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/keyword_only_args.py: -------------------------------------------------------------------------------- 1 | """Forcing keyword-only arguments.""" 2 | 3 | 4 | def foo(a, b, *, c, d): 5 | print(a, b, c, d) 6 | 7 | 8 | foo(1, 2, c=3, d=4) 9 | 10 | 11 | def foo_with_args(*args, last): 12 | for arg in args: 13 | print(arg) 14 | print("last =", last) 15 | 16 | 17 | foo_with_args(8, 9, 10, last=50) 18 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/parameter_passing.py: -------------------------------------------------------------------------------- 1 | """Parameter passing: value vs. reference semantics.""" 2 | 3 | 4 | def foo_immutable(x): 5 | x = 5 6 | 7 | 8 | def foo_mutable(a_list): 9 | a_list.append(4) 10 | 11 | 12 | def foo_mutable_reassign(a_list): 13 | a_list = [50, 60, 70] 14 | a_list.append(50) 15 | 16 | 17 | def foo_aug_assign(a_list): 18 | a_list += [4, 5] 19 | 20 | 21 | def bar_rebind(a_list): 22 | a_list = a_list + [4, 5] 23 | 24 | 25 | var = 10 26 | print("var before foo_immutable():", var) 27 | foo_immutable(var) 28 | print("var after foo_immutable():", var) 29 | 30 | my_list = [1, 2, 3] 31 | print("my_list before foo_mutable():", my_list) 32 | foo_mutable(my_list) 33 | print("my_list after foo_mutable():", my_list) 34 | 35 | my_list = [1, 2, "Max"] 36 | print("my_list before reassign:", my_list) 37 | foo_mutable_reassign(my_list) 38 | print("my_list after reassign:", my_list) 39 | 40 | my_list = [1, 2, 3] 41 | print("my_list before += :", my_list) 42 | foo_aug_assign(my_list) 43 | print("my_list after += :", my_list) 44 | 45 | my_list = [1, 2, 3] 46 | print("my_list before + :", my_list) 47 | bar_rebind(my_list) 48 | print("my_list after + :", my_list) 49 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/positional_vs_keyword.py: -------------------------------------------------------------------------------- 1 | """Positional and keyword arguments.""" 2 | 3 | 4 | def foo(a, b, c): 5 | print(a, b, c) 6 | 7 | 8 | foo(1, 2, 3) 9 | foo(a=1, b=2, c=3) 10 | foo(c=3, b=2, a=1) 11 | foo(1, b=2, c=3) 12 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/unpacking_arguments.py: -------------------------------------------------------------------------------- 1 | """Unpacking containers into function arguments.""" 2 | 3 | 4 | def foo(a, b, c): 5 | print(a, b, c) 6 | 7 | 8 | my_list = [4, 5, 6] 9 | foo(*my_list) 10 | 11 | my_dict = {"a": 1, "b": 2, "c": 3} 12 | foo(**my_dict) 13 | -------------------------------------------------------------------------------- /python-advanced/11-function-arguments/variable_length_args.py: -------------------------------------------------------------------------------- 1 | """Using *args and **kwargs.""" 2 | 3 | 4 | def foo(a, b, *args, **kwargs): 5 | print(a, b) 6 | for arg in args: 7 | print(arg) 8 | for key, val in kwargs.items(): 9 | print(f"{key} = {val}") 10 | 11 | 12 | foo(1, 2, 3, 4, 5, six=6, seven=7) 13 | print() 14 | foo(1, 2, three=3) 15 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/README.md: -------------------------------------------------------------------------------- 1 | # The Asterisk (`*`) in Python and Its Use Cases 2 | 3 | The asterisk `*` is a versatile operator in Python. This module covers: 4 | 5 | - Multiplication and power operations 6 | - Repeating elements in lists, tuples, and strings 7 | - `*args`, `**kwargs`, and enforcing keyword-only arguments 8 | - Unpacking containers and function arguments 9 | - Merging containers and dictionaries 10 | 11 | ## Files 12 | 13 | | File | Purpose | 14 | | --------------------------------- | --------------------------------------------------------------- | 15 | | `arithmetic_operations.py` | Demonstrates `*` and `**` in math expressions | 16 | | `repeat_elements.py` | Creates repeated sequences using `*` | 17 | | `args_kwargs.py` | Shows how to use `*args` and `**kwargs` in functions | 18 | | `keyword_only_arguments.py` | Enforces keyword-only function arguments | 19 | | `unpacking_for_function_calls.py` | Uses `*` and `**` to unpack containers for function calls | 20 | | `unpacking_containers.py` | Splits sequences into head/tail using unpacking | 21 | | `merge_iterables_dicts.py` | Merges iterables and dictionaries using unpacking | 22 | | `non_string_dict_merge_error.py` | Highlights common error when merging dicts with non-string keys | 23 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/args_kwargs.py: -------------------------------------------------------------------------------- 1 | """Variable-length arguments with *args and **kwargs""" 2 | 3 | 4 | def my_function(*args, **kwargs): 5 | for arg in args: 6 | print(arg) 7 | for key in kwargs: 8 | print(key, kwargs[key]) 9 | 10 | 11 | my_function("hello", 42, [1, 2], name="Shaw", age=25) 12 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/arithmetic_operations.py: -------------------------------------------------------------------------------- 1 | """Multiplication and exponentiation using * and **""" 2 | 3 | print(7 * 5) # 35 4 | print(2**4) # 16 5 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/keyword_only_arguments.py: -------------------------------------------------------------------------------- 1 | """Enforce keyword-only arguments""" 2 | 3 | 4 | def greet(name, *, age): 5 | print(f"Name: {name}, Age: {age}") 6 | 7 | 8 | greet("Shaw", age=30) 9 | # greet("Shaw", 30) # Uncomment to raise TypeError 10 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/merge_iterables_dicts.py: -------------------------------------------------------------------------------- 1 | """Merge iterables and dictionaries using unpacking""" 2 | 3 | # Merging lists 4 | t = (1, 2) 5 | s = {3, 4} 6 | print([*t, *s]) 7 | 8 | # Merging dictionaries 9 | a = {"x": 1, "y": 2} 10 | b = {"z": 3} 11 | print({**a, **b}) 12 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/non_string_dict_merge_error.py: -------------------------------------------------------------------------------- 1 | """Demonstrate failure when merging dicts with non-string keys using dict() constructor""" 2 | 3 | a = {"one": 1, "two": 2} 4 | b = {3: "three", "four": 4} 5 | 6 | # Raises: TypeError: keywords must be strings 7 | # print(dict(a, **b)) 8 | 9 | # Correct way 10 | print({**a, **b}) 11 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/repeat_elements.py: -------------------------------------------------------------------------------- 1 | """Repeat elements using * for list, tuple, and str""" 2 | 3 | # Lists 4 | print([0] * 5) 5 | print([1, 2] * 3) 6 | 7 | # Tuples 8 | print((0,) * 5) 9 | print((1, 2) * 3) 10 | 11 | # Strings 12 | print("A" * 5) 13 | print("AB" * 3) 14 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/unpacking_containers.py: -------------------------------------------------------------------------------- 1 | """Unpack containers using * to split sequences""" 2 | 3 | data = (1, 2, 3, 4, 5, 6, 7) 4 | 5 | *start, last = data 6 | print(start, last) 7 | 8 | first, *end = data 9 | print(first, end) 10 | 11 | first, *middle, last = data 12 | print(first, middle, last) 13 | -------------------------------------------------------------------------------- /python-advanced/12-asterisk/unpacking_for_function_calls.py: -------------------------------------------------------------------------------- 1 | """Unpack iterable and dictionary into function arguments""" 2 | 3 | 4 | def foo(a, b, c): 5 | print(a, b, c) 6 | 7 | 8 | foo(*[1, 2, 3]) 9 | foo(*"XYZ") 10 | foo(**{"a": 10, "b": 20, "c": 30}) 11 | -------------------------------------------------------------------------------- /python-advanced/13-copying/README.md: -------------------------------------------------------------------------------- 1 | # Shallow vs Deep Copying in Python 2 | 3 | This module demonstrates the difference between **assignment**, **shallow copy**, and **deep copy** in Python using built-in types and custom classes. 4 | 5 | ## Topics Covered 6 | 7 | - Assignment (`obj_b = obj_a`) 8 | - Shallow copying using `copy.copy()` 9 | - Deep copying using `copy.deepcopy()` 10 | - Nested lists and objects 11 | - Copying custom objects 12 | 13 | ## Summary Table 14 | 15 | | Operation | Effect | 16 | | ------------------ | -------------------------------------------------- | 17 | | `b = a` | Both point to the same object | 18 | | `copy.copy(a)` | One-level shallow copy — nested objects are shared | 19 | | `copy.deepcopy(a)` | Full recursive copy — completely independent | 20 | 21 | ## Files 22 | 23 | | File | Description | 24 | | ------------------------- | --------------------------------------------- | 25 | | `assignment_reference.py` | Demonstrates simple assignment (shared ref) | 26 | | `shallow_copy_flat.py` | Shallow copy of a flat list | 27 | | `shallow_copy_nested.py` | Shallow copy of nested lists (shared inner) | 28 | | `deep_copy_nested.py` | Deep copy of nested lists (full independence) | 29 | | `custom_class_copy.py` | Shallow/deep copy on custom classes | 30 | -------------------------------------------------------------------------------- /python-advanced/13-copying/assignment_reference.py: -------------------------------------------------------------------------------- 1 | """ 2 | Assignment reference — creates alias to the same object. 3 | """ 4 | 5 | list_a = [1, 2, 3, 4, 5] 6 | list_b = list_a # same reference 7 | 8 | list_a[0] = -10 9 | 10 | print("list_a:", list_a) 11 | print("list_b:", list_b) 12 | -------------------------------------------------------------------------------- /python-advanced/13-copying/custom_class_copy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copying custom objects with shallow vs deep copy. 3 | """ 4 | 5 | import copy 6 | 7 | 8 | class Person: 9 | 10 | def __init__(self, name, age): 11 | self.name = name 12 | self.age = age 13 | 14 | 15 | class Company: 16 | 17 | def __init__(self, boss, employee): 18 | self.boss = boss 19 | self.employee = employee 20 | 21 | 22 | print("--- Shallow copy ---") 23 | boss = Person("Jane", 55) 24 | employee = Person("Joe", 28) 25 | company = Company(boss, employee) 26 | 27 | clone = copy.copy(company) 28 | clone.boss.age = 56 29 | 30 | print("Original boss age:", company.boss.age) 31 | print("Cloned boss age:", clone.boss.age) 32 | 33 | print("\n--- Deep copy ---") 34 | boss = Person("Jane", 55) 35 | employee = Person("Joe", 28) 36 | company = Company(boss, employee) 37 | 38 | clone = copy.deepcopy(company) 39 | clone.boss.age = 60 40 | 41 | print("Original boss age:", company.boss.age) 42 | print("Cloned boss age:", clone.boss.age) 43 | -------------------------------------------------------------------------------- /python-advanced/13-copying/deep_copy_nested.py: -------------------------------------------------------------------------------- 1 | """ 2 | Deep copy with nested lists — full independence. 3 | """ 4 | 5 | import copy 6 | 7 | list_a = [[1, 2], [3, 4]] 8 | list_b = copy.deepcopy(list_a) 9 | 10 | list_a[0][0] = -100 11 | 12 | print("list_a:", list_a) 13 | print("list_b:", list_b) 14 | -------------------------------------------------------------------------------- /python-advanced/13-copying/shallow_copy_flat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shallow copy of a flat list. 3 | """ 4 | 5 | import copy 6 | 7 | list_a = [1, 2, 3, 4, 5] 8 | list_b = copy.copy(list_a) 9 | 10 | list_b[0] = -10 11 | 12 | print("list_a:", list_a) 13 | print("list_b:", list_b) 14 | -------------------------------------------------------------------------------- /python-advanced/13-copying/shallow_copy_nested.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shallow copy with nested lists — inner objects are shared. 3 | """ 4 | 5 | import copy 6 | 7 | list_a = [[1, 2], [3, 4]] 8 | list_b = copy.copy(list_a) 9 | 10 | list_a[0][0] = -100 11 | 12 | print("list_a:", list_a) 13 | print("list_b:", list_b) 14 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/README.md: -------------------------------------------------------------------------------- 1 | # Context Managers in Python 2 | 3 | Context managers allow for clean and reliable acquisition and release of resources in Python. 4 | This section demonstrates the use and implementation of context managers using the `with` statement. 5 | 6 | ## ✅ Topics Covered 7 | 8 | - What is a context manager 9 | - Using `with` for resource management 10 | - Implementing context managers using a class 11 | - Exception handling in context managers 12 | - Implementing context managers using generator functions 13 | 14 | ## Files 15 | 16 | | File | Description | 17 | | --------------------------------- | -------------------------------------------------------- | 18 | | `file_with_statement.py` | Basic usage of `with open(...)` | 19 | | `managed_file_class.py` | Custom context manager using a class | 20 | | `managed_file_class_exception.py` | Handle exceptions in class-based context manager | 21 | | `managed_file_class_handled.py` | Suppress exceptions by returning True in `__exit__` | 22 | | `managed_file_generator.py` | Custom context manager using `@contextmanager` decorator | 23 | 24 | ## References 25 | 26 | - [Python docs: contextlib](https://docs.python.org/3/library/contextlib.html) 27 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/file_with_statement.py: -------------------------------------------------------------------------------- 1 | """Open and write to a file using a context manager.""" 2 | 3 | with open("notes.txt", "w") as f: 4 | f.write("some todo...") 5 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/managed_file_class.py: -------------------------------------------------------------------------------- 1 | """Custom context manager using a class.""" 2 | 3 | 4 | class ManagedFile: 5 | 6 | def __init__(self, filename): 7 | print("init", filename) 8 | self.filename = filename 9 | 10 | def __enter__(self): 11 | print("enter") 12 | self.file = open(self.filename, "w") 13 | return self.file 14 | 15 | def __exit__(self, exc_type, exc_val, exc_tb): 16 | if self.file: 17 | self.file.close() 18 | print("exit") 19 | 20 | 21 | with ManagedFile("notes.txt") as f: 22 | print("doing stuff...") 23 | f.write("some todo...") 24 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/managed_file_class_exception.py: -------------------------------------------------------------------------------- 1 | """Custom context manager that logs exceptions but re-raises them.""" 2 | 3 | 4 | class ManagedFile: 5 | 6 | def __init__(self, filename): 7 | print("init", filename) 8 | self.filename = filename 9 | 10 | def __enter__(self): 11 | print("enter") 12 | self.file = open(self.filename, "w") 13 | return self.file 14 | 15 | def __exit__(self, exc_type, exc_val, exc_tb): 16 | if self.file: 17 | self.file.close() 18 | print("exc:", exc_type, exc_val) 19 | print("exit") 20 | 21 | 22 | # Normal case 23 | with ManagedFile("notes.txt") as f: 24 | print("doing stuff...") 25 | f.write("some todo...") 26 | 27 | # With an error 28 | with ManagedFile("notes2.txt") as f: 29 | print("doing stuff...") 30 | f.write("some todo...") 31 | f.do_something() 32 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/managed_file_class_handled.py: -------------------------------------------------------------------------------- 1 | """Custom context manager that suppresses exceptions.""" 2 | 3 | 4 | class ManagedFile: 5 | 6 | def __init__(self, filename): 7 | print("init", filename) 8 | self.filename = filename 9 | 10 | def __enter__(self): 11 | print("enter") 12 | self.file = open(self.filename, "w") 13 | return self.file 14 | 15 | def __exit__(self, exc_type, exc_val, exc_tb): 16 | if self.file: 17 | self.file.close() 18 | if exc_type: 19 | print("Exception has been handled") 20 | print("exit") 21 | return True 22 | 23 | 24 | with ManagedFile("notes2.txt") as f: 25 | print("doing stuff...") 26 | f.write("some todo...") 27 | f.do_something() 28 | 29 | print("continuing...") 30 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/managed_file_generator.py: -------------------------------------------------------------------------------- 1 | """Context manager implemented with @contextmanager and yield.""" 2 | 3 | from contextlib import contextmanager 4 | 5 | 6 | @contextmanager 7 | def open_managed_file(filename): 8 | f = open(filename, "w") 9 | try: 10 | yield f 11 | finally: 12 | f.close() 13 | 14 | 15 | with open_managed_file("notes.txt") as f: 16 | f.write("some todo...") 17 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/notes.txt: -------------------------------------------------------------------------------- 1 | some todo... 2 | -------------------------------------------------------------------------------- /python-advanced/14-context-manager/notes2.txt: -------------------------------------------------------------------------------- 1 | some todo... 2 | -------------------------------------------------------------------------------- /python-advanced/README.md: -------------------------------------------------------------------------------- 1 | # Python Advanced 2 | 3 | In-depth modules showcasing Python's expressive and powerful capabilities: 4 | 5 | - `01-collections`: Specialized containers like `Counter`, `deque`, and `namedtuple` 6 | - `02-itertools`: Functional tools for creating and manipulating iterators 7 | - `03-lambda`: Anonymous functions, `map`, `filter`, `reduce`, and custom sorting 8 | - `04-exception`: Proper exception handling, custom exceptions, and common patterns 9 | - `05-logging`: Structured logging, configuration files, rotating handlers 10 | - `06-json`: JSON encoding/decoding with custom and nested objects 11 | - `07-random-number`: Deterministic vs secure random generation, NumPy random 12 | - `08-decorators`: Function/class decorators, use cases, `functools.wraps` 13 | - `09-generators`: Lazy iteration, memory-efficient pipelines, expressions 14 | - `10-threading-multiprocessing`: Concurrency vs parallelism, GIL, shared memory 15 | - `11-function-arguments`: Arguments, `*args`, `**kwargs`, parameter passing rules 16 | - `12-asterisk`: Multiple uses of `*` and `**` in Python: unpacking, merging, arguments 17 | - `13-copying`: Deep vs shallow copies, nested structures, custom class behavior 18 | - `14-context-manager`: The `with` statement, resource cleanup, class vs generator approach 19 | -------------------------------------------------------------------------------- /python-basic/01-list/README.md: -------------------------------------------------------------------------------- 1 | # Python Lists 2 | 3 | This section covers the fundamental operations and behaviors of Python `list` — an ordered, mutable collection that allows duplicate elements. 4 | 5 | ## Contents 6 | 7 | - `list_basics.py` – Creating and accessing lists 8 | - `list_methods.py` – Common list methods like `append`, `pop`, `insert`, etc. 9 | - `list_slicing.py` – Slicing, step sizes, and copying via slices 10 | - `list_copying.py` – Copying lists properly to avoid shared references 11 | - `list_comprehension.py` – List comprehensions for elegant list construction 12 | - `nested_lists.py` – Working with lists of lists 13 | 14 | ## Run Examples 15 | 16 | ```bash 17 | python list_basics.py 18 | ``` 19 | -------------------------------------------------------------------------------- /python-basic/01-list/list_basics.py: -------------------------------------------------------------------------------- 1 | # Creating a list 2 | fruits = ["banana", "cherry", "apple"] 3 | print("Original list:", fruits) 4 | 5 | # Accessing items 6 | print("First item:", fruits[0]) 7 | print("Last item:", fruits[-1]) 8 | 9 | # Changing items 10 | fruits[2] = "lemon" 11 | print("After modification:", fruits) 12 | -------------------------------------------------------------------------------- /python-basic/01-list/list_comprehension.py: -------------------------------------------------------------------------------- 1 | a = [1, 2, 3, 4, 5, 6, 7, 8] 2 | b = [x * x for x in a] 3 | print("Squares:", b) 4 | 5 | even = [x for x in a if x % 2 == 0] 6 | print("Evens:", even) 7 | -------------------------------------------------------------------------------- /python-basic/01-list/list_copying.py: -------------------------------------------------------------------------------- 1 | # Reference copy (both point to the same object) 2 | list_org = ["banana", "cherry", "apple"] 3 | list_copy = list_org 4 | list_copy.append(True) 5 | print("Shared ref:", list_copy) 6 | print("Original list also changed:", list_org) 7 | 8 | # Actual copy 9 | list_org = ["banana", "cherry", "apple"] 10 | list_copy = list_org.copy() 11 | list_copy.append(False) 12 | print("Copied list:", list_copy) 13 | print("Original unchanged:", list_org) 14 | -------------------------------------------------------------------------------- /python-basic/01-list/list_methods.py: -------------------------------------------------------------------------------- 1 | my_list = ["banana", "cherry", "apple"] 2 | my_list.append("orange") 3 | my_list.insert(1, "blueberry") 4 | print("After append & insert:", my_list) 5 | 6 | item = my_list.pop() 7 | print("Popped:", item) 8 | 9 | my_list.remove("cherry") 10 | print("After removal:", my_list) 11 | 12 | my_list.reverse() 13 | print("Reversed:", my_list) 14 | 15 | my_list.sort() 16 | print("Sorted:", my_list) 17 | -------------------------------------------------------------------------------- /python-basic/01-list/list_slicing.py: -------------------------------------------------------------------------------- 1 | a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 2 | 3 | print("Slice [1:3]:", a[1:3]) 4 | print("Slice [2:]:", a[2:]) 5 | print("Slice [:3]:", a[:3]) 6 | a[0:3] = [0] 7 | print("Replace first 3:", a) 8 | print("Every second element:", a[::2]) 9 | print("Reversed list:", a[::-1]) 10 | -------------------------------------------------------------------------------- /python-basic/01-list/nested_lists.py: -------------------------------------------------------------------------------- 1 | matrix = [[1, 2], [3, 4]] 2 | print("Matrix:", matrix) 3 | print("First row:", matrix[0]) 4 | print("Element [1][1]:", matrix[1][1]) 5 | -------------------------------------------------------------------------------- /python-basic/02-tuple/README.md: -------------------------------------------------------------------------------- 1 | # Python Tuples 2 | 3 | This section covers the core concepts and usage of Python `tuple` — an ordered, immutable collection that can store heterogeneous items. 4 | 5 | ## Contents 6 | 7 | - `tuple_basics.py` – Creating and accessing tuples 8 | - `tuple_methods.py` – Useful methods and conversions 9 | - `tuple_slicing.py` – Slicing operations 10 | - `tuple_unpacking.py` – Unpacking tuples with or without \* 11 | - `nested_tuples.py` – Tuples inside tuples 12 | - `tuple_vs_list.py` – Efficiency comparison with lists 13 | 14 | ## Run Examples 15 | 16 | ```bash 17 | python tuple_basics.py 18 | ``` 19 | -------------------------------------------------------------------------------- /python-basic/02-tuple/nested_tuples.py: -------------------------------------------------------------------------------- 1 | a = ((0, 1), ("age", "height")) 2 | print(a) 3 | print(a[0]) 4 | -------------------------------------------------------------------------------- /python-basic/02-tuple/tuple_basics.py: -------------------------------------------------------------------------------- 1 | tuple_1 = ("Max", 28, "New York") 2 | tuple_2 = "Linda", 25, "Miami" 3 | tuple_3 = (25,) 4 | 5 | print(tuple_1) 6 | print(tuple_2) 7 | print(tuple_3) 8 | 9 | tuple_4 = tuple([1, 2, 3]) 10 | print(tuple_4) 11 | 12 | # Access elements 13 | print(tuple_1[0]) 14 | print(tuple_1[-1]) 15 | 16 | # Attempting to modify raises TypeError 17 | try: 18 | tuple_1[2] = "Boston" 19 | except TypeError as e: 20 | print("Error:", e) 21 | 22 | # Delete tuple 23 | del tuple_2 24 | 25 | # Iterate 26 | for item in tuple_1: 27 | print(item) 28 | 29 | # Membership test 30 | if "New York" in tuple_1: 31 | print("yes") 32 | else: 33 | print("no") 34 | -------------------------------------------------------------------------------- /python-basic/02-tuple/tuple_methods.py: -------------------------------------------------------------------------------- 1 | my_tuple = ("a", "p", "p", "l", "e") 2 | 3 | print(len(my_tuple)) 4 | print(my_tuple.count("p")) 5 | print(my_tuple.index("l")) 6 | 7 | # Repetition 8 | repeated = ("a", "b") * 5 9 | print(repeated) 10 | 11 | # Concatenation 12 | combined = (1, 2, 3) + (4, 5, 6) 13 | print(combined) 14 | 15 | # Conversion 16 | my_list = ["a", "b", "c"] 17 | list_to_tuple = tuple(my_list) 18 | print(list_to_tuple) 19 | 20 | tuple_to_list = list(list_to_tuple) 21 | print(tuple_to_list) 22 | 23 | string_to_tuple = tuple("Hello") 24 | print(string_to_tuple) 25 | -------------------------------------------------------------------------------- /python-basic/02-tuple/tuple_slicing.py: -------------------------------------------------------------------------------- 1 | a = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 2 | 3 | print(a[1:3]) 4 | print(a[2:]) 5 | print(a[:3]) 6 | print(a[::2]) 7 | print(a[::-1]) 8 | -------------------------------------------------------------------------------- /python-basic/02-tuple/tuple_unpacking.py: -------------------------------------------------------------------------------- 1 | tuple_1 = ("Max", 28, "New York") 2 | name, age, city = tuple_1 3 | print(name) 4 | print(age) 5 | print(city) 6 | 7 | my_tuple = (0, 1, 2, 3, 4, 5) 8 | first, *middle, last = my_tuple 9 | print(first) 10 | print(middle) 11 | print(last) 12 | -------------------------------------------------------------------------------- /python-basic/02-tuple/tuple_vs_list.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import timeit 3 | 4 | my_list = [0, 1, 2, "hello", True] 5 | my_tuple = (0, 1, 2, "hello", True) 6 | 7 | print(sys.getsizeof(my_list), "bytes") 8 | print(sys.getsizeof(my_tuple), "bytes") 9 | 10 | print(timeit.timeit(stmt="[0, 1, 2, 3, 4, 5]", number=1_000_000)) 11 | print(timeit.timeit(stmt="(0, 1, 2, 3, 4, 5)", number=1_000_000)) 12 | 13 | # 104 bytes ← list 14 | # 80 bytes ← tuple 15 | 16 | # 0.03388... seconds ← list creation 1 million times 17 | # 0.00469... seconds ← tuple creation 1 million times 18 | 19 | # timeit.timeit() measures the execution time of creating the list vs. tuple 1 million times. 20 | 21 | # Tuples are faster to create because: 22 | # - There is no dynamic memory allocation needed. 23 | # - Internally, tuples are simpler data structures. 24 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/README.md: -------------------------------------------------------------------------------- 1 | # Python Dictionaries 2 | 3 | This section covers Python's `dict` type — a powerful, mutable, and indexed collection of key-value pairs. 4 | 5 | ## Contents 6 | 7 | - `dict_basics.py` – Creating and printing dictionaries 8 | - `dict_access.py` – Accessing items safely 9 | - `dict_modification.py` – Adding, updating, deleting key-value pairs 10 | - `dict_looping.py` – Iterating over keys, values, and pairs 11 | - `dict_copy_merge.py` – Copying and merging dictionaries properly 12 | - `dict_key_types.py` – Valid key types including tuples 13 | - `nested_dicts.py` – Dictionaries containing other containers 14 | 15 | ## Run Examples 16 | 17 | ```bash 18 | python dict_basics.py 19 | ``` 20 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/dict_access.py: -------------------------------------------------------------------------------- 1 | my_dict = {"name": "Max", "age": 28, "city": "New York"} 2 | 3 | # Access by key 4 | print(my_dict["name"]) 5 | 6 | # Handle missing key safely 7 | try: 8 | print(my_dict["firstname"]) 9 | except KeyError: 10 | print("No key found") 11 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/dict_basics.py: -------------------------------------------------------------------------------- 1 | my_dict = {"name": "Max", "age": 28, "city": "New York"} 2 | print(my_dict) 3 | 4 | my_dict_2 = dict(name="Lisa", age=27, city="Boston") 5 | print(my_dict_2) 6 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/dict_copy_merge.py: -------------------------------------------------------------------------------- 1 | dict_org = {"name": "Max", "age": 28, "city": "New York"} 2 | 3 | # Shallow copy (shared reference) 4 | dict_copy = dict_org 5 | dict_copy["name"] = "Lisa" 6 | print("Shared copy:", dict_copy) 7 | print("Original also changed:", dict_org) 8 | 9 | # True copy 10 | dict_org = {"name": "Max", "age": 28, "city": "New York"} 11 | dict_copy = dict_org.copy() 12 | dict_copy["name"] = "Lisa" 13 | print("Copied dict:", dict_copy) 14 | print("Original unchanged:", dict_org) 15 | 16 | # Merge 17 | dict_a = {"name": "Max", "email": "max@xyz.com"} 18 | dict_b = {"name": "Lisa", "age": 27, "city": "Boston"} 19 | dict_a.update(dict_b) 20 | print("Merged:", dict_a) 21 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/dict_key_types.py: -------------------------------------------------------------------------------- 1 | # Numeric keys 2 | my_dict = {3: 9, 6: 36, 9: 81} 3 | print(my_dict[3], my_dict[6], my_dict[9]) 4 | 5 | # Tuple as key 6 | my_tuple = (8, 7) 7 | my_dict = {my_tuple: 15} 8 | print(my_dict[my_tuple]) 9 | 10 | # Invalid: list as key (will raise error) 11 | # my_list = [8, 7] 12 | # my_dict = {my_list: 15} 13 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/dict_looping.py: -------------------------------------------------------------------------------- 1 | my_dict = {"name": "Max", "age": 28, "city": "New York"} 2 | 3 | for key in my_dict: 4 | print(key, my_dict[key]) 5 | 6 | for key in my_dict.keys(): 7 | print("Key:", key) 8 | 9 | for value in my_dict.values(): 10 | print("Value:", value) 11 | 12 | for key, value in my_dict.items(): 13 | print(f"{key}: {value}") 14 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/dict_modification.py: -------------------------------------------------------------------------------- 1 | my_dict = {"name": "Max", "age": 28, "city": "New York"} 2 | 3 | # Add or update 4 | my_dict["email"] = "max@xyz.com" 5 | print(my_dict) 6 | 7 | my_dict["email"] = "coolmax@xyz.com" 8 | print(my_dict) 9 | 10 | # Delete items 11 | del my_dict["email"] 12 | print(my_dict) 13 | 14 | popped_value = my_dict.pop("age") 15 | print("Popped:", popped_value) 16 | 17 | popped_item = my_dict.popitem() 18 | print("Popped item:", popped_item) 19 | 20 | print(my_dict) 21 | -------------------------------------------------------------------------------- /python-basic/03-dictionary/nested_dicts.py: -------------------------------------------------------------------------------- 1 | dict_a = {"name": "Max", "age": 28} 2 | dict_b = {"name": "Alex", "age": 25} 3 | 4 | nested_dict = {"dictA": dict_a, "dictB": dict_b} 5 | 6 | print(nested_dict) 7 | -------------------------------------------------------------------------------- /python-basic/04-set/README.md: -------------------------------------------------------------------------------- 1 | # Python Sets 2 | 3 | This section covers Python `set` — an unordered, mutable collection of unique elements, and `frozenset` — its immutable counterpart. 4 | 5 | ## Contents 6 | 7 | - `set_basics.py` – Creating sets and understanding their uniqueness 8 | - `set_add_remove.py` – Adding/removing elements safely 9 | - `set_operations.py` – Union, intersection, difference, symmetric difference 10 | - `set_update.py` – In-place modifications using update methods 11 | - `set_copy.py` – Copying sets with/without reference issues 12 | - `set_relations.py` – Subset, superset, and disjoint checks 13 | - `frozenset_example.py` – Read-only version of sets 14 | 15 | ## Run Examples 16 | 17 | ```bash 18 | python set_basics.py 19 | ``` 20 | -------------------------------------------------------------------------------- /python-basic/04-set/frozenset_example.py: -------------------------------------------------------------------------------- 1 | a = frozenset([0, 1, 2, 3, 4]) 2 | # a.add(5) # Error: immutable 3 | 4 | odds = frozenset({1, 3, 5, 7, 9}) 5 | evens = frozenset({0, 2, 4, 6, 8}) 6 | 7 | print("Union:", odds.union(evens)) 8 | print("Intersection:", odds.intersection(evens)) 9 | print("Difference:", odds.difference(evens)) 10 | -------------------------------------------------------------------------------- /python-basic/04-set/set_add_remove.py: -------------------------------------------------------------------------------- 1 | my_set = set() 2 | 3 | my_set.add(42) 4 | my_set.add(True) 5 | my_set.add("Hello") 6 | print(my_set) 7 | 8 | # Add duplicate 9 | my_set.add(42) 10 | print(my_set) 11 | 12 | # Remove 13 | my_set = {"apple", "banana", "cherry"} 14 | my_set.remove("apple") 15 | print(my_set) 16 | 17 | # my_set.remove("orange") # KeyError 18 | 19 | # Discard 20 | my_set.discard("cherry") 21 | my_set.discard("blueberry") 22 | print(my_set) 23 | 24 | # Clear 25 | my_set.clear() 26 | print(my_set) 27 | 28 | # Pop 29 | a = {True, 2, False, "hi", "hello"} 30 | print(a.pop()) # Random element 31 | print(a.pop()) # Random element 32 | print(a.pop()) # Random element 33 | print(a.pop()) # Random element 34 | print(a) 35 | -------------------------------------------------------------------------------- /python-basic/04-set/set_basics.py: -------------------------------------------------------------------------------- 1 | my_set = {"apple", "banana", "cherry"} 2 | print(my_set) 3 | 4 | my_set_2 = set(["one", "two", "three"]) 5 | print(my_set_2) 6 | 7 | my_set_3 = set("aaabbbcccdddeeeeeffff") 8 | print(my_set_3) 9 | 10 | # Empty set must be created with set(), not {} 11 | a = {} 12 | print(type(a)) # dict 13 | a = set() 14 | print(type(a)) # set 15 | -------------------------------------------------------------------------------- /python-basic/04-set/set_copy.py: -------------------------------------------------------------------------------- 1 | set_org = {1, 2, 3, 4, 5} 2 | set_copy = set_org # reference copy 3 | set_copy.update([6, 7]) 4 | print("Shared ref:", set_copy) 5 | print("Original affected:", set_org) 6 | 7 | set_org = {1, 2, 3, 4, 5} 8 | set_copy = set_org.copy() 9 | set_copy.update([6, 7]) 10 | print("Copied set:", set_copy) 11 | print("Original unchanged:", set_org) 12 | -------------------------------------------------------------------------------- /python-basic/04-set/set_operations.py: -------------------------------------------------------------------------------- 1 | odds = {1, 3, 5, 7, 9} 2 | evens = {0, 2, 4, 6, 8} 3 | primes = {2, 3, 5, 7} 4 | 5 | print("Union:", odds.union(evens)) 6 | print("Intersection:", odds.intersection(primes)) 7 | print("Evens ∩ Primes:", evens.intersection(primes)) 8 | 9 | setA = {1, 2, 3, 4, 5, 6, 7, 8, 9} 10 | setB = {1, 2, 3, 10, 11, 12} 11 | 12 | print("A - B:", setA.difference(setB)) 13 | print("B - A:", setB.difference(setA)) 14 | print("A △ B:", setA.symmetric_difference(setB)) 15 | -------------------------------------------------------------------------------- /python-basic/04-set/set_relations.py: -------------------------------------------------------------------------------- 1 | setA = {1, 2, 3, 4, 5, 6} 2 | setB = {1, 2, 3} 3 | setC = {7, 8, 9} 4 | 5 | print("B ⊆ A:", setB.issubset(setA)) 6 | print("A ⊇ B:", setA.issuperset(setB)) 7 | print("A ⊥ C:", setA.isdisjoint(setC)) 8 | -------------------------------------------------------------------------------- /python-basic/04-set/set_update.py: -------------------------------------------------------------------------------- 1 | setA = {1, 2, 3, 4, 5, 6, 7, 8, 9} 2 | setB = {1, 2, 3, 10, 11, 12} 3 | 4 | setA.update(setB) 5 | print("update():", setA) 6 | 7 | setA = {1, 2, 3, 4, 5, 6, 7, 8, 9} 8 | setA.intersection_update(setB) 9 | print("intersection_update():", setA) 10 | 11 | setA = {1, 2, 3, 4, 5, 6, 7, 8, 9} 12 | setA.difference_update(setB) 13 | print("difference_update():", setA) 14 | 15 | setA = {1, 2, 3, 4, 5, 6, 7, 8, 9} 16 | setA.symmetric_difference_update(setB) 17 | print("symmetric_difference_update():", setA) 18 | -------------------------------------------------------------------------------- /python-basic/05-string/README.md: -------------------------------------------------------------------------------- 1 | # Python Strings 2 | 3 | This section covers the `str` data type in Python — an immutable sequence of characters. It includes access patterns, useful methods, formatting, and performance tips. 4 | 5 | ## Contents 6 | 7 | - `string_basics.py` – Creation, quotes, escape characters, multiline 8 | - `string_access.py` – Access characters, slicing, reversing 9 | - `string_methods.py` – Built-in methods like `strip`, `upper`, `replace`, `split`, etc. 10 | - `string_format.py` – Old-style and new-style string formatting 11 | - `string_fstrings.py` – f-Strings (Python 3.6+) 12 | - `string_concat_vs_join.py` – Performance: `+` vs `join()` for large concatenation 13 | 14 | ## Run Examples 15 | 16 | ```bash 17 | python string_basics.py 18 | ``` 19 | -------------------------------------------------------------------------------- /python-basic/05-string/string_access.py: -------------------------------------------------------------------------------- 1 | my_string = "Hello World" 2 | 3 | print(my_string[0]) # H 4 | print(my_string[1:3]) # el 5 | print(my_string[:5]) # Hello 6 | print(my_string[6:]) # World 7 | print(my_string[::2]) # HloWrd 8 | print(my_string[::-1]) # dlroW olleH 9 | -------------------------------------------------------------------------------- /python-basic/05-string/string_basics.py: -------------------------------------------------------------------------------- 1 | # Single and double quotes 2 | my_string = "Hello" 3 | my_string = "Hello" 4 | my_string = "I'm a 'Geek'" 5 | print(my_string) 6 | 7 | # Escape characters 8 | my_string = 'I\'m a "Geek"' 9 | print(my_string) 10 | 11 | # Multiline string 12 | my_string = """Hello 13 | World""" 14 | print(my_string) 15 | 16 | # Backslash to continue on next line 17 | my_string = "Hello \ 18 | World" 19 | 20 | print(my_string) 21 | -------------------------------------------------------------------------------- /python-basic/05-string/string_concat_vs_join.py: -------------------------------------------------------------------------------- 1 | from timeit import default_timer as timer 2 | 3 | my_list = ["a"] * 1000000 4 | 5 | # Bad: using + 6 | start = timer() 7 | a = "" 8 | for i in my_list: 9 | a += i 10 | end = timer() 11 | print("Concatenate with + : %.5f" % (end - start)) 12 | 13 | # Good: using join 14 | start = timer() 15 | a = "".join(my_list) 16 | end = timer() 17 | print("Concatenate with join(): %.5f" % (end - start)) 18 | -------------------------------------------------------------------------------- /python-basic/05-string/string_format.py: -------------------------------------------------------------------------------- 1 | a = "Hello {0} and {1}".format("Bob", "Tom") 2 | print(a) 3 | 4 | a = "Hello {} and {}".format("Bob", "Tom") 5 | print(a) 6 | 7 | a = "The integer value is {}".format(2) 8 | print(a) 9 | 10 | a = "The float value is {0:.3f}".format(2.1234) 11 | print(a) 12 | 13 | a = "The float value is {0:e}".format(2.1234) 14 | print(a) 15 | 16 | a = "The binary value is {0:b}".format(2) 17 | print(a) 18 | 19 | # Old-style 20 | print("Hello %s and %s" % ("Bob", "Tom")) 21 | val = 10.12345 22 | print("The decimal value is %d" % val) 23 | print("The float value is %f" % val) 24 | print("The float value is %.2f" % val) 25 | -------------------------------------------------------------------------------- /python-basic/05-string/string_fstrings.py: -------------------------------------------------------------------------------- 1 | name = "Eric" 2 | age = 25 3 | print(f"Hello, {name}. You are {age}.") 4 | 5 | pi = 3.14159 6 | print(f"Pi is {pi:.3f}") 7 | 8 | print(f"The value is {2*60}") 9 | -------------------------------------------------------------------------------- /python-basic/05-string/string_methods.py: -------------------------------------------------------------------------------- 1 | my_string = " Hello World " 2 | my_string = my_string.strip() 3 | print(my_string) 4 | 5 | print(len(my_string)) 6 | print(my_string.upper()) 7 | print(my_string.lower()) 8 | print("hello".startswith("he")) 9 | print("hello".endswith("llo")) 10 | print("Hello".find("o")) 11 | print("Hello".count("e")) 12 | 13 | message = "Hello World" 14 | new_message = message.replace("World", "Universe") 15 | print(new_message) 16 | 17 | # Splitting 18 | print("how are you doing".split()) 19 | print("one,two,three".split(",")) 20 | 21 | # Joining 22 | my_list = ["How", "are", "you", "doing"] 23 | print(" ".join(my_list)) 24 | -------------------------------------------------------------------------------- /python-basic/README.md: -------------------------------------------------------------------------------- 1 | # Python Basics 2 | 3 | Fundamental data structures and operations in Python: 4 | 5 | - `01-list`: List operations, slicing, comprehensions, copy behaviors 6 | - `02-tuple`: Tuple immutability, methods, and comparisons 7 | - `03-dictionary`: Key access, merging, nested dicts, type variations 8 | - `04-set`: Set theory basics, frozensets, operations 9 | - `05-string`: String formatting, f-strings, methods, and performance tips 10 | --------------------------------------------------------------------------------