Seehttps://mail.python.org/pipermail/python-3000/2007-July/008784.html.
This PEP proposes a new standard library module,overloading, toprovide generic programming features including dynamic overloading(aka generic functions), interfaces, adaptation, method combining (alaCLOS and AspectJ), and simple forms of aspect-oriented programming(AOP).
The proposed API is also open to extension; that is, it will bepossible for library developers to implement their own specializedinterface types, generic function dispatchers, method combinationalgorithms, etc., and those extensions will be treated as first-classcitizens by the proposed API.
The API will be implemented in pure Python with no C, but may havesome dependency on CPython-specific features such assys._getframeand thefunc_code attribute of functions. It is expected thate.g. Jython and IronPython will have other ways of implementingsimilar functionality (perhaps using Java or C#).
Python has always provided a variety of built-in and standard-librarygeneric functions, such aslen(),iter(),pprint.pprint(),and most of the functions in theoperator module. However, itcurrently:
__special__ methods,possibly by monkeypatching), and__r*__) methods can be used to do two-argument dispatch.In addition, it is currently a common anti-pattern for Python codeto inspect the types of received arguments, in order to decide whatto do with the objects. For example, code may wish to accept eitheran object of some type, or a sequence of objects of that type.
Currently, the “obvious way” to do this is by type inspection, butthis is brittle and closed to extension. A developer using analready-written library may be unable to change how their objects aretreated by such code, especially if the objects they are using werecreated by a third party.
Therefore, this PEP proposes a standard library module to addressthese, and related issues, using decorators and argument annotations(PEP 3107). The primary features to be provided are:
These features are to be provided in such a way that extendedimplementations can be created and used. For example, it should bepossible for libraries to define new dispatching criteria forgeneric functions, and new kinds of interfaces, and use them inplace of the predefined features. For example, it should be possibleto use azope.interface interface object to specify the desiredtype of a function argument, as long as thezope.interface packageregistered itself correctly (or a third party did the registration).
In this way, the proposed API simply offers a uniform way of accessingthe functionality within its scope, rather than prescribing a singleimplementation to be used for all libraries, frameworks, andapplications.
The overloading API will be implemented as a single module, namedoverloading, providing the following features:
The@overload decorator allows you to define alternateimplementations of a function, specialized by argument type(s). Afunction with the same name must already exist in the local namespace.The existing function is modified in-place by the decorator to addthe new implementation, and the modified function is returned by thedecorator. Thus, the following code:
fromoverloadingimportoverloadfromcollectionsimportIterabledefflatten(ob):"""Flatten an object to its component iterables"""yieldob@overloaddefflatten(ob:Iterable):foroinob:forobinflatten(o):yieldob@overloaddefflatten(ob:basestring):yieldob
creates a singleflatten() function whose implementation roughlyequates to:
defflatten(ob):ifisinstance(ob,basestring)ornotisinstance(ob,Iterable):yieldobelse:foroinob:forobinflatten(o):yieldob
except that theflatten() function defined by overloadingremains open to extension by adding more overloads, while thehardcoded version cannot be extended.
For example, if someone wants to useflatten() with a string-liketype that doesn’t subclassbasestring, they would be out of luckwith the second implementation. With the overloaded implementation,however, they can either write this:
@overloaddefflatten(ob:MyString):yieldob
or this (to avoid copying the implementation):
fromoverloadingimportRuleSetRuleSet(flatten).copy_rules((basestring,),(MyString,))
(Note also that, althoughPEP 3119 proposes that it should be possiblefor abstract base classes likeIterable to allow classes likeMyString to claim subclass-hood, such a claim isglobal,throughout the application. In contrast, adding a specific overloador copying a rule is specific to an individual function, and thereforeless likely to have undesired side effects.)
@overload vs.@whenThe@overload decorator is a common-case shorthand for the moregeneral@when decorator. It allows you to leave out the name ofthe function you are overloading, at the expense of requiring thetarget function to be in the local namespace. It also doesn’t supportadding additional criteria besides the ones specified via argumentannotations. The following function definitions have identicaleffects, except for name binding side-effects (which will be describedbelow):
fromoverloadingimportwhen@overloaddefflatten(ob:basestring):yieldob@when(flatten)defflatten(ob:basestring):yieldob@when(flatten)defflatten_basestring(ob:basestring):yieldob@when(flatten,(basestring,))defflatten_basestring(ob):yieldob
The first definition above will bindflatten to whatever it waspreviously bound to. The second will do the same, if it was alreadybound to thewhen decorator’s first argument. Ifflatten isunbound or bound to something else, it will be rebound to the functiondefinition as given. The last two definitions above will always bindflatten_basestring to the function definition as given.
Using this approach allows you to both give a method a descriptivename (often useful in tracebacks!) and to reuse the method later.
Except as otherwise specified, alloverloading decorators have thesame signature and binding rules as@when. They accept a functionand an optional “predicate” object.
The default predicate implementation is a tuple of types withpositional matching to the overloaded function’s arguments. However,an arbitrary number of other kinds of predicates can be created andregistered using theExtension API, and will then be usable with@when and other decorators created by this module (like@before,@after, and@around).
When an overloaded function is invoked, the implementation with thesignature thatmost specifically matches the calling arguments isthe one used. If no implementation matches, aNoApplicableMethodserror is raised. If more than one implementation matches, but none ofthe signatures are more specific than the others, anAmbiguousMethodserror is raised.
For example, the following pair of implementations are ambiguous, ifthefoo() function is ever called with two integer arguments,because both signatures would apply, but neither signature is morespecific than the other (i.e., neither implies the other):
deffoo(bar:int,baz:object):pass@overloaddeffoo(bar:object,baz:int):pass
In contrast, the following pair of implementations can never beambiguous, because one signature always implies the other; theint/int signature is more specific than theobject/objectsignature:
deffoo(bar:object,baz:object):pass@overloaddeffoo(bar:int,baz:int):pass
A signature S1 implies another signature S2, if whenever S1 wouldapply, S2 would also. A signature S1 is “more specific” than anothersignature S2, if S1 implies S2, but S2 does not imply S1.
Although the examples above have all used concrete or abstract typesas argument annotations, there is no requirement that the annotationsbe such. They can also be “interface” objects (discussed in theInterfaces and Adaptation section), including user-definedinterface types. (They can also be other objects whose types areappropriately registered via theExtension API.)
If the first parameter of an overloaded function is named__proceed__, it will be passed a callable representing the nextmost-specific method. For example, this code:
deffoo(bar:object,baz:object):print"got objects!"@overloaddeffoo(__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 bebound to aNoApplicableMethods instance. When called, a newNoApplicableMethods instance will be raised, with the argumentspassed to the first instance.
Similarly, if the next most-specific methods have ambiguous precedencewith respect to each other,__proceed__ will be bound to anAmbiguousMethods instance, and if called, it will raise a newinstance.
Thus, a method can either check if__proceed__ is an errorinstance, or simply invoke it. TheNoApplicableMethods andAmbiguousMethods error classes have a commonDispatchErrorbase class, soisinstance(__proceed__,overloading.DispatchError)is sufficient to identify whether__proceed__ can be safelycalled.
(Implementation note: using a magic argument name like__proceed__could potentially be replaced by a magic function that would be calledto obtain the next method. A magic function, however, would degradeperformance and might be more difficult to implement on non-CPythonplatforms. Method chaining via magic argument names, however, can beefficiently implemented on any Python platform that supports creatingbound methods from functions – one simply recursively binds eachfunction to be chained, using the following function or error as theim_self of the bound method.)
In addition to the simple next-method chaining shown above, it issometimes useful to have other ways of combining methods. Forexample, the “observer pattern” can sometimes be implemented by addingextra methods to a function, that execute before or after the normalimplementation.
To support these use cases, theoverloading module will supply@before,@after, and@around decorators, that roughlycorrespond to the same types of methods in the Common Lisp ObjectSystem (CLOS), or the corresponding “advice” types in AspectJ.
Like@when, all of these decorators must be passed the function tobe overloaded, and can optionally accept a predicate as well:
fromoverloadingimportbefore,afterdefbegin_transaction(db):print"Beginning the actual transaction"@before(begin_transaction)defcheck_single_access(db:SingletonDB):ifdb.inuse:raiseTransactionError("Database already in use")@after(begin_transaction)defstart_logging(db:LoggableDB):db.set_log_level(VERBOSE)
@before and@after methods are invoked either before or afterthe main function body, and arenever considered ambiguous. Thatis, it will not cause any errors to have multiple “before” or “after”methods with identical or overlapping signatures. Ambiguities areresolved using the order in which the methods were added to thetarget function.
“Before” methods are invoked most-specific method first, withambiguous 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 thereverse order, after all of thefunction’s “primary” methods are executed. That is, they are executedleast-specific methods first, with ambiguous methods being executed inthe 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 byany methods (primary or other)immediately end the dispatching process. “Before” and “after” methodscannot have__proceed__ arguments, as they are not responsiblefor calling any other methods. They are simply called as anotification before or after the primary methods.
Thus, “before” and “after” methods can be used to check or establishpreconditions (e.g. by raising an error if the conditions aren’t met)or to ensure postconditions, without needing to duplicate any existingfunctionality.
The@around decorator declares a method as an “around” method.“Around” methods are much like primary methods, except that theleast-specific “around” method has higher precedence than themost-specific “before” method.
Unlike “before” and “after” methods, however, “Around” methodsareresponsible for calling their__proceed__ argument, in order tocontinue the invocation process. “Around” methods are usually usedto transform input arguments or return values, or to wrap specificcases with special error handling or try/finally conditions, e.g.:
fromoverloadingimportaround@around(commit_transaction)deflock_while_committing(__proceed__,db:SingletonDB):withdb.global_lock:return__proceed__(db)
They can also be used to replace the normal handling for a specificcase, bynot invoking the__proceed__ function.
The__proceed__ given to an “around” method will either be thenext applicable “around” method, aDispatchError 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 forDispatchError-ness, or simply invoked. The “around” method shouldreturn the value returned by__proceed__, unless of course itwishes to modify or replace it with a different return value for thefunction as a whole.
The decorators described above (@overload,@when,@before,@after, and@around) collectively implement what in CLOS iscalled the “standard method combination” – the most common patternsused in combining methods.
Sometimes, however, an application or library may have use for a moresophisticated type of method combination. For example, if youwould 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:
fromoverloadingimportalways_overrides,merge_by_defaultfromoverloadingimportAround,Before,After,Method,MethodListclassDiscount(MethodList):"""Apply return values as discounts"""def__call__(self,*args,**kw):retval=self.tail(*args,**kw)forsig,bodyinself.sorted():retval-=retval*body(*args,**kw)returnretval# merge discounts by prioritymerge_by_default(Discount)# discounts have precedence over before/after/primary methodsalways_overrides(Discount,Before)always_overrides(Discount,After)always_overrides(Discount,Method)# but not over "around" methodsalways_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...defprice(product):returnproduct.list_price@discount(price)deften_percent_off_shoes(product:Shoe)returnDecimal('0.1')
Similar techniques can be used to implement a wide variety ofCLOS-style method qualifiers and combination rules. The process ofcreating custom method combination objects and their correspondingdecorators is described in more detail under theExtension APIsection.
Note, by the way, that the@discount decorator shown will workcorrectly with any new predicates defined by other code. For example,ifzope.interface were to register its interface types to workcorrectly as argument annotations, you would be able to specifydiscounts on the basis of its interface types, not just classes oroverloading-defined interface types.
Similarly, if a library like RuleDispatch or PEAK-Rules were toregister an appropriate predicate implementation and dispatch engine,one would then be able to use those predicates for discounts as well,e.g.:
fromsomewhereimportPred# some predicate implementation@discount(price,Pred("isinstance(product,Shoe) and"" product.material.name=='Blue Suede'"))defforty_off_blue_suede_shoes(product):returnDecimal('0.4')
The process of defining custom predicate types and dispatching enginesis also described in more detail under theExtension API section.
All of the decorators above have a special additional behavior whenthey are directly invoked within a class body: the first parameter(other than__proceed__, if present) of the decorated functionwill be treated as though it had an annotation equal to the classin which it was defined.
That is, this code:
classAnd(object):# ...@when(get_conjuncts)def__conjuncts(self):returnself.conjuncts
produces the same effect as this (apart from the existence of aprivate method):
classAnd(object):# ...@when(get_conjuncts)defget_conjuncts_of_and(ob:And):returnob.conjuncts
This behavior is both a convenience enhancement when defining lots ofmethods, and a requirement for safely distinguishing multi-argumentoverloads in subclasses. Consider, for example, the following code:
classA(object):deffoo(self,ob):print"got an object"@overloaddeffoo(__proceed__,self,ob:Iterable):print"it's iterable!"return__proceed__(self,ob)classB(A):foo=A.foo# foo must be defined in local namespace@overloaddeffoo(__proceed__,self,ob:Iterable):print"B got an iterable!"return__proceed__(self,ob)
Due to the implicit class rule, callingB().foo([]) will print“B got an iterable!” followed by “it’s iterable!”, and finally,“got an object”, whileA().foo([]) would print only the messagesdefined inA.
Conversely, without the implicit class rule, the two “Iterable”methods would have the exact same applicability conditions, so callingeitherA().foo([]) orB().foo([]) would result in anAmbiguousMethods error.
It is currently an open issue to determine the best way to implementthis rule in Python 3.0. Under Python 2.x, a class’ metaclass wasnot chosen until the end of the class body, which means thatdecorators could insert a custom metaclass to do processing of thissort. (This is how RuleDispatch, for example, implements the implicitclass rule.)
PEP 3115, however, requires that a class’ metaclass be determinedbefore the class body has executed, making it impossible to use thistechnique for class decoration any more.
At this writing, discussion on this issue is ongoing.
Theoverloading module provides a simple implementation ofinterfaces and adaptation. The following example defines anIStack interface, and declares thatlist objects support it:
fromoverloadingimportabstract,InterfaceclassIStack(Interface):@abstractdefpush(self,ob)"""Push 'ob' onto the stack"""@abstractdefpop(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)assertmystack.pop()==42
TheInterface class is a kind of “universal adapter”. It acceptsa single argument: an object to adapt. It then binds all its methodsto the target object, in place of itself. Thus, callingmystack.push(42) is the same as callingIStack.push(mylist,42).
The@abstract decorator marks a function as being abstract: i.e.,having no implementation. If an@abstract function is called,it raisesNoApplicableMethods. To become executable, overloadedmethods must be added using the techniques previously described. (Thatis, methods can be added using@when,@before,@after,@around, or any custom method combination decorators.)
In the example above, thelist.append method is added as a methodforIStack.push() when its arguments are a list and an arbitraryobject. Thus,IStack.push(mylist,42) is translated tolist.append(mylist,42), thereby implementing the desiredoperation.
Note, by the way, that the@abstract decorator is not limited touse in interface definitions; it can be used anywhere that you wish tocreate an “empty” generic function that initially has no methods. Inparticular, it need not be used inside a class.
Also note that interface methods need not be abstract; one could, forexample, write an interface like this:
classIWriteMapping(Interface):@abstractdef__setitem__(self,key,value):"""This has to be implemented"""defupdate(self,other:IReadMapping):fork,vinIReadMapping(other).items():self[k]=v
As long as__setitem__ is defined for some type, the aboveinterface will provide a usableupdate() implementation. However,if some specific type (or pair of types) has a more efficient way ofhandlingupdate() operations, an appropriate overload can stillbe registered for use in that case.
Interfaces can be subclassed:
classISizedStack(IStack):@abstractdef__len__(self):"""Return the number of items on the stack"""# define __len__ support for ISizedStackwhen(ISizedStack.__len__,(list,))(list.__len__)
Or assembled by combining functions from existing interfaces:
classSizable(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 givenpoint in time, if no method defined in the interface is guaranteed toraise aNoApplicableMethods error if invoked on an instance ofthat class at that point in time.
In normal usage, however, it is “easier to ask forgiveness thanpermission”. That is, it is easier to simply use an interface onan 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.
It is possible to declare that a class directly implements aninterface, using thedeclare_implementation() function:
fromoverloadingimportdeclare_implementationclassStack(object):def__init__(self):self.data=[]defpush(self,ob):self.data.append(ob)defpop(self):returnself.data.pop()declare_implementation(IStack,Stack)
Thedeclare_implementation() call above is roughly equivalent tothe following steps:
when(IStack.push,(Stack,object))(lambdaself,ob:self.push(ob))when(IStack.pop,(Stack,))(lambdaself,ob:self.pop())
That is, callingIStack.push() orIStack.pop() on an instanceof any subclass ofStack, will simply delegate to the actualpush() orpop() methods thereof.
For the sake of efficiency, callingIStack(s) wheres is aninstance ofStack,may returns rather than anIStackadapter. (Note that callingIStack(x) wherex is already anIStack adapter will always returnx unchanged; this is anadditional optimization allowed in cases where the adaptee is knowntodirectly implement the interface, without adaptation.)
For convenience, it may be useful to declare implementations in theclass header, e.g.:
classStack(metaclass=Implementer,implements=IStack):...
Instead of callingdeclare_implementation() after the end of thesuite.
Interface subclasses can be used as argument annotations toindicate what type of objects are acceptable to an overload, e.g.:
@overloaddeftraverse(g:IGraph,s:IStack):g=IGraph(g)s=IStack(s)# etc....
Note, however, that the actual arguments arenot changed or adaptedin any way by the mere use of an interface as a type specifier. Youmust explicitly cast the objects to the appropriate interface, asshown above.
Note, however, that other patterns of interface use are possible.For example, other interface implementations might not supportadaptation, or might require that function arguments already beadapted to the specified interface. So the exact semantics of usingan interface as a type specifier are dependent on the interfaceobjects you actually use.
For the interface objects defined by this PEP, however, the semanticsare as described above. An interface I1 is considered “more specific”than another interface I2, if the set of descriptors in I1’sinheritance hierarchy are a proper superset of the descriptors in I2’sinheritance hierarchy.
So, for example,ISizedStack is more specific than bothISizable andISizedStack, irrespective of the inheritancerelationships between these interfaces. It is purely a question ofwhat operations are included within those interfaces – and thenames of the operations are unimportant.
Interfaces (at least the ones provided byoverloading) are alwaysconsidered less-specific than concrete classes. Other interfaceimplementations can decide on their own specificity rules, bothbetween interfaces and other interfaces, and between interfaces andclasses.
TheInterface implementation actually treats all attributes andmethods (i.e. descriptors) in the same way: their__get__ (and__set__ and__delete__, if present) methods are called withthe wrapped (adapted) object as “self”. For functions, this has theeffect of creating a bound method linking the generic function to thewrapped object.
For non-function attributes, it may be easiest to specify them usingtheproperty built-in, and the correspondingfget,fset,andfdel attributes:
classILength(Interface):@property@abstractdeflength(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 definedin terms of those methods, but this is a bit more difficult for usersto implement correctly when creating a class that directly implementsthe interface, as they would then need to match all the individualmethod names, not just the name of the property or attribute.
The adaptation system described above assumes that adapters are “stateless”,which is to say that adapters have no attributes or state apart fromthat of the adapted object. This follows the “typeclass/instance”model of Haskell, and the concept of “pure” (i.e., transitivelycomposable) adapters.
However, there are occasionally cases where, to provide a completeimplementation of some interface, some sort of additional state isrequired.
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 (since any code usingthese attributes has to check for their existence and initialize themif necessary). It also doesn’t work on objects that don’t have a__dict__ attribute.
So theAspect class is provided to make it easy to attach extrainformation to objects that either:
__dict__ attribute (so aspect instances can be storedin it, keyed by aspect class),overloading.IAspectOwnerinterface (technically, #1 or #2 imply this).SubclassingAspect creates an adapter class whose state is tiedto the life of the adapted object.
For example, suppose you would like to count all the times a certainmethod is called on instances ofTarget (a classic AOP example).You might do something like:
fromoverloadingimportAspectclassCount(Aspect):count=0@after(Target.some_method)defcount_after_call(self:Target,*args,**kw):Count(self).count+=1
The above code will keep track of the number of times thatTarget.some_method() is successfully called on an instance ofTarget (i.e., it will not count errors unless they occur in amore-specific “after” method). Other code can then access the countusingCount(someTarget).count.
Aspect instances can of course have__init__ methods, toinitialize any data structures. They can use either__slots__or dictionary-based attributes for storage.
While this facility is rather primitive compared to a full-featuredAOP tool like AspectJ, persons who wish to build pointcut librariesor other AspectJ-like features can certainly useAspect objectsand method-combination decorators as a base for building moreexpressive AOP tools.
IAspectOwnerinterface.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
In discussion on the Python-3000 list, the proposed feature of allowingarbitrary functions to be overloaded has been somewhat controversial,with some people expressing concern that this would make programs moredifficult to understand.
The general thrust of this argument is that one cannot rely on what afunction does, if it can be changed from anywhere in the program at anytime. Even though in principle this can already happen throughmonkeypatching or code substitution, it is considered poor practice todo so.
However, providing support for overloading any function (or so theargument goes), is implicitly blessing such changes as being anacceptable practice.
This argument appears to make sense in theory, but it is almost entirelymooted in practice for two reasons.
First, people are generally not perverse, defining a function to do onething in one place, and then summarily defining it to do the oppositesomewhere else! The principal reasons to extend the behavior of afunction that hasnot been specifically made generic are to:
None of these reasons for adding overloads imply any change to theintended default or overall behavior of the existing function, however.Just as a base class method may be overridden by a subclass for thesesame two reasons, so too may a function be overloaded to provide forsuch enhancements.
In other words, universal overloading does not equalarbitraryoverloading, in the sense that we need not expect people to randomlyredefine the behavior of existing functions in illogical orunpredictable ways. If they did so, it would be no less of a badpractice than any other way of writing illogical or unpredictable code!
However, to distinguish bad practice from good, it is perhaps necessaryto clarify further what good practice for defining overloadsis. Andthat brings us to the second reason why generic functions do notnecessarily make programs harder to understand: overloading patterns inactual programs tend to follow very predictable patterns. (Both inPython and in languages that have nonon-generic functions.)
If a module is defining a new generic operation, it will usually alsodefine any required overloads for existing types in the same place.Likewise, if a module is defining a new type, then it will usuallydefine overloads there for any generic functions that it knows or caresabout.
As a result, the vast majority of overloads can be found adjacent toeither the function being overloaded, or to a newly-defined type forwhich the overload is adding support. Thus, overloads arehighly-discoverable in the common case, as you are either looking at thefunction or the type, or both.
It is only in rather infrequent cases that one will have overloads in amodule that contains neither the function nor the type(s) for which theoverload is added. This would be the case if, say, a third-partycreated a bridge of support between one library’s types and anotherlibrary’s generic function(s). In such a case, however, best practicesuggests prominently advertising this, especially by way of the modulename.
For example, PyProtocols defines such bridge support for working withZope interfaces and legacy Twisted interfaces, using modules calledprotocols.twisted_support andprotocols.zope_support. (Thesebridges are done with interface adapters, rather than generic functions,but the basic principle is the same.)
In short, understanding programs in the presence of universaloverloading need not be any more difficult, given that the vast majorityof overloads will either be adjacent to a function, or the definition ofa type that is passed to that function.
And, in the absence of incompetence or deliberate intention to beobscure, the few overloads that are not adjacent to the relevant type(s)or function(s), will generally not need to be understood or known aboutoutside the scope where those overloads are defined. (Except in the“support modules” case, where best practice suggests naming themaccordingly.)
Most of the functionality described in this PEP is already implementedin the in-development version of the PEAK-Rules framework. Inparticular, the basic overloading and method combination framework(minus the@overload decorator) already exists there. Theimplementation of all of these features inpeak.rules.core is 656lines of Python at this writing.
peak.rules.core currently relies on the DecoratorTools andBytecodeAssembler modules, but both of these dependencies can bereplaced, as DecoratorTools is used mainly for Python 2.3compatibility and to implement structure types (which can be donewith named tuples in later versions of Python). The use ofBytecodeAssembler can be replaced using an “exec” or “compile”workaround, given a reasonable effort. (It would be easier to do thisif thefunc_closure attribute of function objects was writable.)
TheInterface class has been previously prototyped, but is notincluded in PEAK-Rules at the present time.
The “implicit class rule” has previously been implemented in theRuleDispatch library. However, it relies on the__metaclass__hook that is currently eliminated inPEP 3115.
I don’t currently know how to make@overload play nicely withclassmethod andstaticmethod in class bodies. It’s not reallyclear if it needs to, however.
This document has been placed in the public domain.
Source:https://github.com/python/peps/blob/main/peps/pep-3124.rst
Last modified:2025-02-01 08:59:27 GMT