diff --git a/README.de.md b/README.de.md index e6838e1..f1808e4 100644 --- a/README.de.md +++ b/README.de.md @@ -88,11 +88,14 @@ Es gibt zwei Möglichkeiten, die Module auszuführen: - Conditional: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Loop: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Function: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Walrus operator: [Assignment expressions :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Argument enforcement: [Positional-only / | Keyword-only *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Daten-Strukturen** - List: [List operations](ultimatepython/data_structures/list.py) ( 🍰 ) - Tuple: [Tuple operations](ultimatepython/data_structures/tuple.py) - Set: [Set operations](ultimatepython/data_structures/set.py) - Dict: [Dictionary operations](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Dict union: [Dictionary merge | and |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprehension: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - String: [String operations](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -121,6 +124,7 @@ Es gibt zwei Möglichkeiten, die Module auszuführen: - Regular expression: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Data format: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Pattern Matching: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## Zusätzliche Ressourcen diff --git a/README.es.md b/README.es.md index 173a078..4b3be9d 100644 --- a/README.es.md +++ b/README.es.md @@ -86,11 +86,14 @@ Hay dos maneras de ejecutar los módulos: - Condicionales: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Iteraciones: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Funciones: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Operador morsa: [Expresiones de asignación :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Aplicación de argumentos: [Solo posicional / | Solo palabra clave *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Estructura de datos** - Lista: [Operaciones con listas](ultimatepython/data_structures/list.py) ( 🍰 ) - Tupla: [Operaciones con tuplas](ultimatepython/data_structures/tuple.py) - Set: [Operaciones con sets](ultimatepython/data_structures/set.py) - Diccionario: [Operaciones con dicts](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Unión de diccionarios: [Fusión de diccionarios | y |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprensión: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - Cadena: [Operaciones con strings](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -119,6 +122,7 @@ Hay dos maneras de ejecutar los módulos: - Expresiones regulares: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Formatos de datos: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Fecha y hora: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Coincidencia de patrones: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## Recursos adicionales diff --git a/README.fr.md b/README.fr.md index d064e2b..ffec3ae 100644 --- a/README.fr.md +++ b/README.fr.md @@ -37,21 +37,21 @@ grâce à ce langage. 🎓 Voici les principaux objectifs de ce guide : -🏆 **Servir de ressource** pour les débutants en Python qui préfèrent apprendre de manière pratique.  +🏆 **Servir de ressource** pour les débutants en Python qui préfèrent apprendre de manière pratique. Ce dépôt contient une collection de modules indépendants pouvant être exécutés dans un IDE comme [PyCharm](https://www.jetbrains.com/pycharm/) ou dans le navigateur via [Replit](https://replit.com/languages/python3). Même un simple terminal suffit pour exécuter les exemples. La plupart des lignes contiennent des commentaires détaillés -qui guident le lecteur pas à pas.  +qui guident le lecteur pas à pas. Les utilisateurs sont encouragés à modifier le code source à leur guise tant que les routines `main` ne sont pas supprimées et que les programmes [s’exécutent correctement](runner.py) après chaque modification. -🏆 **Servir de guide pur** pour ceux qui souhaitent revoir les concepts fondamentaux de Python.  +🏆 **Servir de guide pur** pour ceux qui souhaitent revoir les concepts fondamentaux de Python. Seules les [bibliothèques intégrées](https://docs.python.org/3/library/) sont utilisées afin de présenter les concepts sans dépendre de notions spécifiques à un domaine. Ainsi, les bibliothèques open-source populaires comme `sqlalchemy`, `requests` ou `pandas` -ne sont pas installées.  +ne sont pas installées. Cependant, lire le code source de ces frameworks est fortement recommandé si ton objectif est de devenir un véritable [Pythonista](https://www.urbandictionary.com/define.php?term=pythonista). @@ -61,24 +61,24 @@ si ton objectif est de devenir un véritable [![Run on Replit](https://replit.com/badge/github/huangsam/ultimate-python)](https://replit.com/github/huangsam/ultimate-python) Clique sur le badge ci-dessus pour lancer un environnement fonctionnel dans ton navigateur -sans avoir besoin d’installer Git ou Python localement.  +sans avoir besoin d’installer Git ou Python localement. Si ces outils sont déjà installés, tu peux cloner directement le dépôt. -Une fois le dépôt accessible, tu es prêt à apprendre à partir des modules indépendants.  +Une fois le dépôt accessible, tu es prêt à apprendre à partir des modules indépendants. Pour tirer le meilleur parti de chaque module, lis le code et exécute-le. Deux méthodes sont possibles : -1. Exécuter un seul module :  +1. Exécuter un seul module :   `python ultimatepython/syntax/variable.py` -2. Exécuter tous les modules :  +2. Exécuter tous les modules :   `python runner.py` ## Table des matières -📚 = Ressource externe  -🍰 = Sujet débutant  -🤯 = Sujet avancé  +📚 = Ressource externe +🍰 = Sujet débutant +🤯 = Sujet avancé 1. **À propos de Python**     - Vue d’ensemble : [Qu’est-ce que Python](https://github.com/trekhleb/learn-python/blob/master/src/getting_started/what_is_python.md) ( 📚, 🍰 ) @@ -95,12 +95,15 @@ Deux méthodes sont possibles :     - Conditionnelle : [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 )     - Boucle : [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 )     - Fonction : [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Opérateur morse : [Expressions d'affectation :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Application d'arguments : [Positionnels uniquement / | Mots-clés uniquement *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Structures de données**     - Liste : [Opérations sur les listes](ultimatepython/data_structures/list.py) ( 🍰 )     - Tuple : [Opérations sur les tuples](ultimatepython/data_structures/tuple.py)     - Ensemble : [Opérations sur les ensembles](ultimatepython/data_structures/set.py)     - Dictionnaire : [Opérations sur les dictionnaires](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Union de dictionnaires : [Fusion de dictionnaires | et |=](ultimatepython/data_structures/dict_union.py) ( 🤯 )     - Compréhension : [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py)     - Chaîne : [Opérations sur les chaînes](ultimatepython/data_structures/string.py) ( 🍰 )     - Deque : [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -131,12 +134,13 @@ Deux méthodes sont possibles :     - Expressions régulières : [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 )     - Format de données : [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 )     - Date et heure : [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Correspondance de motifs : [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## Ressources supplémentaires -👔 = Ressource d’entretien  -🧪 = Exemples de code  -🧠 = Idées de projets  +👔 = Ressource d’entretien +🧪 = Exemples de code +🧠 = Idées de projets ### Dépôts GitHub diff --git a/README.hi.md b/README.hi.md index 9ef3302..c148c2c 100644 --- a/README.hi.md +++ b/README.hi.md @@ -69,11 +69,14 @@ print("Ultimate Python स्टडी गाइड") - कंडीशनल: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - लूप: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - फ़ंक्शन: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - वॉलरस ऑपरेटर: [असाइनमेंट एक्सप्रेशन :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - तर्क प्रवर्तन: [केवल स्थितीय / | केवल कीवर्ड *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **डेटा संरचनाएँ** - लिसट: [लिसट ऑपरेशन्स](ultimatepython/data_structures/list.py) ( 🍰 ) - ट्यूपल: [ट्यूपल ऑपरेशन्स](ultimatepython/data_structures/tuple.py) - सेट: [सेट ऑपरेशन्स](ultimatepython/data_structures/set.py) - डिक्ट: [डिक्शनरी ऑपरेशन्स](ultimatepython/data_structures/dict.py) ( 🍰 ) + - डिक्शनरी यूनियन: [डिक्शनरी मर्ज | और |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - संकलन: [लिसट | ट्यूपल | सेट | डिक्ट](ultimatepython/data_structures/comprehension.py) - स्ट्रिंग: [स्ट्रिंग ऑपरेशन्स](ultimatepython/data_structures/string.py) ( 🍰 ) - डेक: [डेक](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -102,6 +105,7 @@ print("Ultimate Python स्टडी गाइड") - नियमित अभिव्यक्ति: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - डेटा फ़ॉर्मेट: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - दिनांक और समय: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - पैटर्न मिलान: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## अतिरिक्त संसाधन diff --git a/README.ko.md b/README.ko.md index c2ab129..5f774ba 100644 --- a/README.ko.md +++ b/README.ko.md @@ -77,11 +77,14 @@ print("Ultimate Python 학습 가이드") - 조건문 : [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - 반복문 : [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - 함수 : [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - 바다코끼리 연산자 : [할당 표현식 :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - 인수 강제 : [위치 전용 / | 키워드 전용 *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **데이터 구조** - 리스트 : [리스트 연산](ultimatepython/data_structures/list.py) ( 🍰 ) - 튜플 : [튜플 연산](ultimatepython/data_structures/tuple.py) - 세트 : [세트 연산](ultimatepython/data_structures/set.py) - 딕셔너리 : [딕셔너리 연산](ultimatepython/data_structures/dict.py) ( 🍰 ) + - 딕셔너리 합병 : [딕셔너리 병합 | 및 |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - 컴프리헨션 : [리스트 | 튜플 | 세트 | 딕셔너리](ultimatepython/data_structures/comprehension.py) - 문자열 : [문자열 연산](ultimatepython/data_structures/string.py) ( 🍰 ) - 덱: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -110,6 +113,7 @@ print("Ultimate Python 학습 가이드") - 정규식 : [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - 데이터 포맷 : [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - 날짜와 시간 : [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - 패턴 매칭: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## 추가 자료 diff --git a/README.md b/README.md index e30df42..e78c2e0 100644 --- a/README.md +++ b/README.md @@ -89,11 +89,14 @@ There are two ways of running the modules: - Conditional: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Loop: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Function: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Walrus operator: [Assignment expressions :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Argument enforcement: [Positional-only / | Keyword-only *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Data Structures** - List: [List operations](ultimatepython/data_structures/list.py) ( 🍰 ) - Tuple: [Tuple operations](ultimatepython/data_structures/tuple.py) - Set: [Set operations](ultimatepython/data_structures/set.py) - Dict: [Dictionary operations](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Dict union: [Dictionary merge | and |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprehension: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - String: [String operations](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -122,6 +125,7 @@ There are two ways of running the modules: - Regular expression: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Data format: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Pattern matching: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## Additional resources diff --git a/README.pt_br.md b/README.pt_br.md index c940387..93bd372 100644 --- a/README.pt_br.md +++ b/README.pt_br.md @@ -75,11 +75,14 @@ Existem duas maneiras de rodar os módulos: - Condicional: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Loop/Laço: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Função: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Operador morsa: [Expressões de atribuição :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Aplicação de argumentos: [Somente posicional / | Somente palavra-chave *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Estrutura de dados** - Lista: [Operações de lista](ultimatepython/data_structures/list.py) ( 🍰 ) - Tupla: [Operações de tuplas](ultimatepython/data_structures/tuple.py) - Conjunto: [Operações de conjuntos](ultimatepython/data_structures/set.py) - Dicionário: [Operações de dicionários](ultimatepython/data_structures/dict.py) ( 🍰 ) + - União de dicionários: [Fusão de dicionários | e |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprehension: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - String: [Operações de String](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -108,6 +111,7 @@ Existem duas maneiras de rodar os módulos: - Expressões regulares (regexp): [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Formato de dados: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Correspondência de padrões: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## Recursos adicionais diff --git a/README.zh_tw.md b/README.zh_tw.md index 0187335..e24a545 100644 --- a/README.zh_tw.md +++ b/README.zh_tw.md @@ -72,11 +72,14 @@ print("Ultimate Python 學習大綱") - 條件運算式:[if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - 迴圈:[for迴圈 | while迴圈](ultimatepython/syntax/loop.py) ( 🍰 ) - 定義函式:[def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - 海象運算子:[賦值表達式 :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - 參數強制:[僅位置 / | 僅關鍵字 *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **資料結構** - 列表:[列表操作](ultimatepython/data_structures/list.py) ( 🍰 ) - 元組:[元組操作](ultimatepython/data_structures/tuple.py) - 集合:[集合操作](ultimatepython/data_structures/set.py) - 字典:[字典操作](ultimatepython/data_structures/dict.py) ( 🍰 ) + - 字典聯合:[字典合併 | 和 |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - 綜合:[list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - 字串:[字串操作](ultimatepython/data_structures/string.py) ( 🍰 ) - 雙端隊列:[deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -104,6 +107,7 @@ print("Ultimate Python 學習大綱") - 正規表示式:[search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - 數據格式:[json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - 日期時間: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - 模式匹配:[match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## 額外資源 diff --git a/codecov.yml b/codecov.yml index e62f4b9..1ba8163 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,6 +2,9 @@ # https://docs.codecov.com/docs/commit-status coverage: status: + project: + default: + target: 90% patch: default: - target: 100% + target: 90% diff --git a/ultimatepython/advanced/pattern_matching.py b/ultimatepython/advanced/pattern_matching.py new file mode 100644 index 0000000..73f8119 --- /dev/null +++ b/ultimatepython/advanced/pattern_matching.py @@ -0,0 +1,335 @@ +""" +Structural pattern matching allows you to match complex data structures +against patterns and extract values in a clean, readable way. This feature +is similar to switch-case statements in other languages but much more powerful. + +Pattern matching was introduced in Python 3.10 through PEP 634, PEP 635, +and PEP 636. It uses the 'match' and 'case' keywords. +""" + + +def classify_number(value) -> str: + """Classify a number using pattern matching with literals. + + This demonstrates matching against specific literal values. + """ + match value: + case 0: + return "zero" + case 1: + return "one" + case 2: + return "two" + case _: + # The underscore _ is a wildcard that matches anything + return "other" + + +def classify_http_status(status) -> str: + """Classify HTTP status codes using pattern matching. + + This shows how pattern matching can make code more readable + than a series of if-elif-else statements. + """ + match status: + case 200: + return "OK" + case 201: + return "Created" + case 400: + return "Bad Request" + case 401: + return "Unauthorized" + case 404: + return "Not Found" + case 500: + return "Internal Server Error" + case _: + return "Unknown Status" + + +def process_point(point) -> str: + """Process a point tuple using pattern matching with sequences. + + This demonstrates pattern matching against tuple structures + and extracting values from them. + """ + match point: + case (0, 0): + return "Origin" + case (0, y): + # Match any point on the y-axis, capture y coordinate + return f"Y-axis at y={y}" + case (x, 0): + # Match any point on the x-axis, capture x coordinate + return f"X-axis at x={x}" + case (x, y): + # Match any other point, capture both coordinates + return f"Point at ({x}, {y})" + case _: + return "Not a valid 2D point" + + +def analyze_sequence(data) -> str: + """Analyze sequences using pattern matching. + + This shows how to match lists with specific structures + and extract elements. + """ + match data: + case []: + return "Empty list" + case [x]: + # Match a list with exactly one element + return f"Single element: {x}" + case [x, y]: + # Match a list with exactly two elements + return f"Pair: {x}, {y}" + case [first, *rest]: + # Match a list with at least one element + # The *rest captures remaining elements + return f"First: {first}, Rest: {rest}" + case _: + return "Not a list" # pragma: no cover + + +def process_command(command) -> str: + """Process commands using pattern matching with guards. + + Guards are if conditions that provide additional filtering + after a pattern match. + """ + match command: + case ["quit"]: + return "Quitting" + case ["go", direction] if direction in ["north", "south", "east", "west"]: + # Guard clause: only match if direction is valid + return f"Going {direction}" + case ["go", direction]: + # This matches any direction that failed the guard above + return f"Invalid direction: {direction}" + case ["take", item]: + return f"Taking {item}" + case ["take", item, quantity] if quantity > 0: + return f"Taking {quantity} {item}" + case ["take", item, quantity]: + return f"Invalid quantity: {quantity}" + case _: + return "Unknown command" + + +class Point: + """A simple 2D point class for pattern matching examples.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + +class Circle: + """A circle with center and radius for pattern matching examples.""" + + def __init__(self, center, radius): + self.center = center + self.radius = radius + + +def describe_shape(shape) -> str: + """Describe shapes using pattern matching with class patterns. + + This demonstrates matching against class instances and + extracting their attributes. + """ + match shape: + case Point(x=0, y=0): + # Match a Point at the origin + return "Point at origin" + case Point(x=0, y=y): + # Match a Point on the y-axis + return f"Point on Y-axis at {y}" + case Point(x=x, y=0): + # Match a Point on the x-axis + return f"Point on X-axis at {x}" + case Point(x=x, y=y) if x == y: + # Match a Point on the diagonal line y=x + return f"Point on diagonal at ({x}, {y})" + case Point(x=x, y=y): + # Match any other Point + return f"Point at ({x}, {y})" + case Circle(center=Point(x=0, y=0), radius=r): + # Match a Circle centered at origin + return f"Circle at origin with radius {r}" + case Circle(center=Point(x=x, y=y), radius=r): + # Match any other Circle + return f"Circle at ({x}, {y}) with radius {r}" + case _: + return "Unknown shape" + + +def analyze_nested(data) -> str: + """Analyze nested structures using pattern matching.""" + match data: + case [["pair", x, y], ["pair", a, b]]: + # Match nested structure + return f"Two pairs: ({x},{y}) and ({a},{b})" + case [["single", val]]: + return f"Single value: {val}" + case _: + return "Unknown structure" + + +def check_value(val) -> str: + """Check value using OR patterns.""" + match val: + case 0 | 1 | 2: + # Match any of these values + return "small" + case 3 | 4 | 5: + return "medium" + case _: + return "large" + + +def process_range(data) -> str: + """Process range data with AS patterns.""" + match data: + case [x, y] as pair if x < y: + # Capture the entire matched value with 'as' + return f"Valid range: {pair}" + case [x, y]: + return f"Invalid range: [{x}, {y}]" + case _: + return "Not a pair" + + +def process_json_data(data) -> str: + """Process JSON-like dictionary data with pattern matching. + + This shows how to match against dictionary structures. + """ + match data: + case {"type": "user", "name": name, "age": age}: + return f"User {name} is {age} years old" + case {"type": "user", "name": name}: + # Match user without age + return f"User {name} with unknown age" + case {"type": "product", "name": name, "price": price} if price > 0: + return f"Product {name} costs ${price}" + case {"type": "product", "name": name, "price": price}: + return f"Product {name} has invalid price: {price}" + case {"type": type_name}: + # Match any dict with a type key + return f"Unknown type: {type_name}" + case _: + return "Invalid data" + + +def main() -> None: + # Test literal pattern matching + assert classify_number(0) == "zero" + assert classify_number(1) == "one" + assert classify_number(2) == "two" + assert classify_number(5) == "other" + assert classify_number(100) == "other" + + # Test HTTP status classification + assert classify_http_status(200) == "OK" + assert classify_http_status(404) == "Not Found" + assert classify_http_status(999) == "Unknown Status" + assert classify_http_status(201) == "Created" + assert classify_http_status(400) == "Bad Request" + assert classify_http_status(401) == "Unauthorized" + assert classify_http_status(500) == "Internal Server Error" + + # Test sequence pattern matching with tuples + assert process_point((0, 0)) == "Origin" + assert process_point((0, 5)) == "Y-axis at y=5" + assert process_point((3, 0)) == "X-axis at x=3" + assert process_point((4, 7)) == "Point at (4, 7)" + assert process_point("invalid") == "Not a valid 2D point" + + # Test sequence pattern matching with lists + assert analyze_sequence([]) == "Empty list" + assert analyze_sequence([42]) == "Single element: 42" + assert analyze_sequence([1, 2]) == "Pair: 1, 2" + assert analyze_sequence([1, 2, 3, 4]) == "First: 1, Rest: [2, 3, 4]" + assert analyze_sequence("not a list") == "Not a list" + + # Test pattern matching with guards + assert process_command(["quit"]) == "Quitting" + assert process_command(["go", "north"]) == "Going north" + assert process_command(["go", "west"]) == "Going west" + assert process_command(["go", "up"]) == "Invalid direction: up" + assert process_command(["take", "key"]) == "Taking key" + assert process_command(["take", "coin", 5]) == "Taking 5 coin" + assert process_command(["take", "coin", 0]) == "Invalid quantity: 0" + assert process_command(["take", "coin", -1]) == "Invalid quantity: -1" + assert process_command(["jump"]) == "Unknown command" + + # Test class pattern matching + p1 = Point(0, 0) + assert describe_shape(p1) == "Point at origin" + + p2 = Point(0, 5) + assert describe_shape(p2) == "Point on Y-axis at 5" + + p3 = Point(3, 0) + assert describe_shape(p3) == "Point on X-axis at 3" + + p4 = Point(5, 5) + assert describe_shape(p4) == "Point on diagonal at (5, 5)" + + p5 = Point(3, 7) + assert describe_shape(p5) == "Point at (3, 7)" + + c1 = Circle(Point(0, 0), 10) + assert describe_shape(c1) == "Circle at origin with radius 10" + + c2 = Circle(Point(5, 5), 3) + assert describe_shape(c2) == "Circle at (5, 5) with radius 3" + + # Test unknown shape + assert describe_shape("unknown") == "Unknown shape" + + # Test dictionary pattern matching + user1 = {"type": "user", "name": "Alice", "age": 30} + assert process_json_data(user1) == "User Alice is 30 years old" + + user2 = {"type": "user", "name": "Bob"} + assert process_json_data(user2) == "User Bob with unknown age" + + product1 = {"type": "product", "name": "Laptop", "price": 999} + assert process_json_data(product1) == "Product Laptop costs $999" + + product2 = {"type": "product", "name": "Free Sample", "price": 0} + assert process_json_data(product2) == "Product Free Sample has invalid price: 0" + + unknown = {"type": "order"} + assert process_json_data(unknown) == "Unknown type: order" + + invalid = {"data": "something"} + assert process_json_data(invalid) == "Invalid data" + + # Pattern matching with OR patterns + assert check_value(0) == "small" + assert check_value(2) == "small" + assert check_value(3) == "medium" + assert check_value(10) == "large" + + # Pattern matching with AS patterns (walrus-like capture) + assert process_range([1, 5]) == "Valid range: [1, 5]" + assert process_range([5, 1]) == "Invalid range: [5, 1]" + assert process_range("not a pair") == "Not a pair" + + # Nested pattern matching + assert analyze_nested([["pair", 1, 2], ["pair", 3, 4]]) == "Two pairs: (1,2) and (3,4)" + assert analyze_nested([["single", 42]]) == "Single value: 42" + assert analyze_nested("invalid") == "Unknown structure" + + # Pattern matching is particularly useful for parsing and handling + # structured data like API responses, configuration files, or + # abstract syntax trees in compilers/interpreters + + +if __name__ == "__main__": + main() diff --git a/ultimatepython/data_structures/dict_union.py b/ultimatepython/data_structures/dict_union.py new file mode 100644 index 0000000..5f52c59 --- /dev/null +++ b/ultimatepython/data_structures/dict_union.py @@ -0,0 +1,174 @@ +""" +Dictionary union operators allow you to merge dictionaries using +the | (union) and |= (in-place union) operators. These operators +provide a clean and intuitive syntax for combining dictionaries. + +This feature was introduced in Python 3.9 through PEP 584. Before +this, you had to use methods like dict.update() or {**dict1, **dict2} +syntax to merge dictionaries. +""" + + +def main() -> None: + # Traditional dictionary merging before Python 3.9 + # Method 1: Using dict.update() (modifies the original) + dict1_old = {"a": 1, "b": 2} + dict2_old = {"c": 3, "d": 4} + dict1_old.update(dict2_old) + assert dict1_old == {"a": 1, "b": 2, "c": 3, "d": 4} + + # Method 2: Using dictionary unpacking (creates a new dict) + dict3_old = {"a": 1, "b": 2} + dict4_old = {"c": 3, "d": 4} + merged_old = {**dict3_old, **dict4_old} + assert merged_old == {"a": 1, "b": 2, "c": 3, "d": 4} + + # With Python 3.9+, we can use the | operator for union + # This creates a new dictionary without modifying the originals + dict1 = {"a": 1, "b": 2} + dict2 = {"c": 3, "d": 4} + merged = dict1 | dict2 + assert merged == {"a": 1, "b": 2, "c": 3, "d": 4} + + # The original dictionaries remain unchanged + assert dict1 == {"a": 1, "b": 2} + assert dict2 == {"c": 3, "d": 4} + + # When there are overlapping keys, the right operand's values take precedence + # This is the same behavior as dict.update() and {**d1, **d2} + dict3 = {"a": 1, "b": 2, "c": 3} + dict4 = {"b": 20, "c": 30, "d": 4} + merged2 = dict3 | dict4 + # Keys 'b' and 'c' from dict4 override those from dict3 + assert merged2 == {"a": 1, "b": 20, "c": 30, "d": 4} + + # The order matters! Left operand is the base, right operand overwrites + merged3 = dict4 | dict3 + # Now keys 'b' and 'c' from dict3 override those from dict4 + assert merged3 == {"b": 2, "c": 3, "d": 4, "a": 1} + + # The |= operator performs in-place union (augmented assignment) + # This is equivalent to dict.update() but with cleaner syntax + dict5 = {"a": 1, "b": 2} + dict6 = {"c": 3, "d": 4} + dict5 |= dict6 + # dict5 is modified in place + assert dict5 == {"a": 1, "b": 2, "c": 3, "d": 4} + # dict6 remains unchanged + assert dict6 == {"c": 3, "d": 4} + + # The |= operator also handles overlapping keys + dict7 = {"a": 1, "b": 2, "c": 3} + dict8 = {"b": 20, "d": 4} + dict7 |= dict8 + # 'b' from dict8 overwrites 'b' in dict7 + assert dict7 == {"a": 1, "b": 20, "c": 3, "d": 4} + + # You can chain multiple | operations + dict9 = {"a": 1} + dict10 = {"b": 2} + dict11 = {"c": 3} + dict12 = {"d": 4} + combined = dict9 | dict10 | dict11 | dict12 + assert combined == {"a": 1, "b": 2, "c": 3, "d": 4} + + # When chaining with overlapping keys, rightmost values win + dict13 = {"a": 1, "x": 10} + dict14 = {"b": 2, "x": 20} + dict15 = {"c": 3, "x": 30} + combined2 = dict13 | dict14 | dict15 + # 'x' ends up with value 30 from the rightmost dictionary + assert combined2 == {"a": 1, "b": 2, "c": 3, "x": 30} + + # The union operator works with empty dictionaries + empty: dict[str, int] = {} + dict16 = {"a": 1, "b": 2} + assert empty | dict16 == {"a": 1, "b": 2} + assert dict16 | empty == {"a": 1, "b": 2} + assert empty | empty == {} + + # The union operator can be used with dict() constructor results + dict17 = dict(a=1, b=2) + dict18 = dict(c=3, d=4) + merged4 = dict17 | dict18 + assert merged4 == {"a": 1, "b": 2, "c": 3, "d": 4} + + # You can mix different value types in merged dictionaries + dict19 = {"name": "Alice", "age": 30} + dict20 = {"city": "NYC", "scores": [85, 90, 95]} + dict21 = {"active": True} + person = dict19 | dict20 | dict21 + assert person == {"name": "Alice", "age": 30, "city": "NYC", "scores": [85, 90, 95], "active": True} + + # Practical use case: Configuration merging + # Start with default configuration + default_config = {"timeout": 30, "retries": 3, "debug": False, "log_level": "INFO"} + + # User provides custom configuration (partial) + user_config = {"timeout": 60, "debug": True} + + # Merge configurations, user settings override defaults + final_config = default_config | user_config + assert final_config == { + "timeout": 60, # Overridden by user + "retries": 3, # From default + "debug": True, # Overridden by user + "log_level": "INFO", # From default + } + + # Practical use case: Building objects incrementally + # Start with base attributes + base = {"id": 1, "type": "user"} + + # Add authentication info + with_auth = base | {"username": "john", "email": "john@example.com"} + + # Add profile info + with_profile = with_auth | {"bio": "Developer", "location": "USA"} + + assert with_profile == {"id": 1, "type": "user", "username": "john", "email": "john@example.com", "bio": "Developer", "location": "USA"} + + # Practical use case: Updating records with |= + user_record = {"id": 100, "name": "Jane", "status": "active", "login_count": 5} + + # Apply update from an external source + update = {"status": "inactive", "login_count": 6, "last_login": "2024-01-15"} + user_record |= update + + assert user_record == {"id": 100, "name": "Jane", "status": "inactive", "login_count": 6, "last_login": "2024-01-15"} + + # The union operators only work with dictionaries + # Attempting to use them with non-dict types raises TypeError + dict22 = {"a": 1} + error_raised = False + try: + # This will fail because list is not a dict + dict22 | [("b", 2)] # type: ignore [operator] + except TypeError: + error_raised = True + assert error_raised is True + + # However, you can use dict() to convert compatible types first + dict23 = {"a": 1} + dict24 = dict([("b", 2), ("c", 3)]) # Convert list of tuples to dict + merged5 = dict23 | dict24 + assert merged5 == {"a": 1, "b": 2, "c": 3} + + # Comparison with the older approaches shows the clarity improvement: + + # OLD: Using update() - modifies original, no expression result + old1 = {"a": 1} + old1.update({"b": 2}) + assert old1 == {"a": 1, "b": 2} + + # OLD: Using unpacking - verbose for multiple merges + old2 = {**{"a": 1}, **{"b": 2}, **{"c": 3}} + assert old2 == {"a": 1, "b": 2, "c": 3} + + # NEW: Using union operator - clean and chainable + new1 = {"a": 1} | {"b": 2} | {"c": 3} + assert new1 == {"a": 1, "b": 2, "c": 3} + + +if __name__ == "__main__": + main() diff --git a/ultimatepython/syntax/arg_enforcement.py b/ultimatepython/syntax/arg_enforcement.py new file mode 100644 index 0000000..65d5ca8 --- /dev/null +++ b/ultimatepython/syntax/arg_enforcement.py @@ -0,0 +1,243 @@ +""" +Positional-only and keyword-only parameters allow you to enforce how +arguments are passed to a function. This feature helps prevent misuse +of function APIs and makes code more maintainable. + +- Positional-only parameters (/) were introduced in Python 3.8 (PEP 570) +- Keyword-only parameters (*) were introduced in Python 3.0 (PEP 3102) + +These features give you fine-grained control over your function signatures. +""" + + +def traditional_function(a, b, c): + """A traditional function where all parameters can be passed either way. + + This function accepts arguments positionally or by keyword name. + While flexible, this can lead to API instability if parameter names change. + """ + return a + b + c + + +def positional_only(a, b, /): + """Function with positional-only parameters. + + The / symbol marks that parameters before it MUST be passed positionally. + This is useful when parameter names are not meaningful or when you want + to reserve the right to change parameter names without breaking callers. + + Parameters before / cannot be passed as keyword arguments. + """ + return a + b + + +def keyword_only(*, x, y): + """Function with keyword-only parameters. + + The * symbol marks that parameters after it MUST be passed by keyword. + This is useful for improving readability at call sites and preventing + accidental argument order mistakes. + + Parameters after * cannot be passed positionally. + """ + return x * y + + +def mixed_parameters(pos_only, /, pos_or_kw, *, kw_only): + """Function that combines all parameter types. + + - pos_only: Must be passed positionally (before /) + - pos_or_kw: Can be passed either way (between / and *) + - kw_only: Must be passed by keyword (after *) + + This gives maximum control over the function interface. + """ + return f"{pos_only}-{pos_or_kw}-{kw_only}" + + +def positional_with_defaults(a, b=10, /): + """Positional-only parameters can have default values. + + Default values work the same way as in traditional functions, + but the parameters still must be passed positionally if provided. + """ + return a + b + + +def keyword_with_defaults(*, x=5, y=3): + """Keyword-only parameters can have default values. + + When providing arguments, you must use the keyword names. + """ + return x**y + + +def complex_signature(a, b, /, c, d=10, *, e, f=20): + """A function demonstrating a complex but valid signature. + + - a, b: positional-only + - c: positional-or-keyword (no default) + - d: positional-or-keyword (with default) + - e: keyword-only (no default) + - f: keyword-only (with default) + """ + return a + b + c + d + e + f + + +def main() -> None: + # Traditional function: can be called either way + assert traditional_function(1, 2, 3) == 6 + assert traditional_function(a=1, b=2, c=3) == 6 + assert traditional_function(1, b=2, c=3) == 6 + + # Positional-only function: must use positional arguments + assert positional_only(5, 3) == 8 + + # Trying to use keyword arguments with positional-only parameters + # will raise a TypeError + positional_error = False + try: + # This will fail because 'a' and 'b' are positional-only + positional_only(a=5, b=3) # type: ignore [call-arg] + except TypeError: + positional_error = True + assert positional_error is True + + # You also can't mix positional and keyword for positional-only params + positional_error2 = False + try: + # This will fail because 'b' is positional-only + positional_only(5, b=3) # type: ignore [call-arg] + except TypeError: + positional_error2 = True + assert positional_error2 is True + + # Keyword-only function: must use keyword arguments + assert keyword_only(x=4, y=5) == 20 + + # Trying to use positional arguments with keyword-only parameters + # will raise a TypeError + keyword_error = False + try: + # This will fail because 'x' and 'y' are keyword-only + keyword_only(4, 5) # type: ignore [misc] + except TypeError: + keyword_error = True + assert keyword_error is True + + # Mixed parameters demonstrate all three types + result = mixed_parameters("first", "second", kw_only="third") + assert result == "first-second-third" + + # The middle parameter can be passed either way + result2 = mixed_parameters("first", pos_or_kw="second", kw_only="third") + assert result2 == "first-second-third" + + # But positional-only must be positional + mixed_error = False + try: + mixed_parameters(pos_only="first", pos_or_kw="second", kw_only="third") # type: ignore [call-arg] + except TypeError: + mixed_error = True + assert mixed_error is True + + # And keyword-only must be keyword + mixed_error2 = False + try: + mixed_parameters("first", "second", "third") # type: ignore [misc] + except TypeError: + mixed_error2 = True + assert mixed_error2 is True + + # Positional-only with defaults + assert positional_with_defaults(5) == 15 # Uses default b=10 + assert positional_with_defaults(5, 20) == 25 # Overrides b with 20 + + # Even with defaults, must use positional syntax + positional_default_error = False + try: + positional_with_defaults(a=5, b=20) # type: ignore [call-arg] + except TypeError: + positional_default_error = True + assert positional_default_error is True + + # Keyword-only with defaults + assert keyword_with_defaults() == 125 # Uses defaults: 5^3 + assert keyword_with_defaults(x=2) == 8 # 2^3 + assert keyword_with_defaults(y=2) == 25 # 5^2 + assert keyword_with_defaults(x=3, y=4) == 81 # 3^4 + + # Must still use keyword syntax even when providing defaults + keyword_default_error = False + try: + keyword_with_defaults(2, 3) # type: ignore [misc] + except TypeError: + keyword_default_error = True + assert keyword_default_error is True + + # Complex signature: demonstrating all parameter types + # Minimal call with required params only + result3 = complex_signature(1, 2, 3, e=4) + assert result3 == 40 # 1+2+3+10(default)+4+20(default) + + # Providing all parameters + result4 = complex_signature(1, 2, 3, 4, e=5, f=6) + assert result4 == 21 # 1+2+3+4+5+6 + + # Middle parameter 'c' can be passed by keyword + result5 = complex_signature(1, 2, c=3, e=4) + assert result5 == 40 + + # Parameter 'd' can also be passed by keyword + result6 = complex_signature(1, 2, 3, d=15, e=4) + assert result6 == 45 # 1+2+3+15+4+20 + + # But 'a' and 'b' must be positional + complex_error = False + try: + complex_signature(a=1, b=2, c=3, e=4) # type: ignore [call-arg] + except TypeError: + complex_error = True + assert complex_error is True + + # And 'e' must be keyword (even though 'f' has a default) + complex_error2 = False + try: + complex_signature(1, 2, 3, 10, 4) # type: ignore [misc] + except TypeError: + complex_error2 = True + assert complex_error2 is True + + # Practical use case: Positional-only is great for functions where + # parameter names are not meaningful or may change + def distance(x1, y1, x2, y2, /): + """Calculate distance between two points. + + The parameter names here are somewhat arbitrary (could be p1_x, etc.) + so we make them positional-only to give us flexibility to rename them + without breaking existing code. + """ + return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 + + assert abs(distance(0, 0, 3, 4) - 5.0) < 0.01 + + # Practical use case: Keyword-only is great for boolean flags or + # optional parameters where the intent should be clear at call site + def create_user(username, *, admin=False, active=True, send_email=False): + """Create a user with clear intent for optional parameters. + + Making admin, active, and send_email keyword-only ensures that + callers must specify exactly what they're setting, improving + readability and preventing accidental mistakes. + """ + return {"username": username, "admin": admin, "active": active, "send_email": send_email} + + # Clear intent at call site + user = create_user("john_doe", admin=True, send_email=True) + assert user["admin"] is True + assert user["active"] is True # Used default + assert user["send_email"] is True + + +if __name__ == "__main__": + main() diff --git a/ultimatepython/syntax/walrus_operator.py b/ultimatepython/syntax/walrus_operator.py new file mode 100644 index 0000000..733e3c0 --- /dev/null +++ b/ultimatepython/syntax/walrus_operator.py @@ -0,0 +1,121 @@ +""" +The walrus operator, also known as the assignment expression operator, +allows you to assign values to variables as part of an expression. This +feature was introduced in Python 3.8 through PEP 572. + +The walrus operator uses the := syntax and is particularly useful in +reducing redundancy when you need to compute a value and then use it +in a condition or comprehension. +""" + + +def main() -> None: + # Traditional approach: compute a value and check it separately + # Let's say we want to check if a string has more than 5 characters + text = "Hello, Python!" + length = len(text) + if length > 5: + assert length == 14 + + # With the walrus operator, we can assign and check in one expression. + # This is cleaner and avoids repeating the computation or storing + # intermediate variables unnecessarily + text2 = "Ultimate" + if (length2 := len(text2)) > 5: + # The walrus operator assigned len(text2) to length2 AND returned it + # for the comparison, all in the same line + assert length2 == 8 + + # The walrus operator is especially powerful in while loops. Here's a + # traditional approach that requires reading input twice + numbers_old = [] + n = int("5") # Simulating user input + while n != 0: + numbers_old.append(n) + n = int("0") # Simulating next input + assert numbers_old == [5] + + # With the walrus operator, we can assign and check in the loop condition. + # This is more concise and avoids the duplication of the input reading logic + numbers_new = [] + inputs = ["3", "7", "0"] # Simulating a sequence of inputs + index = 0 + + # Note: In a real scenario, you'd read from input() instead of a list + def get_next_input(): + nonlocal index + if index < len(inputs): + result = int(inputs[index]) + index += 1 + return result + return 0 + + while (num := get_next_input()) != 0: + # The walrus operator assigns get_next_input() to num and checks if it's not 0 + numbers_new.append(num) + assert numbers_new == [3, 7] + + # Ensure the function returns 0 when no more inputs + assert get_next_input() == 0 + + # The walrus operator shines in list comprehensions when you need to + # compute a value once and reuse it. Without walrus, you'd either: + # 1. Compute the value multiple times (inefficient) + # 2. Use a for loop instead of comprehension (less elegant) + data = ["apple", "banana", "cherry", "date"] + + # Traditional approach: computing len() twice per item is wasteful + long_words_old = [word for word in data if len(word) > 5 and len(word) < 7] + assert long_words_old == ["banana", "cherry"] + + # With walrus operator: compute len() once and reuse the result + # The walrus operator assigns len(word) to word_len, which we can + # then use multiple times in the same comprehension + long_words_new = [word for word in data if 5 < (word_len := len(word)) < 7] + assert long_words_new == ["banana", "cherry"] + + # The walrus operator can be used in comprehensions to filter and transform + # data simultaneously. Here we square numbers but only keep those where + # the square is less than 50 + numbers = [3, 5, 7, 9, 11] + + # Without walrus: we'd need to compute the square twice or use a for loop + squares_old = [n * n for n in numbers if n * n < 50] + assert squares_old == [9, 25, 49] + + # With walrus: compute the square once, store it, and use it + # This is both more efficient and clearer about the intent + squares_new = [square for n in numbers if (square := n * n) < 50] + assert squares_new == [9, 25, 49] + + # The walrus operator works in set comprehensions too + # Let's find unique word lengths greater than 4 + words = ["hi", "hello", "world", "python", "code"] + + # With walrus operator in a set comprehension + long_lengths = {word_len for word in words if (word_len := len(word)) > 4} + assert long_lengths == {5, 6} + + # And in dictionary comprehensions as well + # Create a dict of words to their lengths, but only for words > 4 chars + word_length_map = {word: word_len for word in words if (word_len := len(word)) > 4} + assert word_length_map == {"hello": 5, "world": 5, "python": 6} + + # Important note: The walrus operator creates variables in the enclosing scope. + # This means variables assigned with := inside a comprehension are accessible + # outside of it (unlike traditional loop variables in comprehensions) + [result for x in [1, 2, 3] if (result := x * 2) > 2] + # The variable 'result' is now accessible here, with its last assigned value + assert result == 6 + + # The walrus operator can be nested, though this can reduce readability + # Use nesting sparingly and only when it genuinely improves the code + values = [1, 2, 3, 4, 5] + if (total := sum(doubled := [x * 2 for x in values])) > 15: + # Here 'doubled' is assigned inside the expression for 'total' + assert doubled == [2, 4, 6, 8, 10] + assert total == 30 + + +if __name__ == "__main__": + main()