Use an Interface¶
To use interfaces, cast an object to the interface by wrapping the object with
the interface (Writable(object)
)
To prevent interface checks from affecting performance, consider putting
interface casts inside if __debug__:
clauses. This performs interface checks
during debugging, but production code can run faster using the original objects
by running Python with the -O
flag.
def write_hello(writer):
if __debug__:
writer = Writer(writer)
writer.write('Hello\n')
output = OutputWriter()
error = ErrorWriter()
wrapped = PrintAttributeAccessWrapper(error)
write_hello(output)
write_hello(error)
write_hello(wrapped)
Not only is the object checked that it supports the write
attribute. Uses
of the interface are checked that they do not use non-supported attributes.
def broken_write_hello(writer):
if __debug__:
writer = Writable(writer)
writer.write('hello')
writer.flush()
broken_write_hello(OutputWriter())
In the above code, writer
will be replaced by the interface, and the attempt
to use flush
, which is not part of the interface, will fail, even though the
passed object does support that attribute.
In optimised Python, broken_hello_world
will use the original object, and
should run faster without the intervening interface replacement. In this case,
the code will work with the current implementation, but may fail if a different
object, that does not support flush
, is passed. Hopefully, by using jute
this bug was caught during development.
Interfaces can also be returned from a function. This is useful to ensure that callers are only using the “public” attributes of the returned object. This makes it easier to modify the implementation to return a different object. As long as the returned object satisfies the interface, all code should continue to work.
def get_output():
output = sys.stdout
if __debug__:
output = Writable(output)
return output
This definition of get_output
can be changed to use a non-flushable object
(e.g. an ssl.SSLSocket
) with no risk that code that uses the returned value
relies on non-supported attributes such as flush
.
Returning interfaces can also be used to divide a complex object’s method into those needed for specific roles, and only pass the appropriate subset to code implementing the roles. For example, an object which signals a condition can be divided into a Notifiable interface and a Watchable interface, to prevent a watching object from accidentally notifying completion.
class Notifiable(jute.Opaque):
def notify(self):
"""Notify that an event occurred."""
class Watchable(jute.Opaque):
def watch(self, callback):
"""Get called when an event occurs."""
@jute.implements(Notifiable, Watchable)
class Signal:
def __init__(self):
self.result = None
self.callbacks = []
def notify(self, result):
self.result = result
for f in self.callbacks:
f(result)
self.callbacks = []
def watch(self, callback):
if self.result:
callback(result)
else:
self.callbacks.append(callback)
def do_task():
signal = Signal()
do_async(subtask, Notifiable(signal))
return Watchable(signal)
task = do_task()
task.watch(func) # OK
task.notify(3) # Error