diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2aa4584 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +.vscode +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..18df3ff --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +black = "*" + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..fa87876 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,94 @@ +{ + "_meta": { + "hash": { + "sha256": "841a2d369692928656cccaf937b8e83c04af6dfa2f7addb75236739b24bff7fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "black": { + "hashes": [ + "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320", + "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351", + "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350", + "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", + "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", + "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148", + "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4", + "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d", + "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc", + "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d", + "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2", + "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f" + ], + "index": "pypi", + "version": "==22.12.0" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "pathspec": { + "hashes": [ + "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229", + "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc" + ], + "markers": "python_version >= '3.7'", + "version": "==0.11.0" + }, + "platformdirs": { + "hashes": [ + "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", + "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" + ], + "markers": "python_version >= '3.7'", + "version": "==2.6.2" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_full_version < '3.11.0a7'", + "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", + "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + ], + "markers": "python_version < '3.10'", + "version": "==4.4.0" + } + } +} diff --git a/README.md b/README.md index ef9d5e9..4310637 100644 --- a/README.md +++ b/README.md @@ -1 +1,210 @@ -# Python Para Programadores \ No newline at end of file +# Python For Programmers + +This is a "tutorial" in a cheatsheet style for programmers who want to learn +Python. Programming basics such as data types, control flow, object-oriented +programming are assumed. + +## Why? + +In recent years there has been a trend to incorporate different programming +languages to solve different problems, this practice was usually referred to as +[Polyglot +Programming](https://www.thoughtworks.com/radar/techniques/polyglot-programming). +Python in particular has been the de facto language for Artificial Intelligence +Applications (AI/ML/DP) for several years, with presence in other fields like QA +automation, Back-End Web Development, Robotic Process Automation, Command Line +Interface development and Infrastructure. Therefore, many programmers coming +from different backgrounds are and will be learning Python. + +However, Python has some distinct aspects that set it apart from most languages, +most notably it is indentation-based and it is interpreted. Not only that but it +also encourages a way to structure the code that differs substantially from +traditional .Net or Java styles. + +This tutorial aims to show what idiomatic Python looks like, the syntax +can be learned fairly quickly, but Programming Python "á la Java/.Net/Javascript" +should be avoided. + +Some examples: +- Java and C# use classes explicitly and extensively, but in Python, many things + are built so that the consumer of the code may not know that it is using + custom classes. Iterators, Context Managers, and Decorators are examples of + these patterns. +- Many design patterns are way simpler in Python due to the feature that it has, + such as first-class functions and first-class Types. No need to have many + classes with a single method if you can pass a user-defined function as a + parameter (Strategy Pattern). +- Python has no private/internal members for classes, and most members can be + treated as C# Properties, so getters and setters are rarely used. +- Python natively incorporates elements from functional programming without + going to Javascript extremes ([callback + hell](https://en.wiktionary.org/wiki/callback_hell)). Mixing it with Object + Oriented Patterns. +- Python types are not enforced but only used by the IDE as suggestions to throw + warnings. +- Many more, but you can notice them by reading the different chapters. + +## Chapters + +This tutorial is divided into chapters, each chapter consists of a file +detailing syntax and examples as well as how the outputs should look like. + +All the chapters are runnable Python files when code that would throws errors +appears, it is always commented out. + +The chapters can be read as a cookbook as there are no cross-references but the +more complex topics assume previous chapter content was understood. + +The following is a summary of each chapter. + +### 0. Introduction - Environment and Tips + +This optional chapter shows IDE configuration, themes, fonts, and extensions +useful for Python developers. + +### 1. Primitive data types and operators. + +Topics covered: + +- Arithmetic +- Logic +- Comparison Operators +- Strings +- Object None +- Non-boolean Values interpreted as Booleans +- Numeric base conversions +- String conversions to Unicode +- Bitwise Operations + +Topics to add: +- [Complex numeric type](https://docs.python.org/3/library/functions.html#complex) +- [Bytes and Bytesarray](https://docs.python.org/3/library/stdtypes.html#binary-sequence-types-bytes-bytearray-memoryview) + + +### 2. Variables and Collections + +Topics covered: + +- Lists +- Tuples, immutable collections +- Unpacking +- Dictionaries - Key-Value Collections +- Sets | Collections without duplicates +- Frozensets | Sets but immutable +- Recursive Collections + + +### 3. Control Flow + +Topics covered: + +- IF | Decision block +- For Loops +- While Loops +- Exceptions | Try Except Else Finally + +### 4. Functions + +Topics covered: + +- Basic Function Definitions +- Arbitrary parameters +- Higher-order functions +- Closures +- Partial Evaluation +- Common higher-order functions (map, filter reduce) +- Comprehensions + +Topics to add: +- [Ellipsis Object](https://docs.python.org/3/library/constants.html#Ellipsis) +- Dictionary and Set Comprehensions +- [Functools](https://docs.python.org/3/library/functools.html) + +### 5. Classes + +Topics covered: + +- Classes +- Initializer and Instance Methods +- Class Variables and Methods +- Static methods +- Dataclasses +- Operator Overloading +- Instances as Functions (`__call__`) +- Properties and Deep Copy +- Inheritance +- Constructor (`__new__`) +- Abstract Classes and Methods +- Interfaces (Protocols) +- Method Overloading +- Mixins (Multiple Inheritance) +- Descriptors + +Topics to add: +- [Generics](https://docs.python.org/3/library/stdtypes.html#generic-alias-type) + +### 6. Modules and Imports Structure + +Topics covered: + +- Import Structure +- Relative Imports +- Programmatic Imports +- Import Reloading + +### 7. Advanced Language Features + +Topics covered: + +- Additional Types + - NamedTuple and namedtuple + - Counter + - Defaultdict + - Enum + - SimpleNameSpace +- Generators +- Iterators +- Semi-coroutines (Generators with send) +- Corrutinas (AsyncIO) +- Decorators + - Stateless decorators + - Stateful Decorators +- Context Managers +- Standard Library Pearls - Pathlib +- Standard Library Pearls - Itertools +- Standard Library Pearls - OS +- Standard Library Pearls - Serialization +- Standard Library Pearls - Emails + +Topics to add: +- [TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict) +- [Secrets module](https://docs.python.org/3/library/secrets.html) + + +### 8. Python Ecosystem + +This is a special no-code chapter that consists of an infography showing which +are the most popular libraries depending on the field. Raging from Web +Development to Data Science and QA Automation. + +The image is high resolution to comfortably zooming in. + +### 9. Appendices + +Topics to add: + +- Caveats of dealing with floats (WIP) +- Type Theory (Bounds, Covariant, Contravariant, Unions) +- Metaprogramming and self-modifying code +- MultiParadigm Programming + + +## Feedback and Contact + +Contact and feedback are much appreciated, please feel free to reach out through +[LinkedIn](https://www.linkedin.com/in/ezequielcastano/) or by submitting a +GitHub issue. + +## Inspiration + +This Tutorial style was inspired by https://learnxinyminutes.com/docs/python/ diff --git a/chapter0/chapter0.md b/chapter0/chapter0.md index 63638ca..cc68057 100644 --- a/chapter0/chapter0.md +++ b/chapter0/chapter0.md @@ -1,72 +1,81 @@ -# Introducción - Entorno y Sugerencias +# Introduction - Environment and Tips -## Instalar Python +## Install Python -### Versión de Python +### Python Version -![versiones de Python](python_versions.png) +![Python versions](python_versions.png) -Source: https://devguide.python.org/#status-of-python-branches +Source: https://devguide.python.org/versions/#versions -Versiones anteriores: https://devguide.python.org/devcycle/#end-of-life-branches -## Software a Instalar +## Software to Install -- Instalar Anaconda: https://www.anaconda.com/products/individual +- Install [PyEnv](https://github.com/pyenv/pyen), a tool that will manage + different Python versions. (Windows users may use + [PyEnv-Win](https://github.com/pyenv-win/pyenv-win)) +- Install Visual Studio Code (VS Code): https://code.visualstudio.com/ +- Choose a confortable theme to your liking. -- Instalar una fuente con ligatures (recomendada Fira Code): https://github.com/tonsky/FiraCode/wiki/Installing +My recommendations in no particular order are (left to right in image below): -- Instalar Visual Studio Code (VS Code): https://code.visualstudio.com/ +- Dark+ (Installed with VS Code) +- [Gruvbox](https://marketplace.visualstudio.com/items?itemName=jdinhlife.gruvbox) +- [Monokai Pro](https://marketplace.visualstudio.com/items?itemName=monokai.theme-monokai-pro-vscode) -- Elegir un Tema cómodo a los ojos (recomendado Monokai Pro y Gruvbox Dark) +![VS Code Themes](vscode_themes.png) -![VS Code Temas](vscode_themes.png) +- Choose a confortable font (potentially with ligatures). +My recommendations in no particular order are (left to right in image below): -### Extensiones Recomendadas para VS Code: +- Consolas (Default font for VS Code) +- [Cascadia Code](https://github.com/microsoft/cascadia-code) +- [Fira Code](https://github.com/tonsky/FiraCode) +- [JetBrains Mono](https://github.com/JetBrains/JetBrainsMono) +- [OpenDyslexic](https://opendyslexic.org/) -- Python (aprox 25.2M Descargas) +![VS Code Fonts](vscode_fonts.png) -- vscode-icons (6.1M Descargas) -- Code Runner (aprox 4.9M Descargas) +### Recommended Extensions for VS Code: -- Python-autopep8 (206K Descargas) +Must Have: -- Pyright (51K Descargas) +- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python): Language Support +- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance): Automatic Type Checking +- [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens): Git with batteries +- [VS Code Icons](https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons): Nicer Icons -- Polacode-2020 (25K Descargas) +Optional +- [LiveServer](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer): For live serving HTML/CSS/JS +- [Polacode](https://marketplace.visualstudio.com/items?itemName=jeff-hykin.polacode-2019): Easy screenshots of code with same style as editor +- [WakaTime](https://marketplace.visualstudio.com/items?itemName=WakaTime.vscode-wakatime): To track time based on project and language +- [SQLite](https://marketplace.visualstudio.com/items?itemName=alexcvzz.vscode-sqlite): Useful Database Viewer for SQLite +- [ThunderClient](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client): Postman-replacement for API Testing -Theme Recomendado (Personal): **Monokai Pro** o **Gruvbox** -## Configuración Custom de VS Code (settings.json): +## Custom VS Code configuration (settings.json): -- Usar CMD en lugar de Powershell: +- Enable AutoSave - "terminal.integrated.shell.windows": "C:\\WINDOWS\\Sysnative\\cmd.exe" + "files.autoSave": "afterDelay" -- Reglas para guia +- Make sure that Pylance is checking your types: - "editor.rulers": [79, 120] + "python.analysis.typeCheckingMode": "strict" -- Habilitar ligaduras (Sólo con Fira Code o similar) - "editor.fontLigatures": true +- Rules for guide + + "editor.rulers": [79, 120]. +- Enable ligatures (Only with Fira Code or similar). -## Capítulos + "editor.fontLigatures": true -### 1. Tipos de datos primitivos y operadores. -### 2. Variables y Colecciones -### 3. Control de Flujo -### 4. Funciones -### 5. Classes -### 6. Módulos y estructura de imports -### 7. Aspectos avanzados del lenguaje -### 8. Recursos adicionales -### 9. Apéndices +- Move the activity bar to the right (personal choice) + "workbench.sideBar.location": "right" -## Fuente -Tutorial adaptado de https://learnxinyminutes.com/docs/es-es/python-es/ diff --git a/chapter0/fonts/cascadia_code/theme_test.py b/chapter0/fonts/cascadia_code/theme_test.py new file mode 100644 index 0000000..322d896 --- /dev/null +++ b/chapter0/fonts/cascadia_code/theme_test.py @@ -0,0 +1,38 @@ +# Font Name: Cascadia Code +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/fonts/consolas/theme_test.py b/chapter0/fonts/consolas/theme_test.py new file mode 100644 index 0000000..4d91876 --- /dev/null +++ b/chapter0/fonts/consolas/theme_test.py @@ -0,0 +1,38 @@ +# Font Name: Consolas (Default in VS Code) +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/fonts/fira_code/theme_test.py b/chapter0/fonts/fira_code/theme_test.py new file mode 100644 index 0000000..ce00d2f --- /dev/null +++ b/chapter0/fonts/fira_code/theme_test.py @@ -0,0 +1,38 @@ +# Font Name: Fira Code +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/fonts/jetbrains_mono/theme_test.py b/chapter0/fonts/jetbrains_mono/theme_test.py new file mode 100644 index 0000000..3f0d21e --- /dev/null +++ b/chapter0/fonts/jetbrains_mono/theme_test.py @@ -0,0 +1,38 @@ +# Font Name: JetBrains Mono +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/fonts/open_dyslexic/theme_test.py b/chapter0/fonts/open_dyslexic/theme_test.py new file mode 100644 index 0000000..b0d304b --- /dev/null +++ b/chapter0/fonts/open_dyslexic/theme_test.py @@ -0,0 +1,38 @@ +# Font Name: OpenDyslexic3 +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/python_versions.png b/chapter0/python_versions.png index 3a24d55..7c1d18c 100644 Binary files a/chapter0/python_versions.png and b/chapter0/python_versions.png differ diff --git a/chapter0/theme_test.py b/chapter0/theme_test.py deleted file mode 100644 index 88c3882..0000000 --- a/chapter0/theme_test.py +++ /dev/null @@ -1,34 +0,0 @@ -una_variable = 100 - -hex(10) -int("0xa", 16) - -def dividir(x, y): # Comentario - return x / y - - -if una_variable % 2 == 0: - print("El valor es par") -elif una_variable < 0: - print("El valor es impar y negativo") -elif una_variable > 100: - print("El valor es impar y mayor a 100") -else: - print("El valor no cumple las condiciones") - -nombres = ["Juan", "Pedro", "Maria"] -edades = [60, 15, 84] -for nombre, edad in zip(nombres, edades): # Zip combina listas - print(f"{nombre} tiene {edad} años") - -class Rectangulo: - def __init__(self, base: float, altura: float) -> None: - self.base: float = base - self.altura: float = altura - - def area(self) -> float: - return self.base * self.altura - -rec = Rectangulo(10, 10) -rec.base -rec.area() \ No newline at end of file diff --git a/chapter0/themes/gruvbox/theme_test.py b/chapter0/themes/gruvbox/theme_test.py new file mode 100644 index 0000000..f94ce62 --- /dev/null +++ b/chapter0/themes/gruvbox/theme_test.py @@ -0,0 +1,37 @@ +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/themes/monokai_pro/theme_test.py b/chapter0/themes/monokai_pro/theme_test.py new file mode 100644 index 0000000..f94ce62 --- /dev/null +++ b/chapter0/themes/monokai_pro/theme_test.py @@ -0,0 +1,37 @@ +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/themes/vscodedark/theme_test.py b/chapter0/themes/vscodedark/theme_test.py new file mode 100644 index 0000000..f94ce62 --- /dev/null +++ b/chapter0/themes/vscodedark/theme_test.py @@ -0,0 +1,37 @@ +a_variable = 100 + +hex(10) +int("0xa", 16) + + +def multiply(x: float, y: float) -> float: # Comment + return x * y + + +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("The value is odd and negative.") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +names = ["John", "Peter", "Mary"] +ages = [60, 15, 84] +for name, age in zip(names, ages): + print(f"{name} is {age} years old") + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base +rec.area() diff --git a/chapter0/vscode_fonts.png b/chapter0/vscode_fonts.png new file mode 100644 index 0000000..fc3f24b Binary files /dev/null and b/chapter0/vscode_fonts.png differ diff --git a/chapter0/vscode_themes.png b/chapter0/vscode_themes.png index e7eb8c5..c3bf426 100644 Binary files a/chapter0/vscode_themes.png and b/chapter0/vscode_themes.png differ diff --git a/chapter1.py b/chapter1.py index 6617712..cb53503 100644 --- a/chapter1.py +++ b/chapter1.py @@ -1,42 +1,42 @@ #################################################### -# 1. Tipos de datos primitivos y operadores. +# 1. Primitive data types and operators. #################################################### -# Comentarios de una línea comienzan con una almohadilla (o numeral) +# One-line comments begin with a hash (or numeral). -""" Strings multilinea pueden escribirse - usando tres ", y comunmente son usados - como comentarios. +""" Multi-line strings can be written + using three ", and are commonly used as comments. + as comments. """ #################################################### -# 1.1 Aritmética +# 1.1 Arithmetic #################################################### 1 + 1 # => 2 8 - 1 # => 7 10 * 2 # => 20 -5 ** 2 # => 25 Potencia -pow(5, 2) # => 25 Potencia -25 ** 0.5 # => 5 Raiz con Potencia fraccionaria -35 / 5 # => 7.0 División (devuelve float) -35 / 0 # => Error -34 // 5 # => 6 División entera (trunca el cociente) -35 % 6 # => 5 Operador Módulo (resto) -3 * 2.0 # => 6.0 Si uno de los operandos es float, el resultado es float +5**2 # => 25 Power +pow(5, 2) # => 25 Power +25**0.5 # => 5 Root with Fractional Power +35 / 5 # => 7.0 Division (returns float) +# 35 / 0 # => Error +34 // 5 # => 6 Integer division (truncates the quotient) +35 % 6 # => 5 Modulo operator (remainder) +3 * 2.0 # => 6.0 If one of the operands is float, result is float #################################################### -# 1.2 Lógica +# 1.2 Logic #################################################### -# Valores 'boolean' (booleanos) son primitivos +# boolean values are primitive True False -# Operadores booleanos nativos +# Native boolean operators # not not True # => False @@ -54,18 +54,18 @@ False or True # => True False or False # => False -# Cortocircuito por defecto +# Default Short Circuit True and False and 1 / 0 # => False -True and True and 1 / 0 # => Error +# True and True and 1 / 0 # => Error False or True or 1 / 0 # => True -False or False or 1 / 0 # => Error +# False or False or 1 / 0 # => Error #################################################### -# 1.3 Operadores de Comparación +# 1.3 Comparison Operators #################################################### -# Operadores básicos +# Basic operators 1 == 1 # => True 1 != 1 # => False 1 < 10 # => True @@ -73,7 +73,7 @@ 2 <= 2 # => True 2 >= 2 # => True -# Comparaciones Combinadas +# Combined Comparisons 1 < 2 < 3 # => True 1 < 3 < 2 # => False 1 < 0 < 1 / 0 # => False (cortocircuito) @@ -82,35 +82,35 @@ #################################################### -# 1.4 Cadena de caracteres (Strings) +# 1.4 Strings #################################################### -# Strings se crean con ", ' o """" -"Esto es un string." -'Esto también es un string.' +# Strings are created with ", ', """" or ''' +"This is a string." +'This is also a string.' -"""Las strings con triple -comillas pueden ser multilinea""" # Se inserta un \n al final de la línea +"""Strings with triple +quotes can be multi-line""" # A \n is inserted at the end of each line. -"Hola " + "mundo!" # => "Hola mundo!" Concatenación -"Hola " "mundo!" # => "Hola mundo!" Concatenación Automática -"Esto es un string"[0] # => 'E' String como Lista +"Hello " + "world!" # => "Hello world!" Concatenation +"Hello " "world!" # => "Hello world!" Automatic Concatenation +"This is a string"[0] # => 'E' String as List -# Formateo de Strings con format -nombre = "Ezequiel" -precio = 12.50 -descuento = 0.8 -comida = "lasaña" -"{} debe pagar {}$".format(nombre, precio) # => "Ezequiel debe pagar 12.50$" -"{0} no vino, {0} se fue, {0} debe aún {1}$".format(nombre, precio * descuento) # => "Ezequiel no vino, Ezequiel se fue, Ezequiel debe aún 10$" -"{nombre} quiere comer {comida}".format(comida=comida, nombre="Bob") # => "Bob quiere comer lasaña" +# Formatting Strings with format +name = "Ezekiel" +price = 12.50 +discount = 0.8 +food = "lasagna" +"{} must pay {}$".format(name, price) # => "Ezequiel must pay $12.50" +"{0} didn't come, {0} left, {0} still owes {1}$".format(name, price * discount) # => "Ezekiel didn't come, Ezekiel left, Ezekiel still owes $10." +"{name} wants to eat {food}".format(food=food, name="Bob") # => "Bob wants to eat lasagna." -# Formateo de Strings con f-Strings -f'{nombre} quiere comer {comida}' # => "Ezequiel quiere comer lasaña" +# Formatting Strings with f-Strings. +f'{name} wants to eat {food}' # => "Ezekiel wants to eat lasagna" #################################################### -# 1.5 Objeto None +# 1.5 Object None #################################################### True is None # => False @@ -119,7 +119,7 @@ #################################################### -# 1.6 Valores interpretados como booleanos +# 1.6 Non-boolean Values interpreted as Booleans #################################################### bool(0) # => False @@ -131,7 +131,7 @@ bool([]) # => False bool([3]) # => True -# Los valores anteriores pueden usarse como booleanos +# The above values can be used as booleans not "1" # => False not [] # => True @@ -154,14 +154,14 @@ #################################################### -# 1.7 Conversiones númericas de base +# 1.7 Numeric base conversions #################################################### # Decimal str(10) # => 10 int("10") # => 10 -# Binario +# Binary bin(10) # => '0b1010' int("0b1010", 2) # => 10 int("1010", 2) # => 10 @@ -174,13 +174,13 @@ # Hexadecimal hex(10) # => '0xa' int("0xa", 16) # => 10 -int("0xA", 16) < # => 10 +int("0xA", 16) # => 10 int("a", 16) # => 10 int("A", 16) # => 10 #################################################### -# 1.7 Conversiones de string +# 1.7 String conversions to Unicode #################################################### chr(65) # => "A" @@ -190,3 +190,17 @@ ord("A") # => 65 ord("¿") # => 191 ord("€") # => 8364 + +#################################################### +# 1.8 Bitwise Operations +#################################################### + +~10 # NOT => not 1010 = -1011 = -11 +10 & 9 # AND => 1010 * 1001 = 1000 = 8 +10 | 9 # OR => 1010 + 1001 = 1011 = 11 +10 ^ 9 # XOR => 1010 ^ 1001 = 0011 = 3 +10 >> 1 # Right Shift => 1010 >> 1 = 101 = 5 +10 << 1 # Left Shift => 1010 << 1 = 10100 = 20 + +# More on Bit Operations on Anurag Verma's post: +# https://www.anurag629.club/posts/the-power-of-bit-manipulation-how-to-solve-problems-efficiently diff --git a/chapter2.py b/chapter2.py new file mode 100644 index 0000000..f596ee3 --- /dev/null +++ b/chapter2.py @@ -0,0 +1,293 @@ +#################################################### +# 2. Variables and Collections +#################################################### + +# Python has a function to print +print("I'm Python. Nice to meet you.") + +# There is no need to declare variables before assigning them. +a_variable = 5 # The convention is to use lowercase underscores (snake_case). +one_variable: float = 5 # Optional type-hints | RECOMMENDED +one_variable # => 5 +# other_variable # Error unassigned variable + +# Operators with re-assignment +one_variable += 2 # one_variable == 7 +one_variable -= 1 # one_variable == 6 +one_variable *= 5 # one_variable == 30 +one_variable /= 5 # one_variable == 3.0 +one_variable **= 3 # one_variable == 27.0 +one_variable %= 10 # one_variable == 7.0 +one_variable //= 5 # one_variable == 1.0 + +# Multiple assignment +a = b = 3 # a = 3 and b = 3 + + +#################################################### +# 2.1 Lists +#################################################### + +# Lists stores sequences +some_list = [] # Empty +other = [4, 5, 6] # With initial values +multiple = [2, "John", [2]] # Values of different type (heterogeneous) + +# List methods +some_list.append(1) # Add an element at the end +some_list.extend([2, 4, 3]) # Add multiple elements +some_list.pop() # => 3 and list=[1, 2, 4]) +some_list.insert(3, 3) # Add an element at the given position +len(some_list) # 4 + +# Simple indexing +some_list[0] # => 1 First element +some_list[-1] # => 4 Last element +# some_list[4] # Error - Out of bounds + +# Slicing list[start:end:step] +some_list[1:3] # => [2, 4] +some_list[2:] # => [4, 3] +some_list[:3] # => [1, 2, 4] +some_list[::2] # => [1, 4] +some_list[::-1] # => [3, 4, 2, 1] +some_list[:] # => Creates an identical copy of list + + +# Operations with Lists +some_list + other # => [1, 2, 4, 3, 4, 5, 6] +some_list * 2 # => [1, 2, 4, 3, 1, 2, 4, 3] + +# Operator in +1 in some_list # => True + +# The not operator can be used before or after +not 5 in some_list # => True +5 not in some_list # => True + +# Operator == +some_list == [1, 2, 4, 3] # => True +some_list == some_list[:] # => True +some_list == some_list # => True + +# Operator is +some_list is [1, 2, 4, 3] # => False +some_list is some_list[:] # => False +some_list is some_list # => True + + +# Special operations for Boolean lists +any(some_list) # => True | Returns True if at least one of the elements is True +all(some_list) # => True | Returns True if all the elements are True + + +#################################################### +# 2.2 Tuples, immutable collections +#################################################### + +empty_tuple = tuple() # Constructed with the tuple function + +some_tuple = (1, 2, 3) # They are defined with (,) instead of [] +some_tuple = 1, 2, 3 # Parentheses are optional +some_tuple[0] # => 1 +# some_tuple[0] = 3 # TypeError + +one_element_tuple = (3,) # Comma is mandatory +one_element_tuple = 3, # Parentheses are optional + +# Methods identical to lists but without assignment +len(some_tuple) # => 3 +some_tuple + (4, 5, 6) # => (1, 2, 3, 4, 5, 6) +some_tuple[:2] # => (1, 2) +2 in some_tuple # => True + + +#################################################### +# 2.3 Unpacking +#################################################### + +# Simple unpacking +a, b, c = (1, 2, 3) # a == 1, b == 2, c == 3 +a, b, c = [1, 2, 3] # a == 1, b == 2, c == 3 +# a, b = [1, 2, 3] # Error | Number of elements must be identical +a, b = b, a # Exchange a == 2, b == 1 + +# Unpacking With wildcards +a, *rest = [1, 2, 3, 4] # a == 1, rest == [2, 3, 4] +*rest, b = [1, 2, 3, 4] # b == 4, rest == [2, 3, 4] +a, *rest, b = [1, 2, 3, 4] # a == 1, b == 4, rest == [2, 3] + +# Nested Unpacking +(a, b), c = [[1, 2], [3]] # a == 1, b == 2, c == [3] + + +#################################################### +# 2.4 Dictionaries - Key-Value Collections +#################################################### + +empty_dictionary = {} # Empty +dictionary = { + "one": 1, # Multiline declaration + "two": 2, + "three": 3, # Comma at the end valid +} +dictionary["one"] # => 1 - Indexed with Keys +# dictionary["four"] # Error + +dictionary.get("one") # => 1 +dictionary.get("four") # => None instead of Error +dictionary.get("one", 4) # => 1 +dictionary.get("four", 4) # => Default value instead of None + +# Methods +list(dictionary.keys()) # => ["three", "two", "one"] # => ["three", "two", "one"] +list(dictionary.values()) # => [3, 2, 1] # => ["three", "two", "one"] # => [3, 2, 1 +list(dictionary.items()) # => [('one', 1), ('two', 2), ('three', 3)] + +# Operators with Dictionaries | in verifies the keys. +"one" in dictionary # => True +1 in dictionary # => False + +# Dictionary update +new_data = {"four": None, "five": 5} +dictionary.update(new_data) +dictionary # {'one': 1, 'two': 2, 'three': 3, 'four': None, 'five': 5} + +# Keys and values could be Heterogeneous +multiple = { + "one": 1, + 2: "two", + (1, 3): [1, 5], +} + +# Keys must be inmutable (hashable) +# invalid = {[1, 2]: "1"} # Error + + +#################################################### +# 2.4 Sets | Collections without duplicates +#################################################### + +empty_set = set() +some_set = {1, 2, 2, 2, 3, 4} # => {1, 2, 3, 4} +some_set.add(5) # => {1, 2, 3, 4, 5} +some_set.add(6) # => {1, 2, 3, 4, 5, 6} +some_set.discard(7) # => {1, 2, 3, 4, 5, 6} +# some_set.remove(7) # => Error | Remove assumes element is in set +some_set.remove(6) # => {1, 2, 3, 4, 5, 6} + +# Set Operations +other_set = {3, 4, 5, 6} + +# In Operator +2 in some_set # => True + +# Intersection +some_set & other_set # => {3, 4, 5} +some_set.intersection(other_set) # => {3, 4, 5} + +# Union +some_set | other_set # => {1, 2, 3, 4, 5, 6} +some_set.union(other_set) # => {1, 2, 3, 4, 5, 6} + +# Difference +some_set - other_set # => {1, 2} +some_set.difference(other_set) # => {1, 2} + +# Symmetric difference +some_set ^ other_set # => {1, 2, 5, 6} +some_set.symmetric_difference(other_set) # => {1, 2, 5, 6} + +# Subset +subset = {1, 2, 3, 4, 5} +subset <= some_set # => True +subset.issubset(some_set) # => True + +# Proper Subset +proper_subset = {1, 2, 3} +subset < some_set # => False +proper_subset < some_set # => True + +# Superset +superset = {1, 2, 3, 4, 5} +superset >= some_set # => True +superset.issuperset(some_set) # => True + +# Proper Superset +proper_superset = {1, 2, 3, 4, 5, 6} +superset > some_set # => False +proper_superset > some_set # => True + +# Disjoint +extra_set = {9, 10, 11} +some_set.intersection(other_set) == set() # => False +some_set.isdisjoint(other_set) # => False + +some_set.intersection(extra_set) == set() # => True +some_set.isdisjoint(extra_set) # => True + +#################################################### +# 2.5 Frozensets | Sets but immutable +#################################################### + +empty_frozenset = frozenset() + +some_frozenset = frozenset({1, 2, 3}) # Can be created from a set +some_frozenset = frozenset([1, 2, 3]) # Or any other iterable +# some_frozenset.add(3) # AttributeError + +# Methods identical to sets but without assignment +some_frozenset = frozenset({1, 2, 3, 4, 5, 6}) +other_set = {3, 4, 5, 6} +other_frozenset = frozenset({3, 4, 5, 6}) + +len(some_frozenset) # => 6 +some_frozenset | other_set # => frozenset({1, 2, 3, 4, 5, 6}) +some_frozenset | other_frozenset # => frozenset({1, 2, 3, 4, 5, 6}) +2 in some_frozenset # => True + + +#################################################### +# 2.6 Recursive Collections +#################################################### + +""" +Although it is not common, due to the Python having mutuable and being +(call-by-sharing)[https://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_sharing], +it is possible to have recursive data structure, that is, data structures that +contain themselves. + +This could be seen in some complex object structures where composition is +heavily used and components are tightly connected. +""" + +# Recursive Lists +a = [] # Typical Empty list +a.append(a) # Addind the list to itself - a => [[...]] +a == a # A list is equal to itself +a == a[0] # A list is equal to its first argument +a == a[0][0] # A list is equal to the first argument of its first argument +a == a[0][0][0] # And so on.. + + +# Same applies with dictionaries +a = {} +a["a"] = a +a == a +a == a["a"] +a == a["a"]["a"] +a == a["a"]["a"]["a"] + + +# Data Structures could be mutually nested +b = {} +a = [b] +b["a"] = a + +a == a +a[0] == b +a[0]['a'] == a + +b == b +b['a'] == a +b['a'][0] == b diff --git a/chapter3.py b/chapter3.py new file mode 100644 index 0000000..8a3871f --- /dev/null +++ b/chapter3.py @@ -0,0 +1,229 @@ +#################################################### +# 3. Control Flow +#################################################### + +#################################################### +# 3.1 IF | Decision block +#################################################### + +a_variable = 5 + + +if a_variable > 10: + print("Value is greater than 10") + +if a_variable: # Implicit conversion to bool + print("Variable must not be 0.") + +# Else + +if a_variable >= 0: + print("Value is positive") +else: + print("Value is negative") + +# Multiple conditions +if a_variable % 2 == 0: + print("Value is even") +elif a_variable < 0: + print("Value is odd and negative") +elif a_variable > 100: + print("The value is odd and greater than 100.") +else: + print("The value does not meet the conditions.") + +# Ternary operator +age = 30 +greater_of_age = "Yes" if age >= 18 else "No" +# Equivalent in C/Java/Net/Javascript +# age_age = age >= 18 ? "Yes" : "No" + + +#################################################### +# 3.2 For Loops +#################################################### + +list(range(4)) # => [0, 1, 2, 3] +for i in range(4): + print(i) +# 0 +# 1 +# 2 +# 3 + +# Foreach + +# For and Foreach are the same in Python +for animal in ["dog", "cat", "mouse"]: + print(f"{animal} is a mammal") +# dog is a mammal +# cat is a mammal +# mouse is a mammal + + +# Enumerate +names = ["John", "Peter", "Mary"] + +for index, name in enumerate(names): # adds indices + print(f"{index} - {name}") +# 0 - John +# 1 - Peter +# 2 - Mary + +for index, name in enumerate(names, start=9): # Can start at any position + print(f"{index} - {name}") +# 9 - John +# 10 - Peter +# 11 - Mary + + +# Zip + +ages = [60, 15, 84] +for name, age in zip(names, ages): # Combines Iterables + print(f"{name} is {age} years old") +# John is 60 years old +# Peter is 15 years old +# Mary is 84 years old + + +# Zip and Enumerate can be combined +for index, (name, age) in enumerate(zip(names, ages)): + print(f"{index} - {name} is {age} years old") +# 0 - John is 60 years old +# 1 - Peter is 15 years old +# 2 - Mary is 84 years old + + +# For with Dictionaries + +students = {"John": 60, "Peter": 15, "Mary": 84} + +for name, age in students.items(): + print(f"{name} is {age} years old") +# John is 60 years old +# Peter is 15 years old +# Mary is 84 years old + + +# For and If + +# Name: [Age, Grade] +students = {"John": [60, 7.5], "Peter": [15, 4.1], "Mary": [84, 9.5]} + +for name, (age, grade) in students.items(): + if grade >= 6: + print(f"{name} passed with {grade} points, being {age} years old") +# John passed with 7.5 points, being 60 years old. +# Mary passed with 9.5 points, being 84 years old. + + +# For and break + +searched = "Peter" +for name, (_, grade) in students.items(): + if name == searched: + print(f"{name} got {grade} points") + break +# Peter got 4.1 points + + +# For and Else +# The else block is executed ONLY if the loop did NOT break + +search = "Martín" +for name, (_, grade) in students.items(): + if name == searched: + print(f"{name} scored {grade} points") + break +else: + print("There is no such student") +# This student does not exist + + +# For and Continue + +# Apply a discount of 20% to all prices greater than 8.0 +prices = [6.43, 7.94, 9.23, 7.97, 4.84, 9.71, 6.52, 8.94, 8.62, 9.72] +for index, price in enumerate(prices): + if price <= 8.0: + continue + + prices[index] *= 0.8 + +prices # => [6.43, 7.94, 7.384, 7.97, 4.84, 7.768, 6.52, 7.152, 6.896, 7.776] + + +#################################################### +# 3.2 While Loops +#################################################### + +# The while loop operates identically to the for loop with break, continue and else. + +# Traditional While loop +x = 0 +while x < 4: + print(x) + x += 1 + +# Do-While Loop +x = 0 +while True: + print(x) + x += 1 + + if x < 4: + break + + +#################################################### +# 3.3 Exceptions | Try Except Else Finally +#################################################### + +try: + a = 1 / 0 +except ZeroDivisionError as exception: + print(f"An error has occurred | {exception}") + +# An error has occurred | division by zero + + +try: + a = 1 / 0 +except ZeroDivisionError as exception: + print(f"An error has occurred | {exception}") +finally: + print("Process completed") +# An error has occurred | division by zero +# Process finished + + +# Some common exceptions +# Complete list: https://docs.python.org/3/library/exceptions.html + +try: + some_list = [1, 2, 3] + list[3] +except IndexError as exception: + print(f"Out-of-range indexes cannot be used | {exception}") + +try: + a = 1 / 0 +except ZeroDivisionError as exception: + print(f"Cannot divide by zero | {exception}") + +try: + grades = {"John": 2, "Mary": 3} + grades["Alejandro"] +except KeyError as exception: + print(f"Only defined keys can be used | {exception}") + +try: + print(hello) +except NameError as exception: + print(f"Only defined variables can be used | {exception}") + +try: + a, b = [1, 2, 3] +except ValueError as exception: + print(f"Values provided do not match expert | {exception}") diff --git a/chapter4.py b/chapter4.py new file mode 100644 index 0000000..e09e391 --- /dev/null +++ b/chapter4.py @@ -0,0 +1,206 @@ +#################################################### +# 4. Functions +#################################################### + + +def divide(x, y): # Function and its parameters + return x / y + + +def without_return(x, y): # Default returns None + x / y + + +def divide(x: float, y: float) -> float: # Type-Hints | RECOMMENDED + return x / y + + +def without_return(x: float, y: float) -> float: # Recommendations with Type-Hints + x / y + + +assert divide(10, 8) == 1.25 # Order identical to the definition +assert divide(8, 10) == 0.8 # Order is important +assert divide(x=10, y=8) == 1.25 # Using keyword parameters +assert divide(y=8, x=10) == 1.25 # Order is irrelevant when using keyword parameters + + +def is_older_age(age: int, limit: int = 18) -> bool: # Default value + if age >= limit: + result = True + else: + result = False + return result + + +def is_older_age(age: int, limit: int = 18) -> bool: # Multiple returns + if age >= limit: + return True + return False + + +def is_older_age(age: int, limit: int = 18) -> bool: # Return expression + return age >= limit + + +# In all cases limit is assumed to be 18 +assert not is_older_age(10) +assert is_older_age(18) +assert is_older_age(24) + +# It can be passed explicitly as well +assert is_older_age(24, 18) +assert is_older_age(24, limit=18) + + +from typing import List, Tuple # Standard Library. + +prices: List[float] = [4.04, 5.37, 7.77, 0.09, 9.11, 4.96, 9.12, 2.28, 8.09, 7.36] + + +# Return with multiple values +def is_discounted(prices: List[float]) -> Tuple[bool, float]: + lowest_price = min(prices) + if lowest_price < 3: + return True, lowest_price + return False, lowest_price + + +assert is_discounted(prices) == (True, 0.09) # => Return Tuple +exists_offer, amount = is_discounted(prices) # => Unpacked +assert exists_offer +assert amount == 0.09 + + +#################################################### +# 4.1 Arbitrary parameters +#################################################### + + +def summation(*args: float): # Arbitrary positional parameters + result = 0 + for value in args: + result += value + return result + + +assert summation(1, 2, 3) == 6 + + +from typing import Dict # Standard Library + + +def concatenate(**kwargs: str): # Arbitrary keyword parameters + return " ".join(kwargs.values()) + + +concatenate(a="Hello", b="World") # => 'Hello World ' + +numbers: List[float] = [1, 2, 3, 4] +words: Dict[str, str] = {"a": "Hello", "b": "World"} +assert summation(*numbers) == 10 +assert concatenate(**words) == "Hello World" + + +#################################################### +# 4.2 Higher order functions +#################################################### + +from typing import Callable # Standard library + +# Functions as parameters + + +def apply(list: List[float], function: Callable[[float], float]) -> List[float]: + results = [] + for element in list: + result = function(element) + results.append(result) + return results + + +def square(x: float) -> float: + return x**2 + + +some_list: List[float] = [1, 2, 3, 4, 5, 6] +assert apply(some_list, square) == [1, 4, 9, 16, 25, 36] + + +# Functions within functions (Closures) + + +def power(y: float) -> Callable[[float], float]: + def auxiliary(x: float) -> float: + return x**y + + return auxiliary + + +some_list: List[float] = [1, 2, 3, 4, 5, 6] +square_power: Callable[[float], float] = power(2) +assert apply(some_list, square_power) == [1, 4, 9, 16, 25, 36] + + +# Partial evaluation + +from functools import partial # Standard library + + +def x_to_the_y_power(x: float, y: float) -> float: + return x**y + + +some_list: List[float] = [1, 2, 3, 4, 5, 6] +square_partial: Callable[[float], float] = partial(x_to_the_y_power, y=2) +assert apply(some_list, square_partial) == [1, 4, 9, 16, 25, 36] + + +# Anonymous functions (Lambdas) + +some_list: List[float] = [1, 2, 3, 4, 5, 6] +assert apply(some_list, lambda x: x**2) == [1, 4, 9, 16, 25, 36] + + +#################################################### +# 4.3 Common higher-order functions (map, filter reduce) +#################################################### + +from typing import Iterator # Standard library +from functools import reduce # Standard library + +some_list: List[float] = [1, 2, 3, 4, 5, 6] +squares: Iterator[float] = map(lambda x: x**2, some_list) # => [1, 4, 9, 16, 25, 36] +squares_filtered: Iterator[float] = filter(lambda x: x > 5, squares) # => [9, 16, 25, 36] +sum_filtered: float = reduce(lambda x, y: x + y, squares_filtered) # => 86 + +assert sum_filtered == 86 + + +#################################################### +# 4.4 Comprehensions +#################################################### + +some_list: List[float] = [1, 2, 3, 4, 5, 6] +squares_: List[float] = [square_power(x) for x in some_list] # => [1, 4, 9, 16, 25, 36] +squares_filtered_: List[float] = [x for x in squares_ if x > 5] # => [9, 16, 25, 36] +sum_filtered: float = summation(*squares_filtered_) # => 86 + +assert sum_filtered == 86 + +some_list: List[float] = [1, 2, 3, 4, 5, 6] +sum_filtered: float = summation(*[square_power(x) for x in some_list if square_power(x) > 5]) + +assert sum_filtered == 86 + + +# Code equivalent using a FOR loop + +result: float = 0 + +for element in some_list: + auxiliary: float = square_power(element) + if auxiliary > 5: + result += auxiliary + +assert result == 86 diff --git a/chapter5.py b/chapter5.py new file mode 100644 index 0000000..10aa5d1 --- /dev/null +++ b/chapter5.py @@ -0,0 +1,683 @@ +#################################################### +# 5. Classes +#################################################### + + +#################################################### +# 5.1 Initializer and Instance Methods +#################################################### + +from __future__ import annotations + + +class Rectangle: + def __init__(self, base: float, height: float) -> None: + self.base: float = base + self.height: float = height + + def area(self) -> float: + return self.base * self.height + + +rec = Rectangle(10, 10) +rec.base # => 10 +rec.height # => 10 +rec.area() # => 100 + +Rectangle(10, 10).area() # => 100 +Rectangle(10, 0).area() # => 0 +Rectangle(0, 10).area() # => 0 + + +#################################################### +# 5.2 Class Variables and Methods +#################################################### + + +class BaseArticle: + _last_id: int = 0 + + def __init__(self, name: str = "") -> None: + self.name: str = name + self.id_: int = self._get_next_id() + + @classmethod + def _get_next_id(cls): + cls._last_id += 1 + return cls._last_id + + +art1 = BaseArticle("apple") +art2 = BaseArticle("pear") +art3 = BaseArticle() +art3.name = "tv" + +art1.name # => "apple" +art2.name # => "pear" +art3.name # => "tv" + +art1.id_ # => 1 +art2.id_ # => 2 +art3.id_ # => 3 + + +#################################################### +# 5.3 Static methods +#################################################### + + +class Temperature: + def __init__(self, region: str, temperature: float) -> None: + self.region = region + self.temperature = temperature + + @staticmethod + def celcius_to_farenheit(temperature: float) -> float: # Without self + return 32 + temperature * 9 / 5 + + @staticmethod + def farenheit_to_celcius(temperature: float) -> float: # Without self + return (temperature - 32) * 5 / 9 + + +temperature_today = Temperature("Mesopotamia", 35) + +assert Temperature.celcius_to_farenheit(35) == 95 # Invocation from class +assert Temperature.farenheit_to_celcius(95) == 35 # Call from class +assert temperature_today.celcius_to_farenheit(35) == 95 # Invocation from instance +assert temperature_today.farenheit_to_celcius(95) == 35 # Invocation from instance + + +#################################################### +# 5.4 Dataclasses +#################################################### + + +from typing import ClassVar +from dataclasses import dataclass, field +import uuid + + +class Person: + _personal_id: int = 0 + + def __init__(self, name: str, age: int, height: float, properties: Optional[List[str]] = None) -> None: + self.name = name + self.age = age + self.height = height + self.properties = properties or [] + self.member_id = f"{name[0].upper()}-{str(uuid.uuid4())[:8]}" + self.personal_id = str(Person._get_next_personal_id()).zfill(8) + + def is_old_enough(self) -> bool: + return self.age >= 17 + + @classmethod + def _get_next_personal_id(cls) -> int: + cls._personal_id += 1 + return cls._personal_id + + +john: Person = Person("John", 18, 175.9) +john.is_old_enough() # => True +Person("Julia", 16, 162.4).is_old_enough() # => False +print(john) # => <__main__.Persona object at 0x000001C90BBF8688> + + +@dataclass +class PersonDataClass: + name: str + age: int + gender: str + weight: float + height: float + properties: List[str] = field(default_factory=list) + member_id: str = field(init=False) + personal_id: str = field(init=False) + _personal_id: ClassVar[int] = 0 + + def __post_init__(self): + self.member_id: str = f"{self.name[0].upper()}-{str(uuid.uuid4())[:8]}" + self.personal_id = str(PersonDataClass._get_next_personal_id()).zfill(8) + + def is_old_enough(self) -> bool: + return self.age >= 18 + + @classmethod + def _get_next_personal_id(cls) -> int: + cls._personal_id += 1 + return cls._personal_id + + +peter: PersonDataClass = PersonDataClass("Peter", 18, "H", 85, 175.9) +peter.is_old_enough() # => True +PersonDataClass("Julia", 16, "M", 65, 162.4).is_old_enough() # => False +print(peter) # => PersonaDataClass(name='Peter', age=18, gender='H', weight=85, height=175.9, properties=[], member_id='P-f642581c', dni='00000001') + +#################################################### +# 5.5 Operator Overloading +#################################################### + + +# Reference: https://docs.python.org/3/reference/datamodel.html#basic-customization + +from typing import List, Optional + + +@dataclass +class Article: + name: str + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Article): + raise NotImplementedError() + + return self.name == other.name + + def __hash__(self) -> int: + return hash(self.name) + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return f"Article('{self.name}')" + + +@dataclass +class ShoppingCart: + articles: List[Article] = field(default_factory=list) + + def add(self, article: Article) -> ShoppingCart: + self.articles.append(article) + return self + + def remove(self, remove_article: Article) -> ShoppingCart: + self.articles = [article for article in self.articles if article != remove_article] + return self + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ShoppingCart): + raise NotImplementedError() + return set(self.articles) == set(other.articles) + + def __str__(self) -> str: + return str(self.articles) + + def __repr__(self) -> str: + return f"ShoppingCart({self.articles})" + + def __add__(self, other: ShoppingCart) -> ShoppingCart: + return ShoppingCart(self.articles + other.articles) + + +apple = Article("Apple") +pear = Article("Pear") +tv = Article("Television") + +# Test conversion to String +str(ShoppingCart().add(apple).add(pear)) # => ['Apple', 'Pear']) + +# Reproducibility test +cart = ShoppingCart().add(apple).add(pear) +assert cart == eval(repr(cart)) +print(repr(cart)) # => ShoppingCart([Article('Apple'), Article('Pear')]) + +# Equality test +assert ShoppingCart().add(apple) == ShoppingCart().add(apple) # => True +print(ShoppingCart().add(apple)) # => ['Apple']] + +# Test object removal +assert ShoppingCart().add(tv).add(pear).remove(tv) == ShoppingCart().add(pear) # => True +print(ShoppingCart().add(tv).add(pear).remove(tv)) # => ['Pear']) + +# Equality test with different order +assert ShoppingCart().add(tv).add(pear) == ShoppingCart().add(pear).add(tv) # => True +print(ShoppingCart().add(tv).add(pear)) # => ['Television', 'Pear'] + +# Sum test +combined = ShoppingCart().add(apple) + ShoppingCart().add(pear) +assert combined == ShoppingCart().add(apple).add(pear) # => True +print(combined) # => ['Apple', 'Pear'] + + +#################################################### +# 5.6 Instances as Functions (__call__) +#################################################### + + +@dataclass +class Accumulator: + initial_value: Union[int, float] = 0 + value: Union[int, float] = field(init=False) + + def __post_init__(self): + self.value = self.initial_value + + def increment(self, value: Union[int, float]) -> None: + self.value += value + + +accumulator_1 = Accumulator() +accumulator_1.increment(5) +accumulator_1.increment(10) +accumulator_1.increment(-2) + +assert accumulator_1.value == 13 + + +@dataclass +class AccumulatorAlternative: + initial_value: Union[int, float] = 0 + value: Union[int, float] = field(init=False) + + def __post_init__(self): + self.value = self.initial_value + + def __call__(self, value: Union[int, float]) -> None: + self.value += value + + +accumulator_2 = AccumulatorAlternative() +accumulator_2(5) +accumulator_2(10) +accumulator_2(-2) + +assert accumulator_2.value == 13 + +#################################################### +# 5.7 Properties and Deep Copy +#################################################### + + +# Reference: https://docs.python.org/3/library/copy.html + + +@dataclass +class Product: + _name: str + _price: float + + @property + def name(self) -> str: + return self._name.capitalize() + + @name.setter + def name(self, value: str) -> None: + self._name = value + + @property + def price(self) -> float: + return round(self._price, 2) + + @price.setter + def price(self, value: float) -> None: + self._price = value + + +from copy import deepcopy # Standard Library + + +def update_price(products: List[Product], percentage_increase: float) -> List[Product]: + new: List[Product] = [] + for product in deepcopy(products): + product.price *= 1 + percentage_increase / 100 + new.append(product) + return new + + +names = ["sheet", "speaker", "computer", "cup", "bottle", "cellular"] +prices = [10.25, 5.258, 350.159, 25.99, 18.759, 215.231] + +products = [Product(name, price) for name, price in zip(names, prices)] +percentage_increase = 10 + +updated_products: List[Product] = update_price(products, percentage_increase) +out_of_date_prices: List[float] = [product.price for product in products] +updated_prices: List[float] = [product.price for product in updated_products] + +print(out_of_date_prices) # => [10.25, 5.26, 350.16, 25.99, 18.76, 215.23] +print(updated_prices) # => [11.28, 5.79, 385.18, 28.59, 20.64, 236.75] + + +#################################################### +# 5.8 Inheritance +#################################################### + + +@dataclass +class Animal: + age: int = 0 + + def description(self) -> str: + return f"I am {self.age} years old." + + +@dataclass +class Dog(Animal): + breed: str = "" + + def description(self) -> str: + return f"I'm a dog and {super().description().lower()}" + + +terrier = Dog(8, "Yorkshire Terrier") +dogo = Dog(breed="Dogo") +puppy = Dog(age=1) + +print(terrier.description()) # => I am a dog and I am 8 years old. + + +#################################################### +# 5.9 Constructor (__new__) +#################################################### + + +@dataclass +class Car: + max_speed: float + price: float + + def __new__(cls, max_speed: float, price: float) -> Car: + auto: Car + + if max_speed >= 300: + auto = super().__new__(SportCar) + elif price >= 100_000: + auto = super().__new__(LuxuryCar) + else: + auto = super().__new__(cls) + + auto.max_speed = max_speed + auto.price = price + return auto + + +class LuxuryCar(Car): + ... + + +class SportCar(Car): + ... + + +family_car = Car(170, 3_000) +f1_car = Car(370, 5_000_000) +famous_car = Car(250, 500_000) + +assert isinstance(family_car, Car) +assert isinstance(f1_car, Car) +assert isinstance(famous_car, Car) +assert isinstance(f1_car, SportCar), f1_car +assert isinstance(famous_car, LuxuryCar) + +#################################################### +# 5.10 Abstract Classes and Methods +#################################################### + + +# Reference: https://docs.python.org/3/library/abc.html + +from abc import ABC, abstractmethod +from typing import final # Python 3.8+ + + +@dataclass +class Item(ABC): + _id: ClassVar[int] + id_: int = field(init=False) + _name: str + + def __post_init__(self): + self.id_ = self.__class__._get_next_id() + + @classmethod + @abstractmethod + def _get_next_id(cls) -> int: + ... + + @abstractmethod + def show_id(self) -> str: + ... + + @property + @abstractmethod + def name(self) -> str: + ... + + @name.setter + @abstractmethod + def name(self, value: str) -> None: + ... + + @final + def description(self) -> str: + return f"ID: {self.id_} - Name: {self.name}" + + +@dataclass +class Clothing(Item): + ... + + +@dataclass +class Material(Item): + _id: ClassVar[int] = 0 + id_: int = field(init=False) + _name: str + + @classmethod + def _get_next_id(cls) -> int: + cls._id += 1 + return cls._id + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + def show_id(self) -> str: + return str(self.id_).zfill(10) + + +@dataclass +class LuxuryMaterial(Material): + def description(self) -> str: + return f"{super().description()} - Luxurious Material" + + +# item = Item("Item") # => Error +# item = Apparel("Shirt") # => Error +luxury_item = LuxuryMaterial("Formula 1") # => No Error - Warning in the declaration +print(luxury_item.description()) + +item = Material("Wood") +print(item) # => Material(id_=1, _name='Wood') + +assert issubclass(type(item), Item) +assert issubclass(type(item), Material) +assert isinstance(item, Item) +assert isinstance(item, Material) + + +#################################################### +# 5.11 Interfaces (Protocols) +#################################################### + + +from typing import Protocol + + +class Identifiable(Protocol): + @property + def name(self) -> str: + ... + + @property + def id_(self) -> int: + ... + + +def get_data_summary(element: Identifiable): + return f"{element.id_} - {element.name}" + + +wood = Material("Wood") + +summary = get_data_summary(wood) # No Warning of Types. + # Even if Material does not inherit from Identifiable + # Even if id_ is not a property + # Even if + + +#################################################### +# 5.12 Method Overloading +#################################################### + + +from typing import overload, Sequence + + +@overload +def duplicate(x: int) -> int: + ... + + +@overload +def duplicate(x: Sequence[int]) -> list[int]: + ... + + +def duplicate(x: int | Sequence[int]) -> int | list[int]: + if isinstance(x, Sequence): + return [i * 2 for i in x] + return x * 2 + + +assert duplicate(2) == 4 # No Warning +assert duplicate([1, 2, 3]) == [2, 4, 6] # No Warning + + +#################################################### +# 5.13 Method Overloading - Special Case - Python 3.8+ +#################################################### + + +from typing import Union + + +@dataclass +class Employee: + salary: float + + def calculate_salary(self, tax: Union[int, float]) -> float: + if isinstance(tax, int) or tax >= 1: + return self.salary - tax + + return self.salary * (1 - tax) + + +cleanning_staff_1 = Employee(10_000) + + +from functools import singledispatchmethod # Standard Library + + +@dataclass +class EmployeeAlternate: + salary: float + + @singledispatchmethod + def calculate_salary(self, tax: float) -> float: + raise NotImplementedError() + + @calculate_salary.register + def _(self, tax: float) -> float: + if tax >= 1: + return self.salary - tax + return self.salary * (1 - tax) + + @calculate_salary.register + def _(self, tax: int) -> float: + return self.salary - tax + + +cleanning_staff_2 = EmployeeAlternate(10_000) +assert cleanning_staff_2.calculate_salary(1500) == 8_500 +assert cleanning_staff_2.calculate_salary(0.1) == 9_000 + +assert cleanning_staff_1.calculate_salary(1500) == cleanning_staff_2.calculate_salary(1500) +assert cleanning_staff_1.calculate_salary(0.1) == cleanning_staff_2.calculate_salary(0.1) + +#################################################### +# 5.14 Mixins (Multiple Inheritance) +#################################################### + + +import json +from typing import Any + + +class JsonSerializer: + def to_json(self) -> str: + return json.dumps(vars(self)) + + def from_json(self, json_string: str) -> Any: + return json.loads(json_string) + + +@dataclass +class EmployeeDatabase(EmployeeAlternate, JsonSerializer): + table: str + + +cleanning_staff_2 = EmployeeDatabase(10_000, "Employees") + +assert cleanning_staff_2.to_json() == '{"salary": 10000, "table": "Employees"}' + + +#################################################### +# 5.15 Descriptors +#################################################### + + +class Positive: + def __set_name__(self, _: Any, name: str) -> None: + self.attribute_name: str = f"_{name}" + + def __get__(self, instance: Any, _: Any = None) -> Union[float, int]: + return getattr(instance, self.attribute_name) # type: ignore. + + def __set__(self, instance: Any, value: Union[float, int]) -> None: + if value < 0: + raise ValueError(f"{self.attribute_name} must be positive") + + setattr(instance, self.attribute_name, value) + + +class Celcius: + def __get__(self, instance: Any, _: Any = None) -> float: + return (instance.farenheit - 32) * 5 / 9 + + def __set__(self, instance: Any, value: float) -> None: + instance.farenheit = 32 + value * 9 / 5 + + +@dataclass +class ExperimentalMaterial: + mass: float = Positive() + temperature: float = Celcius() + + +reinforced_concrete = ExperimentalMaterial(mass=50, temperature=100) +assert reinforced_concrete.mass == 50 +assert reinforced_concrete.temperature == 100 +assert reinforced_concrete.farenheit == 212 # Warning but no Error + +reinforced_concrete.temperature = 50 +assert reinforced_concrete.temperature == 50 +assert reinforced_concrete.farenheit == 122 +# oxygen = MaterialExperiment(mass=-21, -30) # Error -> ValueError: _mass must be positive diff --git a/chapter6.py b/chapter6.py new file mode 100644 index 0000000..ad78404 --- /dev/null +++ b/chapter6.py @@ -0,0 +1,54 @@ +#################################################### +## 6. Modules +#################################################### + +# Import the whole module with name +import math + +math.sqrt(16) # => 4.0 + + +# Import specific members (submodules, variables, functions) only +from math import ceil, floor + +ceil(3.7) # => 4.0 +floor(3.7) # => 3.0 + + +# Import all members of a module into the global namespace | NOT RECOMMENDED +from math import * + + +# Alias for modules +import math as m + +# Modules as objects + +# Modules are objects and can be compared with each other +math == m # => True + +# There is a single instance of a module in memory +math is m # => True + +math.__doc__ # => This module provides access to the mathematical functions + # defined by the C standard. + +# Programmatic Import (Advanced) + +# It is possible to programmatically import a module +import importlib + +math_programmatically = importlib.import_module("math") + +assert math_programmatically is m + +floor_programmatically = getattr(math_programmatically, "floor") + +assert floor_programmatically is floor + +# If the module changes at execution time it can be reloaded +# The import/from..import syntax does NOT reload modules +importlib.reload(math) +importlib.reload(math_programmatically) + +# Continue reading in chapter6/chapter6_1.py diff --git a/chapter6/chapter6_1.py b/chapter6/chapter6_1.py new file mode 100644 index 0000000..3531027 --- /dev/null +++ b/chapter6/chapter6_1.py @@ -0,0 +1,54 @@ +# Ways to run the file (while standing inside the chapter6 folder): +# $PATH$/chapter6> python chaper6_1.py +# $PATH$/chapter6> python -m chapter6_1 + +# Reference: https://docs.python.org/3/reference/import.html#the-import-system +# https://docs.python.org/3/tutorial/modules.html + +#################################################### +## 6.1 Import into the same directory +#################################################### + + +import main + +# => I was invoked directly or indirectly. +# => I was invoked indirectly (via an import) +# => You successfully imported main.py + +print(f"The value of {main.name=}") # => The value of main.name='main' +main.__doc__ # => This is the main module of the application (Docstring) + + +#################################################### +## 6.2 Import from a neighboring directory +#################################################### + +from source import items + +# => This message will be executed before the imports of this module + +items # => {'sheet': 10.25, 'speaker': 5.258, 'computer': 350.159, + # 'cup': 25.99, 'bottle': 18.759, 'cellular': 215.231} + + +import source.util as util + +# => Successfully imported util.py + +print(f"The value of {util.name=}") # => The value of util.name='util'. + + +#################################################### +## 6.3 Import from a child of a neighboring directory +#################################################### + + +import source.controller.controller as controller + +# => You successfully imported controller.py + +print(f'The value of {controller.name=}') # => The value of controller.name='controller' + + +# Continue reading in chapter6/source/chapter6_2.py diff --git a/chapter6/config/db_config/migrations.py b/chapter6/config/db_config/migrations.py new file mode 100644 index 0000000..82833d9 --- /dev/null +++ b/chapter6/config/db_config/migrations.py @@ -0,0 +1,5 @@ +from pathlib import Path + +print(f"You successfully imported {Path(__file__).parts[-1]}") + +name = "migrations" diff --git a/chapter6/config/test_config.py b/chapter6/config/test_config.py new file mode 100644 index 0000000..0b3372c --- /dev/null +++ b/chapter6/config/test_config.py @@ -0,0 +1,5 @@ +from pathlib import Path + +print(f"You successfully imported {Path(__file__).parts[-1]}") + +name = "test_config" diff --git a/chapter6/main.py b/chapter6/main.py new file mode 100644 index 0000000..3bc2d40 --- /dev/null +++ b/chapter6/main.py @@ -0,0 +1,17 @@ +"""This is the main module of the application (Docstring)""" + +print("I was invoked directly or indirectly.") + + +if __name__ == "__main__": + print("I was invoked directly") + + +if __name__ != "__main__": + print("I was invoked indirectly (via an import)") + + from pathlib import Path + + print(f"You successfully imported {Path(__file__).parts[-1]}") + + name = "main" diff --git a/chapter6/source/__init__.py b/chapter6/source/__init__.py new file mode 100644 index 0000000..bc378de --- /dev/null +++ b/chapter6/source/__init__.py @@ -0,0 +1,8 @@ +"""In this module you can find the main source code of the application""" + +print("This message will be executed before the imports of this module") + +names = ["sheet", "speaker", "computer", "cup", "bottle", "cellular"] +prices = [10.25, 5.258, 350.159, 25.99, 18.759, 215.231] + +items = {name: price for name, price in zip(names, prices)} diff --git a/chapter6/source/__main__.py b/chapter6/source/__main__.py new file mode 100644 index 0000000..0fbfae0 --- /dev/null +++ b/chapter6/source/__main__.py @@ -0,0 +1 @@ +print("I am executed when the directory is called") diff --git a/chapter6/source/chapter6_2.py b/chapter6/source/chapter6_2.py new file mode 100644 index 0000000..f17c32e --- /dev/null +++ b/chapter6/source/chapter6_2.py @@ -0,0 +1,39 @@ +# How to execute the file (while standing inside the chapter6 folder): +# $PATH$/chapter6> python -m source.chapter6_2 + +# => This message will be executed before the imports of this module. + +#################################################### +## 6.4 Importing from a parent from a child directory +#################################################### + +import main + +# => I was invoked directly or indirectly +# => I was invoked indirectly (via an import) +# => You successfully imported main.py + +#################################################### +## 6.5 Import from a parent's neighbor +#################################################### + +import config.test_config as test_config + +# => You successfully imported test_config.py + + +#################################################### +## 6.5 Import from a parent's neighbor child +#################################################### + +import config.db_config.migrations as migrations + +# => Successfully imported migrations.py + + +# Imports from the same directory and neighbors work the same as in chapter6_1 +# Prints omitted +import source.util as util +import source.controller.controller as controller + +# Continue reading in chapter6/source/controller/chapter6_3.py diff --git a/chapter6/source/controller/chapter6_3.py b/chapter6/source/controller/chapter6_3.py new file mode 100644 index 0000000..b392171 --- /dev/null +++ b/chapter6/source/controller/chapter6_3.py @@ -0,0 +1,57 @@ +# How to execute the file (while standing inside the chapter6 folder): +# $PATH$/chapter6> python -m source.controller.chapter6_3 + +# => This message will be executed before the imports of this module + +#################################################### +## 6.5 Same logic for n depth levels +#################################################### + +## Prints omitted +import main +import source.util as util +import source.controller.controller as controller +import config.test_config as test_config +import config.db_config.migrations as migrations + + +#################################################### +## 6.6 Singleton behavior of packages +#################################################### + +import source.controller.controller as controller + +# => You successfully imported controller.py +print(controller.name) # => controller + +import source.data.data as data + +# => Successfully imported data.py +print(controller.name) # => I was modified in another module + + +# Re-importing with the import keyword will NOT reset the value +import source.controller.controller as controller + +print(controller.name) # => I was modified in another module + + +# Re-importing using imporlib.reload will reset the value +import importlib + +importlib.reload(controller) +print(controller.name) # => controller + + +#################################################### +## 6.7 Relative (intra-package) Imports +#################################################### + +from . import controller as controller # => You imported with Success controller.py +from .. import util as util # => Successfully imported util.py +from ..data import data as data # => You imported with Success data.py + +# You cannot import modules that are in the root of the directory tree. +from ... import main # => Error +from ...config import test_config as test_config # => Error +from ...config.db_config import migrations as migrations # => Error< diff --git a/chapter6/source/controller/controller.py b/chapter6/source/controller/controller.py new file mode 100644 index 0000000..414c252 --- /dev/null +++ b/chapter6/source/controller/controller.py @@ -0,0 +1,5 @@ +from pathlib import Path + +print(f"You successfully imported {Path(__file__).parts[-1]}") + +name = "controller" diff --git a/chapter6/source/data/data.py b/chapter6/source/data/data.py new file mode 100644 index 0000000..575b0d8 --- /dev/null +++ b/chapter6/source/data/data.py @@ -0,0 +1,9 @@ +from pathlib import Path + +print(f"You successfully imported {Path(__file__).parts[-1]}") + +name = "data" + +from source.controller import controller + +controller.name = "I was modified in another module" diff --git a/chapter6/source/util.py b/chapter6/source/util.py new file mode 100644 index 0000000..df761d9 --- /dev/null +++ b/chapter6/source/util.py @@ -0,0 +1,5 @@ +from pathlib import Path + +print(f"You successfully imported {Path(__file__).parts[-1]}") + +name = "util" diff --git a/chapter7.py b/chapter7.py new file mode 100644 index 0000000..7664716 --- /dev/null +++ b/chapter7.py @@ -0,0 +1,1335 @@ +#################################################### +## 7. Advanced language Features +#################################################### + +from __future__ import annotations + +#################################################### +## Index +#################################################### + +# 7.1 Additional types +# 7.2 Generators +# 7.3 Iterators +# 7.4 Semi-corroutines (Advanced Generators) +# 7.5 Corrutines (AsyncIO) +# 7.6 Decorators +# 7.7 Context Manager +# 7.8 Pearls of the Standard Library - Pathlib +# 7.9 Pearls of the Standard Library - Itertools +# 7.10 Pearls of the Standard Library - OS +# 7.11 Pearls of the Standard Library - Serialization +# 7.12 Pearls of the Standard Library - Emails + + +#################################################### +## 7.1 Additional Types +#################################################### + + +#################################################### +## 7.1.1 NamedTuple and namedtuple +#################################################### + +## Reference: https://docs.python.org/3/library/collections.html#collections.namedtuple + +from dataclasses import dataclass, astuple +from collections import namedtuple + + +@dataclass +class Vector: + x: float + y: float + + def modulo(self) -> float: + return (self.x**2 + self.y**2) ** 0.5 + + +origin = Vector(0, 0) +origin.x # => 0 +origin.y # => 0 +print(origin) # => Vector(x=0, y=0) +# x, y = origin # => Error +x, y = astuple(origin) # => No Error +# origin[0] # => Error Cannot use indexes in classes +origin.x = 3 # => Default mutable attributes +origin.y = 4 # => Default mutable attributes +print(origin) # => Vector(x=3, y=4) + +assert origin.modulo() == 5 + + +# Using frozen=True + + +@dataclass(frozen=True) +class VectorImmutable: + x: float + y: float + + def modulo(self) -> float: + return (self.x**2 + self.y**2) ** 0.5 + + +origin = VectorImmutable(0, 0) +origin.x # => 0 +origin.y # => 0 +print(origin) # => Vector(x=0, y=0) +# x, y = origin # => Error +x, y = astuple(origin) # => No Error +# origin[0] # => Error Cannot use indexes in classes +# origin.x = 3 # => Error attributes are immutable +# origin.y = 4 # => Error attributes are immutable + +assert origin.modulo() == 0 + + +# Using collections.namedtuple + + +VectorAlternate = namedtuple("VectorAlternate", ["x", "y"]) + + +def modulo_without_types(vector: VectorAlternateVector) -> float: + return (vector.x**2 + vector.y**2) ** 0.5 # Warning for not knowing types. + + +point = VectorAlternate(3, 4) +point.x # => 3 with Warning: No type specified +point.y # => 4 with Warning: No type specified +print(point) # => VectorAlternate(x=3, y=4) +x, y = point # => x=3, y=4 with Warning: No type specified +# point.x = 1 # => Error + +assert point[0] == 3 +assert point[1] == 4 +assert modulo_without_types(point) == 5 + + +# Using NamedTuple as superclass + + +from typing import NamedTuple + + +class VectorAlternativeTyped(NamedTuple): + x: float + y: float + + def modulo(self) -> float: + return (self.x**2 + self.y**2) ** 0.5 + + +def typed_modulo_1(vector: VectorAlternativeTyped) -> float: + return (vector.x**2 + vector.y**2) ** 0.5 + + +typed_point_1_str = VectorAlternativeTyped("1", "1") # Warning String != Float + +typed_point_1 = VectorAlternativeTyped(3, 4) +typed_point_1.x # => 3 +typed_point_1.y # => 4 +print(typed_point_1) # => VectorAlternativeTyped(x=3, y=4) +x, y = typed_point_1 # => x=3, y=4 +# typed_point_1.x = 1 # => Error + +assert typed_point_1[0] == 3 +assert typed_point_1[1] == 4 +assert typed_modulo_1(typed_point_1) == 5 + + +# Using NamedTuple as a function + +VectorAlternativeTyped_2 = NamedTuple( + "VectorAlternativeTyped_2", [("x", float), ("y", float)] +) + + +def typed_modulo_2(vector: VectorAlternativeTyped_2) -> float: + return (vector.x**2 + vector.y**2) ** 0.5 + + +typed_point_2 = VectorAlternativeTyped_2(3, 4) +typed_point_2.x # => 1 +typed_point_2.x # => 1 +print(typed_point_2) # => VectorAlternate(x=1, y=1) +x, y = typed_point_2 # => x=1, y=1 +# typed_point_2.x = 1 # => Error + +assert typed_point_2[0] == 3 +assert typed_point_2[1] == 4 +assert typed_modulo_2(typed_point_2) == 5 + +#################################################### +## 7.1.2 Counter +#################################################### + + +# Reference: https://docs.python.org/3/library/collections.html#collections.Counter + +from collections import Counter + +# Excerpt from Moby-Dick by Herman Melville +# Source: https://www.gutenberg.org/files/2701/2701-h/2701-h.htm#link2HCH0001 +text = """ +Call me Ishmael. Some years ago—never mind how long precisely—having little or +no money in my purse, and nothing particular to interest me on shore, I thought +I would sail about a little and see the watery part of the world. It is a way I +have of driving off the spleen and regulating the circulation. Whenever I find +myself growing grim about the mouth; whenever it is a damp, drizzly November in +my soul; whenever I find myself involuntarily pausing before coffin warehouses, +and bringing up the rear of every funeral I meet; and especially whenever my +hypos get such an upper hand of me, that it requires a strong moral principle to +prevent me from deliberately stepping into the street, and methodically knocking +people’s hats off—then, I account it high time to get to sea as soon as I can. +This is my substitute for pistol and ball. With a philosophical flourish Cato +throws himself upon his sword; I quietly take to the ship. There is nothing +surprising in this. If they but knew it, almost all men in their degree, some +time or other, cherish very nearly the same feelings towards the ocean with me. +""" + +letter_counter = Counter(text) +letter_counter.most_common(4) # => [(' ', 184), ('e', 107), ('t', 74), ('i', 68)] + +word_counter = Counter(text.split()) +word_counter.most_common(4) # => [('the', 10), ('I', 9), ('and', 7), ('to', 5)] + + +#################################################### +## 7.1.3 Defaultdict +#################################################### + + +# Reference: https://docs.python.org/3/library/collections.html#collections.defaultdict + +from collections import defaultdict + +student_grade: defaultdict[str, List[int]] = defaultdict(list) + +student_grade["Peter"].append(8) # Does not throw KeyError +student_grade["Mary"].append(9) +student_grade["Peter"].append(3) +student_grade["Mary"].append(8) +student_grade["Peter"].append(7) + +assert student_grade == {"Peter": [8, 3, 7], "Mary": [9, 8]} + + +#################################################### +## 7.1.4 Enum +#################################################### + + +# Reference: https://docs.python.org/3/library/enum.html + +from enum import Enum, auto + + +class Permissions(Enum): + ADMIN = auto() + USER = auto() + EDITOR = auto() + + @staticmethod + def has_access_control_panel_1(permission: Permissions) -> bool: + return permission is Permissions.ADMIN or permission is Permissions.EDITOR + + +Permissions.ADMIN # => Permissions.ADMIN +Permissions.ADMIN.value # => 1 +Permissions['ADMIN'] # => Permissions.ADMIN +Permissions['ADMIN'].value # => 1 + +print(Permissions.__members__.keys()) # => ['ADMIN', 'USER', 'EDITOR'] +print(Permissions.__members__.values()) # => [, , ] + +Permissions.has_access_control_panel_1("REDACTOR") # No Error - Warning Incompatible Type. + +assert Permissions.has_access_control_panel_1(Permissions.ADMIN) + + +# Alternative using typing.Literal + +from typing import Literal + +PermissionsA = Literal["ADMIN", "EDITOR", "USER"] + + +class PermissionsAlternative: + @staticmethod + def has_access_control_panel_2(permission: PermissionsA) -> bool: + return permission in ["ADMIN", "EDITOR"] + + +PermissionsAlternative.has_access_control_panel_2("EDITOR") # No Error - Warning Type Incompatible + +assert PermissionsAlternative.has_access_control_panel_2("ADMIN") + +#################################################### +## 7.1.4 SimpleNameSpace +#################################################### + + +from dataclasses import dataclass + + +@dataclass +class Person: + name: str + + +employee_1 = Person("John") +employee_1.name # => John +# employee_1["name"] # => Error - Cannot use keys with dataclasses +employee_1.age = 24 # => No Error - Monkey Patching - NOT RECOMMENDED + + +# Using Dictionaries + +employee_2 = {} +employee_2["name"] = "John" +employee_2["name"] # => John +# employee_2.name # => Error - Cannot access values using . + + +# Using SimpleNameSpace + +# Reference: https://docs.python.org/3/library/types.html#types.SimpleNamespace + +from types import SimpleNamespace + +employee_3 = SimpleNamespace() +employee_3.name = "John" # => No Error - Not Mokey Patching - Correct Usage +# employee_3["name"] # => Error - Cannot use keys with SimpleNamespace + + +# Custom SimpleNamespace + +from dataclasses import field + +from typing import Any + + +class Employee(SimpleNamespace): + def __setitem__(self, key: str, value: Any) -> None: + setattr(self, key, value) + + def __getitem__(self, key: str) -> Any: + return getattr(self, key) + + +employee_4 = Employee() +employee_4["name"] = "John" # => No Error - Not Mokey Patching - Correct Usage +employee_4.age = 24 # => No Error - Not Mokey Patching - Correct Usage + +assert employee_4.name == "John" +assert employee_4["name"] == "John" +assert employee_4.age == 24 +assert employee_4["age"] == 24 + + +#################################################### +## 7.2 Generators +#################################################### + +# Reference: https://docs.python.org/3/tutorial/classes.html#generators + +from typing import Generator, Iterator, List + + +def fibonacci_generator() -> Generator[int, None, None]: + last: int = 1 + current: int = 1 + yield last + yield current + + while True: + current, last = last + current, current + yield current + + +generator = fibonacci_generator() +fibonacci_10_first: List[int] = [] + +for _ in range(10): + next_fibonacci = next(generator) + fibonacci_10_first.append(next_fibonacci) + +assert fibonacci_10_first == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] + + +# Can take parameters like any function +def primes_smaller_than(number: int) -> Generator[int, None, None]: + visited: List[int] = [] + for number in range(2, number): + not_prime = any(number % possible_divisor == 0 for possible_divisor in visited) + + if not_prime: + continue + + visited.append(number) + yield number + + +# With traditional loop + +generator = primes_smaller_than(25) +primes_smaller_than_25: List[int] = [] +for prime in generator: + primes_smaller_than_25.append(prime) + +assert primes_smaller_than_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + + +# With comprehension + +generator = primes_smaller_than(25) +primes_smaller_than_25 = [prime for prime in generator] +assert primes_smaller_than_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + + +# With list + +generator = primes_smaller_than(25) +primes_smaller_than_25 = list(generator) +assert primes_smaller_than_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + + +#################################################### +## 7.3 Iterators +#################################################### + +# Reference: https://docs.python.org/3/library/stdtypes.html#iterator-types + +from dataclasses import dataclass, field +from typing import List + + +@dataclass +class PrimesSmallerThan: + number: int + current_number: int = 1 + visited: List[int] = field(default_factory=list) + + def __iter__(self): # Required for use in For + return self + + def __next__(self): # Necessary for function next + while True: + self.current_number += 1 + no_is_prime = any( + self.current_number % possible_divisor == 0 + for possible_divisor in self.visited + ) + + if self.current_number >= self.number: + raise StopIteration() + + if not no_is_prime or len(self.visited) == 0: + break + + self.visited.append(self.current_number) + return self.current_number + + +# With traditional loop + +class_generator: Iterator[int] = PrimesSmallerThan(25) +primes_less_than_25: List[int] = [] +for prime in class_generator: + primes_less_than_25.append(prime) + +assert primes_less_than_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + +# With comprehension + +class_generator: Iterator[int] = PrimesSmallerThan(25) +primes_smaller_than_25 = [prime for prime in class_generator] +assert primes_smaller_than_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + +# With list + +class_generator: Iterator[int] = PrimesSmallerThan(25) +primes_smaller_than_25 = list(class_generator) +assert primes_smaller_than_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + +#################################################### +## 7.4 Semi-coroutines (Generators with send) +#################################################### + +from typing import Generator, Any + + +def accumulate() -> Generator[float, float, None]: + accumulator = 0 + while True: + following = yield accumulator + accumulator += following + + +semicoroutine: Generator[float, float, None] = accumulate() +next(semicoroutine) # Initialize + +semicoroutine.send(10) +semicoroutine.send(-1) + +accumulator_result: float = semicoroutine.send(20) # Finish +assert accumulator_result == 29 + + +## Use Case + + +def processing_deferred() -> Generator[Any, Any, Any]: + data = yield + print(f"Processing data... - {data}") # Replace with complex process + processed_data = str(data) + new_data = yield processed_data + print(f"Processing data... - {new_data}") # Replace with complex process + yield "Done" + + +semicoroutine_deferred: Generator[Any, Any, Any] = processing_deferred() +next(semicoroutine_deferred) # Initialize + +deferred_result: float = semicoroutine_deferred.send(123) # => Processing data... - 123 + +# Deferred processing +deferred_result = semicoroutine_deferred.send(654) # => Processing data... - 654 + +assert deferred_result == "Done" + +#################################################### +## 7.5 Corrutinas (AsyncIO) +#################################################### + + +# Reference: https://docs.python.org/3/library/asyncio-task.html + +import asyncio +import time +from typing import Tuple + +# Defining corrutines + + +async def processing_db() -> int: # Async modifier + print("DB: Sending database query") + await asyncio.sleep(1) # Similar behavior to yield + print("DB: Processing query 1") + await asyncio.sleep(3) + print("DB: Saving Results") + return 0 + + +async def responding_api() -> int: + print("API: Requesting data from user") + await asyncio.sleep(1) + print("API: Building Response") + await asyncio.sleep(1) + print("API: Writing Logs") + await asyncio.sleep(1) + print("API: Sending Response") + await asyncio.sleep(1) + print("API: Receiving confirmation") + return 1 + + +# Serial Execution of Corrutines + + +async def main_serial() -> Tuple[int, int]: + result_db = await processing_db() + result_api = await responding_api() + return (result_db, result_api) + + +async def main_serial_inverted() -> Tuple[int, int]: + result_api = await responding_api() + result_db = await processing_db() + return (result_api, result_db) + + +print(f"Started: {time.strftime('%X')}") +results: Tuple[int, int] = asyncio.run(main_serial()) +print(f"Completed: {time.strftime('%X')}") +assert results == (0, 1) + +# Started: 23:22:24 +# DB: Sending database query +# DB: Processing query 1 +# DB: Saving Results +# API: Requesting data from user +# API: Building Response +# API: Writing Logs +# API: Sending Response +# API: Receiving confirmation +# Completed: 23:22:32 + +print(f"Started: {time.strftime('%X')}") +results: Tuple[int, int] = asyncio.run(main_serial_inverted()) +print(f"Completed: {time.strftime('%X')}") +assert results == (1, 0) + +# Started: 23:23:06 +# API: Requesting data from user +# API: Building Response +# API: Writing Logs +# API: Sending Response +# API: Receiving confirmation +# DB: Sending database query +# DB: Processing query 1 +# DB: Saving Results +# Completed: 23:23:14 + +# Concurrent execution of Tasks (Tasks) + + +async def main_task() -> Tuple[int, int]: + db_task = asyncio.create_task(processing_db()) + api_task = asyncio.create_task(responding_api()) + return (await db_task, await api_task) + + +async def main_task_inverted() -> Tuple[int, int]: + api_task = asyncio.create_task(responding_api()) + db_task = asyncio.create_task(processing_db()) + return (await api_task, await db_task) + + +print(f"Started: {time.strftime('%X')}") +results = asyncio.run(main_task()) +print(f"Completed: {time.strftime('%X')}") +assert results == (0, 1) + +# Started: 23:23:55 +# DB: Sending database query +# API: Requesting data from user +# DB: Processing query 1 +# API: Building Response +# API: Writing Logs +# API: Sending Response +# DB: Saving Results +# API: Receiving confirmation +# Completed: 23:23:59 + +print(f"Started: {time.strftime('%X')}") +results = asyncio.run(main_task_inverted()) +print(f"Completed: {time.strftime('%X')}") +assert results == (1, 0) + +# Started: 23:24:30 +# API: Requesting data from user +# DB: Sending database query +# API: Building Response +# DB: Processing query 1 +# API: Writing Logs +# API: Sending Response +# DB: Saving Results +# API: Receiving confirmation +# Completed: 23:24:34 + + +# Concurrent Execution of Corrutines (Gather) + + +async def main_gather() -> Tuple[int, int]: + return await asyncio.gather(processing_db(), responding_api()) + + +async def main_gather_inverted() -> Tuple[int, int]: + return await asyncio.gather(responding_api(), processing_db()) + + +print(f"Started: {time.strftime('%X')}") +results = asyncio.run(main_gather()) +print(f"Completed: {time.strftime('%X')}") +assert results == [0, 1] + +# Started: 23:27:10 +# DB: Sending database query +# API: Requesting data from user +# DB: Processing query 1 +# API: Building Response +# API: Writing Logs +# API: Sending Response +# DB: Saving Results +# API: Receiving confirmation +# Completed: 23:27:14 + +print(f"Started: {time.strftime('%X')}") +results = asyncio.run(main_gather_inverted()) +print(f"Completed: {time.strftime('%X')}") +assert results == [1, 0] + +# Started: 23:27:51 +# API: Requesting data from user +# DB: Sending database query +# API: Building Response +# DB: Processing query 1 +# API: Writing Logs +# API: Sending Response +# DB: Saving Results +# API: Receiving confirmation +# Completed: 23:27:55 + + +# Executing asynchronously synchronous code + + +# Asynchronously executing synchronous code + +import time +import asyncio +from typing import List + + +def wait() -> None: + time.sleep(1) + + +def main_blocking() -> List[None]: + return [wait() for _ in range(10)] + + +async def main_blocking_async(): + loop = asyncio.get_running_loop() + futures = [loop.run_in_executor(None, wait) for _ in range(10)] + return await asyncio.gather(*futures) + + +print(f"Started: {time.strftime('%X')}") +results_blocking: List[None] = main_blocking() +print(f"Completed: {time.strftime('%X')}") +assert results_blocking == [None] * 10 + +# Started: 23:30:18 +# Completed: 23:30:28 + + +print(f"Started: {time.strftime('%X')}") +results_blocking_async: Tuple[None] = asyncio.run(main_blocking_async()) +print(f"Completed: {time.strftime('%X')}") +assert results_blocking_async == [None] * 10 + +# Started: 23:30:59 +# Completed: 23:31:00 + +#################################################### +## 7.6 Decorators +#################################################### + + +#################################################### +## 7.6.1 Stateless decorators +#################################################### + + +import time +from typing import Tuple, Any + + +def measure_time(function: Callable[..., Any]) -> Callable[..., Tuple[Any, float]]: + def helper(*args: Any, **kargs: Any) -> Tuple[Any, float]: + """Helper function""" + start: float = time.perf_counter() + + result = function(*args, **kargs) + + end: float = time.perf_counter() + + elapsed: float = end - start + + return result, elapsed + + return helper + + +def slow_function() -> str: + """Original function""" + time.sleep(2) + return "Hello world" + + +# Normal invocation +response: str = slow_function() +assert response == "Hello world" + +import math + +# Invocation with wrapper without decorator +slow_function_with_time = measure_time(slow_function) +result, run_time = slow_function_with_time() + +assert result == "Hello world" +assert math.isclose(run_time, 2, abs_tol=0.1) + +assert slow_function.__name__ == "slow_function" +assert slow_function_with_time.__name__ == "helper" # Undesirable +assert slow_function_with_time.__doc__ == "Helper function" # Not Desired + + +# Using wraps to "inherit" name and docstring + +from functools import wraps + + +def measure_time_alternative( + function: Callable[..., Any] +) -> Callable[..., Tuple[Any, float]]: + @wraps(function) + def helper(*args: Any, **kargs: Any) -> Tuple[Any, float]: + start: float = time.perf_counter() + + result = function(*args, **kargs) + + end: float = time.perf_counter() + + elapsed: float = end - start + + return result, elapsed + + return helper + + +slow_function_with_time = measure_time_alternative(slow_function) +result, execution_time = slow_function_with_time() + +assert result == "Hello world" +assert math.isclose(execution_time, 2, abs_tol=0.1) + +assert slow_function.__name__ == "slow_function" +assert slow_function_with_time.__name__ == "slow_function" # Desirable +assert slow_function_with_time.__doc__ == "Original function" # Desirable + + +# Invocation with decorator + + +@measure_time_alternative +def function_slow_measure(): + time.sleep(2) + return "Hello world" + + +result, execution_time = function_slow_measure() + +assert result == "Hello world" +assert math.isclose(execution_time, 2, abs_tol=0.1) + + +# Stateful decorator + + +def count_runs(function: Callable[..., Any]) -> Callable[..., Any]: + @wraps(function) + def helper(*args: Any, **kwargs: Any): + helper.runs += 1 + return function(*args, **kwargs) + + helper.runs = 0 # Warning - Monkey Patching - NOT RECOMMENDED + + return helper + + +@count_runs +def function_count() -> str: + return "Hello world" + + +for _ in range(10): + function_count() + +assert function_count.runs == 10 # Warning Unknown attribute + + +#################################################### +## 7.6.2 Stateful Decorators +#################################################### + + +from dataclasses import dataclass +from typing import Callable + + +@dataclass +class Counter: + func: Callable[..., Any] + runs: int = 0 + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + self.runs += 1 + return self.func(*args, **kwargs) + + +@Counter +def function_counted_class(): + return "Hello world" + + +for _ in range(10): + function_counted_class() + +assert function_counted_class.runs == 10 # No Warning + + +# Use cases - Cache and Memoization Manual + + +@Counter +def fibonacci(number: int) -> int: + if number < 2: + return number + return fibonacci(number - 1) + fibonacci(number - 2) + + +result = fibonacci(20) +assert result == 6765 +assert fibonacci.runs == 21_891 + + +from typing import Dict + + +def memoization(function: Callable[..., Any]) -> Callable[..., Any]: + cache: Dict[str, Any] = {} + + def helper(*args: Any, **kwargs: Any) -> Any: + nonlocal cache + key = str(tuple(sorted(args)) + tuple(sorted(kwargs.items()))) + if key not in cache: + cache[key] = function(*args, **kwargs) + return cache[key] + + return helper + + +@Counter +@memoization +def fibonacci_memo(number: int) -> int: + if number < 2: + return number + return fibonacci_memo(number - 1) + fibonacci_memo(number - 2) + + +result = fibonacci_memo(20) +assert result == 6765 +assert fibonacci_memo.runs == 39 # 500 times fewer runs + + +# Use cases - Cache and Memoization LRU + +from functools import lru_cache + +# Reference: https://docs.python.org/3/library/functools.html#functools.lru_cache + + +@Counter +@lru_cache(maxsize=None) # Equivalent to @cache in Python 3.9+ +def fibonacci_lru(number: int) -> int: + if number < 2: + return number + return fibonacci_lru(number - 1) + fibonacci_lru(number - 2) + + +result = fibonacci_lru(20) +assert result == 6765 +assert fibonacci_memo.runs == 39 # 500 times fewer runs + + +#################################################### +## 7.7 Context Manager +#################################################### + +# Reference: https://docs.python.org/3/library/stdtypes.html#context-manager-types + + +#################################################### +## 7.7.1 Context Manager with Classes +#################################################### + + +from dataclasses import dataclass +from typing import IO, Optional, Any +import time + + +@dataclass +class Timer: + start: float = 0 + end: Optional[float] = None + elapsed: float = -1 + exception: bool = False + + def __enter__(self) -> Timer: + self.start = time.perf_counter() + return self + + def __exit__(self, exception_type: Optional[Any], _: Any, __: Any) -> bool: + self.exception = exception_type is not None + self.end = time.perf_counter() + self.elapsed = round(self.end - self.start, 3) + return True + + +with Timer() as tempo: + time.sleep(5) + raise ValueError + +print(tempo) # => Timer(start=0.1141484, end=5.1158644, elapsed=5.002, exception=True) + +assert math.isclose(tempo.elapsed, 5, abs_tol=0.1) +assert tempo.exception == True + + +#################################################### +## 7.7.2 Context Manager with Generators and Contextlib +#################################################### + +## Reference: https://docs.python.org/3/library/contextlib.html + +from contextlib import contextmanager +import time + + +@contextmanager +def timer(): + internal_data = { + "start": time.perf_counter(), + "end": -1, + "elapsed": None, + "exception": False, + } + try: + yield internal_data + except ValueError: + internal_data["exception"] = True + finally: + internal_data['end'] = time.perf_counter() + internal_data['elapsed'] = round(internal_data['end'] - internal_data['start'], 3) + + +with timer() as tempo: + time.sleep(5) + raise ValueError() + +print(tempo) # => Timer(start=0.1141484, end=5.1158644, elapsed=5.002, exception=True) + +assert math.isclose(tempo["elapsed"], 5, abs_tol=0.1) +assert tempo["exception"] == True + + +## Use Case - Temporary Files + +## Reference: https://docs.python.org/3/library/tempfile.html + +import tempfile +from typing import IO, Any + +temporary_file: IO[Any] = tempfile.TemporaryFile() +temporary_file.write(b"Hello world!") +temporary_file.seek(0) +temporary_file.read() # => b'Hello world!' +temporary_file.close() + +with tempfile.TemporaryFile() as temporary_file: # Automatic closing + temporary_file.write(b"Hello world!") + temporary_file.seek(0) + temporary_file.read() # => b'Hello world!' + + +## Use case - Database + +# Reference: https://docs.python.org/3/library/sqlite3.html + +import sqlite3 + +# No Context Manager + +connection: sqlite3.Connection = sqlite3.connect(":memory:") +try: + connection.execute("select * from Person") + connection.commit() +except sqlite3.OperationalError as exception: + connection.rollback() + +connection.close() # Close the connection + + +# With Context Manager + +connection: sqlite3.Connection = sqlite3.connect(":memory:") +try: + with connection: # Automatic Commit and Rollback + connection.execute("select * from Person") +except sqlite3.OperationalError as exception: + pass + +connection.close() # Close the connection + + +# Close automatically + +from contextlib import closing + +connection: sqlite3.Connection = sqlite3.connect(":memory:") +try: + with closing(connection): # Commit, Rollback and Close Automatically. + connection.execute("select * from Person") +except sqlite3.OperationalError as exception: + pass + + +# Ignoring Exceptions Automatically + +from contextlib import suppress + +connection: sqlite3.Connection = sqlite3.connect(":memory:") +with suppress(sqlite3.OperationalError): # Exception ignored automatically + with closing(connection): # Commit, Rollback and Close Automatically + connection.execute("select * from Person") + + +# Using Nested Context Manager Syntax +# Commit, Rollback, Close and Automatic Exception Handling + +connection: sqlite3.Connection = sqlite3.connect(":memory:") +with suppress(sqlite3.OperationalError), closing(connection): + connection.execute("select * from Person") + +#################################################### +## 7.8 Standard Library Pearls - Pathlib +#################################################### + +from pathlib import Path + +## Attributes + +example_file = Path("/data/example/main.py") +print(example_file.parent) # => "/data/example" on Windows, /data/example on Unix +assert example_file.name == "main.py" +assert example_file.stem == "main" +assert example_file.suffix == ".py" + +# Concatenation + +folder = Path("/data/example") +new_file = folder / "test.py" + +# Special case: Current file + +current_file = Path(__file__).resolve() + +# File manipulation with automatic open and close + +current_folder = current_file.parent +new_folder = current_folder / "chapter7" # New_folder = current_file / "chapter7 + +new_folder.mkdir(exist_ok=True, parents=True) # Create directory tree + +empty_file = new_folder / "test_pathlib.txt" # Define new file +empty_file.touch(exist_ok=True) # Create empty file + +text_file = new_folder / "test_pathlib.txt" # Define new file +text_file.write_text("Hello World") # Write Text +contents_text = text_file.read_text() # Read content + +file_bytes = new_folder / "test_bytes.txt" # Define new file +file_bytes.write_bytes(b"Hello World") # Write Text +contents_bytes = file_bytes.read_bytes() # Read contents + +empty_file.unlink(missing_ok=True) # Delete file +text_file.unlink(missing_ok=True) # Delete file +file_bytes.unlink(missing_ok=True) # Delete file +new_folder.rmdir() # Delete folder + +assert contents_text == "Hello World" +assert contents_bytes == b"Hello World" + + +#################################################### +## 7.9 Standard Library Pearls - Itertools +#################################################### + +import itertools + +# Reference: https://docs.python.org/3/library/itertools.html + +itertools.accumulate # Equivalent to reduce but returns intermediate results +itertools.takewhile # Returns elements of a collection until condition is false +itertools.dropwhile # Returns elements that do not meet the condition, then returns all of them +itertools.filterfalse # Equivalent to filter but condition is negated + +# Combinatorial +itertools.product # Cartesian product +itertools.permutations # Permutations +itertools.combinations # Combinations +itertools.combinations_with_replacement # Combinations with replacement + +# Featured recipes +# Reference: https://docs.python.org/3/library/itertools.html#itertools-recipes + +from typing import Iterable, Any + + +def pairwise(iterable: Iterable[Any]) -> Iterator[Tuple[Any, Any]]: + "s -> (s0, s1), (s1, s2), (s2, s3), ..." + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + + +def powerset(iterable: Iterable[Any]) -> Iterator[Tuple[Any, ...]]: + "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" + s = list(iterable) + return itertools.chain.from_iterable(itertools.combinations(s, r) for r in range(len(s)+1)) + + +def roundrobin(*iterables: List[Iterable[Any]]) -> Generator[Any, None, None]: + "roundrobin('ABC', 'D', 'EF') --> A D E B F C" + # Recipe credited to George Sakkis + num_active = len(iterables) + nexts = itertools.cycle(iter(it).__next__ for it in iterables) + while num_active: + try: + for next in nexts: + yield next() + except StopIteration: + num_active -= 1 + nexts = itertools.cycle(itertools.islice(nexts, num_active)) + + +#################################################### +## 7.10 Standard Library Pearls - OS +#################################################### + +import os + +# Reference: https://docs.python.org/3/library/os.html + +# Read environment variables + +os.environ["username"] # => Error +os.environ.get("username", None) # => None if username does not exist + + +#################################################### +## 7.11 Standard Library Pearls - Serialization +#################################################### + +## Using Pickle + +import pickle + +# Reference: https://docs.python.org/3/library/pickle.html +# https://docs.python.org/3/library/pickle.html#examples + +from dataclasses import dataclass +from collections import Counter + + +@dataclass +class Student: + name: str = "" + + +data = { + "a": [1, 2.0, 3, 4 + 6j], + "b": ("character string", b"byte string"), + "c": {None, True, False}, + "d": [Student(), Student("Mary"), Student("John")], + "e": Counter(a=10, b=4, c=2), +} + +data_file = Path("data.pickle") + +with open(data_file, "wb") as pickle_file: + pickle.dump(data, pickle_file) + +with open(data_file, "rb") as f: + loaded_data = pickle.load(f) + +data_file.unlink() + +assert data == loaded_data + + +# Using JSON + +import json + +# Reference: https://docs.python.org/3/library/json.html + +# Only Compatible Data Types: +# dict, list, tuple, str, int, float, bool, None + +data = { + "a": [1, 2.0, 3], + "b": ("character string"), + "c": [None, True, False], +} + +data_file = Path("data.json") + +with open(data_file, "w") as json_file: + json.dump(data, json_file) + +with open(data_file, "r") as json_file: + loaded_data = json.load(json_file) + +data_file.unlink() + +assert data == loaded_data + + +#################################################### +## 7.12 Standard Library Pearls - Emails +#################################################### + +import os +import smtplib +import ssl +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders + + +def send_email(email: str, subject: str, content: str = "", file: Optional[str] = None): + user = os.environ["email_username"] + password = os.environ["email_password"] + + mime = MIMEMultipart() + + mime["Subject"] = subject + mime["From"] = "Email Sent Automatically with Python" + mime["To"] = email + + mime.attach(MIMEText(content, "plain")) + + if file is not None: + base = MIMEBase("application", "octet-stream") + file_bytes = Path(file).read_bytes() + base.set_payload(file_bytes) + encoders.encode_base64(base) + base.add_header("Content-Disposition", f"attachment; filename={file}") + mime.attach(base) + + server_url = os.environ["email_server"] + port = int(os.environ["email_port"]) + + context = ssl.create_default_context() + with smtplib.SMTP_SSL(server_url, port, context=context) as server: + server.ehlo() + server.login(user, password) + server.sendmail(user, email, mime.as_string()) + + +# quick setup for Gmail: + +# email_username = source gmail account +# email_password = application password | Get password from App Passwords at: +# https://myaccount.google.com/security +# email_server = 'smtp.gmail.com' # email_server = 'smtp.gmail.com' # email_port = 465 +# email_port = 465 diff --git a/chapter8-8K.png b/chapter8-8K.png new file mode 100644 index 0000000..8bf5cc8 Binary files /dev/null and b/chapter8-8K.png differ