6
\$\begingroup\$

I use Python daily for long-running simulations (yes, very very optimal, I know). As you could probably guess, my coworkers and I have issues with simulations running for several minutes before finally bombing out. Part of what occupies my free time is finding ways to make out simulation framework detect and report error sooner to reduce turn-around time on bugs.

To that effect, I've written a string of implementations of a "strict interface" library, this being the ultimate version. The user creates "interfaces", which are merely sets of attribute names, that implementing objects are required to define. These sets can be composed to define new interfaces and compared to check mutual compliance. After a user defines a new interface, they "implement" the interface by calling the Implement() function. Implement() generates a mixin with a metaclass, the objective of which is to save the interface into the object and hook the objects's __init__ with an interface compliant check. The mixin class also prevents deletions of interface attributes.

This library seems to me to be more useful than the ABC library, at least for me. With ABC, all abstract values are required to be defined before object creation. This library does the check after __init__, but this could be a class's __init__, or a metaclass's __init__.

We use the concept of interfaces throughout our simulation framework, so it's not just useful for arbitrary attribute checks. Those could be done ad-hoc. The typical usage will follow:

""" WRITTEN BY LIBRARY DEV """
# this part is new
WriteDriverInterface = Interface('data', 'start', 'write_now_signal')
# this part is the same as the old way
class WriteDriverInterfaceObject(Implements(WriteDriverInterface), AnonymousObject): pass
def Writer(interfaceObj):
    assert WriteDriverInterface <= interfaceObj # but this is new
    # from here we know 'data', 'start', and 'write_now_signal' exist and are protected from being deleted
    yield RisingEdge(interfaceObj.start)  # wait until start
    while True:
        yield RisingEdge(interfaceObj.write_now_signal)
        port <= interfaceObj.data

""" WRITTEN BY LIBRARY USER """
# absolutely no change here, this is how it was done before (for better or worse)
a = Write(WriteDriverInterfaceObject(data = inputport, start = start, write_now_signal = sampler_signal)

Because this library will be used by people fairly new to programming (co-workers are mostly older engineers and new graduates), I want it to be as intuitive and noninvasive as possible. If the implementation is transparent enough, I could even re-write portions of our reuse libraries with it. And as seen in the above example, the end-user will never touch mixins or metaclasses. Even the library dev doesn't have to touch any of that, which was the point of the Implements() function.

class Interface(frozenset):
    """
    Defines an interface for a type that must be defined by instantiation (after __init__). Works in concert with Implements().
    Interfaces are just sets of valid attributes and may be composed and compared.
    """

    def __new__(cls, *attrs, copyconstruct=None):
        if copyconstruct:
            return super().__new__(cls, copyconstruct)
        # run check to ensure all args are valid attribute names
        for attr in attrs:
            if not attr.isidentifier():
                raise AttributeError(f"{attr} cannot be used as an attribute name.")
        #
        return super().__new__(cls, attrs)

    def __repr__(cls):
        return f"Interface({', '.join(cls)})"

    """
    Run type fix-ups on result of set operations
    """
    # FIXME, is there a cleaner way to go about this?
    def union(self, *others):
        return Interface(copyconstruct=(super().union(*others)))

    def __or__(self, other):
        return Interface(copyconstruct=(super().__or__(other)))

    def intersection(self, *others):
        return Interface(copyconstruct=(super().intersection(*others)))

    def __and__(self, other):
        return Interface(copyconstruct=(super().__and__(other)))

    def difference(self, *others):
        return Interface(copyconstruct=(super().difference(*others)))

    def __sub__(self, other):
        return Interface(copyconstruct=(super().__sub__(other)))

    def symmetric_difference(self, other):
        return Interface(copyconstruct=(super().symmetric_difference(other)))

    def __xor__(self, other):
        return Interface(copyconstruct=(super().__xor__(other)))

    def copy(self):
        return Interface(copyconstructor=self)    

    def __copy__(self):
        return self.copy()


class ImplementMixinBase():
    """
    Base class for Interface Implementer mixin
    """
    def __delattr__(self, attr):
        """
        Protects against deleting interface attributes that would cause the interface spec to not be met
        """
        if attr in self.__interface__:
            raise AttributeError("{attr} in type's interface, cannot remove.")
        else:
            # forward del call
            super().__del__(attr)

    def __init__(self, *args, **kwargs):
        """Simple __init__ that forwards __init__ calls to base objects"""
        super().__init__(*args, **kwargs)


class Implementer(type):
    """
    Metaclass used to ensure compliance of given interface after the user __init__
    """
    def __new__(self, name, parents, namespace):
        # hook __init__ with compliance checker
        user_init = namespace['__init__'] if '__init__' in namespace else self._fake_no_init
        init_hook = self._init_hook
        namespace['__init__'] = lambda self, *args, **kwargs: init_hook(user_init, self, *args, **kwargs)
        #
        return super().__new__(self, name, parents, namespace)

    def _init_hook(user_init, self, *args, **kwargs):
        # run user __init__
        user_init(self, *args, **kwargs)
        # run interface compliance check
        not_implemented = tuple(attr for attr in self.__interface__ if not hasattr(self, attr))
        if len(not_implemented) > 0:
            raise NotImplementedError(f"Interface not compliant, missing: {', '.join(not_implemented)}")

    def _fake_no_init(self, *args, **kwargs):
        """
        If no __init__ is present in class, it is inherited from the superclass. When we hook __init__ we define an __init__
        that would otherwise not be there, which ends up overloading the superclass's __init__. So this simple method calls
        the superclass's __init__ as there were no __init__ defined in the subclass
        """
        super(type(self).__base__, self).__init__(*args, **kwargs)


def Implements(interface):
    """
    Function that generates a new interface implementer mixin with the implementer metaclass and __interface__ set
    """
    if not isinstance(interface, Interface):
        raise ValueError("'interface' argument must be an Interface.")
    return Implementer("ImplementMixin", (ImplementMixinBase,), {'__interface__':interface})


# Example
class AnonymousObject():
    """
    Creates an object using keyword arguments at __init__

    AnonymousObject(a=1, b=2, c=3) # creates an object with the attributes 'a', 'b', and 'c' set to 1, 2, and 3, respectively
    """

    def __init__(self, **kwargs):
        vars(self).update(kwargs)


# Tests, obviously need to flush this out
InterfaceA = Interface('a', 'b', 'c')
InterfaceB = Interface('d') | InterfaceA

class InterfaceAAnonymousObject(Implements(InterfaceA), AnonymousObject): pass

a = InterfaceAAnonymousObject(a=1, b=2, c=3)
try:
    b = InterfaceAAnonymousObject(a=1, b=2)
    assert False  # should complain about the interface missing 'c'
except NotImplementedError as err:
    print(err)

required_interface = Interface('a', 'b')
def do_stuff(obj):
    assert isinstance(obj, ImplementMixinBase) # FIXME might change the names around so this is clearer
    #assert isinstance(obj, InterfaceObject)   # this maybe?
    assert required_interface <= obj.__interface__
    pass # do stuff here with attributes known to be in object
do_stuff(a)

I need some feedback on the architecture of my implementation. Could it be simplified? Is there some way to utilize the builtin abstract base classes library to achieve post-__init__ interface compliance checking? Am I handling the mixin correct or is there some corner-case I'm missing? Is there some simple feature that would fit well with the rest of the code that would increase the useful of the code? And is there an easier way to get set operations to return an Interface and not a frozenset without the repetitive and probably incomplete set of type-fixing proxy functions I wrote?

Response to @theodox

As you can see in the above example of a typical usage, there isn't much expected of either the end-user or the "library" writer. At least given the current implementation. We could easily just define InterfaceObjects like so:

class InterfaceObject():
    def __init__(self, data, start, write_now_signal, **kwargs):
        pass # this will error if you don't give it all the required args, and you can still use keyword argument syntax
        # but who am I?

I totally agree that creating attributes on the fly using something like AnonymousObject is not a great idea, but it is pervasive throughout the reuse libraries and embedded like shrapnel into the minds of most of the older engineers I work with. It's better to make something that fits easily with what they know. They might actually start using it then. So compliance can only ever be really checked post-__init__.

A unit test framework that tests for the presence of the expected attributes might be a better "promise" to maintain than a complex metaprogramming system that may not be well understood by the newer members of the staff.

This, however is a great idea. I'm not 100% sure how I would end up going about it with our simulation framework, but I might wander down this path in the future. It might even play well with the suggestion of mypy given by a commenter on the OP.

Finally, I do like your class decorator implementation, it's surprisingly simple and does what I need it to. I had written a class decorator before, but the way it was written defined a new class instead of monkey patching the given class, which I was afraid would break a lot of old code. I am somewhat new to python myself, and it never occurred to me that you could use a decorator to monkey patch class. This is the cleaner solution by far.

\$\endgroup\$
  • \$\begingroup\$ I've already thought of a few things. First, it would be nice to define a function on an Interface called implemented_by() that would take an object and would return a boolean if that object had a compliant interface. \$\endgroup\$ – ktb Aug 6 '18 at 3:58
  • 1
    \$\begingroup\$ have you tried code analyzers like mypy with typing module? \$\endgroup\$ – Azat Ibrakov Aug 6 '18 at 7:58
  • \$\begingroup\$ @Azat I had thought it would be nice to have something like it, but had not heard of mypy in particular. I appreciate it, but I'm trying to bolt on something to the existing system and I think adding type annotations and a type checker would vastly increase the required knowledge overhead for newbies. And I doubt the older engineers would use it at all since it's an additional step. I'll probably use it at home though =P \$\endgroup\$ – ktb Aug 6 '18 at 13:22
  • \$\begingroup\$ One minor thing I forgot to mention -- you probably want to keep implements() as a static function that's not tied to the presence of any particular class; you don't want to complicate your error-checking with exceptions getting thrown by classes that are duck-typed the way you want but are not derived from a particular base :) \$\endgroup\$ – theodox Aug 6 '18 at 19:01
  • \$\begingroup\$ When I first read you question, it reminded me of discussions with scientists writing lots of code at the local university (billions and billions of data points) struggling to get their code working. Your comment @theodox too has made me add a comment here. Pytest and other testing frameworks are specifically designed to assist in promoting functional code. The concept is you have a known data set, apply it against your code, and get an expected result. This is kind of what you're trying to do too - but how do you validate that your checker is correct? Easier to rewrite with pytest I think. \$\endgroup\$ – C. Harley Aug 7 '18 at 8:35
5
\$\begingroup\$

You may want to step back and make sure you know where you want to put the responsibility for compliance, and how you want your users to express their expectations. My initial impression (an outsider's to your problem set, of course) is that you're expecting a good deal from "novice" users -- multiple inheritance, classes with arbitrary fields, and set-like operator overloads. I'm a bit worried that less advanced Python users may not be able to make effective use of what you're offering them. I know I had to read it over a couple of times to get the relationship between the moving parts down.

Some questions to consider when thinking it over:

When does compliance matter?

This really depends on your usage pattern. Do you want or expect the objects to get dynamic attributes at runtime? Generally that's not a great pattern for adopt for the scenario you sketched in your post, since it's an invitation to attribute errors; but of course you might be dealing with existing code that likes adding and deleting attributes.

On the other hand you might simply want to enforce compliance late: a simple assert hasattr(arg, attribute) in the calling code would handle those arguments in a structured way -- you could make a function with clear error messages encourage use that way. If you knew the gamut of expected properties in advance, you could invest in providing compound assertions for your users.

Which attributes?

Unless you really need dynamic attribute creation during runs, it seems like a bad practice to encourage in the kind of environment you're describing. It might be a more sustainable choice to invest in preventing attribute addition and deletion of any kind; that will enforce something approximating type safety regardless of the public interface the class presents. A unit test framework that tests for the presence of the expected attributes might be a better "promise" to maintain than a complex metaprogramming system that may not be well understood by the newer members of the staff.

A related question is whether you care about the distinction between instance and class attributes. If you really care about support for dynamic attributes you may need to check for class attributes separately, since dynamically editing those could change the state of other instance without warning. Again, here's a case where it might be better to invest in tests rather than trying to anticipate a very arbitrary execution environment.

Overall, it feels as if you want to support both extremes of Python at once: a very loosey-goosey approach with dynamism and a buttoned-down approach with, eg, type checking. I fear that will send a hard-to-decipher message to less experienced Python users; it's probably safer to run all one way with true duck-typing, or all the other with a stricter class based approach.

architecture

With all that said, I think the meat of what you're trying to get at here can probably be done in a simpler way that's also a bit more newbie friendly.

Rather than metaclasses and mixins, a class decorator is pretty simple for even junior coders to grok -- and it also has the advantage of making the intentions really explicit. Here's a rough idea of what feels like a more entry level version:

class Interfaces(object):

    def __init__(self, interfaces):
        self.interface_set = set(interfaces)

    def __call__(self, cls):
        # note when patching the class SLF is needed to replace 'self'
        # which will be the decorator's self in this context!

        # save the promise set
        cls_interface_set = set(self.interface_set)
        if hasattr(cls, '_interfaces'):
            cls_interface_set &= cls._interfaces
        cls._interfaces = cls_interface_set

        # stash the raw __init__ and replace with a checked version
        _raw_init = cls.__init__

        def replace_init(SLF, *args, **kwargs):
            _raw_init(SLF, *args, **kwargs)
            for attrib in SLF._interfaces:
                assert hasattr(SLF, attrib), "{} instance does not define '{}'".format(cls, attrib)
        cls.__init__ = replace_init

        # make the promises available as
        def get_interfaces(SLF):
            return SLF._interfaces

        cls.interfaces = property(get_interfaces)

        # prevent deletions.  I'd probably prevent this in all cases...
        def no_deletion(SLF, name):
            if name in SLF._interfaces:
                raise AttributeError("Cannot delete interface property")
            del SLF.__dict__[name]

        cls.__delattr__ = no_deletion

        return cls

def implements(obj, required):
    # fast path, using interfaces:
    if hasattr(obj, 'interfaces'):
        return obj.interfaces >= required

    #slow path, using hasattr
    for attrib in required:
        if not hasattr(obj, attrib):
            return False
    return true



@Interfaces({'a', 'b'})
class Example(object):

    def __init__(self, *args, **kwargs):
        self.a = args[0]
        for k, v in kwargs.items():
            setattr(self, k, v)

# this works:
test = Example(1, b=2)
print test.interfaces
# set(['a','b'])

required = {'b','a'}
print implements(test, required)
# True

more_required = {'a', 'c'}
print implements(test, more_required)
# False

# this fails -- it doesn't get a 'b'
test2 = Example(3)
# AssertionError: <class '__main__.Example'> instance does not define 'b'

Doing the same job in a metaclass would make the code a little cleaner, but you'd have to ask you junior coders to include the metaclass and also to move the attribute interface declaration into a class-level attribute.

As written this would take any iterable as argument to the decorator. Good practice would be to declare interface sets as named constants and use those in preference to literals:

BIRD = {'fly', 'nest'}
AQUATIC {'swim'}

and then compose those in the decorator

@Interfaces (BIRD | AQUATIC}
class Duck (object):
    #...etc

This would not be appropriate if the gamut of names were very large, however -- if people have to run to another file to know what OVIPAROUS | SEQUIPEDALIAN gets them as an interface to implement the system will have a hard time picking up adopters.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.