Rough first draft of generic function PEP
This commit is contained in:
parent
09824f7379
commit
c302a8889b
|
@ -120,6 +120,7 @@ Index by Category
|
|||
S 3120 Using UTF-8 as the default source encoding von Löwis
|
||||
S 3121 Module Initialization and finalization von Löwis
|
||||
S 3123 Making PyObject_HEAD conform to standard C von Löwis
|
||||
S 3124 Overloading, Generic Functions, Interfaces Eby
|
||||
S 3141 A Type Hierarchy for Numbers Yasskin
|
||||
|
||||
Finished PEPs (done, implemented in Subversion)
|
||||
|
@ -482,6 +483,7 @@ Numerical Index
|
|||
S 3121 Module Initialization and finalization von Löwis
|
||||
SR 3122 Delineation of the main module Cannon
|
||||
S 3123 Making PyObject_HEAD conform to standard C von Löwis
|
||||
S 3124 Overloading, Generic Functions, Interfaces Eby
|
||||
S 3141 A Type Hierarchy for Numbers Yasskin
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,908 @@
|
|||
PEP: 3124
|
||||
Title: Overloading, Generic Functions, Interfaces, and Adaptation
|
||||
Version: $Revision: 52916 $
|
||||
Last-Modified: $Date: 2006-12-04 14:59:42 -0500 (Mon, 04 Dec 2006) $
|
||||
Author: Phillip J. Eby <pje@telecommunity.com>
|
||||
Discussions-To: Python 3000 List <python-3000@python.org>
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Requires: 3107, 3115, 3119
|
||||
Replaces: 245, 246
|
||||
Content-Type: text/x-rst
|
||||
Created: 28-Apr-2007
|
||||
Post-History: 30-Apr-2007
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
This PEP proposes a new standard library module, ``overloading``, to
|
||||
provide generic programming features including dynamic overloading
|
||||
(aka generic functions), interfaces, adaptation, method combining (ala
|
||||
CLOS and AspectJ), and simple forms of aspect-oriented programming.
|
||||
|
||||
The proposed API is also open to extension; that is, it will be
|
||||
possible for library developers to implement their own specialized
|
||||
interface types, generic function dispatchers, method combination
|
||||
algorithms, etc., and those extensions will be treated as first-class
|
||||
citizens by the proposed API.
|
||||
|
||||
The API will be implemented in pure Python with no C, but may have
|
||||
some dependency on CPython-specific features such as ``sys._getframe``
|
||||
and the ``func_code`` attribute of functions. It is expected that
|
||||
e.g. Jython and IronPython will have other ways of implementing
|
||||
similar functionality (perhaps using Java or C#).
|
||||
|
||||
|
||||
Rationale and Goals
|
||||
===================
|
||||
|
||||
Python has always provided a variety of built-in and standard-library
|
||||
generic functions, such as ``len()``, ``iter()``, ``pprint.pprint()``,
|
||||
and most of the functions in the ``operator`` module. However, it
|
||||
currently:
|
||||
|
||||
1. does not have a simple or straightforward way for developers to
|
||||
create new generic functions,
|
||||
|
||||
2. does not have a standard way for methods to be added to existing
|
||||
generic functions (i.e., some are added using registration
|
||||
functions, others require defining ``__special__`` methods,
|
||||
possibly by monkeypatching), and
|
||||
|
||||
3. does not allow dispatching on multiple argument types (except in
|
||||
a limited form for arithmetic operators, where "right-hand"
|
||||
(``__r*__``) methods can be used to do two-argument dispatch.
|
||||
|
||||
In addition, it is currently a common anti-pattern for Python code
|
||||
to inspect the types of received arguments, in order to decide what
|
||||
to do with the objects. For example, code may wish to accept either
|
||||
an object of some type, or a sequence of objects of that type.
|
||||
|
||||
Currently, the "obvious way" to do this is by type inspection, but
|
||||
this is brittle and closed to extension. A developer using an
|
||||
already-written library may be unable to change how their objects are
|
||||
treated by such code, especially if the objects they are using were
|
||||
created by a third party.
|
||||
|
||||
Therefore, this PEP proposes a standard library module to address
|
||||
these, and related issues, using decorators and argument annotations
|
||||
(PEP 3107). The primary features to be provided are:
|
||||
|
||||
* a dynamic overloading facility, similar to the static overloading
|
||||
found in languages such as Java and C++, but including optional
|
||||
method combination features as found in CLOS and AspectJ.
|
||||
|
||||
* a simple "interfaces and adaptation" library inspired by Haskell's
|
||||
typeclasses (but more dynamic, and without any static type-checking),
|
||||
with an extension API to allow registering user-defined interface
|
||||
types such as those found in PyProtocols and Zope.
|
||||
|
||||
* a simple "aspect" implementation to make it easy to create stateful
|
||||
adapters and to do other stateful AOP.
|
||||
|
||||
These features are to be provided in such a way that extended
|
||||
implementations can be created and used. For example, it should be
|
||||
possible for libraries to define new dispatching criteria for
|
||||
generic functions, and new kinds of interfaces, and use them in
|
||||
place of the predefined features. For example, it should be possible
|
||||
to use a ``zope.interface`` interface object to specify the desired
|
||||
type of a function argument, as long as the ``zope.interface``
|
||||
registered itself correctly (or a third party did the registration).
|
||||
|
||||
In this way, the proposed API simply offers a uniform way of accessing
|
||||
the functionality within its scope, rather than prescribing a single
|
||||
implementation to be used for all libraries, frameworks, and
|
||||
applications.
|
||||
|
||||
|
||||
User API
|
||||
========
|
||||
|
||||
The overloading API will be implemented as a single module, named
|
||||
``overloading``, providing the following features:
|
||||
|
||||
|
||||
Overloading/Generic Functions
|
||||
-----------------------------
|
||||
|
||||
The ``@overload`` decorator allows you to define alternate
|
||||
implementations of a function, specialized by argument type(s). A
|
||||
function with the same name must already exist in the local namespace.
|
||||
The existing function is modified in-place by the decorator to add
|
||||
the new implementation, and the modified function is returned by the
|
||||
decorator. Thus, the following code::
|
||||
|
||||
from overloading import overload
|
||||
from collections import Iterable
|
||||
|
||||
def flatten(ob):
|
||||
"""Flatten an object to its component iterables"""
|
||||
yield ob
|
||||
|
||||
@overload
|
||||
def flatten(ob: Iterable):
|
||||
for o in ob:
|
||||
for ob in flatten(o):
|
||||
yield ob
|
||||
|
||||
@overload
|
||||
def flatten(ob: basestring):
|
||||
yield ob
|
||||
|
||||
creates a single ``flatten()`` function whose implementation roughly
|
||||
equates to::
|
||||
|
||||
def flatten(ob):
|
||||
if isinstance(ob, basestring) or not isinstance(ob, Iterable):
|
||||
yield ob
|
||||
else:
|
||||
for o in ob:
|
||||
for ob in flatten(o):
|
||||
yield ob
|
||||
|
||||
**except** that the ``flatten()`` function defined by overloading
|
||||
remains open to extension by adding more overloads, while the
|
||||
hardcoded version cannot be extended.
|
||||
|
||||
For example, if someone wants to use ``flatten()`` with a string-like
|
||||
type that doesn't subclass ``basestring``, they would be out of luck
|
||||
with the second implementation. With the overloaded implementation,
|
||||
however, they can either write this::
|
||||
|
||||
@overload
|
||||
def flatten(ob: MyString):
|
||||
yield ob
|
||||
|
||||
or this (to avoid copying the implementation)::
|
||||
|
||||
from overloading import RuleSet
|
||||
RuleSet(flatten).copy_rules((basestring,), (MyString,))
|
||||
|
||||
(Note also that, although PEP 3119 proposes that it should be possible
|
||||
for abstract base classes like ``Iterable`` to allow classes like
|
||||
``MyString`` to claim subclass-hood, such a claim is *global*,
|
||||
throughout the application. In contrast, adding a specific overload
|
||||
or copying a rule is specific to an individual function, and therefore
|
||||
less likely to have undesired side effects.)
|
||||
|
||||
|
||||
``@overload`` vs. ``@when``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``@overload`` decorator is a common-case shorthand for the more
|
||||
general ``@when`` decorator. It allows you to leave out the name of
|
||||
the function you are overloading, at the expense of requiring the
|
||||
target function to be in the local namespace. It also doesn't support
|
||||
adding additional criteria besides the ones specified via argument
|
||||
annotations. The following function definitions have identical
|
||||
effects, except for name binding side-effects (which will be described
|
||||
below)::
|
||||
|
||||
@overload
|
||||
def flatten(ob: basestring):
|
||||
yield ob
|
||||
|
||||
@when(flatten)
|
||||
def flatten(ob: basestring):
|
||||
yield ob
|
||||
|
||||
@when(flatten)
|
||||
def flatten_basestring(ob: basestring):
|
||||
yield ob
|
||||
|
||||
@when(flatten, (basestring,))
|
||||
def flatten_basestring(ob):
|
||||
yield ob
|
||||
|
||||
The first definition above will bind ``flatten`` to whatever it was
|
||||
previously bound to. The second will do the same, if it was already
|
||||
bound to the ``when`` decorator's first argument. If ``flatten`` is
|
||||
unbound or bound to something else, it will be rebound to the function
|
||||
definition as given. The last two definitions above will always bind
|
||||
``flatten_basestring`` to the function definition as given.
|
||||
|
||||
Using this approach allows you to both give a method a descriptive
|
||||
name (often useful in tracebacks!) and to reuse the method later.
|
||||
|
||||
Except as otherwise specified, all ``overloading`` decorators have the
|
||||
same signature and binding rules as ``@when``. They accept a function
|
||||
and an optional "predicate" object.
|
||||
|
||||
The default predicate implementation is a tuple of types with
|
||||
positional matching to the overloaded function's arguments. However,
|
||||
an arbitrary number of other kinds of of predicates can be created and
|
||||
registered using the `Extension API`_, and will then be usable with
|
||||
``@when`` and other decorators created by this module (like
|
||||
``@before``, ``@after``, and ``@around``).
|
||||
|
||||
|
||||
Method Combination and Overriding
|
||||
---------------------------------
|
||||
|
||||
When an overloaded function is invoked, the implementation with the
|
||||
signature that *most specifically matches* the calling arguments is
|
||||
the one used. If no implementation matches, a ``NoApplicableMethods``
|
||||
error is raised. If more than one implementation matches, but none of
|
||||
the signatures are more specific than the others, an ``AmbiguousMethods``
|
||||
error is raised.
|
||||
|
||||
For example, the following pair of implementations are ambiguous, if
|
||||
the ``foo()`` function is ever called with two integer arguments,
|
||||
because both signatures would apply, but neither signature is more
|
||||
*specific* than the other (i.e., neither implies the other)::
|
||||
|
||||
def foo(bar:int, baz:object):
|
||||
pass
|
||||
|
||||
@overload
|
||||
def foo(bar:object, baz:int):
|
||||
pass
|
||||
|
||||
In contrast, the following pair of implementations can never be
|
||||
ambiguous, because one signature always implies the other; the
|
||||
``int/int`` signature is more specific than the ``object/object``
|
||||
signature::
|
||||
|
||||
def foo(bar:object, baz:object):
|
||||
pass
|
||||
|
||||
@overload
|
||||
def foo(bar:int, baz:int):
|
||||
pass
|
||||
|
||||
A signature S1 implies another signature S2, if whenever S1 would
|
||||
apply, S2 would also. A signature S1 is "more specific" than another
|
||||
signature S2, if S1 implies S2, but S2 does not imply S1.
|
||||
|
||||
Although the examples above have all used concrete or abstract types
|
||||
as argument annotations, there is no requirement that the annotations
|
||||
be such. They can also be "interface" objects (discussed in the
|
||||
`Interfaces and Adaptation`_ section), including user-defined
|
||||
interface types. (They can also be other objects whose types are
|
||||
appropriately registered via the `Extension API`_.)
|
||||
|
||||
|
||||
Proceeding to the "Next" Method
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If the first parameter of an overloaded function is named
|
||||
``__proceed__``, it will be passed a callable representing the next
|
||||
most-specific method. For example, this code::
|
||||
|
||||
def foo(bar:object, baz:object):
|
||||
print "got objects!"
|
||||
|
||||
@overload
|
||||
def foo(__proceed__, bar:int, baz:int):
|
||||
print "got integers!"
|
||||
return __proceed__(bar, baz)
|
||||
|
||||
Will print "got integers!" followed by "got objects!".
|
||||
|
||||
If there is no next most-specific method, ``__proceed__`` will be
|
||||
bound to a ``NoApplicableMethods`` instance. When called, a new
|
||||
``NoApplicableMethods`` instance will be raised, with the arguments
|
||||
passed to the first instance.
|
||||
|
||||
Similarly, if the next most-specific methods have ambiguous precedence
|
||||
with respect to each other, ``__proceed__`` will be bound to an
|
||||
``AmbiguousMethods`` instance, and if called, it will raise a new
|
||||
instance.
|
||||
|
||||
Thus, a method can either check if ``__proceed__`` is an error
|
||||
instance, or simply invoke it. The ``NoApplicableMethods`` and
|
||||
``AmbiguousMethods`` error classes have a common ``DispatchError``
|
||||
base class, so ``isinstance(__proceed__, overloading.DispatchError)``
|
||||
is sufficient to identify whether ``__proceed__`` can be safely
|
||||
called.
|
||||
|
||||
(Implementation note: using a magic argument name like ``__proceed__``
|
||||
could potentially be replaced by a magic function that would be called
|
||||
to obtain the next method. A magic function, however, would degrade
|
||||
performance and might be more difficult to implement on non-CPython
|
||||
platforms. Method chaining via magic argument names, however, can be
|
||||
efficiently implemented on any Python platform that supports creating
|
||||
bound methods from functions -- one simply recursively binds each
|
||||
function to be chained, using the following function or error as the
|
||||
``im_self`` of the bound method.)
|
||||
|
||||
|
||||
"Before" and "After" Methods
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In addition to the simple next-method chaining shown above, it is
|
||||
sometimes useful to have other ways of combining methods. For
|
||||
example, the "observer pattern" can sometimes be implemented by adding
|
||||
extra methods to a function, that execute before or after the normal
|
||||
implementation.
|
||||
|
||||
To support these use cases, the ``overloading`` module will supply
|
||||
``@before``, ``@after``, and ``@around`` decorators, that roughly
|
||||
correspond to the same types of methods in the Common Lisp Object
|
||||
System (CLOS), or the corresponding "advice" types in AspectJ.
|
||||
|
||||
Like ``@when``, all of these decorators must be passed the function to
|
||||
be overloaded, and can optionally accept a predicate as well::
|
||||
|
||||
def begin_transaction(db):
|
||||
print "Beginning the actual transaction"
|
||||
|
||||
|
||||
@before(begin_transaction)
|
||||
def check_single_access(db: SingletonDB):
|
||||
if db.inuse:
|
||||
raise TransactionError("Database already in use")
|
||||
|
||||
@after(begin_transaction)
|
||||
def start_logging(db: LoggableDB):
|
||||
db.set_log_level(VERBOSE)
|
||||
|
||||
|
||||
``@before`` and ``@after`` methods are invoked either before or after
|
||||
the main function body, and are *never considered ambiguous*. That
|
||||
is, it will not cause any errors to have multiple "before" or "after"
|
||||
methods with identical or overlapping signatures. Ambiguities are
|
||||
resolved using the order in which the methods were added to the
|
||||
target function.
|
||||
|
||||
"Before" methods are invoked most-specific method first, with
|
||||
ambiguous methods being executed in the order they were added. All
|
||||
"before" methods are called before any of the function's "primary"
|
||||
methods (i.e. normal ``@overload`` methods) are executed.
|
||||
|
||||
"After" methods are invoked in the *reverse* order, after all of the
|
||||
function's "primary" methods are executed. That is, they are executed
|
||||
least-specific methods first, with ambiguous methods being executed in
|
||||
the reverse of the order in which they were added.
|
||||
|
||||
The return values of both "before" and "after" methods are ignored,
|
||||
and any uncaught exceptions raised by *any* methods (primary or other)
|
||||
immediately end the dispatching process. "Before" and "after" methods
|
||||
cannot have ``__proceed__`` arguments, as they are not responsible
|
||||
for calling any other methods. They are simply called as a
|
||||
notification before or after the primary methods.
|
||||
|
||||
Thus, "before" and "after" methods can be used to check or establish
|
||||
preconditions (e.g. by raising an error if the conditions aren't met)
|
||||
or to ensure postconditions, without needing to duplicate any existing
|
||||
functionality.
|
||||
|
||||
|
||||
"Around" Methods
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``@around`` decorator declares a method as an "around" method.
|
||||
"Around" methods are much like primary methods, except that the
|
||||
least-specific "around" method has higher precedence than the
|
||||
most-specific "before" or method.
|
||||
|
||||
Unlike "before" and "after" methods, however, "Around" methods *are*
|
||||
responsible for calling their ``__proceed__`` argument, in order to
|
||||
continue the invocation process. "Around" methods are usually used
|
||||
to transform input arguments or return values, or to wrap specific
|
||||
cases with special error handling or try/finally conditions, e.g.::
|
||||
|
||||
@around(commit_transaction)
|
||||
def lock_while_committing(__proceed__, db: SingletonDB):
|
||||
with db.global_lock:
|
||||
return __proceed__(db)
|
||||
|
||||
They can also be used to replace the normal handling for a specific
|
||||
case, by *not* invoking the ``__proceed__`` function.
|
||||
|
||||
The ``__proceed__`` given to an "around" method will either be the
|
||||
next applicable "around" method, a ``DispatchError`` instance,
|
||||
or a synthetic method object that will call all the "before" methods,
|
||||
followed by the primary method chain, followed by all the "after"
|
||||
methods, and return the result from the primary method chain.
|
||||
|
||||
Thus, just as with normal methods, ``__proceed__`` can be checked for
|
||||
``DispatchError``-ness, or simply invoked. The "around" method should
|
||||
return the value returned by ``__proceed__``, unless of course it
|
||||
wishes to modify or replace it with a different return value for the
|
||||
function as a whole.
|
||||
|
||||
|
||||
Custom Combinations
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The decorators described above (``@overload``, ``@when``, ``@before``,
|
||||
``@after``, and ``@around``) collectively implement what in CLOS is
|
||||
called the "standard method combination" -- the most common patterns
|
||||
used in combining methods.
|
||||
|
||||
Sometimes, however, an application or library may have use for a more
|
||||
sophisticated type of method combination. For example, if you
|
||||
would like to have "discount" methods that return a percentage off,
|
||||
to be subtracted from the value returned by the primary method(s),
|
||||
you might write something like this::
|
||||
|
||||
from overloading import always_overrides, merge_by_default
|
||||
from overloading import Around, Before, After, Method, MethodList
|
||||
|
||||
class Discount(MethodList):
|
||||
"""Apply return values as discounts"""
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
retval = self.tail(*args, **kw)
|
||||
for sig, body in self.sorted():
|
||||
retval -= retval * body(*args, **kw)
|
||||
return retval
|
||||
|
||||
# merge discounts by priority
|
||||
merge_by_default(Discount)
|
||||
|
||||
# discounts have precedence over before/after/primary methods
|
||||
always_overrides(Discount, Before)
|
||||
always_overrides(Discount, After)
|
||||
always_overrides(Discount, Method)
|
||||
|
||||
# but not over "around" methods
|
||||
always_overrides(Around, Discount)
|
||||
|
||||
# Make a decorator called "discount" that works just like the
|
||||
# standard decorators...
|
||||
discount = Discount.make_decorator('discount')
|
||||
|
||||
# and now let's use it...
|
||||
def price(product):
|
||||
return product.list_price
|
||||
|
||||
@discount(price)
|
||||
def ten_percent_off_shoes(product: Shoe)
|
||||
return Decimal('0.1')
|
||||
|
||||
Similar techniques can be used to implement a wide variety of
|
||||
CLOS-style method qualifiers and combination rules. The process of
|
||||
creating custom method combination objects and their corresponding
|
||||
decorators is described in more detail under the `Extension API`_
|
||||
section.
|
||||
|
||||
Note, by the way, that the ``@discount`` decorator shown will work
|
||||
correctly with any new predicates defined by other code. For example,
|
||||
if ``zope.interface`` were to register its interface types to work
|
||||
correctly as argument annotations, you would be able to specify
|
||||
discounts on the basis of its interface types, not just classes or
|
||||
``overloading``-defined interface types.
|
||||
|
||||
Similarly, if a library like RuleDispatch or PEAK-Rules were to
|
||||
register an appropriate predicate implementation and dispatch engine,
|
||||
one would then be able to use those predicates for discounts as well,
|
||||
e.g.::
|
||||
|
||||
from somewhere import Pred # some predicate implementation
|
||||
|
||||
@discount(
|
||||
price,
|
||||
Pred("isinstance(product,Shoe) and"
|
||||
" product.material.name=='Blue Suede'")
|
||||
)
|
||||
def forty_off_blue_suede_shoes(product):
|
||||
return Decimal('0.4')
|
||||
|
||||
The process of defining custom predicate types and dispatching engines
|
||||
is also described in more detail under the `Extension API`_ section.
|
||||
|
||||
|
||||
Overloading Inside Classes
|
||||
--------------------------
|
||||
|
||||
All of the decorators above have a special additional behavior when
|
||||
they are directly invoked within a class body: the first parameter
|
||||
(other than ``__proceed__``, if present) of the decorated function
|
||||
will be treated as though it had an annotation equal to the class
|
||||
in which it was defined.
|
||||
|
||||
That is, this code::
|
||||
|
||||
class And(object):
|
||||
# ...
|
||||
@when(get_conjuncts)
|
||||
def __conjuncts(self):
|
||||
return self.conjuncts
|
||||
|
||||
produces the same effect as this (apart from the existence of a
|
||||
private method)::
|
||||
|
||||
class And(object):
|
||||
# ...
|
||||
|
||||
@when(get_conjuncts)
|
||||
def get_conjuncts_of_and(ob: And):
|
||||
return ob.conjuncts
|
||||
|
||||
This behavior is both a convenience enhancement when defining lots of
|
||||
methods, and a requirement for safely distinguishing multi-argument
|
||||
overloads in subclasses. Consider, for example, the following code::
|
||||
|
||||
class A(object):
|
||||
def foo(self, ob):
|
||||
print "got an object"
|
||||
|
||||
@overload
|
||||
def foo(__proceed__, self, ob:Iterable):
|
||||
print "it's iterable!"
|
||||
return __proceed__(self, ob)
|
||||
|
||||
|
||||
class B(A):
|
||||
foo = A.foo # foo must be defined in local namespace
|
||||
|
||||
@overload
|
||||
def foo(__proceed__, self, ob:Iterable):
|
||||
print "B got an iterable!"
|
||||
return __proceed__(self, ob)
|
||||
|
||||
Due to the implicit class rule, calling ``B().foo([])`` will print
|
||||
"B got an iterable!" followed by "it's iterable!", and finally,
|
||||
"got an object", while ``A().foo([])`` would print only the messages
|
||||
defined in ``A``.
|
||||
|
||||
Conversely, without the implicit class rule, the two "Iterable"
|
||||
methods would have the exact same applicability conditions, so calling
|
||||
either ``A().foo([])`` or ``B().foo([])`` would result in an
|
||||
``AmbiguousMethods`` error.
|
||||
|
||||
It is currently an open issue to determine the best way to implement
|
||||
this rule in Python 3.0. Under Python 2.x, a class' metaclass was
|
||||
not chosen until the end of the class body, which means that
|
||||
decorators could insert a custom metaclass to do processing of this
|
||||
sort. (This is how RuleDispatch, for example, implements the implicit
|
||||
class rule.)
|
||||
|
||||
PEP 3115, however, requires that a class' metaclass be determined
|
||||
*before* the class body has executed, making it impossible to use this
|
||||
technique for class decoration any more.
|
||||
|
||||
At this writing, discussion on this issue is ongoing.
|
||||
|
||||
|
||||
Interfaces and Adaptation
|
||||
-------------------------
|
||||
|
||||
The ``overloading`` module provides a simple implementation of
|
||||
interfaces and adaptation. The following example defines an
|
||||
``IStack`` interface, and declares that ``list`` objects support it::
|
||||
|
||||
from overloading import abstract, Interface
|
||||
|
||||
class IStack(Interface):
|
||||
@abstract
|
||||
def push(self, ob)
|
||||
"""Push 'ob' onto the stack"""
|
||||
|
||||
@abstract
|
||||
def pop(self):
|
||||
"""Pop a value and return it"""
|
||||
|
||||
|
||||
when(IStack.push, (list, object))(list.append)
|
||||
when(IStack.pop, (list,))(list.pop)
|
||||
|
||||
mylist = []
|
||||
mystack = IStack(mylist)
|
||||
mystack.push(42)
|
||||
assert mystack.pop()==42
|
||||
|
||||
The ``Interface`` class is a kind of "universal adapter". It accepts
|
||||
a single argument: an object to adapt. It then binds all its methods
|
||||
to the target object, in place of itself. Thus, calling
|
||||
``mystack.push(42``) is the same as calling
|
||||
``IStack.push(mylist, 42)``.
|
||||
|
||||
The ``@abstract`` decorator marks a function as being abstract: i.e.,
|
||||
having no implementation. If an ``@abstract`` function is called,
|
||||
it raises ``NoApplicableMethods``. To become executable, overloaded
|
||||
methods must be added using the techniques previously described. (That
|
||||
is, methods can be added using ``@when``, ``@before``, ``@after``,
|
||||
``@around``, or any custom method combination decorators.)
|
||||
|
||||
In the example above, the ``list.append`` method is added as a method
|
||||
for ``IStack.push()`` when its arguments are a list and an arbitrary
|
||||
object. Thus, ``IStack.push(mylist, 42)`` is translated to
|
||||
``list.append(mylist, 42)``, thereby implementing the desired
|
||||
operation.
|
||||
|
||||
(Note: the ``@abstract`` decorator is not limited to use in interface
|
||||
definitions; it can be used anywhere that you wish to create an
|
||||
"empty" generic function that initially has no methods. In
|
||||
particular, it need not be used inside a class.)
|
||||
|
||||
|
||||
Subclassing and Re-assembly
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Interfaces can be subclassed::
|
||||
|
||||
class ISizedStack(IStack):
|
||||
@abstract
|
||||
def __len__(self):
|
||||
"""Return the number of items on the stack"""
|
||||
|
||||
# define __len__ support for ISizedStack
|
||||
when(ISizedStack.__len__, (list,))(list.__len__)
|
||||
|
||||
Or assembled by combining functions from existing interfaces::
|
||||
|
||||
class Sizable(Interface):
|
||||
__len__ = ISizedStack.__len__
|
||||
|
||||
# list now implements Sizable as well as ISizedStack, without
|
||||
# making any new declarations!
|
||||
|
||||
A class can be considered to "adapt to" an interface at a given
|
||||
point in time, if no method defined in the interface is guaranteed to
|
||||
raise a ``NoApplicableMethods`` error if invoked on an instance of
|
||||
that class at that point in time.
|
||||
|
||||
In normal usage, however, it is "easier to ask forgiveness than
|
||||
permission". That is, it is easier to simply use an interface on
|
||||
an object by adapting it to the interface (e.g. ``IStack(mylist)``)
|
||||
or invoking interface methods directly (e.g. ``IStack.push(mylist,
|
||||
42)``), than to try to figure out whether the object is adaptable to
|
||||
(or directly implements) the interface.
|
||||
|
||||
|
||||
Implementing an Interface in a Class
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It is possible to declare that a class directly implements an
|
||||
interface, using the ``declare_implementation()`` function::
|
||||
|
||||
from overloading import declare_implementation
|
||||
|
||||
class Stack(object):
|
||||
def __init__(self):
|
||||
self.data = []
|
||||
def push(self, ob):
|
||||
self.data.append(ob)
|
||||
def pop(self):
|
||||
return self.data.pop()
|
||||
|
||||
declare_implementation(IStack, Stack)
|
||||
|
||||
The ``declare_implementation()`` call above is roughly equivalent to
|
||||
the following steps::
|
||||
|
||||
when(IStack.push, (Stack,object))(lambda self, ob: self.push(ob))
|
||||
when(IStack.pop, (Stack,))(lambda self, ob: self.pop())
|
||||
|
||||
That is, calling ``IStack.push()`` or ``IStack.pop()`` on an instance
|
||||
of any subclass of ``Stack``, will simply delegate to the actual
|
||||
``push()`` or ``pop()`` methods thereof.
|
||||
|
||||
For the sake of efficiency, calling ``IStack(s)`` where ``s`` is an
|
||||
instance of ``Stack``, **may** return ``s`` rather than an ``IStack``
|
||||
adapter. (Note that calling ``IStack(x)`` where ``x`` is already an
|
||||
``IStack`` adapter will always return ``x`` unchanged; this is an
|
||||
additional optimization allowed in cases where the adaptee is known
|
||||
to *directly* implement the interface, without adaptation.)
|
||||
|
||||
For convenience, it may be useful to declare implementations in the
|
||||
class header, e.g.::
|
||||
|
||||
class Stack(metaclass=Implementer, implements=IStack):
|
||||
...
|
||||
|
||||
Instead of calling ``declare_implementation()`` after the end of the
|
||||
suite.
|
||||
|
||||
|
||||
Interfaces as Type Specifiers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``Interface`` subclasses can be used as argument annotations to
|
||||
indicate what type of objects are acceptable to an overload, e.g.::
|
||||
|
||||
@overload
|
||||
def traverse(g: IGraph, s: IStack):
|
||||
g = IGraph(g)
|
||||
s = IStack(s)
|
||||
# etc....
|
||||
|
||||
Note, however, that the actual arguments are *not* changed or adapted
|
||||
in any way by the mere use of an interface as a type specifier. You
|
||||
must explicitly cast the objects to the appropriate interface, as
|
||||
shown above.
|
||||
|
||||
Note, however, that other patterns of interface use are possible.
|
||||
For example, other interface implementations might not support
|
||||
adaptation, or might require that function arguments already be
|
||||
adapted to the specified interface. So the exact semantics of using
|
||||
an interface as a type specifier are dependent on the interface
|
||||
objects you actually use.
|
||||
|
||||
For the interface objects defined by this PEP, however, the semantics
|
||||
are as described above. An interface I1 is considered "more specific"
|
||||
than another interface I2, if the set of descriptors in I1's
|
||||
inheritance hierarchy are a proper superset of the descriptors in I2's
|
||||
inheritance hierarchy.
|
||||
|
||||
So, for example, ``ISizedStack`` is more specific than both
|
||||
``ISizable`` and ``ISizedStack``, irrespective of the inheritance
|
||||
relationships between these interfaces. It is purely a question of
|
||||
what operations are included within those interfaces -- and the
|
||||
*names* of the operations are unimportant.
|
||||
|
||||
Interfaces (at least the ones provided by ``overloading``) are always
|
||||
considered less-specific than concrete classes. Other interface
|
||||
implementations can decide on their own specificity rules, both
|
||||
between interfaces and other interfaces, and between interfaces and
|
||||
classes.
|
||||
|
||||
|
||||
Non-Method Attributes in Interfaces
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``Interface`` implementation actually treats all attributes and
|
||||
methods (i.e. descriptors) in the same way: their ``__get__`` (and
|
||||
``__set__`` and ``__delete__``, if present) methods are called with
|
||||
the wrapped (adapted) object as "self". For functions, this has the
|
||||
effect of creating a bound method linking the generic function to the
|
||||
wrapped object.
|
||||
|
||||
For non-function attributes, it may be easiest to specify them using
|
||||
the ``property`` built-in, and the corresponding ``fget``, ``fset``,
|
||||
and ``fdel`` attributes::
|
||||
|
||||
class ILength(Interface):
|
||||
@property
|
||||
@abstract
|
||||
def length(self):
|
||||
"""Read-only length attribute"""
|
||||
|
||||
# ILength(aList).length == list.__len__(aList)
|
||||
when(ILength.length.fget, (list,))(list.__len__)
|
||||
|
||||
Alternatively, methods such as ``_get_foo()`` and ``_set_foo()``
|
||||
may be defined as part of the interface, and the property defined
|
||||
in terms of those methods, but this a bit more difficult for users
|
||||
to implement correctly when creating a class that directly implements
|
||||
the interface, as they would then need to match all the individual
|
||||
method names, not just the name of the property or attribute.
|
||||
|
||||
|
||||
Aspects
|
||||
-------
|
||||
|
||||
The adaptation system provided assumes that adapters are "stateless",
|
||||
which is to say that adapters have no attributes or storage apart from
|
||||
those of the adapted object. This follows the "typeclass/instance"
|
||||
model of Haskell, and the concept of "pure" (i.e., transitively
|
||||
composable) adapters.
|
||||
|
||||
However, there are occasionally cases where, to provide a complete
|
||||
implementation of some interface, some sort of additional state is
|
||||
required.
|
||||
|
||||
One possibility of course, would be to attach monkeypatched "private"
|
||||
attributes to the adaptee. But this is subject to name collisions,
|
||||
and complicates the process of initialization. It also doesn't work
|
||||
on objects that don't have a ``__dict__`` attribute.
|
||||
|
||||
So the ``Aspect`` class is provided to make it easy to attach extra
|
||||
information to objects that either:
|
||||
|
||||
1. have a ``__dict__`` attribute (so aspect instances can be stored
|
||||
in it, keyed by aspect class),
|
||||
|
||||
2. support weak referencing (so aspect instances can be managed using
|
||||
a global but thread-safe weak-reference dictionary), or
|
||||
|
||||
3. implement or can be adapt to the ``overloading.IAspectOwner``
|
||||
interface (technically, #1 or #2 imply this)
|
||||
|
||||
Subclassing ``Aspect`` creates an adapter class whose state is tied
|
||||
to the life of the adapted object.
|
||||
|
||||
For example, suppose you would like to count all the times a certain
|
||||
method is called on instances of ``Target`` (a classic AOP example).
|
||||
You might do something like::
|
||||
|
||||
from overloading import Aspect
|
||||
|
||||
class Count(Aspect):
|
||||
count = 0
|
||||
|
||||
@after(Target.some_method)
|
||||
def count_after_call(self, *args, **kw):
|
||||
Count(self).count += 1
|
||||
|
||||
The above code will keep track of the number of times that
|
||||
``Target.some_method()`` is successfully called (i.e., it will not
|
||||
count errors). Other code can then access the count using
|
||||
``Count(someTarget).count``.
|
||||
|
||||
``Aspect`` instances can of course have ``__init__`` methods, to
|
||||
initialize any data structures. They can use either ``__slots__``
|
||||
or dictionary-based attributes for storage.
|
||||
|
||||
While this facility is rather primitive compared to a full-featured
|
||||
AOP tool like AspectJ, persons who wish to build pointcut libraries
|
||||
or other AspectJ-like features can certainly use ``Aspect`` objects
|
||||
and method-combination decorators as a base for more expressive AOP
|
||||
tools.
|
||||
|
||||
XXX spec out full aspect API, including keys, N-to-1 aspects, manual
|
||||
attach/detach/delete of aspect instances, and the ``IAspectOwner``
|
||||
interface.
|
||||
|
||||
|
||||
Extension API
|
||||
=============
|
||||
|
||||
TODO: explain how all of these work
|
||||
|
||||
implies(o1, o2)
|
||||
|
||||
declare_implementation(iface, class)
|
||||
|
||||
predicate_signatures(ob)
|
||||
|
||||
parse_rule(ruleset, body, predicate, actiontype, localdict, globaldict)
|
||||
|
||||
combine_actions(a1, a2)
|
||||
|
||||
rules_for(f)
|
||||
|
||||
Rule objects
|
||||
|
||||
ActionDef objects
|
||||
|
||||
RuleSet objects
|
||||
|
||||
Method objects
|
||||
|
||||
MethodList objects
|
||||
|
||||
IAspectOwner
|
||||
|
||||
|
||||
|
||||
Implementation Notes
|
||||
====================
|
||||
|
||||
Most of the functionality described in this PEP is already implemented
|
||||
in the in-development version of the PEAK-Rules framework. In
|
||||
particular, the basic overloading and method combination framework
|
||||
(minus the ``@overload`` decorator) already exists there. The
|
||||
implementation of all of these features in ``peak.rules.core`` is 656
|
||||
lines of Python at this writing.
|
||||
|
||||
``peak.rules.core`` currently relies on the DecoratorTools and
|
||||
BytecodeAssembler modules, but both of these dependencies can be
|
||||
replaced, as DecoratorTools is used mainly for Python 2.3
|
||||
compatibility and to implement structure types (which can be done
|
||||
with named tuples in later versions of Python). The use of
|
||||
BytecodeAssembler can be replaced using an "exec" or "compile"
|
||||
workaround, given a reasonable effort. (It would be easier to do this
|
||||
if the ``func_closure`` attribute of function objects was writable.)
|
||||
|
||||
The ``Interface`` class has been previously prototyped, but is not
|
||||
included in PEAK-Rules at the present time.
|
||||
|
||||
The "implicit class rule" has previously been implemented in the
|
||||
RuleDispatch library. However, it relies on the ``__metaclass__``
|
||||
hook that is currently eliminated in PEP 3115.
|
||||
|
||||
I don't currently know how to make ``@overload`` play nicely with
|
||||
``classmethod`` and ``staticmethod`` in class bodies. It's not really
|
||||
clear if it needs to, however.
|
||||
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document has been placed in the public domain.
|
||||
|
||||
|
||||
|
||||
..
|
||||
Local Variables:
|
||||
mode: indented-text
|
||||
indent-tabs-mode: nil
|
||||
sentence-end-double-space: t
|
||||
fill-column: 70
|
||||
coding: utf-8
|
||||
End:
|
Loading…
Reference in New Issue