Cooperative class tree filtering

Alberto Berti alberto at metapensiero.it
Thu Oct 12 19:07:38 EDT 2017


Hello,

suppose you have you have three classes A, B, C where C is a subclass of
B and B of A. They provide a method with signature `filter(element)`. It is
called by an external function that collects the elements and then to an
instance of the classes A, B and C is given a chance to filter these
elements, one at a time. So the `filter(element)` method returns `True`  if
the element has to be included in the final set and `False` if it
doesn't, easy peasy.
(This is a simplified example of a real use case).

What I want to have is that if an instance of class C has doesn't know
what to return about an element, it calls `super()` to return the value
from the same method implemented in its superclass, and so on up on the
tree. To show it in code:

  class A:
      def filter(self, element):
          # the default implementation always returns True
          return True


  class B(A):
      def filter(self, element):
          if element == 'foo':
              return True
          elif element == 'bar':
              return False
          else:
              return super().filter(element)


  class C(B):
      def filter(self, element):
          if element == 'bar':
              return True
          else:
              return super().filter(element)


  def collect_elements(instance):
      "A semplified element collect function"
      all_elts = {'foo', 'bar', 'baz', 'zoo'}
      filtered_elts = set(el for el in all_elts if instance.filter(el))
      return filtered_elts

  b = B()
  c = C()

  print(collect_elements(b))
  print(collect_elements(c))

which run gives the following result:

  >>> {'foo', 'zoo', 'baz'}
  {'bar', 'foo', 'zoo', 'baz'}


Now, what i ideally want is to get rid of that super() call at the end of
the methods in classes B and c and to code that "fallback to what my
superclass says" coded into A's implementation, where it kicks in if the
method in the target instance returns None. So to write it in code, I
would like some like:


  class A:
      def _filter_impl(self, element):
          # the default implementation always returns True
          return True

      def filter(self, element):
          # here a logic that if self._filter_impl(element) returns
          # None, it defaults to super(type(self), self)._filter_impl(element)
          # until A._filter_impl() is reached
          pass


  class B(A):
      def _filter_impl(self, element):
          if element == 'foo':
              return True
          elif element == 'bar':
              return False


  class C(B):
      def filter(self, element):
          if element == 'bar':
              return True


I've tried some variants of the 'super()' trick, that sometimes seems to
change behavior if it's written like that or like super(type(self),
self) in no clear (to me, and I failed to find extensive doc on
super()'s behavior) way, with things that stop working if mixins are
involved (even if the mixins do not reimplement the methods involved
here). Eventually i ended implementing a decorator:

  from functools import partial, wraps


  class delegate_super:
      """A method decorator that delegates part of the computation to the same
      method on the ancestor class."""

      _name = None
      _owner = None

      def __init__(self, meth):
          self._meth = meth
          @wraps(meth)
          def wrapper(*args, **kwargs):
              return self.delegator(*args, **kwargs)
          self.wrapper = wrapper

      def __get__(self, instance, owner):
          return partial(self.wrapper, instance)

      def __set_name__(self, owner, name):
          self._owner = owner
          self._name = name

      def delegator(self, instance, *args, **kwargs):
          result = self._meth(instance, *args, **kwargs)
          if result is None:
              result = getattr(super(self._owner, instance), self._name)(
                  *args, **kwargs)
          return result

  class A:
      def filter(self, element):
          # the default implementation always returns True
          return True


  class B(A):
      @delegate_super
      def filter(self, element):
          if element == 'foo':
              return True
          elif element == 'bar':
              return False


  class C(B):
      @delegate_super
      def filter(self, element):
          if element == 'bar':
              return True
          else:
              return super().filter(element)


  def collect_elements(instance):
      "A semplified element collect function"
      all_elts = {'foo', 'bar', 'baz', 'zoo'}
      filtered_elts = set(el for el in all_elts if instance.filter(el))
      return filtered_elts

  b = B()
  c = C()

  print(collect_elements(b))
  print(collect_elements(c))

which has the same result as before:

  >>> {'foo', 'zoo', 'baz'}
  {'bar', 'foo', 'zoo', 'baz'}


I would really like to find a way to do this that doesn't involve
decorating the methods in A subclasses to free the final developer to
remember to import the decorator and apply it, just like I don't want
him (probably me six months from now) to have to remember to add an
`else: super()...` to its computation...

Has anyone done this before and has any suggestion about a better way to
do it? Am I getting it wrong?

cheers,

Alberto




More information about the Python-list mailing list