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.
Interface
calledimplemented_by()
that would take an object and would return a boolean if that object had a compliant interface. \$\endgroup\$typing
module? \$\endgroup\$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\$