[Python-3000] Abilities / Interfaces
Phillip J. Eby
pje at telecommunity.com
Tue Nov 21 21:26:50 CET 2006
At 11:16 AM 11/21/2006 -0800, Guido van Rossum wrote:
>Phillip then argues that he doesn't want to encourage introspectable
>interfaces. I think others have use cases for those though.
Examples?
> It seems
>that Phillip's approach only hanges well together if everybody totally
>adopts the generic functions religion; I think that's an unlikely
>(since too radical) change of course for Python 3.0.
Is it really radical?
Consider the fact that single-dispatch generic functions are ubiquitous in
the language and the stdlib, and have been pretty much forever. They just
vary in how they're implemented, and there's no One Obvious Way for
developers to implement new ones, nor is there a standard way to add a
method to an existing one.
That's because some generic functions use if-isinstance checks (bad), while
others use custom __special__ methods (not bad, but not great), registries
(good), adaptation (okay), or generic function libraries.
So, all I am proposing is that we:
* provide a standard GF implementation for the simple cases
* ...that is extensible to allow others to handle the more complex
cases, by having a generic API for manipulating generic functions
So that the "one obvious way" to create new generics is to use the standard
GF implementation unless you need something else. And, there would be a
standard way to add new methods to existing generic functions, perhaps
using an addmethod() builtin or decorator, and it could be designed so that
e.g.:
addmethod(iter, somefunc, sometype)
would actually work by doing 'sometype.__iter__ = somefunc' under the
hood. This allows us not to have to change any builtin generics that are
based on special method names. In other words, the mechanism isn't
radical, nor is the idea of having generics. It's just an improvement in
ease-of-use: an HCI change, not a comp-sci change!
And, with this relatively simple mechanism, all the fancier forms of
generic functions (or interfaces and adapters) can be implemented via user
libraries. (More on how, below.)
> It also doesn't
>seem to work for abilities that are tied to an instance instead of to
>a class as Zope allows (see below).
Actually, it still allows for that, as long as the base generic function
system is extensible. For example, RuleDispatch generic functions can
choose implementations based on conditions such as whether an object has a
particular attribute. Zope could easily add a generic function type that
uses their instance-based interface stuff to do the same thing.
I am merely proposing that Python not provide this sort of
introspection-oriented stuff in the core or stdlib, not that nobody should
be *allowed* to have it.
Indeed, I explicitly want the Python generic function API (e.g. addmethod,
hasmethod, or whatever we call them) to *itself* be generic, so that users
can create their own types that work with the Python-provided operations
for generic function manipulation. That is, I should be able to call:
addmethod(addmethod, adder_for_my_gf_type, my_gf_type)
So that others can then call:
# this calls adder_for_my_gf_type under the hood:
addmethod(some_gf, some_method, some_cls)
where 'some_gf' is an instance of 'my_gf_type'. This allows us not to keep
the core or stdlib implementation of generic functions quite simple to
handle the 80% (single dispatch) or 90% (concrete type-based multiple
dispatch) with ease. The final 10% (predicate dispatch, fancy interfaces,
etc.) can then be done by outside libraries, since the use cases in that
final 10% are more likely to vary than the base 80-90%.
Using the 'simplegeneric' library from PyPI (easy_install simplegeneric):
from simplegeneric import generic
@generic
def addmethod(gf, method, cls):
"""Add a method to a generic function"""
raise TypeError("Unknown generic function type", gf)
@addmethod.when_object(iter)
def add_to_iter(gf, method, cls):
# XXX should have more error checking here, e.g.
# check for a local __iter__ first
cls.__iter__ = method
# ... similar declarations for other builtin generics
@addmethod.when_type(FunctionType)
def add_to_function(gf, method, cls):
if hasattr(gf, 'when_type'):
gf.when_type(cls)(method)
else:
raise TypeError("Not a generic function", gf)
And there you are: an extensible way to add new extensible function types,
while using the same API for all of them.
(If you need multiple or predicate dispatch or method combining, it's easy
to have decorators that apply to the method itself, so that the same
three-argument addmethod() can still be used.)
>3. API design -- how do we spell the various concepts? E.g.
>has_ability(x, A) asks whether object x has the ability A;
>provides_ability(C, A) asks whether class C provides the ability A to
>its instances. We could state that provides_ability(C, A) and
>isinstance(x, C) implies has_ability(x, A).
Even if we explicitly have some type of "ability" object, I'd like them to
be defined in terms of generic functions, so that I could effectively say
something like:
sequence_like = (iter & len)
(or perhaps spelled out in some other fashion) to create an "ability"
representing iter-ability and len-ability.
One of the biggest conceptual/modeling issues with Zope-style interfaces is
that they don't allow this kind of fine-grained protocol combination. See,
for example, zope.interface's long history of trying to nail down various
Python protocols through elaborate interface inheritance
hierarchies. Defining interfaces strictly in terms of operations
eliminates this issue.
>- How does this interact with generic functions?
The advanced abilities (per-instance/on-the-fly and multi-operation tests)
will likely affect performance and/or simplicity of the default
implementation. A purely type-based system can be implemented efficiently,
because declaring an interface can simultaneously add a concrete type
registration for all affected generics. (And registering an
interface-linked method in a generic function can pull in all the concrete
classes.)
Per-instance tests, however, increase code complexity. Generally speaking,
RuleDispatch does per-instance tests after class tests, but the way it gets
reasonable performance is by building decision trees beforehand to manage
the possible tests and avoid overlap. If it actually had to call
individual methods, or call a method more than once (ob.has_ability(Foo),
ob.has_ability(Bar), etc.) it would be considerably slower to select an
option.
Also -- and this is the real kicker -- what do you do if more than one
ability applies? Now we have to have precedence rules for what's more
specific. The advantage of defining an ability as a set of one or more
applicable generic functions is that precedence is comparatively
straightforward: supersets take precedence over subsets, and overlaps are
ambiguous. You also have some possibility of being able to implement this
by registration-time checks, without needing to build a dispatch tree at all.
I don't know of anybody who really uses per-instance interface declarations
except for Zope. I used them for a little while with PEAK, and decided
they were the wrong idea for me; it made more sense to adapt to an
interface that provides the metadata, or to use a RuleDispatch function
that just directly introspected for whatever was needed. I can't think of
anything in the core or stdlib that would even need to go that far.
Again, my argument is that anything other than isinstance-based single and
multiple-dispatch is too framework-dependent to be part of the
language. Simple things should be simple, complex things should be possible.
>Answering these and similar questions probably requires a
>standardization committed. (Any volunteers?)
Note that Zope has already tried for many years to do exactly this, and
there is no indication that they have actually succeeded. When I first
mentioned PyProtocols on Python-Dev many years ago, Samuele Pedroni argued
that individual operations (and groups thereof) should be the currency of
interfaces, and after playing with them a bit, I agreed. PyProtocols
defines such interfaces as e.g.:
protocolForType(file, ['read', 'close'])
to mean "file-like object with 'read' and 'close' methods". If I define a
class as supporting that protocol, and somebody wants to adapt to
'protocolForType(file, ["read"])', then my type will work.
See
http://peak.telecommunity.com/protocol_ref/protocols-generated-type.html
for a more complete explanation, including how to get the system to
actually check for the method names (so as not to require explicit
declarations).
I think Samuele's idea (i.e., this) works a lot better than trying to
define a rigid interface hierarchy for builtin-protocols, although I'd just
as soon be able to do something like:
file.read & file.close
rather than having to use strings. But all this assumes that we are
providing a builtin way to spell abilities and introspect them, which I
still think is overkill for what Python actually needs to provide as a base.
In short, I think it's adding interface introspection that's the radical
move, where generic functions are just a bit of polish that brings a bit
more order to what Python already has. In contrast, interfaces are a
foreign religion imported from other languages, while the roots of the
generic function faith (len, iter, etc.) have already been in place since
the dawn of time! :)
More information about the Python-3000
mailing list