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