diff --git a/documenting-python-code/README.md b/documenting-python-code/README.md new file mode 100644 index 0000000000..6a25540df6 --- /dev/null +++ b/documenting-python-code/README.md @@ -0,0 +1,52 @@ +# Documenting Python Code: A Complete Guide + +This folder provides the code examples for the Real Python tutorial [Documenting Python Code: A Complete Guide](https://realpython.com/documenting-python-code/). + +## Installation + +Only the spreadsheet examples need third-party packages. Install them into a virtual environment with: + +```sh +$ python -m pip install -r requirements.txt +``` + +The standalone examples run on Python 3.6 or later. The spreadsheet examples use the pinned pandas version, which requires Python 3.11 or later. + +## Files + +- **`comments.py`**: Functions showing short and multi-line comments. +- **`type_hints.py`**: A function that uses type hints in place of comments. +- **`say_hello_v1.py`**: Sets a function's docstring by assigning to `__doc__`. +- **`say_hello_v2.py`**: Sets the same docstring with a string literal below the `def` line. +- **`simple_class.py`**: Shows where class and method docstrings go. +- **`animal.py`**: The `Animal` class with NumPy/SciPy-style class and method docstrings. +- **`spreadsheet_columns.py`**: A command-line script with a script docstring that prints a spreadsheet's column headers. +- **`docstring_formats.py`**: The spreadsheet function documented in the Google, reStructuredText, NumPy/SciPy, and Epytext formats. + +## Usage + +Inspect docstrings with `help()`: + +```pycon +>>> from say_hello_v2 import say_hello +>>> help(say_hello) +Help on function say_hello in module say_hello_v2: + +say_hello(name) + A simple function that says hello... Richie style + +>>> from animal import Animal +>>> Animal("dog", "woof").says() +A dog says woof +``` + +Print the column headers of an Excel spreadsheet: + +```sh +$ python spreadsheet_columns.py people.xlsx +name +age +city +``` + +Run `python spreadsheet_columns.py -h` to see the script's docstring used as the help message, or run `python docstring_formats.py` to print the same docstring in all four formats. diff --git a/documenting-python-code/animal.py b/documenting-python-code/animal.py new file mode 100644 index 0000000000..2d2ac999e7 --- /dev/null +++ b/documenting-python-code/animal.py @@ -0,0 +1,69 @@ +class Animal: + """ + A class used to represent an Animal + + ... + + Attributes + ---------- + says_str : str + a formatted string to print out what the animal says + name : str + the name of the animal + sound : str + the sound that the animal makes + num_legs : int + the number of legs the animal has (default 4) + + Methods + ------- + says(sound=None) + Prints the animals name and what sound it makes + """ + + says_str = "A {name} says {sound}" + + def __init__(self, name, sound, num_legs=4): + """ + Parameters + ---------- + name : str + The name of the animal + sound : str + The sound the animal makes + num_legs : int, optional + The number of legs the animal (default is 4) + """ + + self.name = name + self.sound = sound + self.num_legs = num_legs + + def says(self, sound=None): + """Prints what the animals name is and what sound it makes. + + If the argument `sound` isn't passed in, the default Animal + sound is used. + + Parameters + ---------- + sound : str, optional + The sound the animal makes (default is None) + + Raises + ------ + NotImplementedError + If no sound is set for the animal or passed in as a + parameter. + """ + + if self.sound is None and sound is None: + raise NotImplementedError("Silent Animals are not supported!") + + out_sound = self.sound if sound is None else sound + print(self.says_str.format(name=self.name, sound=out_sound)) + + +if __name__ == "__main__": + Animal("dog", "woof").says() + help(Animal) diff --git a/documenting-python-code/comments.py b/documenting-python-code/comments.py new file mode 100644 index 0000000000..9c1daaa4e1 --- /dev/null +++ b/documenting-python-code/comments.py @@ -0,0 +1,14 @@ +def hello_world(): + # A simple comment preceding a simple print statement + print("Hello World") + + +def hello_long_world(): + # A very long statement that just goes on and on and on and on and + # never ends until after it's reached the 80 char limit + print("Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooo World") + + +if __name__ == "__main__": + hello_world() + hello_long_world() diff --git a/documenting-python-code/docstring_formats.py b/documenting-python-code/docstring_formats.py new file mode 100644 index 0000000000..2960ed9364 --- /dev/null +++ b/documenting-python-code/docstring_formats.py @@ -0,0 +1,106 @@ +"""The same function documented in four common docstring formats. + +Compare the output of help() on each function to see how the Google, +reStructuredText, NumPy/SciPy, and Epytext formats look. +""" + +import pandas as pd + + +def get_spreadsheet_cols_google(file_loc, print_cols=False): + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + list: a list of strings representing the header columns + """ + + file_data = pd.read_excel(file_loc) + col_headers = list(file_data.columns.values) + + if print_cols: + print("\n".join(col_headers)) + + return col_headers + + +def get_spreadsheet_cols_rest(file_loc, print_cols=False): + """Gets and prints the spreadsheet's header columns + + :param file_loc: The file location of the spreadsheet + :type file_loc: str + :param print_cols: A flag used to print the columns to the console + (default is False) + :type print_cols: bool + :returns: a list of strings representing the header columns + :rtype: list + """ + + file_data = pd.read_excel(file_loc) + col_headers = list(file_data.columns.values) + + if print_cols: + print("\n".join(col_headers)) + + return col_headers + + +def get_spreadsheet_cols_numpy(file_loc, print_cols=False): + """Gets and prints the spreadsheet's header columns + + Parameters + ---------- + file_loc : str + The file location of the spreadsheet + print_cols : bool, optional + A flag used to print the columns to the console (default is False) + + Returns + ------- + list + a list of strings representing the header columns + """ + + file_data = pd.read_excel(file_loc) + col_headers = list(file_data.columns.values) + + if print_cols: + print("\n".join(col_headers)) + + return col_headers + + +def get_spreadsheet_cols_epytext(file_loc, print_cols=False): + """Gets and prints the spreadsheet's header columns + + @type file_loc: str + @param file_loc: The file location of the spreadsheet + @type print_cols: bool + @param print_cols: A flag used to print the columns to the console + (default is False) + @rtype: list + @returns: a list of strings representing the header columns + """ + + file_data = pd.read_excel(file_loc) + col_headers = list(file_data.columns.values) + + if print_cols: + print("\n".join(col_headers)) + + return col_headers + + +if __name__ == "__main__": + for func in ( + get_spreadsheet_cols_google, + get_spreadsheet_cols_rest, + get_spreadsheet_cols_numpy, + get_spreadsheet_cols_epytext, + ): + print(f"--- {func.__name__} ---") + print(func.__doc__) diff --git a/documenting-python-code/requirements.txt b/documenting-python-code/requirements.txt new file mode 100644 index 0000000000..d7b5fe6d17 --- /dev/null +++ b/documenting-python-code/requirements.txt @@ -0,0 +1,3 @@ +pandas==3.0.6 +openpyxl==3.1.5 +xlrd==2.0.2 diff --git a/documenting-python-code/say_hello_v1.py b/documenting-python-code/say_hello_v1.py new file mode 100644 index 0000000000..38c8b6cdd6 --- /dev/null +++ b/documenting-python-code/say_hello_v1.py @@ -0,0 +1,8 @@ +def say_hello(name): + print(f"Hello {name}, is it me you're looking for?") + + +say_hello.__doc__ = "A simple function that says hello... Richie style" + +if __name__ == "__main__": + help(say_hello) diff --git a/documenting-python-code/say_hello_v2.py b/documenting-python-code/say_hello_v2.py new file mode 100644 index 0000000000..3b955c7f5a --- /dev/null +++ b/documenting-python-code/say_hello_v2.py @@ -0,0 +1,7 @@ +def say_hello(name): + """A simple function that says hello... Richie style""" + print(f"Hello {name}, is it me you're looking for?") + + +if __name__ == "__main__": + help(say_hello) diff --git a/documenting-python-code/simple_class.py b/documenting-python-code/simple_class.py new file mode 100644 index 0000000000..5a4274a6ec --- /dev/null +++ b/documenting-python-code/simple_class.py @@ -0,0 +1,11 @@ +class SimpleClass: + """Class docstrings go here.""" + + def say_hello(self, name: str): + """Class method docstrings go here.""" + + print(f"Hello {name}") + + +if __name__ == "__main__": + help(SimpleClass) diff --git a/documenting-python-code/spreadsheet_columns.py b/documenting-python-code/spreadsheet_columns.py new file mode 100644 index 0000000000..9abeb55137 --- /dev/null +++ b/documenting-python-code/spreadsheet_columns.py @@ -0,0 +1,63 @@ +"""Spreadsheet Column Printer + +This script allows the user to print to the console all columns in the +spreadsheet. It is assumed that the first row of the spreadsheet is the +location of the columns. + +This tool accepts comma separated value files (.csv) as well as excel +(.xls, .xlsx) files. + +This script requires that `pandas` be installed within the Python +environment you are running this script in. + +This file can also be imported as a module and contains the following +functions: + + * get_spreadsheet_cols - returns the column headers of the file + * main - the main function of the script +""" + +import argparse + +import pandas as pd + + +def get_spreadsheet_cols(file_loc, print_cols=False): + """Gets and prints the spreadsheet's header columns + + Parameters + ---------- + file_loc : str + The file location of the spreadsheet + print_cols : bool, optional + A flag used to print the columns to the console (default is + False) + + Returns + ------- + list + a list of strings used that are the header columns + """ + + file_data = pd.read_excel(file_loc) + col_headers = list(file_data.columns.values) + + if print_cols: + print("\n".join(col_headers)) + + return col_headers + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "input_file", + type=str, + help="The spreadsheet file to print the columns of", + ) + args = parser.parse_args() + get_spreadsheet_cols(args.input_file, print_cols=True) + + +if __name__ == "__main__": + main() diff --git a/documenting-python-code/type_hints.py b/documenting-python-code/type_hints.py new file mode 100644 index 0000000000..71c852fb74 --- /dev/null +++ b/documenting-python-code/type_hints.py @@ -0,0 +1,6 @@ +def hello_name(name: str) -> str: + return f"Hello {name}" + + +if __name__ == "__main__": + print(hello_name("Real Python")) diff --git a/modeling-polymorphism-django-python/README.md b/modeling-polymorphism-django-python/README.md new file mode 100644 index 0000000000..4570d16a22 --- /dev/null +++ b/modeling-polymorphism-django-python/README.md @@ -0,0 +1,60 @@ +# Modeling Polymorphism in Django With Python + +This folder provides the code examples for the Real Python tutorial [Modeling Polymorphism in Django With Python](https://realpython.com/modeling-polymorphism-django-python/). + +The code is a runnable Django project called `bookstore`, with one Django app per modeling technique covered in the tutorial. It requires Python 3.12 or later, which is the minimum version supported by Django 6.1. + +## Installation + +Create and activate a virtual environment, install Django, and create the database tables: + +```console +$ python -m venv venv/ +$ source venv/bin/activate +(venv) $ python -m pip install -r requirements.txt +(venv) $ python manage.py migrate +``` + +The project uses SQLite, so you don't need a database server. The tutorial recommends PostgreSQL, but every example also works on SQLite. In particular, Django's built-in `models.JSONField`, used by the semi-structured model, supports SQLite, including key lookups like `extra__weight__gt=100`. To use PostgreSQL instead, install `psycopg` and change `DATABASES` in `bookstore/settings.py`. + +## Apps + +Each app maps to a section of the tutorial: + +| App | Tutorial section | Models | +| --- | --- | --- | +| `naive/` | Naive Implementation | `Book`, `Cart` | +| `sparse/` | Sparse Model | `Book` with a `type` field and `clean()` validation, `Cart` | +| `semi_structured/` | Semi-Structured Model | `Book` with an `extra` `JSONField` and `clean()` validation, `Cart` | +| `abstract_base_model/` | Abstract Base Model | Abstract `Product`, `Book`, `EBook`, `Cart` with one many-to-many field per product type | +| `concrete_base_model/` | Concrete Base Model | Concrete `Product`, `Book`, `EBook`, `Cart` with a many-to-many field to `Product` | +| `generic_foreign_key/` | Generic Foreign Key | `Product` protocol, `Book` with a `GenericRelation`, `EBook`, `Cart` with `add_item()`, `CartItem` with a `GenericForeignKey` | + +All apps share Django's built-in user model. Because each app defines a `Cart` with a one-to-one field to the user, each of these fields has an app-specific `related_name`, like `"naive_cart"`, to avoid clashing reverse accessors. + +## Usage + +Open a Django shell with `python manage.py shell` and follow along with the tutorial. For example: + +```pycon +>>> from django.contrib.auth import get_user_model +>>> user = get_user_model().objects.create_user("haki") + +>>> from concrete_base_model.models import Book, EBook, Cart +>>> book = Book.objects.create(name="Python Tricks", price=1000, weight=200) +>>> ebook = EBook.objects.create( +... name="The Old Man and the Sea", +... price=1500, +... download_link="http://books.com/12345", +... ) +>>> cart = Cart.objects.create(user=user) +>>> cart.items.add(book, ebook) +>>> cart.items.all() +, ]> + +>>> from django.db.models import Sum +>>> cart.items.aggregate(total_price=Sum("price")) +{'total_price': 2500} +``` + +To start over with an empty database, delete `db.sqlite3` and run `python manage.py migrate` again. diff --git a/modeling-polymorphism-django-python/abstract_base_model/__init__.py b/modeling-polymorphism-django-python/abstract_base_model/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/abstract_base_model/apps.py b/modeling-polymorphism-django-python/abstract_base_model/apps.py new file mode 100644 index 0000000000..612ec8430a --- /dev/null +++ b/modeling-polymorphism-django-python/abstract_base_model/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AbstractBaseModelConfig(AppConfig): + name = "abstract_base_model" diff --git a/modeling-polymorphism-django-python/abstract_base_model/migrations/0001_initial.py b/modeling-polymorphism-django-python/abstract_base_model/migrations/0001_initial.py new file mode 100644 index 0000000000..7615993ae3 --- /dev/null +++ b/modeling-polymorphism-django-python/abstract_base_model/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 6.1.1 on 2026-10-19 17:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ('weight', models.PositiveIntegerField(help_text='in grams')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EBook', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ('download_link', models.URLField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='abstract_base_model_cart', serialize=False, to=settings.AUTH_USER_MODEL)), + ('books', models.ManyToManyField(to='abstract_base_model.book')), + ('ebooks', models.ManyToManyField(to='abstract_base_model.ebook')), + ], + ), + ] diff --git a/modeling-polymorphism-django-python/abstract_base_model/migrations/__init__.py b/modeling-polymorphism-django-python/abstract_base_model/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/abstract_base_model/models.py b/modeling-polymorphism-django-python/abstract_base_model/models.py new file mode 100644 index 0000000000..74d993f45d --- /dev/null +++ b/modeling-polymorphism-django-python/abstract_base_model/models.py @@ -0,0 +1,46 @@ +from django.contrib.auth import get_user_model +from django.db import models + + +class Product(models.Model): + class Meta: + abstract = True + + name = models.CharField( + max_length=100, + ) + price = models.PositiveIntegerField( + help_text="in cents", + ) + + def __str__(self) -> str: + return self.name + + +class Book(Product): + weight = models.PositiveIntegerField( + help_text="in grams", + ) + + +class EBook(Product): + download_link = models.URLField() + + +# A ManyToManyField to the abstract Product model doesn't work: +# +# items = models.ManyToManyField(Product) +# +# abstract_base_model.Cart.items: (fields.E300) Field defines a relation +# with model 'Product', which is either not installed, or is abstract. +# +# Instead, the cart references books and e-books directly: +class Cart(models.Model): + user = models.OneToOneField( + get_user_model(), + primary_key=True, + on_delete=models.CASCADE, + related_name="abstract_base_model_cart", + ) + books = models.ManyToManyField(Book) + ebooks = models.ManyToManyField(EBook) diff --git a/modeling-polymorphism-django-python/bookstore/__init__.py b/modeling-polymorphism-django-python/bookstore/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/bookstore/asgi.py b/modeling-polymorphism-django-python/bookstore/asgi.py new file mode 100644 index 0000000000..f0ada6c640 --- /dev/null +++ b/modeling-polymorphism-django-python/bookstore/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for bookstore project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bookstore.settings") + +application = get_asgi_application() diff --git a/modeling-polymorphism-django-python/bookstore/settings.py b/modeling-polymorphism-django-python/bookstore/settings.py new file mode 100644 index 0000000000..d066904aa7 --- /dev/null +++ b/modeling-polymorphism-django-python/bookstore/settings.py @@ -0,0 +1,135 @@ +""" +Django settings for bookstore project. + +Generated by 'django-admin startproject' using Django 6.1.1. + +For more information on this file, see +https://docs.djangoproject.com/en/6.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = ( + "django-insecure-_^2)==biy++v7d1=t2i0xs49bn*1(!im*@+%s*cee9h4+b(vo$" +) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "naive", + "sparse", + "semi_structured", + "abstract_base_model", + "concrete_base_model", + "generic_foreign_key", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "bookstore.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "bookstore.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/6.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.1/howto/static-files/ + +STATIC_URL = "static/" + + +# Email +# https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration + +MAILERS = { + "default": { + "BACKEND": "django.core.mail.backends.console.EmailBackend", + }, +} diff --git a/modeling-polymorphism-django-python/bookstore/urls.py b/modeling-polymorphism-django-python/bookstore/urls.py new file mode 100644 index 0000000000..b6e95a4c3e --- /dev/null +++ b/modeling-polymorphism-django-python/bookstore/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for bookstore project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/modeling-polymorphism-django-python/bookstore/wsgi.py b/modeling-polymorphism-django-python/bookstore/wsgi.py new file mode 100644 index 0000000000..06b4a6b8e8 --- /dev/null +++ b/modeling-polymorphism-django-python/bookstore/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for bookstore project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bookstore.settings") + +application = get_wsgi_application() diff --git a/modeling-polymorphism-django-python/concrete_base_model/__init__.py b/modeling-polymorphism-django-python/concrete_base_model/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/concrete_base_model/apps.py b/modeling-polymorphism-django-python/concrete_base_model/apps.py new file mode 100644 index 0000000000..436fe438a8 --- /dev/null +++ b/modeling-polymorphism-django-python/concrete_base_model/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ConcreteBaseModelConfig(AppConfig): + name = "concrete_base_model" diff --git a/modeling-polymorphism-django-python/concrete_base_model/migrations/0001_initial.py b/modeling-polymorphism-django-python/concrete_base_model/migrations/0001_initial.py new file mode 100644 index 0000000000..2b43255a6f --- /dev/null +++ b/modeling-polymorphism-django-python/concrete_base_model/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 6.1.1 on 2026-10-19 17:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ], + ), + migrations.CreateModel( + name='Book', + fields=[ + ('product_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='concrete_base_model.product')), + ('weight', models.PositiveIntegerField()), + ], + bases=('concrete_base_model.product',), + ), + migrations.CreateModel( + name='EBook', + fields=[ + ('product_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='concrete_base_model.product')), + ('download_link', models.URLField()), + ], + bases=('concrete_base_model.product',), + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='concrete_base_model_cart', serialize=False, to=settings.AUTH_USER_MODEL)), + ('items', models.ManyToManyField(to='concrete_base_model.product')), + ], + ), + ] diff --git a/modeling-polymorphism-django-python/concrete_base_model/migrations/__init__.py b/modeling-polymorphism-django-python/concrete_base_model/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/concrete_base_model/models.py b/modeling-polymorphism-django-python/concrete_base_model/models.py new file mode 100644 index 0000000000..18794060e8 --- /dev/null +++ b/modeling-polymorphism-django-python/concrete_base_model/models.py @@ -0,0 +1,32 @@ +from django.contrib.auth import get_user_model +from django.db import models + + +class Product(models.Model): + name = models.CharField( + max_length=100, + ) + price = models.PositiveIntegerField( + help_text="in cents", + ) + + def __str__(self) -> str: + return self.name + + +class Book(Product): + weight = models.PositiveIntegerField() + + +class EBook(Product): + download_link = models.URLField() + + +class Cart(models.Model): + user = models.OneToOneField( + get_user_model(), + primary_key=True, + on_delete=models.CASCADE, + related_name="concrete_base_model_cart", + ) + items = models.ManyToManyField(Product) diff --git a/modeling-polymorphism-django-python/generic_foreign_key/__init__.py b/modeling-polymorphism-django-python/generic_foreign_key/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/generic_foreign_key/apps.py b/modeling-polymorphism-django-python/generic_foreign_key/apps.py new file mode 100644 index 0000000000..5541b5199c --- /dev/null +++ b/modeling-polymorphism-django-python/generic_foreign_key/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GenericForeignKeyConfig(AppConfig): + name = "generic_foreign_key" diff --git a/modeling-polymorphism-django-python/generic_foreign_key/migrations/0001_initial.py b/modeling-polymorphism-django-python/generic_foreign_key/migrations/0001_initial.py new file mode 100644 index 0000000000..825e2492ea --- /dev/null +++ b/modeling-polymorphism-django-python/generic_foreign_key/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 6.1.1 on 2026-10-19 17:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ('weight', models.PositiveIntegerField(help_text='in grams')), + ], + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='generic_foreign_key_cart', serialize=False, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='EBook', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ('download_link', models.URLField()), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_object_id', models.IntegerField()), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='generic_foreign_key.cart')), + ('product_content_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')), + ], + ), + ] diff --git a/modeling-polymorphism-django-python/generic_foreign_key/migrations/__init__.py b/modeling-polymorphism-django-python/generic_foreign_key/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/generic_foreign_key/models.py b/modeling-polymorphism-django-python/generic_foreign_key/models.py new file mode 100644 index 0000000000..8d11085f40 --- /dev/null +++ b/modeling-polymorphism-django-python/generic_foreign_key/models.py @@ -0,0 +1,89 @@ +from typing import Protocol + +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.fields import ( + GenericForeignKey, + GenericRelation, +) +from django.contrib.contenttypes.models import ContentType +from django.db import models + + +class Product(Protocol): + pk: int + name: str + price: int + + def __str__(self) -> str: ... + + +class Book(models.Model): + name = models.CharField( + max_length=100, + ) + price = models.PositiveIntegerField( + help_text="in cents", + ) + weight = models.PositiveIntegerField( + help_text="in grams", + ) + cart_items = GenericRelation( + "CartItem", + "product_object_id", + "product_content_type_id", + related_query_name="books", + ) + + def __str__(self) -> str: + return self.name + + +class EBook(models.Model): + name = models.CharField( + max_length=100, + ) + price = models.PositiveIntegerField( + help_text="in cents", + ) + download_link = models.URLField() + + def __str__(self) -> str: + return self.name + + +class Cart(models.Model): + user = models.OneToOneField( + get_user_model(), + primary_key=True, + on_delete=models.CASCADE, + related_name="generic_foreign_key_cart", + ) + + def add_item( + self, + product: Product, + ) -> "CartItem": + product_content_type = ContentType.objects.get_for_model(product) + + return CartItem.objects.create( + cart=self, + product_content_type=product_content_type, + product_object_id=product.pk, + ) + + +class CartItem(models.Model): + cart = models.ForeignKey( + Cart, + on_delete=models.CASCADE, + related_name="items", + ) + product_object_id = models.IntegerField() + product_content_type = models.ForeignKey( + ContentType, + on_delete=models.PROTECT, + ) + product = GenericForeignKey( + "product_content_type", + "product_object_id", + ) diff --git a/modeling-polymorphism-django-python/manage.py b/modeling-polymorphism-django-python/manage.py new file mode 100755 index 0000000000..4a76587240 --- /dev/null +++ b/modeling-polymorphism-django-python/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bookstore.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/modeling-polymorphism-django-python/naive/__init__.py b/modeling-polymorphism-django-python/naive/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/naive/apps.py b/modeling-polymorphism-django-python/naive/apps.py new file mode 100644 index 0000000000..7e4975bbda --- /dev/null +++ b/modeling-polymorphism-django-python/naive/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NaiveConfig(AppConfig): + name = "naive" diff --git a/modeling-polymorphism-django-python/naive/migrations/0001_initial.py b/modeling-polymorphism-django-python/naive/migrations/0001_initial.py new file mode 100644 index 0000000000..32ac358052 --- /dev/null +++ b/modeling-polymorphism-django-python/naive/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 6.1.1 on 2026-10-19 17:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ('weight', models.PositiveIntegerField(help_text='in grams')), + ], + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='naive_cart', serialize=False, to=settings.AUTH_USER_MODEL)), + ('books', models.ManyToManyField(to='naive.book')), + ], + ), + ] diff --git a/modeling-polymorphism-django-python/naive/migrations/__init__.py b/modeling-polymorphism-django-python/naive/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/naive/models.py b/modeling-polymorphism-django-python/naive/models.py new file mode 100644 index 0000000000..c475641149 --- /dev/null +++ b/modeling-polymorphism-django-python/naive/models.py @@ -0,0 +1,27 @@ +from django.contrib.auth import get_user_model +from django.db import models + + +class Book(models.Model): + name = models.CharField( + max_length=100, + ) + price = models.PositiveIntegerField( + help_text="in cents", + ) + weight = models.PositiveIntegerField( + help_text="in grams", + ) + + def __str__(self) -> str: + return self.name + + +class Cart(models.Model): + user = models.OneToOneField( + get_user_model(), + primary_key=True, + on_delete=models.CASCADE, + related_name="naive_cart", + ) + books = models.ManyToManyField(Book) diff --git a/modeling-polymorphism-django-python/requirements.txt b/modeling-polymorphism-django-python/requirements.txt new file mode 100644 index 0000000000..a9626bf780 --- /dev/null +++ b/modeling-polymorphism-django-python/requirements.txt @@ -0,0 +1 @@ +django==6.1.1 diff --git a/modeling-polymorphism-django-python/semi_structured/__init__.py b/modeling-polymorphism-django-python/semi_structured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/semi_structured/apps.py b/modeling-polymorphism-django-python/semi_structured/apps.py new file mode 100644 index 0000000000..6513cd7f2a --- /dev/null +++ b/modeling-polymorphism-django-python/semi_structured/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SemiStructuredConfig(AppConfig): + name = "semi_structured" diff --git a/modeling-polymorphism-django-python/semi_structured/migrations/0001_initial.py b/modeling-polymorphism-django-python/semi_structured/migrations/0001_initial.py new file mode 100644 index 0000000000..5714094adf --- /dev/null +++ b/modeling-polymorphism-django-python/semi_structured/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 6.1.1 on 2026-10-19 17:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('physical', 'Physical'), ('virtual', 'Virtual')], max_length=20)), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ('extra', models.JSONField()), + ], + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='semi_structured_cart', serialize=False, to=settings.AUTH_USER_MODEL)), + ('books', models.ManyToManyField(related_name='+', to='semi_structured.book')), + ], + ), + ] diff --git a/modeling-polymorphism-django-python/semi_structured/migrations/__init__.py b/modeling-polymorphism-django-python/semi_structured/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/semi_structured/models.py b/modeling-polymorphism-django-python/semi_structured/models.py new file mode 100644 index 0000000000..4593cd6523 --- /dev/null +++ b/modeling-polymorphism-django-python/semi_structured/models.py @@ -0,0 +1,91 @@ +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.core.validators import URLValidator +from django.db import models + + +class Book(models.Model): + TYPE_PHYSICAL = "physical" + TYPE_VIRTUAL = "virtual" + TYPE_CHOICES = ( + (TYPE_PHYSICAL, "Physical"), + (TYPE_VIRTUAL, "Virtual"), + ) + type = models.CharField( + max_length=20, + choices=TYPE_CHOICES, + ) + + # Common attributes + name = models.CharField( + max_length=100, + ) + price = models.PositiveIntegerField( + help_text="in cents", + ) + + extra = models.JSONField() + + def __str__(self) -> str: + return f"[{self.get_type_display()}] {self.name}" + + def clean(self) -> None: + if self.type == Book.TYPE_VIRTUAL: + try: + weight = int(self.extra["weight"]) + except ValueError: + raise ValidationError("Weight must be a number") + except KeyError: + pass + else: + if weight != 0: + raise ValidationError( + "A virtual product weight cannot exceed zero." + ) + + try: + download_link = self.extra["download_link"] + except KeyError: + pass + else: + # Will raise a validation error + URLValidator()(download_link) + + elif self.type == Book.TYPE_PHYSICAL: + try: + weight = int(self.extra["weight"]) + except ValueError: + raise ValidationError("Weight must be a number") + except KeyError: + pass + else: + if weight == 0: + raise ValidationError( + "A physical product weight must exceed zero." + ) + + try: + download_link = self.extra["download_link"] + except KeyError: + pass + else: + if download_link is not None: + raise ValidationError( + "A physical product cannot have a download link." + ) + + else: + raise ValidationError(f'Unknown product type "{self.type}"') + + +class Cart(models.Model): + user = models.OneToOneField( + get_user_model(), + primary_key=True, + on_delete=models.CASCADE, + related_name="semi_structured_cart", + ) + books = models.ManyToManyField( + Book, + related_name="+", + ) diff --git a/modeling-polymorphism-django-python/sparse/__init__.py b/modeling-polymorphism-django-python/sparse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/sparse/apps.py b/modeling-polymorphism-django-python/sparse/apps.py new file mode 100644 index 0000000000..c5d2550cb3 --- /dev/null +++ b/modeling-polymorphism-django-python/sparse/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SparseConfig(AppConfig): + name = "sparse" diff --git a/modeling-polymorphism-django-python/sparse/migrations/0001_initial.py b/modeling-polymorphism-django-python/sparse/migrations/0001_initial.py new file mode 100644 index 0000000000..c620e5a112 --- /dev/null +++ b/modeling-polymorphism-django-python/sparse/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 6.1.1 on 2026-10-19 17:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('physical', 'Physical'), ('virtual', 'Virtual')], max_length=20)), + ('name', models.CharField(max_length=100)), + ('price', models.PositiveIntegerField(help_text='in cents')), + ('weight', models.PositiveIntegerField(help_text='in grams')), + ('download_link', models.URLField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='sparse_cart', serialize=False, to=settings.AUTH_USER_MODEL)), + ('books', models.ManyToManyField(to='sparse.book')), + ], + ), + ] diff --git a/modeling-polymorphism-django-python/sparse/migrations/__init__.py b/modeling-polymorphism-django-python/sparse/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modeling-polymorphism-django-python/sparse/models.py b/modeling-polymorphism-django-python/sparse/models.py new file mode 100644 index 0000000000..e38a07812f --- /dev/null +++ b/modeling-polymorphism-django-python/sparse/models.py @@ -0,0 +1,74 @@ +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.db import models + + +class Book(models.Model): + TYPE_PHYSICAL = "physical" + TYPE_VIRTUAL = "virtual" + TYPE_CHOICES = ( + (TYPE_PHYSICAL, "Physical"), + (TYPE_VIRTUAL, "Virtual"), + ) + type = models.CharField( + max_length=20, + choices=TYPE_CHOICES, + ) + + # Common attributes + name = models.CharField( + max_length=100, + ) + price = models.PositiveIntegerField( + help_text="in cents", + ) + + # Specific attributes + weight = models.PositiveIntegerField( + help_text="in grams", + ) + download_link = models.URLField( + null=True, + blank=True, + ) + + def __str__(self) -> str: + return f"[{self.get_type_display()}] {self.name}" + + def clean(self) -> None: + if self.type == Book.TYPE_VIRTUAL: + if self.weight != 0: + raise ValidationError( + "A virtual product weight cannot exceed zero." + ) + + if self.download_link is None: + raise ValidationError( + "A virtual product must have a download link." + ) + + elif self.type == Book.TYPE_PHYSICAL: + if self.weight == 0: + raise ValidationError( + "A physical product weight must exceed zero." + ) + + if self.download_link is not None: + raise ValidationError( + "A physical product cannot have a download link." + ) + + else: + assert False, f'Unknown product type "{self.type}"' + + +class Cart(models.Model): + user = models.OneToOneField( + get_user_model(), + primary_key=True, + on_delete=models.CASCADE, + related_name="sparse_cart", + ) + books = models.ManyToManyField( + Book, + ) diff --git a/operator-function-overloading/README.md b/operator-function-overloading/README.md new file mode 100644 index 0000000000..58f246e205 --- /dev/null +++ b/operator-function-overloading/README.md @@ -0,0 +1,63 @@ +# Operator and Function Overloading in Custom Python Classes + +This folder provides the code examples for the Real Python tutorial [Operator and Function Overloading in Custom Python Classes](https://realpython.com/operator-function-overloading/). + +The code requires Python 3.6 or later and has no third-party dependencies. + +## Overloading Built-in Functions + +Each of these modules defines the `Order` or `Vector` class from one section of the tutorial: + +- `order_len.py`: `Order` with `.__len__()`, so `len()` returns the number of cart items. +- `order_len_float.py`: `Order` whose `.__len__()` returns a `float`, which makes `len()` raise a `TypeError`. +- `vector_abs.py`: `Vector` with `.__abs__()`, so `abs()` returns the vector's magnitude. +- `vector_str.py`: `Vector` with `.__str__()` for a user-friendly `xi+yj` string. +- `vector_repr.py`: `Vector` with `.__repr__()` for a string you can pass to `eval()`. +- `order_bool.py`: `Order` with `.__bool__()`, so an empty order is falsy. + +```pycon +>>> from order_len import Order +>>> order = Order(["banana", "apple", "mango"], "Real Python") +>>> len(order) +3 + +>>> from vector_str import Vector +>>> print(Vector(3, 4)) +3i+4j +``` + +## Overloading Built-in Operators + +- `order_add.py`: `Order` with `.__add__()`, so `order + item` returns a new order. +- `order_iadd.py`: `Order` with `.__iadd__()`, so `order += item` updates the order in place. +- `order_iadd_string.py`: `Order` whose `.__iadd__()` returns a string, showing that `+=` rebinds the name to whatever the method returns. +- `order_iadd_none.py`: `Order` whose `.__iadd__()` forgets to return anything, so `order += item` sets `order` to `None`. +- `order_getitem.py`: `Order` with `.__getitem__()` for indexing and slicing the cart. +- `mock.py`: `Mock` with only `.__add__()`, showing that `6 + Mock(5)` raises a `TypeError`. +- `order_radd.py`: `Order` with `.__add__()` and `.__radd__()`, so `item + order` adds the item to the front of the cart. + +```pycon +>>> from order_radd import Order +>>> order = Order(["banana", "apple"], "Real Python") +>>> order = "mango" + order +>>> order.cart +['mango', 'banana', 'apple'] +``` + +## A Complete Example + +- `custom_complex.py`: The `CustomComplex` class, a complex number that supports `abs()`, `str()`, `repr()`, `+`, `-`, `*`, `**`, `==`, and `!=`. + +```pycon +>>> from custom_complex import CustomComplex +>>> a = CustomComplex(1, 2) +>>> b = CustomComplex(3, -4) +>>> a + b +CustomComplex(4, -2) +>>> 3 - a +CustomComplex(2, -2) +>>> b ** 5 +CustomComplex(-237, 3116) +``` + +Run the examples from inside this folder so that Python can import the modules. diff --git a/operator-function-overloading/custom_complex.py b/operator-function-overloading/custom_complex.py new file mode 100644 index 0000000000..a0cd7c184b --- /dev/null +++ b/operator-function-overloading/custom_complex.py @@ -0,0 +1,207 @@ +from math import hypot, atan, sin, cos + + +class CustomComplex: + """ + A class to represent a complex number, a+bj. + Attributes: + real - int, representing the real part + imag - int, representing the imaginary part + + Implements the following: + + * Addition with a complex number or a real number using `+` + * Multiplication with a complex number or a real number using `*` + * Subtraction of a complex number or a real number using `-` + * Calculation of absolute value using `abs` + * Raise complex number to a power using `**` + * Nice string representation using `__repr__` + * Nice user-end viewing using `__str__` + + Notes: + * The constructor has been intentionally kept simple + * It is configured to support one kind of call: + CustomComplex(a, b) + * Error handling was avoided to keep things simple + """ + + def __init__(self, real, imag): + """ + Initializes a complex number, setting real and imag part + Arguments: + real: Number, real part of the complex number + imag: Number, imaginary part of the complex number + """ + self.real = real + self.imag = imag + + def conjugate(self): + """ + Returns the complex conjugate of a complex number + Return: + CustomComplex instance + """ + return CustomComplex(self.real, -self.imag) + + def argz(self): + """ + Returns the argument of a complex number + The argument is given by: + atan(imag_part/real_part) + Return: + float + """ + return atan(self.imag / self.real) + + def __abs__(self): + """ + Returns the modulus of a complex number + Return: + float + """ + return hypot(self.real, self.imag) + + def __repr__(self): + """ + Returns str representation of an instance of the + class. Can be used with eval() to get another + instance of the class + Return: + str + """ + return f"CustomComplex({self.real}, {self.imag})" + + def __str__(self): + """ + Returns user-friendly str representation of an instance + of the class + Return: + str + """ + return f"({self.real}{self.imag:+}j)" + + def __add__(self, other): + """ + Returns the addition of a complex number with + int, float or another complex number + Return: + CustomComplex instance + """ + if isinstance(other, float) or isinstance(other, int): + real_part = self.real + other + imag_part = self.imag + + if isinstance(other, CustomComplex): + real_part = self.real + other.real + imag_part = self.imag + other.imag + + return CustomComplex(real_part, imag_part) + + def __sub__(self, other): + """ + Returns the subtraction from a complex number of + int, float or another complex number + Return: + CustomComplex instance + """ + if isinstance(other, float) or isinstance(other, int): + real_part = self.real - other + imag_part = self.imag + + if isinstance(other, CustomComplex): + real_part = self.real - other.real + imag_part = self.imag - other.imag + + return CustomComplex(real_part, imag_part) + + def __mul__(self, other): + """ + Returns the multiplication of a complex number with + int, float or another complex number + Return: + CustomComplex instance + """ + if isinstance(other, int) or isinstance(other, float): + real_part = self.real * other + imag_part = self.imag * other + + if isinstance(other, CustomComplex): + real_part = (self.real * other.real) - (self.imag * other.imag) + imag_part = (self.real * other.imag) + (self.imag * other.real) + + return CustomComplex(real_part, imag_part) + + def __radd__(self, other): + """ + Same as __add__; allows 1 + CustomComplex('x+yj') + x + y == y + x + """ + return self.__add__(other) + + def __rmul__(self, other): + """ + Same as __mul__; allows 2 * CustomComplex('x+yj') + x * y == y * x + """ + return self.__mul__(other) + + def __rsub__(self, other): + """ + Returns the subtraction of a complex number from + int or float + x - y != y - x + Subtraction of another complex number is not handled by __rsub__ + Instead, __sub__ handles it since both sides are instances of + this class + Return: + CustomComplex instance + """ + if isinstance(other, float) or isinstance(other, int): + real_part = other - self.real + imag_part = -self.imag + + return CustomComplex(real_part, imag_part) + + def __eq__(self, other): + """ + Checks equality of two complex numbers + Two complex numbers are equal when: + * Their real parts are equal AND + * Their imaginary parts are equal + Return: + bool + """ + # Note: comparing floats directly is not a good idea in general + # due to floating-point precision + return (self.real == other.real) and (self.imag == other.imag) + + def __ne__(self, other): + """ + Checks inequality of two complex numbers + Two complex numbers are unequal when: + * Their real parts are unequal OR + * Their imaginary parts are unequal + Return: + bool + """ + return (self.real != other.real) or (self.imag != other.imag) + + def __pow__(self, other): + """ + Raises a complex number to a power + Formula: + z**n = (r**n)*[cos(n*argz) + sin(n*argz)j], where + z = complex number + n = power + r = absolute value of z + argz = argument of z + Return: + CustomComplex instance + """ + r_raised = abs(self) ** other + argz_multiplied = self.argz() * other + + real_part = round(r_raised * cos(argz_multiplied)) + imag_part = round(r_raised * sin(argz_multiplied)) + + return CustomComplex(real_part, imag_part) diff --git a/operator-function-overloading/mock.py b/operator-function-overloading/mock.py new file mode 100644 index 0000000000..e875c133b7 --- /dev/null +++ b/operator-function-overloading/mock.py @@ -0,0 +1,6 @@ +class Mock: + def __init__(self, num): + self.num = num + + def __add__(self, other): + return Mock(self.num + other) diff --git a/operator-function-overloading/order_add.py b/operator-function-overloading/order_add.py new file mode 100644 index 0000000000..038bfb10a3 --- /dev/null +++ b/operator-function-overloading/order_add.py @@ -0,0 +1,9 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __add__(self, other): + new_cart = self.cart.copy() + new_cart.append(other) + return Order(new_cart, self.customer) diff --git a/operator-function-overloading/order_bool.py b/operator-function-overloading/order_bool.py new file mode 100644 index 0000000000..ed41a6ab8f --- /dev/null +++ b/operator-function-overloading/order_bool.py @@ -0,0 +1,7 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __bool__(self): + return len(self.cart) > 0 diff --git a/operator-function-overloading/order_getitem.py b/operator-function-overloading/order_getitem.py new file mode 100644 index 0000000000..3a49aaf710 --- /dev/null +++ b/operator-function-overloading/order_getitem.py @@ -0,0 +1,7 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __getitem__(self, key): + return self.cart[key] diff --git a/operator-function-overloading/order_iadd.py b/operator-function-overloading/order_iadd.py new file mode 100644 index 0000000000..430fcc1b3b --- /dev/null +++ b/operator-function-overloading/order_iadd.py @@ -0,0 +1,8 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __iadd__(self, other): + self.cart.append(other) + return self diff --git a/operator-function-overloading/order_iadd_none.py b/operator-function-overloading/order_iadd_none.py new file mode 100644 index 0000000000..287b50c60a --- /dev/null +++ b/operator-function-overloading/order_iadd_none.py @@ -0,0 +1,7 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __iadd__(self, other): + self.cart.append(other) diff --git a/operator-function-overloading/order_iadd_string.py b/operator-function-overloading/order_iadd_string.py new file mode 100644 index 0000000000..789ac06b3a --- /dev/null +++ b/operator-function-overloading/order_iadd_string.py @@ -0,0 +1,8 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __iadd__(self, other): + self.cart.append(other) + return "Hey, I am string!" diff --git a/operator-function-overloading/order_len.py b/operator-function-overloading/order_len.py new file mode 100644 index 0000000000..6e606bf265 --- /dev/null +++ b/operator-function-overloading/order_len.py @@ -0,0 +1,7 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __len__(self): + return len(self.cart) diff --git a/operator-function-overloading/order_len_float.py b/operator-function-overloading/order_len_float.py new file mode 100644 index 0000000000..bf14fd7f56 --- /dev/null +++ b/operator-function-overloading/order_len_float.py @@ -0,0 +1,7 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __len__(self): + return float(len(self.cart)) # Return type changed to float diff --git a/operator-function-overloading/order_radd.py b/operator-function-overloading/order_radd.py new file mode 100644 index 0000000000..fc16961d70 --- /dev/null +++ b/operator-function-overloading/order_radd.py @@ -0,0 +1,14 @@ +class Order: + def __init__(self, cart, customer): + self.cart = list(cart) + self.customer = customer + + def __add__(self, other): + new_cart = self.cart.copy() + new_cart.append(other) + return Order(new_cart, self.customer) + + def __radd__(self, other): + new_cart = self.cart.copy() + new_cart.insert(0, other) + return Order(new_cart, self.customer) diff --git a/operator-function-overloading/vector_abs.py b/operator-function-overloading/vector_abs.py new file mode 100644 index 0000000000..1c5b642552 --- /dev/null +++ b/operator-function-overloading/vector_abs.py @@ -0,0 +1,7 @@ +class Vector: + def __init__(self, x_comp, y_comp): + self.x_comp = x_comp + self.y_comp = y_comp + + def __abs__(self): + return (self.x_comp**2 + self.y_comp**2) ** 0.5 diff --git a/operator-function-overloading/vector_repr.py b/operator-function-overloading/vector_repr.py new file mode 100644 index 0000000000..1c972ee78f --- /dev/null +++ b/operator-function-overloading/vector_repr.py @@ -0,0 +1,7 @@ +class Vector: + def __init__(self, x_comp, y_comp): + self.x_comp = x_comp + self.y_comp = y_comp + + def __repr__(self): + return f"Vector({self.x_comp}, {self.y_comp})" diff --git a/operator-function-overloading/vector_str.py b/operator-function-overloading/vector_str.py new file mode 100644 index 0000000000..d891044132 --- /dev/null +++ b/operator-function-overloading/vector_str.py @@ -0,0 +1,9 @@ +class Vector: + def __init__(self, x_comp, y_comp): + self.x_comp = x_comp + self.y_comp = y_comp + + def __str__(self): + # By default, sign of +ve number is not displayed + # Using `+`, sign is always displayed + return f"{self.x_comp}i{self.y_comp:+}j" diff --git a/python-data-classes/README.md b/python-data-classes/README.md new file mode 100644 index 0000000000..df36737710 --- /dev/null +++ b/python-data-classes/README.md @@ -0,0 +1,103 @@ +# Data Classes in Python (Guide) + +This folder provides the code examples for the Real Python tutorial [Data Classes in Python (Guide)](https://realpython.com/python-data-classes/). + +## Installation + +Most examples only use the standard library. To run the `attrs` example and the memory measurements with Pympler, install the third-party packages into a virtual environment: + +```sh +$ python -m pip install -r requirements.txt +``` + +The code requires Python 3.10 or later because `slots.py` uses `@dataclass(slots=True)`. All other data class examples run on Python 3.7 or later, and `attrs_card.py` needs Python 3.9 or later for `attrs` 26.1. + +## Files + +### Data Class Alternatives + +- `data_class_card.py`: A minimal `DataClassCard` data class +- `regular_card_v1.py`: A minimal `RegularCard` regular class, without a nice representation or equality +- `regular_card_v2.py`: `RegularCard` with hand-written `.__repr__()` and `.__eq__()` methods +- `named_tuple_card.py`: `NamedTupleCard` built with `collections.namedtuple` +- `attrs_card.py`: `AttrsCard` built with the third-party `attrs` package + +```pycon +>>> from data_class_card import DataClassCard +>>> queen_of_hearts = DataClassCard("Q", "Hearts") +>>> queen_of_hearts +DataClassCard(rank='Q', suit='Hearts') +>>> queen_of_hearts == DataClassCard("Q", "Hearts") +True +``` + +### Basic Data Classes + +- `position.py`: A basic `Position` data class with `name`, `lon`, and `lat` fields +- `position_make_dataclass.py`: `Position` created with `make_dataclass()` +- `position_defaults.py`: `Position` with default values +- `without_explicit_types.py`: A data class using `typing.Any` instead of explicit types +- `position_distance.py`: `Position` with a `.distance_to()` method using the haversine formula + +```pycon +>>> from position_distance import Position +>>> oslo = Position("Oslo", 10.8, 59.9) +>>> vancouver = Position("Vancouver", -123.1, 49.3) +>>> oslo.distance_to(vancouver) +7181.7841229421165 +``` + +### More Flexible Data Classes + +- `deck_v1.py`: `PlayingCard` and a `Deck` holding a list of cards +- `deck_v2.py`: Adds `RANKS`, `SUITS`, `make_french_deck()`, and a `default_factory` for `Deck` +- `position_metadata.py`: `Position` with field metadata set through `field()` +- `deck_v3.py`: Adds a `.__str__()` method to `PlayingCard` and a concise `.__repr__()` to `Deck` +- `deck_v4.py`: Makes `PlayingCard` orderable with `order=True`, which compares cards alphabetically +- `deck_v5.py`: Adds a `.sort_index` computed in `.__post_init__()` so cards compare in the right order + +```pycon +>>> from deck_v5 import Deck, PlayingCard, make_french_deck +>>> PlayingCard("A", "♠") > PlayingCard("Q", "♡") +True +>>> Deck(sorted(make_french_deck())) +Deck(♣2, ♢2, ♡2, ♠2, ♣3, ♢3, ♡3, ♠3, ♣4, ♢4, ♡4, ♠4, ♣5, ♢5, ♡5, ♠5, ♣6, ♢6, ♡6, ♠6, ♣7, ♢7, ♡7, ♠7, ♣8, ♢8, ♡8, ♠8, ♣9, ♢9, ♡9, ♠9, ♣10, ♢10, ♡10, ♠10, ♣J, ♢J, ♡J, ♠J, ♣Q, ♢Q, ♡Q, ♠Q, ♣K, ♢K, ♡K, ♠K, ♣A, ♢A, ♡A, ♠A) +``` + +### Immutable Data Classes + +- `position_frozen.py`: A frozen, immutable version of `Position` +- `immutable_deck.py`: Frozen `ImmutableCard` and `ImmutableDeck` classes, where the list of cards can still change + +```pycon +>>> from position_frozen import Position +>>> pos = Position("Oslo", 10.8, 59.9) +>>> pos.name = "Stockholm" +Traceback (most recent call last): + ... +dataclasses.FrozenInstanceError: cannot assign to field 'name' +``` + +### Inheritance + +- `capital.py`: A `Capital` subclass that adds a `country` field to `Position` +- `capital_defaults.py`: A `Capital` subclass that redefines the default value of `lat` + +```pycon +>>> from capital_defaults import Capital +>>> Capital("Madrid", country="Spain") +Capital(name='Madrid', lon=0.0, lat=40.0, country='Spain') +``` + +### Optimizing Data Classes + +- `slots.py`: `SimplePosition` and `SlotPosition`, a regular data class and a data class with `slots=True` + +```pycon +>>> from pympler import asizeof +>>> from slots import SimplePosition, SlotPosition +>>> simple = SimplePosition("London", -0.1, 51.5) +>>> slot = SlotPosition("Madrid", -3.7, 40.4) +>>> asizeof.asizesof(simple, slot) +(584, 152) +``` diff --git a/python-data-classes/attrs_card.py b/python-data-classes/attrs_card.py new file mode 100644 index 0000000000..fa7619dc4f --- /dev/null +++ b/python-data-classes/attrs_card.py @@ -0,0 +1,7 @@ +from attrs import define + + +@define +class AttrsCard: + rank: str + suit: str diff --git a/python-data-classes/capital.py b/python-data-classes/capital.py new file mode 100644 index 0000000000..c0f2bc6758 --- /dev/null +++ b/python-data-classes/capital.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + + +@dataclass +class Position: + name: str + lon: float + lat: float + + +@dataclass +class Capital(Position): + country: str diff --git a/python-data-classes/capital_defaults.py b/python-data-classes/capital_defaults.py new file mode 100644 index 0000000000..7a4fb7b811 --- /dev/null +++ b/python-data-classes/capital_defaults.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + + +@dataclass +class Position: + name: str + lon: float = 0.0 + lat: float = 0.0 + + +@dataclass +class Capital(Position): + country: str = "Unknown" + lat: float = 40.0 diff --git a/python-data-classes/data_class_card.py b/python-data-classes/data_class_card.py new file mode 100644 index 0000000000..508406bf84 --- /dev/null +++ b/python-data-classes/data_class_card.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class DataClassCard: + rank: str + suit: str diff --git a/python-data-classes/deck_v1.py b/python-data-classes/deck_v1.py new file mode 100644 index 0000000000..5231a51258 --- /dev/null +++ b/python-data-classes/deck_v1.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class PlayingCard: + rank: str + suit: str + + +@dataclass +class Deck: + cards: List[PlayingCard] diff --git a/python-data-classes/deck_v2.py b/python-data-classes/deck_v2.py new file mode 100644 index 0000000000..14546c8edd --- /dev/null +++ b/python-data-classes/deck_v2.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass, field +from typing import List + +RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() +SUITS = "♣ ♢ ♡ ♠".split() + + +@dataclass +class PlayingCard: + rank: str + suit: str + + +def make_french_deck(): + return [PlayingCard(r, s) for s in SUITS for r in RANKS] + + +@dataclass +class Deck: + cards: List[PlayingCard] = field(default_factory=make_french_deck) diff --git a/python-data-classes/deck_v3.py b/python-data-classes/deck_v3.py new file mode 100644 index 0000000000..ff136d00a1 --- /dev/null +++ b/python-data-classes/deck_v3.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass, field +from typing import List + +RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() +SUITS = "♣ ♢ ♡ ♠".split() + + +@dataclass +class PlayingCard: + rank: str + suit: str + + def __str__(self): + return f"{self.suit}{self.rank}" + + +def make_french_deck(): + return [PlayingCard(r, s) for s in SUITS for r in RANKS] + + +@dataclass +class Deck: + cards: List[PlayingCard] = field(default_factory=make_french_deck) + + def __repr__(self): + cards = ", ".join(f"{c!s}" for c in self.cards) + return f"{self.__class__.__name__}({cards})" diff --git a/python-data-classes/deck_v4.py b/python-data-classes/deck_v4.py new file mode 100644 index 0000000000..79a86ea97b --- /dev/null +++ b/python-data-classes/deck_v4.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass, field +from typing import List + +RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() +SUITS = "♣ ♢ ♡ ♠".split() + + +@dataclass(order=True) +class PlayingCard: + rank: str + suit: str + + def __str__(self): + return f"{self.suit}{self.rank}" + + +def make_french_deck(): + return [PlayingCard(r, s) for s in SUITS for r in RANKS] + + +@dataclass +class Deck: + cards: List[PlayingCard] = field(default_factory=make_french_deck) + + def __repr__(self): + cards = ", ".join(f"{c!s}" for c in self.cards) + return f"{self.__class__.__name__}({cards})" diff --git a/python-data-classes/deck_v5.py b/python-data-classes/deck_v5.py new file mode 100644 index 0000000000..6f4f9f7447 --- /dev/null +++ b/python-data-classes/deck_v5.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass, field +from typing import List + +RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() +SUITS = "♣ ♢ ♡ ♠".split() + + +@dataclass(order=True) +class PlayingCard: + sort_index: int = field(init=False, repr=False) + rank: str + suit: str + + def __post_init__(self): + self.sort_index = RANKS.index(self.rank) * len(SUITS) + SUITS.index( + self.suit + ) + + def __str__(self): + return f"{self.suit}{self.rank}" + + +def make_french_deck(): + return [PlayingCard(r, s) for s in SUITS for r in RANKS] + + +@dataclass +class Deck: + cards: List[PlayingCard] = field(default_factory=make_french_deck) + + def __repr__(self): + cards = ", ".join(f"{c!s}" for c in self.cards) + return f"{self.__class__.__name__}({cards})" diff --git a/python-data-classes/immutable_deck.py b/python-data-classes/immutable_deck.py new file mode 100644 index 0000000000..ffae79b1a2 --- /dev/null +++ b/python-data-classes/immutable_deck.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass(frozen=True) +class ImmutableCard: + rank: str + suit: str + + +@dataclass(frozen=True) +class ImmutableDeck: + cards: List[ImmutableCard] diff --git a/python-data-classes/named_tuple_card.py b/python-data-classes/named_tuple_card.py new file mode 100644 index 0000000000..643315f090 --- /dev/null +++ b/python-data-classes/named_tuple_card.py @@ -0,0 +1,3 @@ +from collections import namedtuple + +NamedTupleCard = namedtuple("NamedTupleCard", ["rank", "suit"]) diff --git a/python-data-classes/position.py b/python-data-classes/position.py new file mode 100644 index 0000000000..608c7d565f --- /dev/null +++ b/python-data-classes/position.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class Position: + name: str + lon: float + lat: float diff --git a/python-data-classes/position_defaults.py b/python-data-classes/position_defaults.py new file mode 100644 index 0000000000..ebc12341a3 --- /dev/null +++ b/python-data-classes/position_defaults.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class Position: + name: str + lon: float = 0.0 + lat: float = 0.0 diff --git a/python-data-classes/position_distance.py b/python-data-classes/position_distance.py new file mode 100644 index 0000000000..8af84d9a27 --- /dev/null +++ b/python-data-classes/position_distance.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from math import asin, cos, radians, sin, sqrt + + +@dataclass +class Position: + name: str + lon: float = 0.0 + lat: float = 0.0 + + def distance_to(self, other): + r = 6371 # Earth radius in kilometers + lam_1, lam_2 = radians(self.lon), radians(other.lon) + phi_1, phi_2 = radians(self.lat), radians(other.lat) + h = ( + sin((phi_2 - phi_1) / 2) ** 2 + + cos(phi_1) * cos(phi_2) * sin((lam_2 - lam_1) / 2) ** 2 + ) + return 2 * r * asin(sqrt(h)) diff --git a/python-data-classes/position_frozen.py b/python-data-classes/position_frozen.py new file mode 100644 index 0000000000..8778051476 --- /dev/null +++ b/python-data-classes/position_frozen.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Position: + name: str + lon: float = 0.0 + lat: float = 0.0 diff --git a/python-data-classes/position_make_dataclass.py b/python-data-classes/position_make_dataclass.py new file mode 100644 index 0000000000..5be429b501 --- /dev/null +++ b/python-data-classes/position_make_dataclass.py @@ -0,0 +1,3 @@ +from dataclasses import make_dataclass + +Position = make_dataclass("Position", ["name", "lat", "lon"]) diff --git a/python-data-classes/position_metadata.py b/python-data-classes/position_metadata.py new file mode 100644 index 0000000000..74c5b1bfe3 --- /dev/null +++ b/python-data-classes/position_metadata.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass, field + + +@dataclass +class Position: + name: str + lon: float = field(default=0.0, metadata={"unit": "degrees"}) + lat: float = field(default=0.0, metadata={"unit": "degrees"}) diff --git a/python-data-classes/regular_card_v1.py b/python-data-classes/regular_card_v1.py new file mode 100644 index 0000000000..25dc552671 --- /dev/null +++ b/python-data-classes/regular_card_v1.py @@ -0,0 +1,4 @@ +class RegularCard: + def __init__(self, rank, suit): + self.rank = rank + self.suit = suit diff --git a/python-data-classes/regular_card_v2.py b/python-data-classes/regular_card_v2.py new file mode 100644 index 0000000000..fdd42b14aa --- /dev/null +++ b/python-data-classes/regular_card_v2.py @@ -0,0 +1,15 @@ +class RegularCard: + def __init__(self, rank, suit): + self.rank = rank + self.suit = suit + + def __repr__(self): + return ( + f"{self.__class__.__name__}" + f"(rank={self.rank!r}, suit={self.suit!r})" + ) + + def __eq__(self, other): + if other.__class__ is not self.__class__: + return NotImplemented + return (self.rank, self.suit) == (other.rank, other.suit) diff --git a/python-data-classes/requirements.txt b/python-data-classes/requirements.txt new file mode 100644 index 0000000000..e57f347cf0 --- /dev/null +++ b/python-data-classes/requirements.txt @@ -0,0 +1,2 @@ +attrs==26.1.0 +pympler==1.1 diff --git a/python-data-classes/slots.py b/python-data-classes/slots.py new file mode 100644 index 0000000000..519a802321 --- /dev/null +++ b/python-data-classes/slots.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + + +@dataclass +class SimplePosition: + name: str + lon: float + lat: float + + +@dataclass(slots=True) +class SlotPosition: + name: str + lon: float + lat: float diff --git a/python-data-classes/without_explicit_types.py b/python-data-classes/without_explicit_types.py new file mode 100644 index 0000000000..c7d13ef488 --- /dev/null +++ b/python-data-classes/without_explicit_types.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from typing import Any + + +@dataclass +class WithoutExplicitTypes: + name: Any + value: Any = 42 diff --git a/python-recursion/README.md b/python-recursion/README.md new file mode 100644 index 0000000000..6dcde78f36 --- /dev/null +++ b/python-recursion/README.md @@ -0,0 +1,89 @@ +# Recursion in Python: An Introduction + +This folder provides the code examples for the Real Python tutorial [Recursion in Python: An Introduction](https://realpython.com/python-recursion/). + +The code requires Python 3.6 or later and has no third-party dependencies. + +## Infinite Recursion + +- `infinite_recursion.py`: A function that calls itself without a base case. Running it raises a `RecursionError`. + +## Count Down to Zero + +- `countdown_recursive.py`: Recursive `countdown()` with an explicit base case and recursive call +- `countdown_concise.py`: A more concise recursive `countdown()` +- `countdown_iterative.py`: A non-recursive `countdown()` that uses a `while` loop + +```console +$ python countdown_recursive.py +5 +4 +3 +2 +1 +0 +``` + +## Calculate Factorial + +- `factorial_recursive.py`: Recursive `factorial()` +- `factorial_recursive_verbose.py`: Recursive `factorial()` with `print()` calls that trace the calls and return values +- `factorial_iterative.py`: Iterative `factorial()` that uses a `for` loop +- `factorial_reduce.py`: `factorial()` implemented with `functools.reduce()` +- `factorial_timing.py`: Compares the speed of the recursive, iterative, `reduce()`, and `math.factorial()` implementations with `timeit()`. It runs each one ten million times, so expect it to take a while. + +```pycon +>>> from factorial_recursive_verbose import factorial +>>> factorial(4) +factorial() called with n = 4 +factorial() called with n = 3 +factorial() called with n = 2 +factorial() called with n = 1 +-> factorial(1) returns 1 +-> factorial(2) returns 2 +-> factorial(3) returns 6 +-> factorial(4) returns 24 +24 +``` + +## Traverse a Nested List + +- `names.py`: The nested `names` list used in the examples +- `count_leaf_items_recursive.py`: Recursive `count_leaf_items()` +- `count_leaf_items_verbose.py`: Recursive `count_leaf_items()` with `print()` calls that trace the traversal +- `count_leaf_items_iterative.py`: Non-recursive `count_leaf_items()` that uses a stack + +```pycon +>>> from names import names +>>> from count_leaf_items_recursive import count_leaf_items +>>> count_leaf_items([1, [2.1, 2.2], 3]) +4 +>>> count_leaf_items(names) +10 +``` + +## Detect Palindromes + +- `palindrome_slicing.py`: `is_palindrome()` that compares a word to its reversed slice +- `palindrome_recursive.py`: Recursive `is_palindrome()` + +```pycon +>>> from palindrome_recursive import is_palindrome +>>> is_palindrome("racecar") +True +>>> is_palindrome("troglodyte") +False +``` + +## Sort With Quicksort + +- `quicksort.py`: Recursive `quicksort()` with median-of-three pivot selection, plus `get_random_numbers()` for generating test data + +```pycon +>>> from quicksort import quicksort, get_random_numbers +>>> quicksort([10, -3, 21, 6, -8]) +[-8, -3, 6, 10, 21] +>>> numbers = get_random_numbers(15, -50, 50) +>>> quicksort(numbers) == sorted(numbers) +True +``` diff --git a/python-recursion/count_leaf_items_iterative.py b/python-recursion/count_leaf_items_iterative.py new file mode 100644 index 0000000000..178474911e --- /dev/null +++ b/python-recursion/count_leaf_items_iterative.py @@ -0,0 +1,36 @@ +from names import names + + +def count_leaf_items(item_list): + """Non-recursively counts and returns the + number of leaf items in a (potentially + nested) list. + """ + count = 0 + stack = [] + current_list = item_list + i = 0 + + while True: + if i == len(current_list): + if current_list == item_list: + return count + else: + current_list, i = stack.pop() + i += 1 + continue + + if isinstance(current_list[i], list): + stack.append([current_list, i]) + current_list = current_list[i] + i = 0 + else: + count += 1 + i += 1 + + +if __name__ == "__main__": + print(count_leaf_items([1, 2, 3, 4])) + print(count_leaf_items([1, [2.1, 2.2], 3])) + print(count_leaf_items([])) + print(count_leaf_items(names)) diff --git a/python-recursion/count_leaf_items_recursive.py b/python-recursion/count_leaf_items_recursive.py new file mode 100644 index 0000000000..25761d2f56 --- /dev/null +++ b/python-recursion/count_leaf_items_recursive.py @@ -0,0 +1,23 @@ +from names import names + + +def count_leaf_items(item_list): + """Recursively counts and returns the + number of leaf items in a (potentially + nested) list. + """ + count = 0 + for item in item_list: + if isinstance(item, list): + count += count_leaf_items(item) + else: + count += 1 + + return count + + +if __name__ == "__main__": + print(count_leaf_items([1, 2, 3, 4])) + print(count_leaf_items([1, [2.1, 2.2], 3])) + print(count_leaf_items([])) + print(count_leaf_items(names)) diff --git a/python-recursion/count_leaf_items_verbose.py b/python-recursion/count_leaf_items_verbose.py new file mode 100644 index 0000000000..5708846685 --- /dev/null +++ b/python-recursion/count_leaf_items_verbose.py @@ -0,0 +1,24 @@ +from names import names + + +def count_leaf_items(item_list): + """Recursively counts and returns the + number of leaf items in a (potentially + nested) list. + """ + print(f"List: {item_list}") + count = 0 + for item in item_list: + if isinstance(item, list): + print("Encountered sublist") + count += count_leaf_items(item) + else: + print(f'Counted leaf item "{item}"') + count += 1 + + print(f"-> Returning count {count}") + return count + + +if __name__ == "__main__": + print(count_leaf_items(names)) diff --git a/python-recursion/countdown_concise.py b/python-recursion/countdown_concise.py new file mode 100644 index 0000000000..589ab07ba8 --- /dev/null +++ b/python-recursion/countdown_concise.py @@ -0,0 +1,8 @@ +def countdown(n): + print(n) + if n > 0: + countdown(n - 1) + + +if __name__ == "__main__": + countdown(5) diff --git a/python-recursion/countdown_iterative.py b/python-recursion/countdown_iterative.py new file mode 100644 index 0000000000..fc5285219e --- /dev/null +++ b/python-recursion/countdown_iterative.py @@ -0,0 +1,8 @@ +def countdown(n): + while n >= 0: + print(n) + n -= 1 + + +if __name__ == "__main__": + countdown(5) diff --git a/python-recursion/countdown_recursive.py b/python-recursion/countdown_recursive.py new file mode 100644 index 0000000000..b64223af3a --- /dev/null +++ b/python-recursion/countdown_recursive.py @@ -0,0 +1,10 @@ +def countdown(n): + print(n) + if n == 0: + return # Terminate recursion + else: + countdown(n - 1) # Recursive call + + +if __name__ == "__main__": + countdown(5) diff --git a/python-recursion/factorial_iterative.py b/python-recursion/factorial_iterative.py new file mode 100644 index 0000000000..96eacb7a8a --- /dev/null +++ b/python-recursion/factorial_iterative.py @@ -0,0 +1,9 @@ +def factorial(n): + return_value = 1 + for i in range(2, n + 1): + return_value *= i + return return_value + + +if __name__ == "__main__": + print(factorial(4)) diff --git a/python-recursion/factorial_recursive.py b/python-recursion/factorial_recursive.py new file mode 100644 index 0000000000..0c9e4c484d --- /dev/null +++ b/python-recursion/factorial_recursive.py @@ -0,0 +1,6 @@ +def factorial(n): + return 1 if n <= 1 else n * factorial(n - 1) + + +if __name__ == "__main__": + print(factorial(4)) diff --git a/python-recursion/factorial_recursive_verbose.py b/python-recursion/factorial_recursive_verbose.py new file mode 100644 index 0000000000..b9f26ef350 --- /dev/null +++ b/python-recursion/factorial_recursive_verbose.py @@ -0,0 +1,9 @@ +def factorial(n): + print(f"factorial() called with n = {n}") + return_value = 1 if n <= 1 else n * factorial(n - 1) + print(f"-> factorial({n}) returns {return_value}") + return return_value + + +if __name__ == "__main__": + print(factorial(4)) diff --git a/python-recursion/factorial_reduce.py b/python-recursion/factorial_reduce.py new file mode 100644 index 0000000000..a7bd359026 --- /dev/null +++ b/python-recursion/factorial_reduce.py @@ -0,0 +1,9 @@ +from functools import reduce + + +def factorial(n): + return reduce(lambda x, y: x * y, range(1, n + 1) or [1]) + + +if __name__ == "__main__": + print(factorial(4)) diff --git a/python-recursion/factorial_timing.py b/python-recursion/factorial_timing.py new file mode 100644 index 0000000000..28adcb7404 --- /dev/null +++ b/python-recursion/factorial_timing.py @@ -0,0 +1,37 @@ +from timeit import timeit + +recursive_setup = """ +print("Recursive:") +def factorial(n): + return 1 if n <= 1 else n * factorial(n - 1) +""" + +iterative_setup = """ +print("Iterative:") +def factorial(n): + return_value = 1 + for i in range(2, n + 1): + return_value *= i + return return_value +""" + +reduce_setup = """ +from functools import reduce +print("reduce():") +def factorial(n): + return reduce(lambda x, y: x * y, range(1, n + 1) or [1]) +""" + +math_setup = """ +print("math.factorial():") +from math import factorial +""" + +if __name__ == "__main__": + for setup_string in ( + recursive_setup, + iterative_setup, + reduce_setup, + math_setup, + ): + print(timeit("factorial(4)", setup=setup_string, number=10000000)) diff --git a/python-recursion/infinite_recursion.py b/python-recursion/infinite_recursion.py new file mode 100644 index 0000000000..0372999610 --- /dev/null +++ b/python-recursion/infinite_recursion.py @@ -0,0 +1,7 @@ +def function(): + x = 10 # noqa: F841 + function() + + +if __name__ == "__main__": + function() diff --git a/python-recursion/names.py b/python-recursion/names.py new file mode 100644 index 0000000000..70db0eb04a --- /dev/null +++ b/python-recursion/names.py @@ -0,0 +1,18 @@ +names = [ + "Adam", + [ + "Bob", + [ + "Chet", + "Cat", + ], + "Barb", + "Bert", + ], + "Alex", + [ + "Bea", + "Bill", + ], + "Ann", +] diff --git a/python-recursion/palindrome_recursive.py b/python-recursion/palindrome_recursive.py new file mode 100644 index 0000000000..c50f651c9a --- /dev/null +++ b/python-recursion/palindrome_recursive.py @@ -0,0 +1,11 @@ +def is_palindrome(word): + """Return True if word is a palindrome, False if not.""" + if len(word) <= 1: + return True + else: + return word[0] == word[-1] and is_palindrome(word[1:-1]) + + +if __name__ == "__main__": + for word in ["", "a", "foo", "racecar", "troglodyte", "civic"]: + print(f"{word!r}: {is_palindrome(word)}") diff --git a/python-recursion/palindrome_slicing.py b/python-recursion/palindrome_slicing.py new file mode 100644 index 0000000000..f6991ea760 --- /dev/null +++ b/python-recursion/palindrome_slicing.py @@ -0,0 +1,8 @@ +def is_palindrome(word): + """Return True if word is a palindrome, False if not.""" + return word == word[::-1] + + +if __name__ == "__main__": + for word in ["foo", "racecar", "troglodyte", "civic"]: + print(f"{word!r}: {is_palindrome(word)}") diff --git a/python-recursion/quicksort.py b/python-recursion/quicksort.py new file mode 100644 index 0000000000..fb09c34370 --- /dev/null +++ b/python-recursion/quicksort.py @@ -0,0 +1,41 @@ +import random +import statistics + + +def quicksort(numbers): + if len(numbers) <= 1: + return numbers + else: + pivot = statistics.median( + [ + numbers[0], + numbers[len(numbers) // 2], + numbers[-1], + ] + ) + items_less, pivot_items, items_greater = ( + [n for n in numbers if n < pivot], + [n for n in numbers if n == pivot], + [n for n in numbers if n > pivot], + ) + + return quicksort(items_less) + pivot_items + quicksort(items_greater) + + +def get_random_numbers(length, minimum=1, maximum=100): + numbers = [] + for _ in range(length): + numbers.append(random.randint(minimum, maximum)) + + return numbers + + +if __name__ == "__main__": + print(quicksort([])) + print(quicksort([42])) + print(quicksort([5, 2, 6, 3])) + print(quicksort([10, -3, 21, 6, -8])) + + numbers = get_random_numbers(20) + print(numbers) + print(quicksort(numbers)) diff --git a/python-sql-libraries/README.md b/python-sql-libraries/README.md new file mode 100644 index 0000000000..70dec3de14 --- /dev/null +++ b/python-sql-libraries/README.md @@ -0,0 +1,101 @@ +# Introduction to Python SQL Libraries + +This folder provides the code examples for the Real Python tutorial [Introduction to Python SQL Libraries](https://realpython.com/python-sql-libraries/). + +Each script builds the same small social media database, `sm_app`, with `users`, `posts`, `comments`, and `likes` tables, and then runs the tutorial's `CREATE`, `INSERT`, `SELECT`, `UPDATE`, and `DELETE` queries against it: + +- `sqlite_app.py`: Uses the standard-library `sqlite3` module to work with an SQLite database file, `sm_app.sqlite`. +- `mysql_app.py`: Uses `mysql-connector-python` to create and work with the `sm_app` database on a MySQL (or MariaDB) server. +- `postgresql_app.py`: Uses `psycopg2` to create and work with the `sm_app` database on a PostgreSQL server. + +The helper functions, such as `create_connection()`, `execute_query()`, and `execute_read_query()`, are importable from each module. The tutorial's queries run when you execute a file as a script. + +## Requirements + +The code needs Python 3.10 or later, as required by the pinned versions of the third-party drivers. The SQLite example uses only the standard library and runs on any Python 3.6+. + +## Installation + +The MySQL and PostgreSQL examples need third-party drivers: + +```console +$ python -m pip install -r requirements.txt +``` + +## Usage + +### SQLite + +No server is needed. Run the script, and it creates `sm_app.sqlite` in the current working directory: + +```console +$ python sqlite_app.py +Connection to SQLite DB successful +Query executed successfully +... +(1, 'James', 25, 'male', 'USA') +(2, 'Leila', 32, 'female', 'France') +... +['post', 'comment', 'name'] +... +('The weather is very hot today',) +Query executed successfully +('The weather has become pleasant now',) +Query executed successfully +``` + +Delete `sm_app.sqlite` before running the script again, or the records are inserted a second time. + +### MySQL + +You need a running MySQL or MariaDB server. The script connects as `root` with an empty password on `localhost`. To use other credentials, edit the `HOST_NAME`, `USER_NAME`, and `USER_PASSWORD` constants at the top of `mysql_app.py`. + +For example, on Ubuntu, you can install and start a MariaDB server and allow `root` to log in with an empty password like this: + +```console +$ sudo apt update && sudo apt install -y mariadb-server +$ sudo service mariadb start +$ sudo mariadb -e "ALTER USER root@localhost IDENTIFIED VIA mysql_native_password USING PASSWORD('') OR unix_socket; FLUSH PRIVILEGES;" +``` + +Then run the script, which creates the `sm_app` database itself: + +```console +$ python mysql_app.py +Connection to MySQL DB successful +Database created successfully +Connection to MySQL DB successful +Query executed successfully +... +``` + +To start over, drop the database with `sudo mariadb -e "DROP DATABASE sm_app"`. + +### PostgreSQL + +You need a running PostgreSQL server. The script connects as the `postgres` user with the password `abc123` on `127.0.0.1:5432`. To use other credentials, edit the `DB_USER`, `DB_PASSWORD`, `DB_HOST`, and `DB_PORT` constants at the top of `postgresql_app.py`. + +For example, on Ubuntu, you can install and start a PostgreSQL server and set the password like this: + +```console +$ sudo apt update && sudo apt install -y postgresql +$ sudo service postgresql start +$ sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'abc123';" +``` + +Then run the script, which creates the `sm_app` database itself: + +```console +$ python postgresql_app.py +Connection to PostgreSQL DB successful +Query executed successfully +Connection to PostgreSQL DB successful +Query executed successfully +... +(1, 'James', 25, 'male', 'USA') +... +('The weather has become pleasant now',) +Query executed successfully +``` + +The script stops with a `DuplicateDatabase` error if `sm_app` already exists, so drop it before running the script again with `sudo -u postgres psql -c "DROP DATABASE sm_app"`. diff --git a/python-sql-libraries/mysql_app.py b/python-sql-libraries/mysql_app.py new file mode 100644 index 0000000000..70540732ff --- /dev/null +++ b/python-sql-libraries/mysql_app.py @@ -0,0 +1,222 @@ +import mysql.connector +from mysql.connector import Error + +# Change these connection parameters to match your MySQL server: +HOST_NAME = "localhost" +USER_NAME = "root" +USER_PASSWORD = "" +DB_NAME = "sm_app" + + +def create_server_connection(host_name, user_name, user_password): + connection = None + try: + connection = mysql.connector.connect( + host=host_name, + user=user_name, + passwd=user_password, + ) + print("Connection to MySQL DB successful") + except Error as e: + print(f"The error '{e}' occurred") + + return connection + + +def create_database(connection, query): + cursor = connection.cursor() + try: + cursor.execute(query) + print("Database created successfully") + except Error as e: + print(f"The error '{e}' occurred") + + +def create_connection(host_name, user_name, user_password, db_name): + connection = None + try: + connection = mysql.connector.connect( + host=host_name, + user=user_name, + passwd=user_password, + database=db_name, + ) + print("Connection to MySQL DB successful") + except Error as e: + print(f"The error '{e}' occurred") + + return connection + + +def execute_query(connection, query): + cursor = connection.cursor() + try: + cursor.execute(query) + connection.commit() + print("Query executed successfully") + except Error as e: + print(f"The error '{e}' occurred") + + +def execute_read_query(connection, query): + cursor = connection.cursor() + result = None + try: + cursor.execute(query) + result = cursor.fetchall() + return result + except Error as e: + print(f"The error '{e}' occurred") + + +if __name__ == "__main__": + # Connecting to the server and creating the database + connection = create_server_connection(HOST_NAME, USER_NAME, USER_PASSWORD) + + create_database_query = "CREATE DATABASE sm_app" + create_database(connection, create_database_query) + + connection = create_connection( + HOST_NAME, USER_NAME, USER_PASSWORD, DB_NAME + ) + + # Creating tables + create_users_table = """ + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT, + name TEXT NOT NULL, + age INT, + gender TEXT, + nationality TEXT, + PRIMARY KEY (id) + ) ENGINE = InnoDB + """ + + execute_query(connection, create_users_table) + + create_posts_table = """ + CREATE TABLE IF NOT EXISTS posts ( + id INT AUTO_INCREMENT, + title TEXT NOT NULL, + description TEXT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY fk_user_id (user_id) REFERENCES users(id), + PRIMARY KEY (id) + ) ENGINE = InnoDB + """ + + execute_query(connection, create_posts_table) + + # Not shown in the tutorial: the comments and likes tables + create_comments_table = """ + CREATE TABLE IF NOT EXISTS comments ( + id INT AUTO_INCREMENT, + text TEXT NOT NULL, + user_id INTEGER NOT NULL, + post_id INTEGER NOT NULL, + FOREIGN KEY fk_comment_user_id (user_id) REFERENCES users(id), + FOREIGN KEY fk_comment_post_id (post_id) REFERENCES posts(id), + PRIMARY KEY (id) + ) ENGINE = InnoDB + """ + + create_likes_table = """ + CREATE TABLE IF NOT EXISTS likes ( + id INT AUTO_INCREMENT, + user_id INTEGER NOT NULL, + post_id INTEGER NOT NULL, + FOREIGN KEY fk_like_user_id (user_id) REFERENCES users(id), + FOREIGN KEY fk_like_post_id (post_id) REFERENCES posts(id), + PRIMARY KEY (id) + ) ENGINE = InnoDB + """ + + execute_query(connection, create_comments_table) + execute_query(connection, create_likes_table) + + # Inserting records with .execute() + create_users = """ + INSERT INTO + `users` (`name`, `age`, `gender`, `nationality`) + VALUES + ('James', 25, 'male', 'USA'), + ('Leila', 32, 'female', 'France'), + ('Brigitte', 35, 'female', 'England'), + ('Mike', 40, 'male', 'Denmark'), + ('Elizabeth', 21, 'female', 'Canada'); + """ + + execute_query(connection, create_users) + + # Not shown in the tutorial: the posts, comments, and likes records + create_posts = """ + INSERT INTO + posts (title, description, user_id) + VALUES + ('Happy', 'I am feeling very happy today', 1), + ('Hot Weather', 'The weather is very hot today', 2), + ('Help', 'I need some help with my work', 2), + ('Great News', 'I am getting married', 1), + ('Interesting Game', 'It was a fantastic game of tennis', 5), + ('Party', 'Anyone up for a late-night party today?', 3); + """ + + create_comments = """ + INSERT INTO + comments (text, user_id, post_id) + VALUES + ('Count me in', 1, 6), + ('What sort of help?', 5, 3), + ('Congrats buddy', 2, 4), + ('I was rooting for Nadal though', 4, 5), + ('Help with your thesis?', 2, 3), + ('Many congratulations', 5, 4); + """ + + create_likes = """ + INSERT INTO + likes (user_id, post_id) + VALUES + (1, 6), + (2, 3), + (1, 5), + (5, 4), + (2, 4), + (4, 2), + (3, 6); + """ + + execute_query(connection, create_posts) + execute_query(connection, create_comments) + execute_query(connection, create_likes) + + # Inserting records with .executemany() + sql = "INSERT INTO likes ( user_id, post_id ) VALUES ( %s, %s )" + val = [(4, 5), (3, 4)] + + cursor = connection.cursor() + cursor.executemany(sql, val) + connection.commit() + + # Selecting records + select_users = "SELECT * FROM users" + users = execute_read_query(connection, select_users) + + for user in users: + print(user) + + # Updating table records + update_post_description = """ + UPDATE + posts + SET + description = "The weather has become pleasant now" + WHERE + id = 2 + """ + + execute_query(connection, update_post_description) + + # Deleting table records + delete_comment = "DELETE FROM comments WHERE id = 2" + execute_query(connection, delete_comment) diff --git a/python-sql-libraries/postgresql_app.py b/python-sql-libraries/postgresql_app.py new file mode 100644 index 0000000000..e434b6c10e --- /dev/null +++ b/python-sql-libraries/postgresql_app.py @@ -0,0 +1,204 @@ +import psycopg2 +from psycopg2 import OperationalError + +# Change these connection parameters to match your PostgreSQL server: +DB_USER = "postgres" +DB_PASSWORD = "abc123" +DB_HOST = "127.0.0.1" +DB_PORT = "5432" + + +def create_connection(db_name, db_user, db_password, db_host, db_port): + connection = None + try: + connection = psycopg2.connect( + database=db_name, + user=db_user, + password=db_password, + host=db_host, + port=db_port, + ) + print("Connection to PostgreSQL DB successful") + except OperationalError as e: + print(f"The error '{e}' occurred") + return connection + + +def create_database(connection, query): + connection.autocommit = True + cursor = connection.cursor() + try: + cursor.execute(query) + print("Query executed successfully") + except OperationalError as e: + print(f"The error '{e}' occurred") + + +def execute_query(connection, query): + connection.autocommit = True + cursor = connection.cursor() + try: + cursor.execute(query) + print("Query executed successfully") + except OperationalError as e: + print(f"The error '{e}' occurred") + + +def execute_read_query(connection, query): + cursor = connection.cursor() + result = None + try: + cursor.execute(query) + result = cursor.fetchall() + return result + except OperationalError as e: + print(f"The error '{e}' occurred") + + +if __name__ == "__main__": + # Connecting to the default database and creating sm_app + connection = create_connection( + "postgres", DB_USER, DB_PASSWORD, DB_HOST, DB_PORT + ) + + create_database_query = "CREATE DATABASE sm_app" + create_database(connection, create_database_query) + + connection = create_connection( + "sm_app", DB_USER, DB_PASSWORD, DB_HOST, DB_PORT + ) + + # Creating tables + create_users_table = """ + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + age INTEGER, + gender TEXT, + nationality TEXT + ) + """ + + execute_query(connection, create_users_table) + + create_posts_table = """ + CREATE TABLE IF NOT EXISTS posts ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + user_id INTEGER REFERENCES users(id) + ) + """ + + execute_query(connection, create_posts_table) + + # Not shown in the tutorial: the comments and likes tables + create_comments_table = """ + CREATE TABLE IF NOT EXISTS comments ( + id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + user_id INTEGER REFERENCES users(id), + post_id INTEGER REFERENCES posts(id) + ) + """ + + create_likes_table = """ + CREATE TABLE IF NOT EXISTS likes ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + post_id INTEGER REFERENCES posts(id) + ) + """ + + execute_query(connection, create_comments_table) + execute_query(connection, create_likes_table) + + # Inserting records + users = [ + ("James", 25, "male", "USA"), + ("Leila", 32, "female", "France"), + ("Brigitte", 35, "female", "England"), + ("Mike", 40, "male", "Denmark"), + ("Elizabeth", 21, "female", "Canada"), + ] + + user_records = ", ".join(["%s"] * len(users)) + + insert_query = f"INSERT INTO users (name, age, gender, nationality) VALUES {user_records}" + + connection.autocommit = True + cursor = connection.cursor() + cursor.execute(insert_query, users) + + posts = [ + ("Happy", "I am feeling very happy today", 1), + ("Hot Weather", "The weather is very hot today", 2), + ("Help", "I need some help with my work", 2), + ("Great News", "I am getting married", 1), + ("Interesting Game", "It was a fantastic game of tennis", 5), + ("Party", "Anyone up for a late-night party today?", 3), + ] + + post_records = ", ".join(["%s"] * len(posts)) + + insert_query = f"INSERT INTO posts (title, description, user_id) VALUES {post_records}" + + connection.autocommit = True + cursor = connection.cursor() + cursor.execute(insert_query, posts) + + # Not shown in the tutorial: the comments and likes records + comments = [ + ("Count me in", 1, 6), + ("What sort of help?", 5, 3), + ("Congrats buddy", 2, 4), + ("I was rooting for Nadal though", 4, 5), + ("Help with your thesis?", 2, 3), + ("Many congratulations", 5, 4), + ] + + comment_records = ", ".join(["%s"] * len(comments)) + + insert_query = f"INSERT INTO comments (text, user_id, post_id) VALUES {comment_records}" + + cursor.execute(insert_query, comments) + + likes = [(1, 6), (2, 3), (1, 5), (5, 4), (2, 4), (4, 2), (3, 6)] + + like_records = ", ".join(["%s"] * len(likes)) + + insert_query = ( + f"INSERT INTO likes (user_id, post_id) VALUES {like_records}" + ) + + cursor.execute(insert_query, likes) + + # Selecting records + select_users = "SELECT * FROM users" + users = execute_read_query(connection, select_users) + + for user in users: + print(user) + + # Updating table records (PostgreSQL needs single quotes for strings) + update_post_description = """ + UPDATE + posts + SET + description = 'The weather has become pleasant now' + WHERE + id = 2 + """ + + execute_query(connection, update_post_description) + + select_post_description = "SELECT description FROM posts WHERE id = 2" + + post_description = execute_read_query(connection, select_post_description) + + for description in post_description: + print(description) + + # Deleting table records + delete_comment = "DELETE FROM comments WHERE id = 2" + execute_query(connection, delete_comment) diff --git a/python-sql-libraries/requirements.txt b/python-sql-libraries/requirements.txt new file mode 100644 index 0000000000..00d127b813 --- /dev/null +++ b/python-sql-libraries/requirements.txt @@ -0,0 +1,2 @@ +mysql-connector-python==26.7.0 +psycopg2-binary==2.9.13 diff --git a/python-sql-libraries/sqlite_app.py b/python-sql-libraries/sqlite_app.py new file mode 100644 index 0000000000..9d79ec2016 --- /dev/null +++ b/python-sql-libraries/sqlite_app.py @@ -0,0 +1,245 @@ +import sqlite3 +from sqlite3 import Error + +# Change the location to match your setup: +DB_PATH = "sm_app.sqlite" + + +def create_connection(path): + connection = None + try: + connection = sqlite3.connect(path) + print("Connection to SQLite DB successful") + except Error as e: + print(f"The error '{e}' occurred") + + return connection + + +def execute_query(connection, query): + cursor = connection.cursor() + try: + cursor.execute(query) + connection.commit() + print("Query executed successfully") + except Error as e: + print(f"The error '{e}' occurred") + + +def execute_read_query(connection, query): + cursor = connection.cursor() + result = None + try: + cursor.execute(query) + result = cursor.fetchall() + return result + except Error as e: + print(f"The error '{e}' occurred") + + +if __name__ == "__main__": + connection = create_connection(DB_PATH) + + # Creating tables + create_users_table = """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + age INTEGER, + gender TEXT, + nationality TEXT + ); + """ + execute_query(connection, create_users_table) + + create_posts_table = """ + CREATE TABLE IF NOT EXISTS posts( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + ); + """ + execute_query(connection, create_posts_table) + + create_comments_table = """ + CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + user_id INTEGER NOT NULL, + post_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (post_id) REFERENCES posts (id) + ); + """ + + create_likes_table = """ + CREATE TABLE IF NOT EXISTS likes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + post_id integer NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (post_id) REFERENCES posts (id) + ); + """ + + execute_query(connection, create_comments_table) + execute_query(connection, create_likes_table) + + # Inserting records + create_users = """ + INSERT INTO + users (name, age, gender, nationality) + VALUES + ('James', 25, 'male', 'USA'), + ('Leila', 32, 'female', 'France'), + ('Brigitte', 35, 'female', 'England'), + ('Mike', 40, 'male', 'Denmark'), + ('Elizabeth', 21, 'female', 'Canada'); + """ + + execute_query(connection, create_users) + + create_posts = """ + INSERT INTO + posts (title, description, user_id) + VALUES + ("Happy", "I am feeling very happy today", 1), + ("Hot Weather", "The weather is very hot today", 2), + ("Help", "I need some help with my work", 2), + ("Great News", "I am getting married", 1), + ("Interesting Game", "It was a fantastic game of tennis", 5), + ("Party", "Anyone up for a late-night party today?", 3); + """ + + execute_query(connection, create_posts) + + create_comments = """ + INSERT INTO + comments (text, user_id, post_id) + VALUES + ('Count me in', 1, 6), + ('What sort of help?', 5, 3), + ('Congrats buddy', 2, 4), + ('I was rooting for Nadal though', 4, 5), + ('Help with your thesis?', 2, 3), + ('Many congratulations', 5, 4); + """ + + create_likes = """ + INSERT INTO + likes (user_id, post_id) + VALUES + (1, 6), + (2, 3), + (1, 5), + (5, 4), + (2, 4), + (4, 2), + (3, 6); + """ + + execute_query(connection, create_comments) + execute_query(connection, create_likes) + + # Selecting records: SELECT + select_users = "SELECT * from users" + users = execute_read_query(connection, select_users) + + for user in users: + print(user) + + select_posts = "SELECT * FROM posts" + posts = execute_read_query(connection, select_posts) + + for post in posts: + print(post) + + # Selecting records: JOIN + select_users_posts = """ + SELECT + users.id, + users.name, + posts.description + FROM + posts + INNER JOIN users ON users.id = posts.user_id + """ + + users_posts = execute_read_query(connection, select_users_posts) + + for users_post in users_posts: + print(users_post) + + select_posts_comments_users = """ + SELECT + posts.description as post, + text as comment, + name + FROM + posts + INNER JOIN comments ON posts.id = comments.post_id + INNER JOIN users ON users.id = comments.user_id + """ + + posts_comments_users = execute_read_query( + connection, select_posts_comments_users + ) + + for posts_comments_user in posts_comments_users: + print(posts_comments_user) + + cursor = connection.cursor() + cursor.execute(select_posts_comments_users) + cursor.fetchall() + + column_names = [description[0] for description in cursor.description] + print(column_names) + + # Selecting records: WHERE + select_post_likes = """ + SELECT + description as Post, + COUNT(likes.id) as Likes + FROM + likes, + posts + WHERE + posts.id = likes.post_id + GROUP BY + likes.post_id + """ + + post_likes = execute_read_query(connection, select_post_likes) + + for post_like in post_likes: + print(post_like) + + # Updating table records + select_post_description = "SELECT description FROM posts WHERE id = 2" + + post_description = execute_read_query(connection, select_post_description) + + for description in post_description: + print(description) + + update_post_description = """ + UPDATE + posts + SET + description = "The weather has become pleasant now" + WHERE + id = 2 + """ + + execute_query(connection, update_post_description) + + post_description = execute_read_query(connection, select_post_description) + + for description in post_description: + print(description) + + # Deleting table records + delete_comment = "DELETE FROM comments WHERE id = 5" + execute_query(connection, delete_comment) diff --git a/regex-python-part-2/README.md b/regex-python-part-2/README.md new file mode 100644 index 0000000000..8d45dbdf25 --- /dev/null +++ b/regex-python-part-2/README.md @@ -0,0 +1,37 @@ +# Regular Expressions: Regexes in Python (Part 2) + +This folder provides the code examples for the Real Python tutorial [Regular Expressions: Regexes in Python (Part 2)](https://realpython.com/regex-python-part-2/). + +The code uses only Python's standard library and runs on Python 3.6 or later. The output shown in the comments was produced with Python 3.14, so a few representations may look slightly different on older versions. + +## Files + +Each script runs the tutorial's key examples from one section with `print()` and shows the expected output in a comment below each call: + +- [`search_functions.py`](search_functions.py): `re.search()`, `re.match()`, `re.fullmatch()`, `re.findall()`, and `re.finditer()` +- [`substitution_functions.py`](substitution_functions.py): `re.sub()` and `re.subn()` with string and function replacements, backreferences, and `count` +- [`utility_functions.py`](utility_functions.py): `re.split()`, including delimiter capture and `maxsplit`, and `re.escape()` +- [`compiled_regex.py`](compiled_regex.py): `re.compile()`, compiled regex methods with `pos` and `endpos`, and the `.flags`, `.groups`, `.groupindex`, and `.pattern` attributes +- [`match_object_methods.py`](match_object_methods.py): `.group()`, `match[n]`, `.groups()`, `.groupdict()`, `.expand()`, `.start()`, `.end()`, and `.span()` +- [`match_object_attributes.py`](match_object_attributes.py): `.pos`, `.endpos`, `.lastindex`, `.lastgroup`, `.re`, and `.string` + +Run any script to see its output: + +```sh +$ python search_functions.py + + +None +... +``` + +You can also import the replacement function `f()` from `substitution_functions.py` to reproduce the tutorial's substitution-by-function examples in the REPL: + +```pycon +>>> import re +>>> from substitution_functions import f +>>> re.sub(r"\w+", f, "foo.10.bar.20.baz.30") +'FOO.100.BAR.200.BAZ.300' +>>> re.subn(r"\w+", f, "foo.10.bar.20.baz.30") +('FOO.100.BAR.200.BAZ.300', 6) +``` diff --git a/regex-python-part-2/compiled_regex.py b/regex-python-part-2/compiled_regex.py new file mode 100644 index 0000000000..93a6590fa6 --- /dev/null +++ b/regex-python-part-2/compiled_regex.py @@ -0,0 +1,75 @@ +"""Compiled regular expression objects: re.compile(), its methods, and +its attributes. +""" + +import re + +# The three equivalent ways to search with a regex +re_obj = re.compile(r"(\d+)") +print(re.search(r"(\d+)", "foo123bar")) +# +print(re.search(re_obj, "foo123bar")) +# +print(re_obj.search("foo123bar")) +# + +# Flags passed to re.compile() apply to every search +r1 = re.search("ba[rz]", "FOOBARBAZ", flags=re.I) +re_obj = re.compile("ba[rz]", flags=re.I) +r2 = re.search(re_obj, "FOOBARBAZ") +r3 = re_obj.search("FOOBARBAZ") +print(r1) +# +print(r2) +# +print(r3) +# + +# Compiling separates the regex definition from its uses +s1, s2, s3, s4 = "foo.bar", "foo123bar", "baz99", "qux & grault" +re_obj = re.compile(r"\d+") +print(re_obj.search(s1)) +# None +print(re_obj.search(s2)) +# +print(re_obj.search(s3)) +# +print(re_obj.search(s4)) +# None + +# The pos and endpos arguments restrict the search to a slice +re_obj = re.compile(r"\d+") +s = "foo123barbaz" +print(re_obj.search(s)) +# +print(s[6:9]) +# bar +print(re_obj.search(s, 6, 9)) +# None + +# Anchors still refer to the whole string, not the slice +re_obj = re.compile("^bar") +s = "foobarbaz" +print(s[3:]) +# barbaz +print(re_obj.search(s, 3)) +# None + +# Regular expression object attributes +re_obj = re.compile(r"(?m)(\w+),(\w+)", re.I) +print(re_obj.flags) +# 42 +print(repr(re.I | re.M | re.UNICODE)) +# re.IGNORECASE|re.UNICODE|re.MULTILINE +print(re_obj.groups) +# 2 +print(repr(re_obj.pattern)) +# '(?m)(\\w+),(\\w+)' + +re_obj = re.compile(r"(?P),(?P)") +print(repr(re_obj.groupindex)) +# mappingproxy({'w1': 1, 'w2': 2}) +print(re_obj.groupindex["w1"]) +# 1 +print(re_obj.groupindex["w2"]) +# 2 diff --git a/regex-python-part-2/match_object_attributes.py b/regex-python-part-2/match_object_attributes.py new file mode 100644 index 0000000000..5c122654e8 --- /dev/null +++ b/regex-python-part-2/match_object_attributes.py @@ -0,0 +1,81 @@ +"""Match object attributes: .pos, .endpos, .lastindex, .lastgroup, .re, +and .string. +""" + +import re + +# .pos and .endpos +re_obj = re.compile(r"\d+") +m = re_obj.search("foo123bar", 2, 7) +print(m) +# +print(m.pos, m.endpos) +# 2 7 +m = re_obj.search("foo123bar") +print(m.pos, m.endpos) +# 0 9 +m = re.search(r"\d+", "foo123bar") +print(m.pos, m.endpos) +# 0 9 + +# .lastindex +m = re.search(r"(\w+),(\w+),(\w+)", "foo,bar,baz") +print(m.lastindex, m[m.lastindex]) +# 3 baz +m = re.search(r"(\w+),(\w+),(\w+)?", "foo,bar,baz") +print(m.groups(), (m.lastindex, m[m.lastindex])) +# ('foo', 'bar', 'baz') (3, 'baz') +m = re.search(r"(\w+),(\w+),(\w+)?", "foo,bar,") +print(m.groups(), (m.lastindex, m[m.lastindex])) +# ('foo', 'bar', None) (2, 'bar') + +# The outermost group is the last one to match +m = re.match("((a)(b))", "ab") +print(m.groups(), m.lastindex, m[m.lastindex]) +# ('ab', 'a', 'b') 1 ab + +# .lastgroup +s = "foo123bar456baz" +m = re.search(r"(?P\d+)\D*(?P\d+)", s) +print(m.lastgroup) +# n2 +m = re.search(r"(\d+)\D*(\d+)", s) +print(m.groups(), m.lastgroup) +# ('123', '456') None +m = re.search(r"\d+\D*\d+", s) +print(m.groups(), m.lastgroup) +# () None + +# .re is the (cached) compiled regular expression object +regex = r"(\w+),(\w+),(\w+)" +m1 = re.search(regex, "foo,bar,baz") +print(m1.re) +# re.compile('(\\w+),(\\w+),(\\w+)') +re_obj = re.compile(regex) +print(re_obj is m1.re) +# True +m2 = re_obj.search("qux,quux,corge") +print(m2) +# +print(m2.re is re_obj is m1.re) +# True + +print(m1.re.groups) +# 3 +print(repr(m1.re.pattern)) +# '(\\w+),(\\w+),(\\w+)' +print(m1.re.pattern == regex) +# True +print(m1.re.flags) +# 32 +print(m1.re.match("quux,corge,grault")) +# + +# .string +m = re.search(r"(\w+),(\w+),(\w+)", "foo,bar,baz") +print(m.string) +# foo,bar,baz +re_obj = re.compile(r"(\w+),(\w+),(\w+)") +m = re_obj.search("foo,bar,baz") +print(m.string) +# foo,bar,baz diff --git a/regex-python-part-2/match_object_methods.py b/regex-python-part-2/match_object_methods.py new file mode 100644 index 0000000000..b8dcf9c1e1 --- /dev/null +++ b/regex-python-part-2/match_object_methods.py @@ -0,0 +1,129 @@ +"""Match object methods: .group(), .__getitem__(), .groups(), +.groupdict(), .expand(), .start(), .end(), and .span(). +""" + +import re + +# A match object is truthy +m = re.search("bar", "foo.bar.baz") +print(m) +# +print(bool(m)) +# True +if re.search("bar", "foo.bar.baz"): + print("Found a match") +# Found a match + +# .group() with numbered and named groups +m = re.search(r"(\w+),(\w+),(\w+)", "foo,bar,baz") +print(m.group(1), m.group(3)) +# foo baz +print(m.group(1, 3)) +# ('foo', 'baz') +print(m.group(3, 3, 1, 1, 2, 2)) +# ('baz', 'baz', 'foo', 'foo', 'bar', 'bar') +print(m.group(0)) +# foo,bar,baz +print(m.group()) +# foo,bar,baz +try: + m.group(4) +except IndexError as e: + print(f"IndexError: {e}") +# IndexError: no such group + +m = re.match(r"(?P\w+),(?P\w+),(?P\w+)", "quux,corge,grault") +print(m.group("w1"), m.group("w3")) +# quux grault +print(m.group("w3", "w1", "w1", "w2")) +# ('grault', 'quux', 'quux', 'corge') + +# A nonparticipating group returns None +m = re.search(r"(\w+),(\w+),(\w+)?", "foo,bar,") +print(m) +# +print(m.group(1, 2)) +# ('foo', 'bar') +print(m.group(3)) +# None + +# A repeated group keeps only its last match +m = re.match(r"(\w{3},)+", "foo,bar,baz,qux") +print(m) +# +print(m.group(1)) +# baz, + +# match[n] is syntactic sugar for match.__getitem__(n) and match.group(n) +m = re.search(r"(\w+),(\w+),(\w+)", "foo,bar,baz") +print(m.group(2), m.__getitem__(2), m[2]) +# bar bar bar +m = re.match( + r"foo,(?P\w+),(?P\w+),qux", + "foo,bar,baz,qux", +) +print(m.group("w2"), m["w2"]) +# baz baz + +# .groups() and .groupdict(), with and without a default +m = re.search(r"(\w+),(\w+),(\w+)", "foo,bar,baz") +print(m.groups()) +# ('foo', 'bar', 'baz') +m = re.search(r"(\w+),(\w+),(\w+)?", "foo,bar,") +print(m.groups()) +# ('foo', 'bar', None) +print(m.groups(default="---")) +# ('foo', 'bar', '---') + +m = re.match( + r"foo,(?P\w+),(?P\w+),qux", + "foo,bar,baz,qux", +) +print(m.groupdict()) +# {'w1': 'bar', 'w2': 'baz'} +print(m.groupdict()["w2"]) +# baz +m = re.match( + r"foo,(?P\w+),(?P\w+)?,qux", + "foo,bar,,qux", +) +print(m.groupdict()) +# {'w1': 'bar', 'w2': None} +print(m.groupdict(default="---")) +# {'w1': 'bar', 'w2': '---'} + +# .expand() performs backreference substitution on a template +m = re.search(r"(\w+),(\w+),(\w+)", "foo,bar,baz") +print(m.expand(r"\2")) +# bar +print(m.expand(r"[\3] -> [\1]")) +# [baz] -> [foo] +m = re.search(r"(?P\d+)", "foo123qux") +print(m.expand(r"--- \g ---")) +# --- 123 --- + +# .start(), .end(), and .span() +s = "foo123bar456baz" +m = re.search(r"\d+", s) +print(m.start(), m.end()) +# 3 6 +print(s[m.start() : m.end()]) +# 123 + +m = re.search(r"(\d+)\D*(?P\d+)", s) +print(m) +# +print(m.start(1), m.end(1), s[m.start(1) : m.end(1)]) +# 3 6 123 +print(m.start("num"), m.end("num"), s[m.start("num") : m.end("num")]) +# 9 12 456 +print(m.span(), m.span(1), m.span("num")) +# (3, 12) (3, 6) (9, 12) + +# A group matching an empty string, and a nonparticipating group +m = re.search(r"foo(\d*)bar", "foobar") +print(repr(m[1]), (m.start(1), m.end(1))) +# '' (3, 3) +m = re.search(r"(\w+),(\w+),(\w+)?", "foo,bar,") +print(m.start(3), m.end(3)) +# -1 -1 diff --git a/regex-python-part-2/search_functions.py b/regex-python-part-2/search_functions.py new file mode 100644 index 0000000000..48d499a40d --- /dev/null +++ b/regex-python-part-2/search_functions.py @@ -0,0 +1,73 @@ +"""Searching functions: re.search(), re.match(), re.fullmatch(), +re.findall(), and re.finditer(). +""" + +import re + +# re.search() scans a string for a regex match +print(re.search(r"(\d+)", "foo123bar")) +# +print(re.search(r"[a-z]+", "123FOO456", flags=re.IGNORECASE)) +# +print(re.search(r"\d+", "foo.bar")) +# None + +# re.match() only matches at the beginning of the string +print(re.search(r"\d+", "123foobar")) +# +print(re.search(r"\d+", "foo123bar")) +# +print(re.match(r"\d+", "123foobar")) +# +print(re.match(r"\d+", "foo123bar")) +# None + +# MULTILINE affects re.search() but not re.match() +s = "foo\nbar\nbaz" +print(re.search("^foo", s)) +# +print(re.search("^bar", s, re.MULTILINE)) +# +print(re.match("^foo", s)) +# +print(re.match("^bar", s, re.MULTILINE)) +# None + +# re.fullmatch() matches only if the regex matches the entire string +print(re.fullmatch(r"\d+", "123foo")) +# None +print(re.fullmatch(r"\d+", "foo123")) +# None +print(re.fullmatch(r"\d+", "foo123bar")) +# None +print(re.fullmatch(r"\d+", "123")) +# +print(re.search(r"^\d+$", "123")) +# + +# re.findall() returns a list of all non-overlapping matches +print(re.findall(r"\w+", "...foo,,,,bar:%$baz//|")) +# ['foo', 'bar', 'baz'] +print(re.findall(r"#(\w+)#", "#foo#.#bar#.#baz#")) +# ['foo', 'bar', 'baz'] +print(re.findall(r"(\w+),(\w+)", "foo,bar,baz,qux,quux,corge")) +# [('foo', 'bar'), ('baz', 'qux'), ('quux', 'corge')] +print(re.findall(r"(\w+),(\w+),(\w+)", "foo,bar,baz,qux,quux,corge")) +# [('foo', 'bar', 'baz'), ('qux', 'quux', 'corge')] + +# re.finditer() returns an iterator that yields match objects +it = re.finditer(r"\w+", "...foo,,,,bar:%$baz//|") +print(next(it)) +# +print(next(it)) +# +print(next(it)) +# +print(next(it, "StopIteration")) # A bare next(it) raises StopIteration +# StopIteration + +for i in re.finditer(r"\w+", "...foo,,,,bar:%$baz//|"): + print(i) +# +# +# diff --git a/regex-python-part-2/substitution_functions.py b/regex-python-part-2/substitution_functions.py new file mode 100644 index 0000000000..20a3a7b060 --- /dev/null +++ b/regex-python-part-2/substitution_functions.py @@ -0,0 +1,81 @@ +"""Substitution functions: re.sub() and re.subn().""" + +import re + + +def f(match_obj): + s = match_obj.group(0) # The matching string + + # s.isdigit() returns True if all characters in s are digits + if s.isdigit(): + return str(int(s) * 10) + else: + return s.upper() + + +if __name__ == "__main__": + # Substitution by string + s = "foo.123.bar.789.baz" + print(re.sub(r"\d+", "#", s)) + # foo.#.bar.#.baz + print(re.sub("[a-z]+", "(*)", s)) + # (*).123.(*).789.(*) + + # Numbered and named backreferences in the replacement string + print( + re.sub( + r"(\w+),bar,baz,(\w+)", + r"\2,bar,baz,\1", + "foo,bar,baz,qux", + ) + ) + # qux,bar,baz,foo + print( + re.sub( + r"foo,(?P\w+),(?P\w+),qux", + r"foo,\g,\g,qux", + "foo,bar,baz,qux", + ) + ) + # foo,baz,bar,qux + print( + re.sub( + r"foo,(\w+),(\w+),qux", + r"foo,\g<2>,\g<1>,qux", + "foo,bar,baz,qux", + ) + ) + # foo,baz,bar,qux + + # \10 is read as a reference to group 10, so use \g<1> instead + try: + re.sub(r"(\d+)", r"\10", "foo 123 bar") + except re.error as e: # re.PatternError in Python 3.13+ + print(f"{type(e).__name__}: {e}") + # PatternError: invalid group reference 10 at position 1 + print(re.sub(r"(\d+)", r"\g<1>0", "foo 123 bar")) + # foo 1230 bar + print(re.sub(r"\d+", r"/\g<0>/", "foo 123 bar")) + # foo /123/ bar + + # A zero-length match substitutes at every character position + print(re.sub("x*", "-", "foo")) + # -f-o-o- + + # Substitution by function + print(re.sub(r"\w+", f, "foo.10.bar.20.baz.30")) + # FOO.100.BAR.200.BAZ.300 + + # Limiting the number of replacements + print(re.sub(r"\w+", "xxx", "foo.bar.baz.qux")) + # xxx.xxx.xxx.xxx + print(re.sub(r"\w+", "xxx", "foo.bar.baz.qux", count=2)) + # xxx.xxx.baz.qux + + # re.subn() also returns the number of substitutions made + print(re.subn(r"\w+", "xxx", "foo.bar.baz.qux")) + # ('xxx.xxx.xxx.xxx', 4) + print(re.subn(r"\w+", "xxx", "foo.bar.baz.qux", count=2)) + # ('xxx.xxx.baz.qux', 2) + print(re.subn(r"\w+", f, "foo.10.bar.20.baz.30")) + # ('FOO.100.BAR.200.BAZ.300', 6) diff --git a/regex-python-part-2/utility_functions.py b/regex-python-part-2/utility_functions.py new file mode 100644 index 0000000000..f3d23c75fc --- /dev/null +++ b/regex-python-part-2/utility_functions.py @@ -0,0 +1,48 @@ +"""Utility functions: re.split() and re.escape().""" + +import re + +# re.split() splits a string using a regex as the delimiter +print(re.split(r"\s*[,;/]\s*", "foo,bar ; baz / qux")) +# ['foo', 'bar', 'baz', 'qux'] + +# Capturing groups include the delimiters in the result +print(re.split(r"(\s*[,;/]\s*)", "foo,bar ; baz / qux")) +# ['foo', ',', 'bar', ' ; ', 'baz', ' / ', 'qux'] + +# Process the tokens, then rejoin them with the original delimiters +string = "foo,bar ; baz / qux" +regex = r"(\s*[,;/]\s*)" +a = re.split(regex, string) +for i, s in enumerate(a): + # This will be True for the tokens but not the delimiters + if not re.fullmatch(regex, s): + a[i] = f"<{s}>" +print("".join(a)) +# , ; / + +# Noncapturing groups leave the delimiters out +regex = r"(?:\s*[,;/]\s*)" +print(re.split(regex, string)) +# ['foo', 'bar', 'baz', 'qux'] + +# Limit the number of splits with maxsplit +s = "foo, bar, baz, qux, quux, corge" +print(re.split(r",\s*", s)) +# ['foo', 'bar', 'baz', 'qux', 'quux', 'corge'] +print(re.split(r",\s*", s, maxsplit=3)) +# ['foo', 'bar', 'baz', 'qux, quux, corge'] + +# Delimiters at the start or end produce empty strings +print(re.split("(/)", "/foo/bar/baz/")) +# ['', '/', 'foo', '/', 'bar', '/', 'baz', '/', ''] + +# re.escape() backslash-escapes characters with special meaning +print(re.match("foo^bar(baz)|qux", "foo^bar(baz)|qux")) +# None +print(re.match(r"foo\^bar\(baz\)\|qux", "foo^bar(baz)|qux")) +# +print(re.escape("foo^bar(baz)|qux") == r"foo\^bar\(baz\)\|qux") +# True +print(re.match(re.escape("foo^bar(baz)|qux"), "foo^bar(baz)|qux")) +# diff --git a/regex-python/README.md b/regex-python/README.md new file mode 100644 index 0000000000..9a5ca669db --- /dev/null +++ b/regex-python/README.md @@ -0,0 +1,37 @@ +# Regular Expressions: Regexes in Python (Part 1) + +This folder provides the code examples for the Real Python tutorial [Regular Expressions: Regexes in Python (Part 1)](https://realpython.com/regex-python/). + +Each script runs the tutorial's key `re.search()` examples for one section. Every `print()` call is followed by a comment showing the output you'll see, so you can read the files like the tutorial's REPL sessions or run them to check the results yourself. + +## Files + +- `search_basics.py`: Two ways to import `re.search()`, a first pattern-matching example, and a first look at character classes and the dot wildcard +- `single_character.py`: Metacharacters that match a single character: `[]`, `.`, `\w`, `\W`, `\d`, `\D`, `\s`, and `\S` +- `escaping.py`: Escaping metacharacters with a backslash, and why raw strings are cleaner +- `anchors.py`: The `^`, `\A`, `$`, `\Z`, `\b`, and `\B` anchors +- `quantifiers.py`: The `*`, `+`, `?`, `{m}`, and `{m,n}` quantifiers and their non-greedy versions +- `grouping.py`: Groups, capturing with `.groups()` and `.group()`, numbered and named backreferences, non-capturing groups, and conditional matches +- `lookarounds.py`: Positive and negative lookahead and lookbehind assertions +- `alternation.py`: Inline comments with `(?#...)` and alternation with `|` +- `flags.py`: The `IGNORECASE`, `MULTILINE`, `DOTALL`, `VERBOSE`, `ASCII`, and `UNICODE` flags, combining flags, and setting or clearing flags inside a regex +- `debug_flag.py`: Inspecting how the parser reads a regex with `re.DEBUG` + +## Usage + +Run any script with Python: + +```console +$ python anchors.py + +None + +None +... +``` + +Since Python 3.8, `re.DEBUG` prints the parse tree followed by a blank line and a dump of the compiled pattern. `debug_flag.py` prints only the parse tree, which is the part the tutorial discusses. + +## Requirements + +The code uses only the standard library and needs Python 3.7 or later. The outputs in the comments come from Python 3.14. On Python 3.12 and earlier, regex errors show up as `error` instead of `PatternError`, and before Python 3.11, a global flag such as `(?s)` that isn't at the start of a regex only triggers a `DeprecationWarning` instead of an error. diff --git a/regex-python/alternation.py b/regex-python/alternation.py new file mode 100644 index 0000000000..80522a8865 --- /dev/null +++ b/regex-python/alternation.py @@ -0,0 +1,37 @@ +"""Miscellaneous metacharacters: inline comments with (?#...) and +alternation with |. +""" + +import re + +# The parser ignores (?#...) comments +print(re.search("bar(?#This is a comment) *baz", "foo bar baz qux")) +# + +# Alternation matches one of several regexes +print(re.search("foo|bar|baz", "bar")) +# +print(re.search("foo|bar|baz", "baz")) +# +print(re.search("foo|bar|baz", "quux")) +# None + +# Alternation returns the first alternative that matches, not the longest +print(re.search("foo", "foograult")) +# +print(re.search("grault", "foograult")) +# +print(re.search("foo|grault", "foograult")) +# + +# Combining alternation with grouping and quantifiers +print(re.search("(foo|bar|baz)+", "foofoofoo")) +# +print(re.search("(foo|bar|baz)+", "bazbazbazbaz")) +# +print(re.search("(foo|bar|baz)+", "barbazfoo")) +# +print(re.search("([0-9]+|[a-f]+)", "456")) +# +print(re.search("([0-9]+|[a-f]+)", "ffda")) +# diff --git a/regex-python/anchors.py b/regex-python/anchors.py new file mode 100644 index 0000000000..7dde5f46df --- /dev/null +++ b/regex-python/anchors.py @@ -0,0 +1,57 @@ +"""Anchors: ^, \\A, $, \\Z, \\b, and \\B.""" + +import re + +# ^ and \A anchor a match to the start of the string +print(re.search("^foo", "foobar")) +# +print(re.search("^foo", "barfoo")) +# None +print(re.search(r"\Afoo", "foobar")) +# +print(re.search(r"\Afoo", "barfoo")) +# None + +# $ and \Z anchor a match to the end of the string +print(re.search("bar$", "foobar")) +# +print(re.search("bar$", "barfoo")) +# None +print(re.search(r"bar\Z", "foobar")) +# +print(re.search(r"bar\Z", "barfoo")) +# None + +# $ (but not \Z) also matches before a single trailing newline +print(re.search("bar$", "foobar\n")) +# + +# \b anchors a match to a word boundary +print(re.search(r"\bbar", "foo bar")) +# +print(re.search(r"\bbar", "foo.bar")) +# +print(re.search(r"\bbar", "foobar")) +# None +print(re.search(r"foo\b", "foo bar")) +# +print(re.search(r"foo\b", "foo.bar")) +# +print(re.search(r"foo\b", "foobar")) +# None + +# \b on both ends matches a whole word +print(re.search(r"\bbar\b", "foo bar baz")) +# +print(re.search(r"\bbar\b", "foo(bar)baz")) +# +print(re.search(r"\bbar\b", "foobarbaz")) +# None + +# \B anchors a match to a location that isn't a word boundary +print(re.search(r"\Bfoo\B", "foo")) +# None +print(re.search(r"\Bfoo\B", ".foo.")) +# None +print(re.search(r"\Bfoo\B", "barfoobaz")) +# diff --git a/regex-python/debug_flag.py b/regex-python/debug_flag.py new file mode 100644 index 0000000000..3ec4fb7bb2 --- /dev/null +++ b/regex-python/debug_flag.py @@ -0,0 +1,91 @@ +"""Displaying how the regex parser interprets a pattern with re.DEBUG. + +Python 3.8 and later print the parse tree shown below, then a blank line +and a dump of the compiled pattern's internal opcodes. This script prints +only the parse tree, which is the part the tutorial discusses. +""" + +import contextlib +import io +import re + + +def show_parse_tree(regex, string, flags=re.DEBUG): + """Search string with a DEBUG flag and print only the parse tree.""" + re.purge() # Clear the cache so the pattern compiles and prints again + buffer = io.StringIO() + with contextlib.redirect_stdout(buffer): + match = re.search(regex, string, flags) + print(buffer.getvalue().split("\n\n")[0]) + print(match) + + +show_parse_tree("foo.bar", "fooxbar") +# LITERAL 102 +# LITERAL 111 +# LITERAL 111 +# ANY None +# LITERAL 98 +# LITERAL 97 +# LITERAL 114 +# + +show_parse_tree(r"^(\(\d{3}\))?\s*\d{3}[-.]\d{4}$", "414.9229") +# AT AT_BEGINNING +# MAX_REPEAT 0 1 +# SUBPATTERN 1 0 0 +# LITERAL 40 +# MAX_REPEAT 3 3 +# IN +# CATEGORY CATEGORY_DIGIT +# LITERAL 41 +# MAX_REPEAT 0 MAXREPEAT +# IN +# CATEGORY CATEGORY_SPACE +# MAX_REPEAT 3 3 +# IN +# CATEGORY CATEGORY_DIGIT +# IN +# LITERAL 45 +# LITERAL 46 +# MAX_REPEAT 4 4 +# IN +# CATEGORY CATEGORY_DIGIT +# AT AT_END +# + +# {2,4} is a quantifier, so the tree contains MAX_REPEAT +show_parse_tree("x[123]{2,4}y", "x222y") +# LITERAL 120 +# MAX_REPEAT 2 4 +# IN +# LITERAL 49 +# LITERAL 50 +# LITERAL 51 +# LITERAL 121 +# + +# {foo} isn't a valid quantifier, so it's matched literally +show_parse_tree("x[123]{foo}y", "x222y") +# LITERAL 120 +# IN +# LITERAL 49 +# LITERAL 50 +# LITERAL 51 +# LITERAL 123 +# LITERAL 102 +# LITERAL 111 +# LITERAL 111 +# LITERAL 125 +# LITERAL 121 +# None + +# The re module has no single-letter DEBUG flag, but you can define one +print(hasattr(re, "D")) +# False +re.D = re.DEBUG +show_parse_tree("foo", "foo", re.D) +# LITERAL 102 +# LITERAL 111 +# LITERAL 111 +# diff --git a/regex-python/escaping.py b/regex-python/escaping.py new file mode 100644 index 0000000000..a20b01bc14 --- /dev/null +++ b/regex-python/escaping.py @@ -0,0 +1,29 @@ +"""Escaping metacharacters with a backslash, and why raw strings help.""" + +import re + +# An unescaped dot is a wildcard, while an escaped dot is literal +print(re.search(".", "foo.bar")) +# +print(re.search(r"\.", "foo.bar")) +# + +s = r"foo\bar" +print(s) +# foo\bar + +# Python turns "\\" into a single backslash, which isn't a valid regex +try: + re.search("\\", s) +except re.error as e: + print(f"{type(e).__name__}: {e}") +# PatternError: bad escape (end of pattern) at position 0 +# (On Python 3.12 and earlier, the exception class is named error.) + +# Fix 1: Escape both backslashes in the string literal +print(re.search("\\\\", s)) +# + +# Fix 2 (cleaner): Use a raw string +print(re.search(r"\\", s)) +# diff --git a/regex-python/flags.py b/regex-python/flags.py new file mode 100644 index 0000000000..1b2e08bc2a --- /dev/null +++ b/regex-python/flags.py @@ -0,0 +1,131 @@ +"""Modifying regex matching with flags, combining flags, and setting or +clearing flags within a regex. +""" + +import re + +# re.I / re.IGNORECASE +print(re.search("a+", "aaaAAA")) +# +print(re.search("A+", "aaaAAA")) +# +print(re.search("a+", "aaaAAA", re.I)) +# +print(re.search("A+", "aaaAAA", re.IGNORECASE)) +# +print(re.search("[a-z]+", "aBcDeF")) +# +print(re.search("[a-z]+", "aBcDeF", re.I)) +# + +# re.M / re.MULTILINE +s = "foo\nbar\nbaz" +print(re.search("^bar", s)) +# None +print(re.search("baz$", s)) +# +print(re.search("^bar", s, re.MULTILINE)) +# +print(re.search("bar$", s, re.M)) +# + +# MULTILINE doesn't affect \A and \Z +print(re.search(r"\Abar", s, re.MULTILINE)) +# None +print(re.search(r"bar\Z", s, re.MULTILINE)) +# None + +# re.S / re.DOTALL +print(re.search("foo.bar", "foo\nbar")) +# None +print(re.search("foo.bar", "foo\nbar", re.DOTALL)) +# +print(re.search("foo.bar", "foo\nbar", re.S)) +# + +# re.X / re.VERBOSE +regex = r"^(\(\d{3}\))?\s*\d{3}[-.]\d{4}$" +print(re.search(regex, "(712) 414-9229")) +# + +regex = r"""^ # Start of string + (\(\d{3}\))? # Optional area code + \s* # Optional whitespace + \d{3} # Three-digit prefix + [-.] # Separator character + \d{4} # Four-digit line number + $ # Anchor at end of string + """ +print(re.search(regex, "414.9229", re.VERBOSE)) +# +print(re.search(regex, "414-9229", re.VERBOSE)) +# +print(re.search(regex, "(712)414-9229", re.X)) +# +print(re.search(regex, "(712) 414-9229", re.X)) +# + +# In VERBOSE mode, escape significant spaces or put them in a class +print(re.search("foo bar", "foo bar", re.VERBOSE)) +# None +print(re.search(r"foo\ bar", "foo bar", re.VERBOSE)) +# +print(re.search("foo[ ]bar", "foo bar", re.VERBOSE)) +# + +# re.A / re.ASCII and re.U / re.UNICODE +s = "\u0967\u096a\u096c" +print(s) +# १४६ +print(re.search(r"\d+", s)) +# + +s = "sch\u00f6n" +print(s) +# schön +print(re.search(r"\w+", s, re.ASCII)) +# +print(re.search(r"\w+", s, re.UNICODE)) +# +print(re.search(r"\w+", s)) +# + +# Combining flags with bitwise OR +print(re.search("^bar", "FOO\nBAR\nBAZ", re.I | re.M)) +# + +# Setting flags within the regex: (?) +print(re.search("(?im)^bar", "FOO\nBAR\nBAZ\n")) +# +print(re.search("(?s)foo.bar.baz", "foo\nbar\nbaz")) +# + +# Global flags must come first (an error since Python 3.11) +try: + re.search("foo.bar.baz(?s)", "foo\nbar\nbaz") +except re.error as e: + print(f"{type(e).__name__}: {e}") +# PatternError: global flags not at the start of the expression at position 11 +# (On Python 3.12 and earlier, the exception class is named error.) + +# Setting or clearing flags for a group: (?-:...) +print(re.search("(?i:foo)bar", "FOObar")) +# +print(re.search("(?i:foo)bar", "FOOBAR")) +# None +print(re.search("(?-i:foo)bar", "FOOBAR", re.IGNORECASE)) +# None + +# Overriding the encoding for a group (requires Python 3.7 or later) +print(re.search(r"(?a:\w+)", s)) +# +print(re.search(r"(?u:\w+)", s)) +# + +# You can set an encoding this way, but you can't remove it +try: + re.search(r"(?-a:\w+)", s) +except re.error as e: + print(f"{type(e).__name__}: {e}") +# PatternError: bad inline flags: cannot turn off flags 'a', 'u' and 'L' at position 4 +# (On Python 3.12 and earlier, the exception class is named error.) diff --git a/regex-python/grouping.py b/regex-python/grouping.py new file mode 100644 index 0000000000..901cec73e3 --- /dev/null +++ b/regex-python/grouping.py @@ -0,0 +1,126 @@ +"""Grouping constructs, capturing groups, backreferences, named groups, +non-capturing groups, and conditional matches. +""" + +import re + +# A group matches its contents, just like the bare regex +print(re.search("(bar)", "foo bar baz")) +# +print(re.search("bar", "foo bar baz")) +# + +# A quantifier after a group applies to the whole group +print(re.search("(bar)+", "foo bar baz")) +# +print(re.search("(bar)+", "foo barbar baz")) +# +print(re.search("(bar)+", "foo barbarbarbar baz")) +# + +print(re.search("(ba[rz]){2,4}(qux)?", "bazbarbazqux")) +# +print(re.search("(ba[rz]){2,4}(qux)?", "barbar")) +# + +# Groups can be nested +print(re.search(r"(foo(bar)?)+(\d\d\d)?", "foofoobar")) +# +print(re.search(r"(foo(bar)?)+(\d\d\d)?", "foofoobar123")) +# +print(re.search(r"(foo(bar)?)+(\d\d\d)?", "foofoo123")) +# + +# Capturing groups: .groups() and .group() +m = re.search(r"(\w+),(\w+),(\w+)", "foo,quux,baz") +print(m) +# +print(m.groups()) +# ('foo', 'quux', 'baz') +print(m.group(1), m.group(2), m.group(3)) +# foo quux baz +print(m.group(0)) +# foo,quux,baz +print(m.group()) +# foo,quux,baz +print(m.group(2, 3)) +# ('quux', 'baz') +print(m.group(3, 2, 1)) +# ('baz', 'quux', 'foo') +print((m.group(3), m.group(2), m.group(1))) +# ('baz', 'quux', 'foo') + +# Numbered backreferences +regex = r"(\w+),\1" +m = re.search(regex, "foo,foo") +print(m, m.group(1)) +# foo +m = re.search(regex, "qux,qux") +print(m, m.group(1)) +# qux +print(re.search(regex, "foo,qux")) +# None + +# Without a raw string, Python reads "\1" as the character with octal value 1 +print(re.search("([a-z])#\1", "d#d")) +# None +print(oct(ord("\1"))) +# 0o1 +print(re.search(r"([a-z])#\1", "d#d")) +# + +# Named groups: (?P) +m = re.search(r"(?P\w+),(?P\w+),(?P\w+)", "foo,quux,baz") +print(m.groups()) +# ('foo', 'quux', 'baz') +print(m.group("w1"), m.group(1)) +# foo foo +print(m.group("w3")) +# baz +print(m.group("w1", "w2", "w3")) +# ('foo', 'quux', 'baz') +print(m.group(1, 2, 3)) +# ('foo', 'quux', 'baz') + +# Named backreferences: (?P=name) +m = re.search(r"(?P\w+),(?P=word)", "foo,foo") +print(m, m.group("word")) +# foo +m = re.match(r"(?P\d+)\.(?P=num)", "135.135") +print(m, m.group("num")) +# 135 + +# Non-capturing groups: (?:) +m = re.search(r"(\w+),(?:\w+),(\w+)", "foo,quux,baz") +print(m.groups()) +# ('foo', 'baz') +print(m.group(1), m.group(2)) +# foo baz + +# Conditional match on a numbered group +regex = r"^(###)?foo(?(1)bar|baz)" +print(re.search(regex, "###foobar")) +# +print(re.search(regex, "###foobaz")) +# None +print(re.search(regex, "foobar")) +# None +print(re.search(regex, "foobaz")) +# + +# Conditional match on a named group +regex = r"^(?P\W)?foo(?(ch)(?P=ch)|)$" +print(re.search(regex, "foo")) +# +print(re.search(regex, "#foo#")) +# +print(re.search(regex, "@foo@")) +# +print(re.search(regex, "#foo")) +# None +print(re.search(regex, "foo@")) +# None +print(re.search(regex, "#foo@")) +# None +print(re.search(regex, "@foo#")) +# None diff --git a/regex-python/lookarounds.py b/regex-python/lookarounds.py new file mode 100644 index 0000000000..421767995b --- /dev/null +++ b/regex-python/lookarounds.py @@ -0,0 +1,49 @@ +"""Lookahead and lookbehind assertions.""" + +import re + +# Positive lookahead: (?=) +print(re.search("foo(?=[a-z])", "foobar")) +# +print(re.search("foo(?=[a-z])", "foo123")) +# None + +# Compare with a group, which consumes the character +print(re.search("foo([a-z])", "foobar")) +# + +# A lookahead doesn't advance the parser past what it matches +m = re.search("foo(?=[a-z])(?P.)", "foobar") +print(m.group("ch")) +# b +m = re.search("foo([a-z])(?P.)", "foobar") +print(m.group("ch")) +# a + +# Negative lookahead: (?!) +print(re.search("foo(?![a-z])", "foobar")) +# None +print(re.search("foo(?![a-z])", "foo123")) +# + +# Positive lookbehind: (?<=) +print(re.search("(?<=foo)bar", "foobar")) +# +print(re.search("(?<=qux)bar", "foobar")) +# None + +# A lookbehind must match a fixed-width pattern +try: + re.search("(?<=a+)def", "aaadef") +except re.error as e: + print(f"{type(e).__name__}: {e}") +# PatternError: look-behind requires fixed-width pattern +# (On Python 3.12 and earlier, the exception class is named error.) +print(re.search("(?<=a{3})def", "aaadef")) +# + +# Negative lookbehind: (?) +print(re.search("(? diff --git a/regex-python/quantifiers.py b/regex-python/quantifiers.py new file mode 100644 index 0000000000..b2d97f0221 --- /dev/null +++ b/regex-python/quantifiers.py @@ -0,0 +1,94 @@ +"""Quantifiers: *, +, ?, {m}, {m,n}, and their non-greedy versions.""" + +import re + +# * matches zero or more repetitions +print(re.search("foo-*bar", "foobar")) # Zero dashes +# +print(re.search("foo-*bar", "foo-bar")) # One dash +# +print(re.search("foo-*bar", "foo--bar")) # Two dashes +# + +print(re.search("foo.*bar", "# foo $qux@grault % bar #")) +# + +# + matches one or more repetitions +print(re.search("foo-+bar", "foobar")) # Zero dashes +# None +print(re.search("foo-+bar", "foo-bar")) # One dash +# +print(re.search("foo-+bar", "foo--bar")) # Two dashes +# + +# ? matches zero or one repetition +print(re.search("foo-?bar", "foobar")) # Zero dashes +# +print(re.search("foo-?bar", "foo-bar")) # One dash +# +print(re.search("foo-?bar", "foo--bar")) # Two dashes +# None + +# Quantifying a character class +print(re.match("foo[1-9]*bar", "foobar")) +# +print(re.match("foo[1-9]*bar", "foo42bar")) +# +print(re.match("foo[1-9]+bar", "foobar")) +# None +print(re.match("foo[1-9]+bar", "foo42bar")) +# +print(re.match("foo[1-9]?bar", "foobar")) +# +print(re.match("foo[1-9]?bar", "foo42bar")) +# None + +# Greedy vs non-greedy (lazy) quantifiers +print(re.search("<.*>", "% %")) +# +print(re.search("<.*?>", "% %")) +# +print(re.search("<.+>", "% %")) +# +print(re.search("<.+?>", "% %")) +# +print(re.search("ba?", "baaaa")) +# +print(re.search("ba??", "baaaa")) +# + +# {m} matches exactly m repetitions +print(re.search("x-{3}x", "x--x")) # Two dashes +# None +print(re.search("x-{3}x", "x---x")) # Three dashes +# +print(re.search("x-{3}x", "x----x")) # Four dashes +# None + +# {m,n} matches from m to n repetitions, inclusive +for i in range(1, 6): + s = f"x{'-' * i}x" + print(f"{i} {s:10}", re.search("x-{2,4}x", s)) +# 1 x-x None +# 2 x--x +# 3 x---x +# 4 x----x +# 5 x-----x None + +# Curly braces that don't hold a valid range match literally +print(re.search("x{}y", "x{}y")) +# +print(re.search("x{foo}y", "x{foo}y")) +# +print(re.search("x{a:b}y", "x{a:b}y")) +# +print(re.search("x{1,3,5}y", "x{1,3,5}y")) +# +print(re.search("x{foo,bar}y", "x{foo,bar}y")) +# + +# {m,n}? is the non-greedy version of {m,n} +print(re.search("a{3,5}", "aaaaaaaa")) +# +print(re.search("a{3,5}?", "aaaaaaaa")) +# diff --git a/regex-python/search_basics.py b/regex-python/search_basics.py new file mode 100644 index 0000000000..7be6cdede0 --- /dev/null +++ b/regex-python/search_basics.py @@ -0,0 +1,46 @@ +"""Importing re.search() and a first look at pattern matching with it.""" + +# Approach 1: Import the module and use the module name as a prefix +import re + +# Approach 2: Import the function by name +from re import search + +print(re.search("123", "foo123bar")) +# +print(search("123", "foo123bar")) +# + +# First pattern-matching example +s = "foo123bar" +print(re.search("123", s)) +# + +# A match object is truthy +if re.search("123", s): + print("Found a match.") +else: + print("No match.") +# Found a match. + +# span=(3, 6) means the same thing as slice notation +print(repr(s[3:6])) +# '123' + +# A character class matches any single character inside the brackets +print(re.search("[0-9][0-9][0-9]", s)) +# +print(re.search("[0-9][0-9][0-9]", "foo456bar")) +# +print(re.search("[0-9][0-9][0-9]", "234baz")) +# +print(re.search("[0-9][0-9][0-9]", "qux678")) +# +print(re.search("[0-9][0-9][0-9]", "12foo34")) +# None + +# The dot is a wildcard for any character except a newline +print(re.search("1.3", "foo123bar")) +# +print(re.search("1.3", "foo13bar")) +# None diff --git a/regex-python/single_character.py b/regex-python/single_character.py new file mode 100644 index 0000000000..d7aa1ca23f --- /dev/null +++ b/regex-python/single_character.py @@ -0,0 +1,84 @@ +"""Metacharacters that match a single character: [], ., \\w, \\W, \\d, \\D, +\\s, and \\S. +""" + +import re + +# Character classes: [] +print(re.search("ba[artz]", "foobarqux")) +# +print(re.search("ba[artz]", "foobazqux")) +# + +print(re.search("[a-z]", "FOObar")) +# +print(re.search("[0-9][0-9]", "foo123bar")) +# +print(re.search("[0-9a-fA-F]", "--- a0 ---")) +# + +# A leading ^ complements the class +print(re.search("[^0-9]", "12345foo")) +# + +# Anywhere else, ^ is a literal character +print(re.search("[#:^]", "foo^bar:baz#qux")) +# + +# A literal hyphen goes first, last, or escaped +print(re.search("[-abc]", "123-456")) +# +print(re.search("[abc-]", "123-456")) +# +print(re.search(r"[ab\-c]", "123-456")) +# + +# A literal ] goes first or escaped +print(re.search("[]]", "foo[1]")) +# +print(re.search(r"[ab\]cd]", "foo[1]")) +# + +# Other metacharacters are literal inside a class +print(re.search("[)*+|]", "123*456")) +# +print(re.search("[)*+|]", "123+456")) +# + +# The dot wildcard: any character except a newline +print(re.search("foo.bar", "fooxbar")) +# +print(re.search("foo.bar", "foobar")) +# None +print(re.search("foo.bar", "foo\nbar")) +# None + +# \w and \W: word and non-word characters +print(re.search(r"\w", "#(.a$@&")) +# +print(re.search("[a-zA-Z0-9_]", "#(.a$@&")) +# +print(re.search(r"\W", "a_1*3Qb")) +# +print(re.search("[^a-zA-Z0-9_]", "a_1*3Qb")) +# + +# \d and \D: decimal digits and non-digits +print(re.search(r"\d", "abc4def")) +# +print(re.search(r"\D", "234Q678")) +# + +# \s and \S: whitespace and non-whitespace (newlines count as whitespace) +print(re.search(r"\s", "foo\nbar baz")) +# +print(re.search(r"\S", " \n foo \n ")) +# + +# Special classes also work inside square brackets +print(re.search(r"[\d\w\s]", "---3---")) +# +print(re.search(r"[\d\w\s]", "---a---")) +# +print(re.search(r"[\d\w\s]", "--- ---")) +#