[Python-ideas] generic Liftable abc-mixin breaks at MRO
Stephan Sahm
Stephan.Sahm at gmx.de
Fri Dec 11 17:15:40 EST 2015
I now created both, and in fact the code for Liftable-signal seems much
cleaner
Nevertheless, the code is rather long for this thread, but it might be
useful for someone
import abc
import inspect
from contextlib import contextmanager
def use_as_needed(func, kwargs):
meta = inspect.getargspec(func)
if meta.keywords is not None:
return func(**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 NotLiftable(RuntimeError):
pass
@contextmanager
def super_liftable(cls, self):
""" this is kind of a hack to replace super.super, however I haven't
found any other nice way to do it """
if cls is object:
raise NotLiftable()
liftables = [l for l in cls.__bases__ if type(l).__name__ == "Liftable"]
if not liftables:
raise NotLiftable()
orig_class = self.__class__
self.__class__ = liftables[0]
yield self
self.__class__ = orig_class
def LiftableFrom(base_cls_name):
class Liftable(type):
def __init__(cls, name, bases, dct):
# for base_cls nothing should be done, as this is the one to
refer to by Lifting
if not cls.__name__ == base_cls_name:
if "__init__" in dct:
raise TypeError("Descendents of Liftable are not
allowed to have own __init__ method. Instead overwrite __initialize__")
def lifted__init__(self, **kwargs):
with super_liftable(cls, self) as s:
use_as_needed(s.__init__, kwargs)
if hasattr(self, "__initialize__"):
use_as_needed(self.__initialize__, kwargs)
cls.__init__ = lifted__init__
#setattr(cls, "__init__", lifted__init__)
super(Liftable, cls).__init__(name, bases, dct)
Liftable.base_cls_name = base_cls_name
#Liftable.__name__ = "LiftableFrom" + base_cls_name # to show that
this is possible
return Liftable
def lift(self, new_class, **kwargs): #TODO adapt to work with both
definitions above
# Stop Conditions:
if self.__class__ is new_class:
return # nothing to do
elif new_class is object: # Base Case
# break recursion at once:
raise NotLiftable()
ls = [l for l in new_class.__bases__ if type(l).__name__ == "Liftable"]
if not ls:
raise NotLiftable()
# recursive case:
if not self.__class__ is ls[0]: # it would also be possible to use tree
like left-first-search here
lift(self, ls[0], **kwargs)
# own case:
self.__class__ = new_class
use_as_needed(self.__initialize__, kwargs)
The least beautiful thing needed is too give the name of the base-class (in
this case "A") as an additional parameter for the lift-meta-class. I
haven't found a way to access A.__name__ directly or even better
automatically get the class where this meta-class was originally inserted.
A second point is that I had to use an own version of super(), however this
works like a charm as far as I can see.
The Metaclass ensures that any child must not have a __init__ but instead
can only use __initialize__ like a replacement. Here an example which works:
class A(object):
__metaclass__ = LiftableFrom("A")
def __init__(self, a):
self.a = a
class B(A):
def __initialize__(self, b):
print "initialize b"
self.b = b
class C(B):
def __initialize__(self, c):
print "initialize c"
self.c = c
a = A(a=1)
a.a
# 1
lift(a, C, b=2, c=3)
print type(a)
print a.a, a.b, a.c
#initialize b
#initialize c
#<class '__main__.C'>
#1 2 3
cheers,
Stephan
On 10 December 2015 at 22:05, Stephan Sahm <Stephan.Sahm at gmx.de> wrote:
> Dear Andrew,
>
> thank you very much for this impressively constructive response. It is for
> sure more constructive than I can react on now.
>
> For the concrete usecase, the Liftable-signal in might be the most
> interesting option, as then already the base class can inherit the
> Liftable-signal and can itself already use lift.
> However, I cannot see how to make the __init__ method conform in this
> setting, but by inidividual implementations (I in fact thought that
> enforcing it by the mixin makes things safer, and it of course should
> reduce boilerplate code)
>
> The Liftable(T) in fact seems also great, as I cannot see how to avoid
> this lifting from a false class in the Liftable-signal chain. I only want
> to Lift from one class at the moment, so this is in fact kind of what I was
> after. The rough outline would look like
>
> class B(Lift(A)):
> pass
> class C(Lift(B)):
> pass
>
>
> which seems rather beautiful to read - thank you very much for pointing
> this out.
>
> If you have an idea how to automatically create the right __init__ method
> when using the Liftable-signal-chain, I would highly welcome it.
>
> I myself need to recap some metaclass basics again before seriously
> tackling this.
> Best,
> Stephan
>
>
>
> On 10 December 2015 at 19:38, Andrew Barnert <abarnert at yahoo.com> wrote:
>
>> 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/20151211/beec7c28/attachment-0001.html>
More information about the Python-ideas
mailing list