This has previously been published on opensource.com.
The Zen of Python is loose enough and contradicts itself enough that you can prove anything from it. Let's meditate upon one of its most famous principles: "Explicit is better than implicit."
One thing that traditionally has been implicit in Python is the expected interface. Functions have been documented to expect a "file-like object" or a "sequence." But what is a file-like object? Does it support .writelines? What about .seek? What is a "sequence"? Does it support step-slicing, such as a[1:10:2]?
Originally, Python's answer was the so-called "duck-typing," taken from the phrase "if it walks like a duck and quacks like a duck, it's probably a duck." In other words, "try it and see," which is possibly the most implicit you could possibly get.
In order to make those things explicit, you need a way to express expected interfaces. One of the first big systems written in Python was the Zope web framework, and it needed those things desperately to make it obvious what rendering code, for example, expected from a "user-like object."
Enter zope.interface, which was part of Zope but published as a separate Python package. The zope.interface package helps declare what interfaces exist, which objects provide them, and how to query for that information.
Imagine writing a simple 2D game that needs various things to support a "sprite" interface; e.g., indicate a bounding box, but also indicate when the object intersects with a box. Unlike some other languages, in Python, attribute access as part of the public interface is a common practice, instead of implementing getters and setters. The bounding box should be an attribute, not a method.
A method that renders the list of sprites might look like:
def render_sprites(render_surface, sprites): """ sprites should be a list of objects complying with the Sprite interface: * An attribute "bounding_box", containing the bounding box. * A method called "intersects", that accepts a box and returns True or False """ pass # some code that would actually render
The game will have many functions that deal with sprites. In each of them, you would have to specify the expected contract in a docstring.
Additionally, some functions might expect a more sophisticated sprite object, maybe one that has a Z-order. We would have to keep track of which methods expect a Sprite object, and which expect a SpriteWithZ object.
Wouldn't it be nice to be able to make what a sprite is explicit and obvious so that methods could declare "I need a sprite" and have that interface strictly defined? Enter zope.interface.
from zope import interface class ISprite(interface.Interface): bounding_box = interface.Attribute( "The bounding box" ) def intersects(box): "Does this intersect with a box"
This code looks a bit strange at first glance. The methods do not include a self, which is a common practice, and it has an Attribute thing. This is the way to declare interfaces in zope.interface. It looks strange because most people are not used to strictly declaring interfaces.
The reason for this practice is that the interface shows how the method will be called, not how it is defined. Because interfaces are not superclasses, they can be used to declare data attributes.
One possible implementation of the interface can be with a circular sprite:
@implementer(ISprite) @attr.s(auto_attribs=True) class CircleSprite: x: float y: float radius: float @property def bounding_box(self): return ( self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius, ) def intersects(self, box): # A box intersects a circle if and only if # at least one corner is inside the circle. top_left, bottom_right = box[:2], box[2:] for choose_x_from (top_left, bottom_right): for choose_y_from (top_left, bottom_right): x = choose_x_from y = choose_y_from if (((x - self.x) ** 2 + (y - self.y) ** 2) <= self.radius ** 2): return True return False
This explicitly declares that the CircleSprite class implements the interface. It even enables us to verify that the class implements it properly:
from zope.interface import verify def test_implementation(): sprite = CircleSprite(x=0, y=0, radius=1) verify.verifyObject(ISprite, sprite)
This is something that can be run by pytest, nose, or another test runner, and it will verify that the sprite created complies with the interface. The test is often partial: it will not test anything only mentioned in the documentation, and it will not even test that the methods can be called without exceptions! However, it does check that the right methods and attributes exist. This is a nice addition to the unit test suite and -- at a minimum -- prevents simple misspellings from passing the tests.
If you have some implicit interfaces in your code, why not document them clearly with zope.interface?
Adding Methods Retroactively
Interfaces are forever
(The following talks about zope.interface interfaces, but applies equally well to Java interfaces, Go interfaces, and probably other similar constructs.)
When we write a function, we can sometimes change it in backwards-compatible ways. For example, we can loosen the type of a variable. We can restrict the type of …read more
Analyzing the Stack Overflow Survey
I am the parent of two young kids. It is easy to sink into random stuff, and not follow up on goals. Strict time management and prioritization means I get to work on open source projects, write programming books and update my blog with a decent cadence. Since a lot …read more
Publishing a Book with Sphinx
A Local LRU Cache
"It is a truth universally acknowledged, that a shared state in possession of mutability, must be in want of a bug." -- with apologies to Jane Austen
As Ms. Austen, and Henrik Eichenhardt, taught us, shared mutable state is the root of all evil.
Yet, the official documentation of functools tells …read more
Don't Make It Callable
There is a lot of code that overloads the
This is the method that
an object activates:
something(x, y, z)
something.__call__(x, y, z)
is a member of a Python-defined class.
At first, like every operator overload, this seems like a …read more
Staying Safe with Open Source
A couple of months ago, a successful attack against the Node ecosystem resulted in stealing an undisclosed amount of bitcoins from CoPay wallets.
The technical flow of the attack is well-summarized by the NPM blog post. Quick summary:
- nodemon, a popular way to run Node applications, depends on event-stream.
- The …