[Python-ideas] generic Liftable abc-mixin breaks at MRO

Andrew Barnert abarnert at yahoo.com
Thu Dec 10 13:38:42 EST 2015


On Dec 10, 2015, at 00:58, Stephan Sahm <Stephan.Sahm at gmx.de> wrote:
> 
> Dear all,
> 
> I think I found a crucial usecase where the standard MRO does not work out. I would appreciate your help to still solve this usecase, or might MRO even be adapted?

First, do you have an actual use case for this? And are you really looking to suggest changes for Python 3.6, or looking for help with using Python 2.7 as-is?

Anyway, I think the first problem here is that you're trying to put the same class, Liftable, on the MRO twice. That doesn't make sense--the whole point of superclass linearization is that each class only appears once in the list.

If you weren't using a metaclass, you wouldn't see this error--but then you'd just get the more subtle problem that C can't be lifted from A to B because Liftable isn't getting called there.

If you made Liftable a class factory, so two calls to Liftable() returned different class objects, then you might be able to make this work. (I suppose you could hide that from the user by giving Liftable a custom metaclass that constructs new class objects for each copy of Liftable in the bases list before calling through to type, but that seems like magic you really don't want to hide if you want anyone to be able to debut this code.)

In fact, you could even make it Liftable(T), which makes your type Liftable from T, rather than from whatever class happens to come after you on the MRO chain. (Think about how this would work with other mixins, or pure-interface ABCs, or full multiple inheritance--you may end up declaring that C can be lifted from Sequence rather than B, which is nonsense, and which will be hard to debug if you don't understand the C3 algorithm.)

Or, if you actually _want_ to be liftable from whatever happens to come next, then isn't liftability a property of the entire tree of classes, not of individual classes in that tree, so you should only be specifying Liftable once (either at A, or at B) in the hierarchy in the first place? From what I can tell, the only benefit you get from installing it twice is tricking the ABCMeta machinery into enforcing that all classes implement _initialize_ instead of just enforcing that one does; the easy solution there is to just write your own metaclass that does that check directly.

Or maybe, instead of enforcing it, use it as a signal: build a "lift chain" for each Liftable type out of all classes on the MRO that directly implement _initialize_ (or just dynamically look for it as you walk the MRO in lift). So lift only works between those classes. I think that gets you all the same benefits as Liftable(T), without needing a class factory, and without having to specify it more than once on a hierarchy.

> The idea is to build a generic Lift-type which I call this way because the derived classes should be able to easily lift from subclasses. So for example if I have an instance a from class A and a class B(A) I want to make a an instance of B in a straightforward way.
> 
> My implementation (Python 2.7):
> 
> import abc
> import inspect
> 
> def use_as_needed(func, kwargs):
>     meta = inspect.getargspec(func)
>     if meta.keywords is not None:
>         return meta(**kwargs)
>     else:
>         # not generic super-constructor - pick only the relevant subentries:
>         return func(**{k:kwargs[k] for k in kwargs if k in meta.args})
> 
> class Liftable(object):
>     __metaclass__ = abc.ABCMeta
>     
>     def __init__(self, **kwargs):
>         use_as_needed(super(Liftable,self).__init__, kwargs)
>         use_as_needed(self.__initialize__, kwargs)
>         
>     @abc.abstractmethod
>     def __initialize__(self, **kwargs):
>         return NotImplemented()
> 
> class NoMatchingAncestor(RuntimeError):
>     pass
> 
> class NotLiftable(RuntimeError):
>     pass
> 
> def lift(self, new_class, **kwargs):
>     # Stop Conditions:
>     if self.__class__ is new_class:
>         return # nothing to do
>     elif new_class is object: # Base Case
>         # break recursion at once:
>         raise NoMatchingAncestor()
>     elif new_class.__base__ is not Liftable: #to ensure this is save
>         raise NotLiftable("Class {} is not Liftable (must be first parent)".format(new_class.__name__))
>     
>     # recursive case:
>     if not self.__class__ is new_class.__bases__[1]:    
>         lift(self, new_class.__bases__[1], **kwargs)
>     # own case:
>     self.__class__ = new_class
>     use_as_needed(self.__initialize__, kwargs)
> 
> and the example usecase:
> class A(object):
>     def __init__(self, a):
>         self.a = a
>     
> class B(Liftable, A):
>     def __initialize__(self, b):
>         self.b = b
> 
> a = A(1)
> print a.a, a.__class__
> # 1 <class '__main__.A'>
> 
> lift(a, B, b=2)
> print a.a, a.b, a.__class__
> # 1 2 <class '__main__.B'>
> 
> this works so far, however if I now put a further level of Liftable (which in principal already works with the generic definition
> class C(Liftable, B):
>     def __initialize__(self, c):
>         self.c = c
> 
> I get the error
> TypeError: Error when calling the metaclass bases Cannot create a consistent method resolution order (MRO) for bases Liftable, B
> 
> ​I read about MRO, and it seems to be the case that this setting somehow raises this generic Error, however I really think having such a Lifting is save and extremely useful​ - how can I make it work in python?
> 
> (one further comment: switching the order of inheritance, i.e. class B(A, Liftable) will call A.__init__ before Liftable.__init__ which makes the whole idea senseless)
> 
> Any constructive help is appreciated!
> best,
> Stephan
> 
> 
> 
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20151210/84e06414/attachment-0001.html>


More information about the Python-ideas mailing list