4

I have a set of filter objects, which inherit the properties of a Filter base class

class Filter():
    def __init__(self):
        self.filterList = []

    def __add__(self,f):
        self.filterList += f.filterList  

    def match(self, entry):
        for f in self.filterList:
            if not f(entry):
               return False
        return True

class thisFilter(Filter):
    def __init__(self, args):
        super().__init__()
        ....
        def thisFilterFunc(entry)->bool:
            return True

        self.filterList.append(thisFilterFunc)

This filter classes are used by various functions to filter entries

def myfunc(myfilter, arg1, ...):
    ...
    for entry in entries:
        if myfilter.match(entry):
            ... do something

Multiple filters can be added (logical and) by adding instances of these filters:

bigFilter = filter1 + filter2 + ...

This is all comming together quite well, but I would love to generalize this in a way to handle more complex logical constraints, e.g.

bigFilter = (filter1 and filter2) or (filter3 and not filter4)

It feels like this should somehow be possible with overwriting __bool__ of the class instead of using __add__ but the boolean value of the class is only known for a given entry and not during assemly of the filter.

Any ideas how to make this possible? Or is there maybe a more pythonic way do do this?

martineau
  • 119,623
  • 25
  • 170
  • 301
user_na
  • 2,154
  • 1
  • 16
  • 36
  • relevant: https://stackoverflow.com/a/2668697/671543 – njzk2 May 14 '21 at 22:00
  • 4
    You can't overload `and` and `or`; they are special language constructs (to allow short-circuiting), not operators. You can, however, overload `&` and `|`. – chepner May 14 '21 at 22:01
  • you may consider using the `+` and `*` notation to represent respectively `or` and `and`. I would return a new object from `__add__` representing the association of 2, and a different type from `__mul__` – njzk2 May 14 '21 at 22:03
  • (You *could* overload `__bool__`, but it wouldn't help; `and` and `or` will both return one of the original operands, not some combination of the two.) – chepner May 14 '21 at 22:03
  • 2
    (note that your `__add__` implementation is more a `__iadd__` as it modifies the instance) – njzk2 May 14 '21 at 22:04
  • @chepner overloading `&`and `|` looks promising. But this will not work together with `not`, right? – user_na May 14 '21 at 22:09
  • @njzk2 indeed, great catch! – user_na May 14 '21 at 22:09
  • 4
    `~` can be used for `not` – rwadman May 14 '21 at 22:11

2 Answers2

3

I would go for something like this:

class Filter:
  def __init__(self, filter: Callable[[Any], bool]):
    self.filter = filter

  def __add__(self, added: Filter):
    return OrFilter(self, added)

  def __mul__(self, mult: Filter):
    return AndFilter(self, mult)

  def __invert__(self):
    return Filter(lambda x: not self.filter(x))

  def __call__(self, entry):
    return self.filter(entry)

class AndFilter(Filter):
  def __init__(self, left: Filter, right: Filter):
    self.left = left
    self.right = right

  def __call__(self, entry):
    return self.left(entry) and self.right(entry)

class OrFilter(Filter):
  def __init__(self, left: Filter, right: Filter):
    self.left = left
    self.right = right

  def __call__(self, entry):
    return self.left(entry) or self.right(entry)

Then you can create filters, and use them as (filterA + ~filterB) * filterC

You'll probably want to replace that Any with a generic type, so that your filter knows what it's dealing with.

njzk2
  • 38,969
  • 7
  • 69
  • 107
  • 2
    This is a good approach, but I would personally not use `__call__` but a method like `.match()`. Also, consider using `|` (`__or__`) and `&`(`__and__`) instead of `__add__` and `__mul__` – juanpa.arrivillaga May 14 '21 at 22:17
  • @juanpa.arrivillaga works too. I learnt boolean algebra using + and . notations, so that's what I'm comfortable with. It's trivial enough to update to use the operators you prefer. (Although from the code's perspective, those operators being named or and and makes it a bit easier to read. – njzk2 May 14 '21 at 22:19
  • @njzk2 , @juanpa.arrivillaga thanks, I will give it a try using the answer with `|`and `&` as it feels more natural for my taste. – user_na May 14 '21 at 22:23
  • @njzk2 Is using `Filter`as type hint inside the definition of `Filter` possible? Up to now I always did something like `Filter_type = TypeVar('Filter_type', bound='Filter')` and then used `Filter_type` for type hints. – user_na May 14 '21 at 22:38
  • @user_na yes, it should just work on the latest versions of Python, otherwise, just use a string, i.e. `"Filter"`. Is not equivalent, actually – juanpa.arrivillaga May 14 '21 at 22:47
0

Thanks, @njzk2 for the solution. In my code I used | and &. To be backwards compatible I also kept the .match() instead of using __call__() and also added the __add__ again.

class Filter:
    def __init__(self, filter: Callable[[Any], bool]):
        self.filter = filter

    def __or__(self, ored: Filter):
        return OrFilter(self, ored)

    def __and__(self, anded: Filter):
        return AndFilter(self, anded)
    
    def __add__(self, added: Filter):
        # self as __and__
        return self.__and__(added)

    def __invert__(self):
        return Filter(lambda x: not self.filter(x))

    def match(self, entry):
        return self.filter(entry)

class AndFilter(Filter):
  def __init__(self, left: Filter, right: Filter):
    self.left = left
    self.right = right

  def filter(self, entry):
    return self.left.filter(entry) and self.right.filter(entry)

class OrFilter(Filter):
  def __init__(self, left: Filter, right: Filter):
    self.left = left
    self.right = right

  def filter(self, entry):
    return self.left.filter(entry) or self.right.filter(entry)

class MyFilter(Filter):
    def __init__(self, args):
        ...
        def ffunc(entry) -> bool:
            ...
        super().__init__(ffunc)
user_na
  • 2,154
  • 1
  • 16
  • 36