ABC validation strictly on an instance

Eric Snow ericsnowcurrently at gmail.com
Fri May 20 21:13:45 CEST 2011


The current ABC implementation in Python implies that the class of a
conformant instance complies with the ABC.  The implication does not carry
down to the compliance of the instance itself.

This means that if you inherit from an ABC that has an abstract property,
your subclass must have a matching name to that property, or you will get a
TypeError.  (Same goes for abstract methods--a matching name must be bound,
even if not to a function).  For example:

class X(metaclass=ABCMeta):
    @abstractproperty
    def id(self): pass

class Y(X):
    id = 1

class Z(X):
    def __init__(self, id):
        self._id = id
    @property
    def id(self):
        return self._id

class Fail(X):
    def __init__(self, id):
        self.id = id

So classes Y and Z will work fine, but class Fail will raise a TypeError
when you instantiate [1] Fail, even though it "implemented" id in the
instance __init__ [2].  I looked at this all yesterday and did not see a
great way to approach this.  The best I could come up with was the
following:

class X(metaclass=ABCMeta):
    @abstractproperty
    def id(self): pass

@X.register
class Y:
    def __init__(self, id):
        self.id = id

So this is a promise that Y comforms to X without any of the automatic
validation.  However, you don't get _any_ validation.  You also lose any
otherwise inherited features, so it is more like an interface than an
abstract class.  I am not so sure about that above solution because it seems
like such a loose constraint.  I discussed the validation problem in another
email [1].

I am not sure if there is a way to bake into Python an effective check that
an instance (not the class of the instance) is compliant with an ABC.
 However, it would be cool if there was.  The current checking mechanism for
ABCs happens in object.__new__ at instantiation time.  At that point it has
no knowledge of what names your instance will have, other than those that
come from the class.

I spent a while looking at this whole problem yesterday and came up with a
bunch of approaches for that Fail situation above.  However, they mostly
seem like overkill to me.  I have included them below.  If anyone has  ideas
on how to approach the problem of using an ABC but satisfying it with
instance names, I would love to hear it.  Thanks!

-eric


[1] In this case it would be nice to know at definition time that the class
is missing the abstract "method".  You don't want an exception at definition
time for every subclass, though, since some you may want to keep abstract.
 I wrote up a decorator that allows you to validate at definition time in an
email yesterday (
http://mail.python.org/pipermail/python-list/2011-May/1272541.html).
[2] A related issue opened just yesterday: http://bugs.python.org/issue12128



################################################################

1 - properties, with a getter and setter.  At definition time.  This seems
like overkill:

class X(object):
    __metaclass__ = ABCMeta
    @abstractproperty
    def name(self): pass

class Y(X):
    def __init__(self, name):
        self._name = name
        super(Y, self).__init__()
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, val):
        self._name = val

2 - getter/setter functions.  At definition time.  This does not guarantee
the name, only access around it:

class X(object):
    __metaclass__ = ABCMeta
    @abstractmethod
    def get_name(self): pass
    @abstractmethod
    def set_name(self): pass

class Y(X):
    def __init__(self, name):
        self.name = name
        super(Y, self).__init__()
    def get_name(self):
        return self.name
    def set_name(self, val):
        self.name = val

3 - descriptors directly.  At definition time.  Like the properties example:

class Name(object):
    def __get__(self, obj, cls):
        if obj is None:
            return self
        return obj._name
    def __set__(self, obj, val):
        obj._name = val

class X(object)
    __metaclass__ = ABCMeta
    @abstractproperty
    def name(self): pass

class Y(X):
    name = Name()
    def __init__(self, name):
        self.name = name
        super(Y, self).__init__()

4 - getattribute.  At run time.  More overkill:

class Enforcer(object):
    API = ()
    def __getattribute__(self, attr):
        if attr in API and attr not in dir(self):
            raise TypeError("Expected attribute: %s" % attr)
        return object.__getattribute__(self, attr)
    def __setattr(self, attr, val):
        if attr in API and attr not in dir(self):
            raise TypeError("Expected attribute: %s" % attr)
        object.__setattribute__(self, attr, val)

class X(Enforcer):
    API = ("name",)

class Y(X):
    def __init__(self, name):
        self.name = name
        super(Y, self).__init__()

5 - metaclass.  At instantiation time.  Overkill again:

class Enforcer(object):
    class SomeMeta(type):
        def enforces_API(f):
            def __init__(self, *args, **kwargs):
                f(self, *args, **kwargs)
                for name in self.API:
                    if name not in dir(self):
                        raise TypeError("Expected attribute: %s" % attr)
            __init__.__doc__ = f.__doc__
            return __init__

        def __new__(self, name, bases, namespace):
            cls = super(SomeMeta, self).__new__(self, name, bases,
namespace)
            __init__ = namespace.get("__init__")
            if not __init__:
                def __init__(self, *args, **kwargs):
                    super(cls, self).__init__(*args, **kwargs)
            namespace["__init__"] = self.enforces_API(__init__)
            return cls
    API = ()

class X(Enforcer):
    API = ("name",)

class Y(X):
    def __init__(self, name):
        self.name = name
        super(Y, self).__init__()

6 - decorator.  At instantiation time.  Apply to the __init__ of each class
that must enforce the API or to the base __init__ and call super after the
assignments...

class Enforcer(object):
    API = ()
    def enforces_API(f):
        def __init__(self, *args, **kwargs):
            f(self, *args, **kwargs)
            for name in self.API:
               if name not in dir(self):
                    raise TypeError("Expected attribute: %s" % attr)
        __init__.__doc__ = f.__doc__
        return __init__

class X(Enforcer):
    API = ("name",)

class Y(X):
    @X.enforces_API
    def __init__(self, name):
        self.name = name
        super(Y, self).__init__()

7 - class decorator.  At definition time. Apply to each class that must
enforce the API...

class Enforcer(object):
    API = ()
    def enforces_API(cls):
        def __init__decorator(f):
            def __init__(self, *args, **kwargs):
                f(self, *args, **kwargs)
                for name in self.API:
                    if name not in dir(self):
                        raise TypeError("Expected attribute: %s" % attr)
            __init__.__doc__ = f.__doc__
            return __init__

        __init__ = cls__dict__.get("__init__")
        if not __init__:
            def __init__(self, *args, **kwargs):
                super(cls, self).__init__(*args, **kwargs)
        cls.__init__ = self.enforces_API(__init__)

@Enforcer.enforces_API
class X(Enforcer):
    API = ("name",)

@Enforcer.enforces_API
class Y(X):
    def __init__(self, name):
        self.name = name
        super(Y, self).__init__()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-list/attachments/20110520/63f5778d/attachment.html>


More information about the Python-list mailing list