interface¶
interface is a library for declaring interfaces and for statically
asserting that classes implement those interfaces. It supports Python 2.7 and
Python 3.4+.
interface differs from Python’s abc module in two important ways:
Interface requirements are checked at class creation time, rather than at instance creation time. This means that
interfacecan tell you if a class fails to implement an interface even if you never create any instances of that class.interfacerequires that method signatures of implementations are compatible with signatures declared in interfaces. For example, the following code usingabcdoes not produce an error:>>> from abc import ABCMeta, abstractmethod >>> class Base(metaclass=ABCMeta): ... @abstractmethod ... def method(self, a, b): ... pass ... >>> class Implementation(MyABC): ... def method(self): ... return "This shouldn't work." ... >>> impl = Implementation() >>>
The equivalent code using
interfaceproduces an error, telling us that the implementation method doesn’t match the interface:>>> from interface import implements, Interface >>> class I(Interface): ... def method(self, a, b): ... pass ... >>> class C(implements(I)): ... def method(self): ... return "This shouldn't work" ... TypeError: class C failed to implement interface I: The following methods were implemented but had invalid signatures: - method(self) != method(self, a, b)
Why interface¶
What is an Interface?¶
In software generally, an interface is a description of the capabilities provided by a unit of code. In object-oriented languages like Python, interfaces are often defined by lists of method signatures which must be provided by a class.
In interface, an interface is a subclass of interface.Interface
that defines a list of methods with empty bodies. For example, the interface
definition for a simple Key-Value Store might look like this:
class KeyValueStore(interface.Interface):
def get(self, key):
pass
def set(self, key, value):
pass
Why Are Interfaces Useful?¶
Interfaces are useful for specifying the contract between two units of code. By marking that a type implements an interface, we give a programatically-verifiable guarantee that the implementation provides the methods specified by the interface signature. That guarantee makes it easier to write code that can work with any implementation of an interface, and it also serves as a form of documentation.
Usage¶
Declaring Interfaces¶
An interface describes a collection of metrhods and properties that should be provided by implementors.
We implement an interface by subclassing from interface.Interface and
defining stubs for methods that should be part of the interface:
class MyInterface(interface.Interface):
def method1(self, x, y, z):
pass
def method2(self):
pass
Implementing Interfaces¶
To declare that a class implements an interface I, that class should
subclass from implements(I):
class MyClass(interface.implements(MyInterface)):
def method1(self, x, y, z):
return x + y + z
def method2(self):
return "foo"
A class can implement more than one interface:
class MathStudent(Interface):
def argue(self, topic):
pass
def calculate(self, x, y):
pass
class PhilosophyStudent(Interface):
def argue(self, topic):
pass
def pontificate(self):
pass
class LiberalArtsStudent(implements(MathStudent, PhilosophyStudent)):
def argue(self, topic):
print(topic, "is", ["good", "bad"][random.choice([0, 1])])
def calculate(self, x, y):
return x + y
def pontificate(self):
print("I think what Wittgenstein was **really** saying is...")
Notice that interfaces can have intersecting methods as long as their signatures match.
Properties¶
Interfaces can declare non-method attributes that should be provided by
implementations using property:
class MyInterface(interface.Interface):
@property
def my_property(self):
pass
Implementations are required to provide a property with the same name.
class MyClass(interface.implements(MyInterface)):
@property
def my_property(self):
return 3
Default Implementations¶
Sometimes we have a method that should be part of an interface, but which can
be implemented in terms of other interface methods. When this happens, you can
use interface.default to provide a default implementation of a method.
class ReadOnlyMapping(interface.Interface):
def get(self, key):
pass
def keys(self):
pass
@interface.default
def get_all(self):
out = {}
for k in self.keys():
out[k] = self.get(k)
return out
Implementors are not required to implement methods with defaults:
class MyReadOnlyMapping(interface.implements(ReadOnlyMapping)):
def __init__(self, data):
self._data = data
def get(self, key):
return self._data[key]
def keys(self):
return self._data.keys()
# get_all(self) will automatically be copied from the interface default.
Default implementations should always be implemented in terms of other interface methods.
In Python 3, default will show a warning if a default implementation
uses non-interface members of an object:
class ReadOnlyMapping(interface.Interface):
def get(self, key):
pass
def keys(self):
pass
@interface.default
def get_all(self):
# This is supposed to be a default implementation for **any**
# ReadOnlyMapping, but this implementation assumes that 'self' has
# an _data attribute that isn't part of the interface!
return self._data.keys()
Running the above example displays a warning about the default implementation
of get_all:
$ python example.py
example.py:4: UnsafeDefault: Default for ReadOnlyMapping.get_all uses non-interface attributes.
The following attributes are used but are not part of the interface:
- _data
Consider changing ReadOnlyMapping.get_all or making these attributes part of ReadOnlyMapping.
class ReadOnlyMapping(interface.Interface):
Example¶
Suppose that we’re writing an application that needs to load and save user preferences.
We expect that we may want to manage preferences differently in different contexts, so we separate out our preference-management code into its own class, and our main application looks like this:
class MyApplication:
def __init__(self, preferences):
self.preferences = preferences
def save_resolution(self, resolution):
self.preferences.set('resolution', resolution)
def get_resolution(self):
return self.preferences.get('resolution')
def save_volume(self, volume):
self.preferences.set('volume', volume)
def get_volume(self):
return self.preferences.get('volume')
...
When we ship our application to users, we store preferences as a json file on the local hard drive:
class JSONFileStore:
def __init__(self, path):
self.path = path
def get(self, key):
with open(self.path) as f:
return json.load(f)[key]
def set(self, key, value):
with open(self.path) as f:
data = json.load(f)
data[key] = value
with open(self.path, 'w') as f:
json.dump(data, f)
In testing, however, we want to use a simpler key-value store that stores preferences in memory:
class InMemoryStore:
def __init__(self):
self.data = {}
def get(self, key):
return self.data[key]
def set(self, key, value):
self.data[key] = value
Later on, we add a cloud-sync feature to our application, so we add a third implementation that stores user preferences in a database:
class SQLStore:
def __init__(self, user, connection):
self.user = user
self.connection = connection
def get(self, key):
self.connection.execute(
"SELECT * FROM preferences where key=%s and user=%s",
[self.key, self.user],
)
def set(self, key, value):
self.connection.execute(
"INSERT INTO preferences VALUES (%s, %s, %s)",
[self.user, self.key, value],
)
As the number of KeyValueStore implementations grows, it becomes more and
more difficult for us to make changes to our application. If we add a new
method to any of the key-value stores, we can’t use it in the application
unless we add the same method to the other implementations, but in a large
codebase we might not even know what other implementations exist!
By declaring KeyValueStore as an Interface we can get
interface to help us keep our implementations in sync:
class KeyValueStore(interface.Interface):
def get(self, key):
pass
def set(self, key, value):
pass
class JSONFileStore(implements(KeyValueStore)):
...
class InMemoryStore(implements(KeyValueStore)):
...
class SQLStore(implements(KeyValueStore)):
...
Now, if we add a method to the interface without adding it to an implementation, we’ll get a helpful error at class definition time.
For example, if we add a get_default method to the interface but forget to
add it to SQLStore:
class KeyValueStore(interface.Interface):
def get(self, key):
pass
def set(self, key, value):
pass
def get_default(self, key, default):
pass
class SQLStore(interface.implements(KeyValueStore)):
def get(self, key):
pass
def set(self, key, value):
pass
# We forgot to define get_default!
We get the following error at import time:
$ python example.py
Traceback (most recent call last):
File "example.py", line 16, in <module>
class SQLStore(interface.implements(KeyValueStore)):
File "/home/ssanderson/projects/interface/interface/interface.py", line 394, in __new__
raise errors[0]
File "/home/ssanderson/projects/interface/interface/interface.py", line 376, in __new__
defaults_from_iface = iface.verify(newtype)
File "/home/ssanderson/projects/interface/interface/interface.py", line 191, in verify
raise self._invalid_implementation(type_, missing, mistyped, mismatched)
interface.interface.InvalidImplementation:
class SQLStore failed to implement interface KeyValueStore:
The following methods of KeyValueStore were not implemented:
- get_default(self, key, default)
API Reference¶
-
class
interface.Interface¶ Base class for interface definitions.
An interface defines a set of methods and/or attributes that should be provided by one or more classes, which are called “implementations” of the interface.
Classes can declare that they implement an interface
Iby subclassingimplements(I). During class construction, the base class generated byimplements(I)will verify that the new class correctly implements all the methods required byI.Interfaces cannot be instantiated.
Examples
Defining an Interface:
class KeyValueStore(Interface): def get(self, key): pass def set(self, key, value): pass def delete(self, key): pass
Implementing an Interface:
class InMemoryKeyValueStore(implements(KeyValueStore)): def __init__(self): self.data = {} def get(self, key): return self.data[key] def set(self, key, value): self.data[key] = value def delete(self, key): del self.data[key]
See also
-
classmethod
from_class(existing_class, subset=None, name=None)¶ Create an interface from an existing class.
Parameters: - existing_class (type) – The type from which to extract an interface.
- subset (list[str], optional) – List of methods that should be included in the interface. Default is to use all attributes not defined in an empty class.
- name (str, optional) – Name of the generated interface.
Default is
existing_class.__name__ + 'Interface'.
Returns: interface – A new interface class with stubs generated from
existing_class.Return type:
-
classmethod
-
interface.implements(*interfaces)¶ Make a base for a class that implements one or more interfaces.
Parameters: *interfaces (tuple) – One or more subclasses of Interface.Returns: base – A type validating that subclasses must implement all interface methods of types in interfaces.Return type: type Examples
Implementing an Interface:
class MyInterface(Interface): def method1(self, x): pass def method2(self, y): pass class MyImplementation(implements(MyInterface)): def method1(self, x): return x + 1 def method2(self, y): return y * 2
Implementing Multiple Interfaces:
class I1(Interface): def method1(self, x): pass class I2(Interface): def method2(self, y): pass class ImplBoth(implements(I1, I2)): def method1(self, x): return x + 1 def method2(self, y): return y * 2
-
class
interface.default(implementation)¶ Default implementation of a function in terms of interface methods.