6.11. OOP Pattern Matching

6.11.1. Problem

>>> language = 'English'
>>>
>>> if language == 'Polish':
...     result = 'Cześć'
... elif language == 'English':
...     result = 'Hello'
... elif language == 'German':
...     result = 'Guten Tag'
... elif language == 'Spanish':
...     result = 'Buenos Días'
... elif language == 'Chinese':
...     result = '你好'
... elif language == 'French':
...     result = 'Bonjour'
... else:
...     result = 'Unknown language'
>>>
>>> print(result)
Hello

6.11.2. Switch

In other languages you may find switch statement: (note that this is not a valid Python code)

>>> language = 'English'
>>>
>>> switch(language):  
...     case 'Polish': result = 'Cześć'
...     case 'English': result = 'Hello'
...     case 'German': result = 'Guten Tag'
...     case 'Spanish': result = 'Buenos Días'
...     case 'Chinese': result = '你好'
...     case 'French': result = 'Bonjour'
...     default: result = 'Unknown language'
>>>
>>> print(result)  
Hello

6.11.3. Pattern Matching

  • Since Python 3.10: PEP 636 -- Structural Pattern Matching: Tutorial

>>> language = 'English'
>>>
>>> match language:  
...     case 'Polish': result = 'Cześć'
...     case 'English': result = 'Hello'
...     case 'German': result = 'Guten Tag'
...     case 'Spanish': result = 'Buenos Días'
...     case 'Chinese': result = '你好'
...     case 'French': result = 'Bonjour'
...     case _: result = 'Unknown language'
>>>
>>> print(result)
Hello

6.11.4. Behavior

Table 6.4. Pattern Matching 1

Statement

Meaning

x

assign x = subject

'x'

test subject == 'x'

x.y

test subject == x.y

x()

test isinstance(subject, x)

{'x': 'y'}

test isinstance(subject, Mapping) and subject.get('x') == 'y'

['x']

test isinstance(subject, Sequence) and len(subject) == 1 and subject[0] == 'x'

6.11.5. Matching Alternatives

>>> status = 418
>>>
>>>
>>> match status:  
...     case 400:
...         result = 'Bad request'
...     case 401 | 403 | 405:
...         result = 'Not allowed'
...     case 404:
...         result = 'Not found'
...     case 418:
...         result = "I'm a teapot"
...     case _:
...         result = 'Unexpected status'

6.11.6. Matching on Sequence

>>> request = 'GET /index.html HTTP/2.0'
>>>
>>>
>>> match request.split():  
...     case ['GET', uri, version]:
...         server.get(uri)
...     case ['POST', uri, version]:
...         server.post(uri)
...     case ['PUT', uri, version]:
...         server.put(uri)
...     case ['DELETE', uri, version]:
...         server.delete(uri)

6.11.7. Matching on Sequence with Assignment

>>> class Hero:
...     def action(self):
...         return  ['move', 'left', 20]
>>>
>>>
>>> match hero.action():  
...     case ['move', ('up'|'down'|'left'|'right') as direction, value]:
...         hero.move(direction, value)
...     case ['make_damage', value]:
...         hero.make_damage(value)
...     case ['take_damage', value]:
...         hero.take_damage(value)

6.11.8. Matching on Enum

>>> from enum import Enum
>>>
>>>
>>> class Key(Enum):
...     ESC = 27
...     ARROW_LEFT = 37
...     ARROW_UP = 38
...     ARROW_RIGHT = 39
...     ARROW_DOWN = 40
>>>
>>>
>>> match keyboard.on_key_press():  
...     case Key.ESC:
...         game.quit()
...     case Key.ARROW_LEFT:
...         hero.move_left()
...     case Key.ARROW_UP:
...         hero.move_up()
...     case Key.ARROW_RIGHT:
...         hero.move_right()
...     case Key.ARROW_DOWN:
...         hero.move_down()
...     case _:
...         raise ValueError(f'Unrecognized key')

6.11.9. References

1

Raymond Hettinger. Retrieved: 2021-03-07. URL: https://twitter.com/raymondh/status/1361780586570948609?s=20

6.11.10. Assignments

Code 6.49. Solution
"""
* Assignment: Idioms Match Range
* Complexity: medium
* Lines of code: 25 lines
* Time: 13 min

English:
    1. Write own implementation of a built-in function `range()`
    2. Note, that function does not take any keyword arguments
    3. How to implement passing only stop argument
       `myrange(start=0, stop=???, step=1)`?
    4. Run doctests - all must succeed

Polish:
    1. Zaimplementuj własne rozwiązanie wbudowanej funkcji `range()`
    2. Zauważ, że funkcja nie przyjmuje żanych argumentów nazwanych (keyword)
    3. Jak zaimplementować możliwość podawania tylko końca
       `myrange(start=0, stop=???, step=1)`?
    4. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * https://github.com/python/cpython/blob/main/Objects/rangeobject.c#LC75
    * `raise TypeError('error message')`
    * use `*args` and `**kwargs`
    * `if len(args) == ...`
    * `match len(args)`

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isfunction

    >>> assert isfunction(myrange)

    >>> myrange(0, 10, 2)
    [0, 2, 4, 6, 8]

    >>> myrange(0, 5)
    [0, 1, 2, 3, 4]

    >>> myrange(5)
    [0, 1, 2, 3, 4]

    >>> myrange()
    Traceback (most recent call last):
    TypeError: myrange expected at least 1 argument, got 0

    >>> myrange(1,2,3,4)
    Traceback (most recent call last):
    TypeError: myrange expected at most 3 arguments, got 4

    >>> myrange(stop=2)
    Traceback (most recent call last):
    TypeError: myrange() takes no keyword arguments

    >>> myrange(start=1, stop=2)
    Traceback (most recent call last):
    TypeError: myrange() takes no keyword arguments

    >>> myrange(start=1, stop=2, step=2)
    Traceback (most recent call last):
    TypeError: myrange() takes no keyword arguments
"""


# myrange(start=0, stop=???, step=1)
# note, function does not take keyword arguments
# type: Callable[[int,int,int], list[int]]
def myrange():
    current = start
    result = []

    while current < stop:
        result.append(current)
        current += step

    return result