Usage
To use, import where
from predicate
.
from predicate import where
where
takes any number of keywork arguments as well as one reserved keyword argument that
is combinator=
which defaults to "and"
.
Each keyword argument (except combinator=
), generates a predicate function and they are combined
into a single predicate function based on the value of combinator=
.
To level set, predicate functions are functions that take a single argument and return True
or
False
. Their type signature looks like Callable[[Any], bool]
. These are useful for separating
a collection of objects or filtering. One of the main uses of predicates is in the builtin filter
function as well as many of the grouping slicing tools in the standard lib itertools
.
Inspiration
This project is heavily inspired by django queryset filters. These make it very easy to build complex SQL filters when working with the Django ORM. This project is an attempt to make the developer friendliness of the ORM filtering available to native python collections.
Basic Usage
The most simple usage of where
is for simple equality filtering of an attribute.
>>> from predicate import where
>>> is_18 = where(age=18)
>>> is_18({'age': 18})
True
>>> list(filter(is_18, [{'name': 'Joe', 'age': 18}, {'name': 'Bob', 'age': 19}]))
[{'name': 'Joe', 'age': 18}]
The equivalent code could be expressed with a lambda:
is_18 = lambda x: x.get('age', None) == 18
however in this case it may or may not be more readable.
However, one of the things the lambda cant handle that where
can is mixed dictionary and
objects.
>>> class Person:
... def __init__(self, name, age):
... self.name = name
... self.age = age
...
... def __repr__(self):
... return f"{self.name} ({self.age})"
>>> mixed_data = [
... {'name': 'Joe', 'age': 18},
... {'name': 'Bob', 'age': 19},
... Person('Jane', 18)
... ]
>>> list(filter(where(age=18), mixed_data))
[{'name': 'Joe', 'age': 18}, Jane (18)]
Compound Predicates
Going further into capabilities, we can pass multiple keyword arguments to where
to produce a compound predicate.
>>> from predicate import where
>>> is_18yo_male = where(age=18, gender='M')
>>> is_18yo_male({'age': 18, 'gender': 'F'})
False
Again this logic can be reproduced using a lambda:
is_18yo_male = lambda x: x.get('age', None) == 18 and x.get('gender', None) == 'F'
You can chain as many of them as you need, making the readablity increase with the more complex predicates.
Complex Combinators
By Default, where
uses combinator='and'
, which combines all of the predicates using and
. This means
by default, that all of the predicates need to be met for where to also return True
. However, there are
other combinators available:
comininator='and'
(default) - ReturnsTrue
only ifall
of the predicates areTrue
combinator='or'
- ReturnsTrue
ifany
of the predicates areTrue
combinator='none'
- ReturnsTrue
only ifnone
of the predicates areTrue
(all areFalse
)combinator='nand'
- Only ReturnsFalse
if all predicates areTrue
otherwise returnsTrue
. Useful for filtering out specific cases.
>>> from predicate import where
>>> is_18_or_is_female = where(age=18, gender='F', combinator='or')
>>> is_18_or_is_female({'age': 18, 'gender': 'F'})
True
>>> is_18_or_is_female({'age': 17, 'gender': 'M'})
False
>>> is_not_18_or_female = where(age=18, gender='F', combinator='none')
>>> is_not_18_or_female({'age': 18, 'gender': 'M'})
False
>>> is_not_18_or_female({'age': 18, 'gender': 'F'})
False
>>> is_not_18_or_female({'age': 17, 'gender': 'M'})
True
>>> is_not_18_and_female = where(age=18, gender='F', combinator='nand')
>>> is_not_18_and_female({'age': 17, 'gender': 'M'})
True
>>> is_not_18_and_female({'age': 18, 'gender': 'M'})
True
>>> is_not_18_and_female({'age': 18, 'gender': 'F'})
False
Nested Data
Getting beyond the simple use case, where
follows django queryset filters in digging into nested
data structures by allowing access to nested fields via __
.
>>> from predicate import where
>>> is_smith = where(name__last='Smith')
>>> is_smith({'name': {'first': 'John', 'last': 'Smith'}})
True
This nested operation digs through both objects and dictionaries. Missing keys will be returned as None
.
NonEquality Comparisons
Another idea borrowed from django querysets is to do filtering beyond simple equality. These non-equality comparators,
like in django querysets, are a suffix with __
. The avaliable comparators are:
__eq
(default) - Equals__gt
Greater than__gte
Greater than or equal to__lt
Less than__lte
Less than or equal to__ne
Not equal to__contains
Checks for membership usingin
. Can be uses on anything thatin
can be used with__startswith
Does the field start with the given string__endswith
Does the field end with the given string__exact
Same as equals but here for compatibility with django filters__iexact
Case insensitive string comparison__in
Does the field exist in a given collection.__type
Check if the value at a given position is a given type
>>> from predicate import where
>>> is_adult = where(age__gte=18)
>>> is_adult({'age': 17})
False
>>> is_adult({'age': 18})
True
>>> age_is_int = where(age__type=int)
>>> age_is_int({'age': 18.5})
False
Custom Comparisons
If the given set of comparators is not enough, there is a __func
, comparator that takes a predicate function that will
be applied at the given key.
>>> from predicate import where
>>> last_name_has_5_characters = where(name__last__func=lambda name: len(name) == 5)
>>> last_name_has_5_characters({'name': {'last': 'Smith'}})
True
>>> last_name_has_5_characters({'name': {'last': 'Bob'}})
False
Custom Combinators
Like with __func
, if the given combinators (and
, or
, none
) dont work for you, you can pass your own combinator function in
to the combinator=
argument. This function should take have the signature Callable[[Iterable[bool]], bool]
.
>>> from predicate import where
>>> only_one_true = lambda xs: sum(xs) == 1
>>> is_eligible = where(age=18, name__first__startswith='K', gender='F', combinator=only_one_true)
>>> is_eligible({'age': 18, 'name': {'first': 'John', 'last': 'Smith'}, 'gender': 'M'})
True
>>> is_eligible({'age': 18, 'name': {'first': 'John', 'last': 'Smith'}, 'gender': 'F'})
False