Last updated:
0 purchases
pampy 0.3.0
Pampy: Pattern Matching for Python
Pampy is pretty small (150 lines), reasonably fast, and often makes your code more readable
and hence easier to reason about. There is also a JavaScript version, called Pampy.js.
You can write many patterns
Patterns are evaluated in the order they appear.
You can write Fibonacci
The operator _ means "any other case I didn't think of".
from pampy import match, _
def fibonacci(n):
return match(n,
1, 1,
2, 1,
_, lambda x: fibonacci(x-1) + fibonacci(x-2)
)
You can write a Lisp calculator in 5 lines
from pampy import match, REST, _
def lisp(exp):
return match(exp,
int, lambda x: x,
callable, lambda x: x,
(callable, REST), lambda f, rest: f(*map(lisp, rest)),
tuple, lambda t: list(map(lisp, t)),
)
plus = lambda a, b: a + b
minus = lambda a, b: a - b
from functools import reduce
lisp((plus, 1, 2)) # => 3
lisp((plus, 1, (minus, 4, 2))) # => 3
lisp((reduce, plus, (range, 10))) # => 45
You can match so many things!
match(x,
3, "this matches the number 3",
int, "matches any integer",
(str, int), lambda a, b: "a tuple (a, b) you can use in a function",
[1, 2, _], "any list of 3 elements that begins with [1, 2]",
{'x': _}, "any dict with a key 'x' and any value associated",
_, "anything else"
)
You can match [HEAD, TAIL]
from pampy import match, HEAD, TAIL, _
x = [1, 2, 3]
match(x, [1, TAIL], lambda t: t) # => [2, 3]
match(x, [HEAD, TAIL], lambda h, t: (h, t)) # => (1, [2, 3])
TAIL and REST actually mean the same thing.
You can nest lists and tuples
from pampy import match, _
x = [1, [2, 3], 4]
match(x, [1, [_, 3], _], lambda a, b: [1, [a, 3], b]) # => [1, [2, 3], 4]
You can nest dicts. And you can use _ as key!
pet = { 'type': 'dog', 'details': { 'age': 3 } }
match(pet, { 'details': { 'age': _ } }, lambda age: age) # => 3
match(pet, { _ : { 'age': _ } }, lambda a, b: (a, b)) # => ('details', 3)
It feels like putting multiple _ inside dicts shouldn't work. Isn't ordering in dicts not guaranteed ?
But it does because
in Python 3.7, dict maintains insertion key order by default
You can match class hierarchies
class Pet: pass
class Dog(Pet): pass
class Cat(Pet): pass
class Hamster(Pet): pass
def what_is(x):
return match(x,
Dog, 'dog',
Cat, 'cat',
Pet, 'any other pet',
_, 'this is not a pet at all',
)
what_is(Cat()) # => 'cat'
what_is(Dog()) # => 'dog'
what_is(Hamster()) # => 'any other pet'
what_is(Pet()) # => 'any other pet'
what_is(42) # => 'this is not a pet at all'
Using Dataclasses
Pampy supports Python 3.7 dataclasses. You can pass the operator _ as arguments and it will match those fields.
@dataclass
class Pet:
name: str
age: int
pet = Pet('rover', 7)
match(pet, Pet('rover', _), lambda age: age) # => 7
match(pet, Pet(_, 7), lambda name: name) # => 'rover'
match(pet, Pet(_, _), lambda name, age: (name, age)) # => ('rover', 7)
Using typing
Pampy supports typing annotations.
class Pet: pass
class Dog(Pet): pass
class Cat(Pet): pass
class Hamster(Pet): pass
timestamp = NewType("year", Union[int, float])
def annotated(a: Tuple[int, float], b: str, c: E) -> timestamp:
pass
match((1, 2), Tuple[int, int], lambda a, b: (a, b)) # => (1, 2)
match(1, Union[str, int], lambda x: x) # => 1
match('a', Union[str, int], lambda x: x) # => 'a'
match('a', Optional[str], lambda x: x) # => 'a'
match(None, Optional[str], lambda x: x) # => None
match(Pet, Type[Pet], lambda x: x) # => Pet
match(Cat, Type[Pet], lambda x: x) # => Cat
match(Dog, Any, lambda x: x) # => Dog
match(Dog, Type[Any], lambda x: x) # => Dog
match(15, timestamp, lambda x: x) # => 15
match(10.0, timestamp, lambda x: x) # => 10.0
match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3]
match({'a': 1, 'b': 2}, Dict[str, int], lambda x: x) # => {'a': 1, 'b': 2}
match(annotated,
Callable[[Tuple[int, float], str, Pet], timestamp], lambda x: x
) # => annotated
For iterable generics actual type of value is guessed based on the first element.
match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3]
match([1, "b", "a"], List[int], lambda x: x) # => [1, "b", "a"]
match(["a", "b", "c"], List[int], lambda x: x) # raises MatchError
match(["a", "b", "c"], List[Union[str, int]], lambda x: x) # ["a", "b", "c"]
match({"a": 1, "b": 2}, Dict[str, int], lambda x: x) # {"a": 1, "b": 2}
match({"a": 1, "b": "dog"}, Dict[str, int], lambda x: x) # {"a": 1, "b": "dog"}
match({"a": 1, 1: 2}, Dict[str, int], lambda x: x) # {"a": 1, 1: 2}
match({2: 1, 1: 2}, Dict[str, int], lambda x: x) # raises MatchError
match({2: 1, 1: 2}, Dict[Union[str, int], int], lambda x: x) # {2: 1, 1: 2}
Iterable generics also match with any of their subtypes.
match([1, 2, 3], Iterable[int], lambda x: x) # => [1, 2, 3]
match({1, 2, 3}, Iterable[int], lambda x: x) # => {1, 2, 3}
match(range(10), Iterable[int], lambda x: x) # => range(10)
match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3]
match({1, 2, 3}, List[int], lambda x: x) # => raises MatchError
match(range(10), List[int], lambda x: x) # => raises MatchError
match([1, 2, 3], Set[int], lambda x: x) # => raises MatchError
match({1, 2, 3}, Set[int], lambda x: x) # => {1, 2, 3}
match(range(10), Set[int], lambda x: x) # => raises MatchError
For Callable any arg without annotation treated as Any.
def annotated(a: int, b: int) -> float:
pass
def not_annotated(a, b):
pass
def partially_annotated(a, b: float):
pass
match(annotated, Callable[[int, int], float], lambda x: x) # => annotated
match(not_annotated, Callable[[int, int], float], lambda x: x) # => raises MatchError
match(not_annotated, Callable[[Any, Any], Any], lambda x: x) # => not_annotated
match(annotated, Callable[[Any, Any], Any], lambda x: x) # => raises MatchError
match(partially_annotated,
Callable[[Any, float], Any], lambda x: x
) # => partially_annotated
TypeVar is not supported.
All the things you can match
As Pattern you can use any Python type, any class, or any Python value.
The operator _ and built-in types like int or str, extract variables that are passed to functions.
Types and Classes are matched via instanceof(value, pattern).
Iterable Patterns match recursively through all their elements. The same goes for dictionaries.
Pattern Example
What it means
Matched Example
Arguments Passed to function
NOT Matched Example
"hello"
only the string "hello" matches
"hello"
nothing
any other value
None
only None
None
nothing
any other value
int
Any integer
42
42
any other value
float
Any float number
2.35
2.35
any other value
str
Any string
"hello"
"hello"
any other value
tuple
Any tuple
(1, 2)
(1, 2)
any other value
list
Any list
[1, 2]
[1, 2]
any other value
MyClass
Any instance of MyClass. And any object that extends MyClass.
MyClass()
that instance
any other object
_
Any object (even None)
that value
ANY
The same as _
that value
(int, int)
A tuple made of any two integers
(1, 2)
1 and 2
(True, False)
[1, 2, _]
A list that starts with 1, 2 and ends with any value
[1, 2, 3]
3
[1, 2, 3, 4]
[1, 2, TAIL]
A list that start with 1, 2 and ends with any sequence
[1, 2, 3, 4]
[3, 4]
[1, 7, 7, 7]
{'type':'dog', age: _ }
Any dict with type: "dog" and with an age
{"type":"dog", "age": 3}
3
{"type":"cat", "age":2}
{'type':'dog', age: int }
Any dict with type: "dog" and with an int age
{"type":"dog", "age": 3}
3
{"type":"dog", "age":2.3}
re.compile('(\w+)-(\w+)-cat$')
Any string that matches that regular expression expr
"my-fuffy-cat"
"my" and "puffy"
"fuffy-dog"
Pet(name=_, age=7)
Any Pet dataclass with age == 7
Pet('rover', 7)
['rover']
Pet('rover', 8)
Any
The same as _
that value
Union[int, float, None]
Any integer or float number or None
2.35
2.35
any other value
Optional[int]
The same as Union[int, None]
2
2
any other value
Type[MyClass]
Any subclass of MyClass. And any class that extends MyClass.
MyClass
that class
any other object
Callable[[int], float]
Any callable with exactly that signature
def a(q:int) -> float: ...
that function
def a(q) -> float: ...
Tuple[MyClass, int, float]
The same as (MyClass, int, float)
Mapping[str, int] Any subtype of Mapping acceptable too
any mapping or subtype of mapping with string keys and integer values
{'a': 2, 'b': 3}
that dict
{'a': 'b', 'b': 'c'}
Iterable[int] Any subtype of Iterable acceptable too
any iterable or subtype of iterable with integer values
range(10) and [1, 2, 3]
that iterable
['a', 'b', 'v']
Using default
By default match() is strict. If no pattern matches, it raises a MatchError.
You can instead provide a fallback value using default to be used when nothing matches.
>>> match([1, 2], [1, 2, 3], "whatever")
MatchError: '_' not provided. This case is not handled: [1, 2]
>>> match([1, 2], [1, 2, 3], "whatever", default=False)
False
Using Regular Expressions
Pampy supports Python's Regex. You can pass a compiled regex as pattern, and Pampy is going to run patter.search(), and then pass to the action function the result of .groups().
def what_is(pet):
return match(pet,
re.compile('(\w+)-(\w+)-cat$'), lambda name, my: 'cat '+name,
re.compile('(\w+)-(\w+)-dog$'), lambda name, my: 'dog '+name,
_, "something else"
)
what_is('fuffy-my-dog') # => 'dog fuffy'
what_is('puffy-her-dog') # => 'dog puffy'
what_is('carla-your-cat') # => 'cat carla'
what_is('roger-my-hamster') # => 'something else'
Install for Python3
Pampy works in Python >= 3.6 Because dict matching can work only in the latest Pythons.
To install it:
$ pip install pampy
or
$ pip3 install pampy
If you really must use Python2
Pampy is Python3-first, but you can use most of its features in Python2 via this backport by Manuel Barkhau:
pip install backports.pampy
from backports.pampy import match, HEAD, TAIL, _
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.