Package: decoratory
IntroductionThedecoratory packageis based on theDecorator Arguments Template, an
integrated concept for Python decorators with and without parameters. In
addition, all decorators created with it support complex arguments, e.g.
lists of values and functions, without unnecessarily complicating the
decoration of simple cases by these extensions. All implementation details
are described on theProject Homepage.Installationpip install --upgrade decoratoryAfter installation, basic information about the package, its individual
modules and their methods is available from the command line.python -m decoratory --helpIn particular, there is acomprehensive unit testfor each module, which
can be executed from the command line using the--testoption.python -m decoratory --testPackage ContentsThedecoratory packageincludes some classic decorators
implemented and functionally extended with this concept, e.g.SingletonMultitonWrapperObserverThis is an open list of modules that possibly will grow over time.DescriptionTo illustrate the functionality of each module, simple as well as
more complex examples are presented. Even if only one particular module
is needed, it is recommended to view the preceding examples as well. For
more examples of the full range of possibilities, please refer toDecorator Implementationson theProject Homepage.SingletonAsingleton patternis a design pattern that limits the instantiation of
a class to a single (unique) instance. This is useful when exactly one unique
object is needed i.e. to manage an expensive resource or coordinate actions
across module boundaries.As a simple example serves the decoration of the classAnimalas a
singleton. In the context of theDecorator Arguments Template, this can be
done both without brackets (decorator class) and with brackets (decorator
instance), meaning both notations describe the same functional situation.# *** example_singleton.py - class Animal with Singleton decorationfromdecoratory.singletonimportSingleton@Singleton# or @Singleton()classAnimal:def__init__(self,name):self.name=namedef__repr__(self):returnf"{self.__class__.__name__}('{self.name}')"# Create Instancesa=Animal(name='Teddy')# Creates Teddy, the primary instanceb=Animal(name='Roxie')# Returns Teddy, no Roxi is createdIf instances of the classAnimalare now created, this is only done for the
very first instantiation, and for all further instantiations always thisprimary instanceis given back.# *** example_singleton.py - verfication of the unique instance# Case 1: Static decoration using @Singleton or @Singleton()print(f"a ={a}")# a = Animal('Teddy')print(f"b ={b}")# b = Animal('Teddy')print(f"a is b:{aisb}")# a is b: Trueprint(f"a == b:{a==b}")# a == b: TrueIf instead of the abovestatic decorationusing pie-notation, i.e. with
@-notation at the class declaration, thedynamic decorationwithin Python
code is used, additional parameters can be passed to the decorator for
passing to or through the class initializer.# *** example_singleton.py - dynamic decoration with extra parameters# Case 2: Dynamic decoration providing extra initial default valuesAnimal=Singleton(Animal,'Teddy')Animal()# Using the decorator's default 'Teddy'a=Animal(name='Roxie')# Returns Teddyprint(a)# Animal('Teddy')Quite generally, for all the following decorators based on thisDecorator Arguments Template, these two properties are always fulfilled:Decoration as a class (without parentheses) and Decoration as an instance
(with empty parentheses) are equivalentFor dynamic decoration, extra parameters can be passed, e.g. for the
class initializerSo far, this singleton implementation follows the concept ofonce
forever, i.e. whenever a new instance of a class is created, one always
gets theprimary instanceback - without any possibility of ever changing
it again.Although this behavior is consistent with the fundamental concept of a
singleton, there are situations where it might be useful to reset asingleton. Such aresettable singleton, also calledsemi-singleton,
could be useful to express in code that an instance is often retrieved but
rarely changed.# *** example_singleton.py - decoration as 'resettable singleton'@Singleton(resettable=True)# Exposes an additional reset methodclassAnimal:def__init__(self,name):self.name=namedef__repr__(self):returnf"{self.__class__.__name__}('{self.name}')"# Case 3: Decoration using @Singleton(resettable=True)print(Animal(name='Teddy'))# Animal('Teddy')print(Animal(name='Roxie'))# Animal('Teddy')   (=primary instance)Animal.reset()# Reset the singletonprint(Animal(name='Roxie'))# Animal('Roxie')print(Animal(name='Teddy'))# Animal('Roxie')   (=primary instance)Without this strikingresettable=TruedecorationAnimalhas noresetmethod and the callAnimal.reset()will fail raising anAttributeError. For situations where this concept needs
to be used more often, a subclass shortcutSemiSingletonis provided.# *** example_singleton.py - decoration as a 'semi singleton'fromdecoratory.singletonimportSemiSingleton@SemiSingleton# or @SemiSingleton()classAnimal:pass# Some code ...BothSingletonandSemiSingletonof course provide aget_instance()method to directly retrieve the primary instance, e.g. usingAnimal.get_instance().Hint— Usingreset()andget_instance()in combinationIt should be noted that the combination ofreset()and immediately
followingget_instance()does not return a valid object, butNone.So areset()should always be followed by an
instantiation to ensure that a valid singleton instance exists.Within the main process of Python’s Global Interpreter Lock (GIL), bothSingleton and SemiSingleton are thread-safe. In example, using aThreadPoolExecutorthreadsafety can be easily demonstrated with sample
code like this:# *** example_singleton.py - threadsafetyfromdecoratory.singletonimportSingletonfromconcurrent.futuresimportThreadPoolExecutor,as_completed@Singleton# or @Singleton()classAnimal:def__init__(self,name):self.name=namedef__repr__(self):returnf"{self.__class__.__name__}('{self.name}')"# Create Instancesnames=["Teddy","Roxie","Molly","Benny"]withThreadPoolExecutor(max_workers=2)astpe:futures=[tpe.submit(Animal,name)fornameinnames]# Case 4: Decoration using @Singletonforfutureinfutures:instance=future.result()# All the same instances, i.e.print(instance)# Animal('Teddy') -- four times!The same instance is always presented, most likelyAnimal('Teddy')of
the first submitted thread, but it could also be any of the others.MultitonAmultiton patternis a design pattern that extends the singleton pattern.
Whereas the singleton allows for exactly one instance per class, the multiton
ensures one single (unique)instance per key.In this implementation, the key parameter can be anything that is possible as
a key for a Pythondict()dictionary, such as an immutable type or a
callable eventually returning such an immutable type etc.In case of an invalid key, key is setNoneand with only
one key value the multiton simply collapses to a singleton, therefore the
decoration@Multitonresp.@Multiton()or even@Multiton(key=17)or@Multiton(key='someconstant value')and so on always creates a
singleton.Normally, the key is part of or is composed from the initial values of the
classified object, as in the following example, where the key function matches
the signature of the initializer and uses the initial value of thenameparameter to construct a key value for the instances ofAnimal.# *** example_multitonton.py - class Animal with Multiton decorationfromdecoratory.multitonimportMultiton@Multiton(key=lambdaspec,name:name)classAnimal:def__init__(self,spec,name):self.spec=specself.name=namedef__repr__(self):returnf"{self.__class__.__name__}('{self.spec}', '{self.name}')"# Create Instancesa=Animal('dog',name='Teddy')b=Animal('cat',name='Molly')c=Animal('dog',name='Roxie')When instances of the classAnimalare now created, this only happens for
thefirst instantiation per key value, the initial name of the animal. For
all subsequent instantiations, thisprimary instance per key valueis
returned. But for each new key value, a newAnimalinstance is created
and stored in the internal directory.# *** example_multitonton.py - One unique instance per name# Case 1: decoration @Multiton(key=lambda spec, name: name)print(a)# Animal('dog', 'Teddy')print(b)# Animal('cat', 'Molly')print(c)# Animal('dog', 'Roxie')With three different names, a separate instance is created in each case.
In contrast, the following variant distinguishes only two types (equivalence
classes): animals with a character ‘y’ in their name and those without and
thus the key values can only beTrueorFalse.# *** example_multitonton.py - One unique instance per equivalence class# Case 2: decoration @Multiton(key=lambda spec, name: 'y' in name)print(a)# Animal('dog', 'Teddy')print(b)# Animal('dog', 'Teddy')print(c)# Animal('dog', 'Roxie')The initial parameter values of the initializer can also be accessed by theirargs-index orkwargs-name. So the following decorations are also
possible:# *** example_multitonton.py - Alternative decoration examples# Case 3: One unique instance per specie@Multiton(key="{0}".format)# spec is args[0]classAnimal:pass# Some code ...# Case 4: One unique instance per name@Multiton(key="{name}".format)# name is kwargs['name']classAnimal:pass# Some code ...# Case 5: One unique instance for all init values, i.e. no duplicates@Multiton(key=lambdaspec,name:(spec,name))classAnimal:pass# Some code ...# Case 6: One unique instance from a @staticmethod or @classmethod@Multiton(key=F("my_key"))# Late binding with F(classmethod_string)classAnimal:pass# Some code ...@classmethoddefmy_key(cls,spec,name):return'y'innameTo actively control access to new equivalence classes,Multitonprovides
theseal(),unseal(), andissealed()methods for sealing, unsealing,
and checking the sealing state of theMultiton. By default, the sealing
state is setFalse, so for every new key a new (unique) object is
instantiated. When sealed (e.g. later in the process) is setTruethe
dictionary has completed, i.e. restricted to the current object set and
any new key raises aKeyError.In situations where it might be useful to reset the multiton to express in
code that instances are often retrieved but rarely modified, setting the
decorator parameterresettable=Truewill expose thereset()method,
by means of which the internal directory of instances can be completely cleared.Last but not least,Multitonprovides ainstancesproperty and
associated getter and setter methods to directly retrieve the internal
dictionary of primary instances. It is obvious that manipulations on this
directory can corrupt the functionality of the multiton, but sometimes it
is useful to have the freedom of access.Hint— Changes affecting key values of classified objectsClassifications into the multiton directory are done only once on
initial key data. Subsequent changes affecting a key value are not
reflected in the multiton directory key, i.e. the directory may then be
corrupted by such modifications.Therefore,never change key related values of classified objects!All these things taken together could give the following exemplary picture:# *** example_multitonton.py - seal, unseal, reset, get_instance# Case 7: with decoration @Multiton(key=lambda spec, name: name,#                                   resettable=True)Animal.reset()# Because of resettable=Trueprint(Animal.get_instances())# {}print(Animal.issealed())# False     (=default)Animal('dog',name='Teddy')# Animal('dog', 'Teddy')print(Animal.get_instances())# {'Teddy': Animal('dog', 'Teddy')}Animal.seal()# Seal the multiton!print(Animal.issealed())# Truetry:# Try to..Animal('cat',name='Molly')# .. add a new animalexceptKeyErrorasex:# .. will failprint(f"Sorry{ex.args[1]},{ex.args[0]}")print(Animal.get_instances())# {'Teddy': Animal('dog', 'Teddy')}Animal.unseal()# Unseal the multiton!print(Animal.issealed())# FalseAnimal('cat',name='Molly')# Now, Molly is addedprint(Animal.get_instances())# {'Teddy': Animal('dog', 'Teddy'),#  'Molly': Animal('cat', 'Molly')}Animal.get_instances().pop('Teddy')print(Animal.get_instances())# {'Molly': Animal('cat', 'Molly')}Animal.get_instances().clear()# Same as Animal.reset()print(Animal.get_instances())# {}The last two lines show the functional equivalence ofAnimal.get_instances().clear()withAnimal.reset(), but theresetoption is more transparent because it is not necessary to look
“behind the scenes”.Within the main process of Python’s Global Interpreter Lock (GIL),Multiton is thread-safe. In example, using aThreadPoolExecutorthreadsafety can be easily demonstrated with sample code like this:# *** example_multiton.py - threadsafetyfromdecoratory.multitonimportMultitonfromconcurrent.futuresimportThreadPoolExecutor,as_completed@Multiton(key=lambdaspec,name:spec)classAnimal:def__init__(self,spec,name):self.spec=specself.name=namedef__repr__(self):returnf"{self.__class__.__name__}('{self.spec}', '{self.name}')"# Create Instancespets=[('dog','Teddy'),('dog','Roxie'),# dogs('cat','Molly'),('cat','Felix')]# catswithThreadPoolExecutor(max_workers=2)astpe:futures=[tpe.submit(Animal,*pet)forpetinpets]# Case 8: Decoration using spec: @Multiton(key=lambda spec, name: spec)forfutureinfutures:# Same instance per spec (=key), i.e.instance=future.result()# Animal('dog', 'Teddy') - for all dogsprint(instance)# Animal('cat', 'Molly') - for all catsPer type of animal (key = spec) always the same instance is presented,
most likelyAnimal('Teddy')for all dogs andAnimal('cat','Molly')for all cats, resulting from the first submitted thread per species, but it
could also be any of the others.WrapperAs the name implies, a wrapper encloses the original function with an(optional)beforecall functionalityand/or an(optional)aftercall functionality.This implementation additionally supports an(optional)replacecall functionality.This generic Wrapper is all the more broadly applicable, the more flexibly
these three activities can be formulated. All three decorator parameters,before,afterandreplace, can be combined with each other and
support both single callables and (nested) lists ofF-types
(imported from module decoratory.basic, seeF signaturebelow for details).
In addition,replacesupports passing a result object from successive
replacement calls through an optional keyword argument namedresultwith
a defaut value, e.g.result=None.Even without any of these arguments, such anempty wrappercan be used
tooverwritedefault values, for example.# *** example_wrapper.py - overwrite default parameter valuesfromdecoratory.wrapperimportWrapper# Case 1: Dynamic decoration with decorator arguments, onlydefsome_function(value:str="original"):print(f"value = '{value}'")# Function call with default parameterssome_function()# value = 'original'some_function=Wrapper(some_function,value="changed")some_function()# value = 'changed'The functionality ofsome_function()itself remains unchanged.
For the sake of clarity, the principle ofall or nothingis applied, i.e.
defaults must be defined for all parameters and they are only used if no
current parameters at all are transmitted. There is no mixing of current and
default parameters. Thus, even a call of the decorated function with an
incomplete parameter set is explicitly not supported and will throw aTypeError.A typical scenario for a wrapper is, of course, the execution of additional
functionality before and/or after a given functionality, which itself remains
unchanged, such asenter/leavemarkers, call data caches, runtime
measurements, etc. Here is a typical example:# *** example_wrapper.py - enclose original functionfromdecoratory.wrapperimportWrapperfromdecoratory.basicimportF# Case 2: Decoration with before and after functionalitiesdefprint_message(message:str="ENTER"):print(message)@Wrapper(before=print_message,after=F(print_message,"LEAVE"))defsome_function(value:str="original"):print(f"value = '{value}'")some_function()# ENTER# value = 'original'# LEAVEWhilebeforecallsprint_messagewith its default parameters theafterparameter uses theF-function fromdecoratory.basic.
It has a signatureF(callable, *args, **kwargs)and encapsulates the
passing of any function with optional positional and keyword parameters.
Accordingly, the keyword parameterafter=F(print_message,message="LEAVE")would also be possible.The idea behind thereplaceoption is not so much to replace the complete
original functionality, because you could simply create your own functionality
for that but to wrap the original functionality, e.g. according to the principle:Edit and/or prepare the call parameters for the original functionalityExecute the original functionality with these modified call parametersEdit and/or revise the result and return this modified resultAll this together could then look like this:# *** example_wrapper.py - enclose and replacing original function# Case 3: Decoration with replace functionalitydefreplace_wrapper(value:str="replace"):# 1. Edit the call parameters for the original functionalityvalue=value.upper()# 2. Execute original functionality with modified call parametersresult=some_function.substitute.callee(value)# (1)# 3. Edit the result and return this modified resultreturnf"result: '{result}'"@Wrapper(replace=replace_wrapper)defsome_function(value:str="original"):print(f"value = '{value}'")returnvalueresult=some_function()# value = 'REPLACE'print(result)# result: 'REPLACE'The first outputvalue = 'REPLACE'comes from the original functionsome_function()but using parameters  modified to uppercase letters
by the``replace_wrapper()``. The second lineresult: 'REPLACE'is the
result of thereturnmodified by thereplace_wrapper(). Please note
the line marked with(1)in thereplace_wrapper(): It is very
important to avoid self-recursions:Hint— Avoidance of self-recursion in the replace wrapperIn the replace wrapper, the undecorated version of the original
functionality must always be called. It is accessible via thesubstitute.calleemethod of the wrapper!For the sake of completeness, a rather more complex example illustrates
the replacement of the original functionality with a sequence of replacement
functionalities, passing aresultobject of typeintbetween
successive calls.# *** example_wrapper.py - enclose and replacing original function# Case 4: Decoration with before, after and multiple replacementsdefprint_message(message:str="UNDEFINED"):print(message)defreplacement_printer(add:int=1,*,result=None):result+=addifisinstance(result,int)else0print(f"result ={result}")returnresult@Wrapper(before=F(print,"ENTER"),# Python's print()replace=[F(replacement_printer,1,result=0),F(replacement_printer,3),F(replacement_printer,5)],after=F(print_message,"LEAVE"))defresult_printer(message:str="UNKNOWN"):print(message)result_printer()# ENTER         (before)# result = 1    (replacement_printer, 1)# result = 4    (replacement_printer, 3)# result = 9    (replacement_printer, 5)# LEAVE         (after)# 9             (output default_printer)The absence of the outputs ofUNDEFINEDandUNKNOWNreflects the
correct replacements by the decoration, and the order of execution is exactly
as expected:beforethenreplacethenafterand in each of these
variables the lists are processed in ascending order.Thedecoration of a classalways refers to the initializer of the class, e.g.# *** example_wrapper.py - class decoration@Wrapper(before=F(print,"BEFORE init"),after=F(print,"AFTER init"))classAnimal:def__init__(self,name):self.name=nameprint("RUNNING init")# Case 5: Decoration of a class always refers to __init__a=Animal(name='Teddy')# BEFORE init# RUNNING init# AFTER initFor all other methods applies:Hint— Dynamic versus static decorationDecorations to@staticmethodor@classmethodcan be done
analogously to the function decorations above, since they already exist
at compile time. Instance methods, on the other hand, do not exist until
an object instance is created and must be decorated dynamically as an
instance (e.g. seeInstance Decorationbelow).WithWrapperand custom service functions, aprivate wrapper librarycan be built and reused.# *** example_wrapper.py - private wrapper library# Case 6: Define a private wrapper librarybefore_wrapper=Wrapper(before=F(print,"BEFORE"))after_wrapper=Wrapper(after=F(print,"AFTER"))# Multiple decorations for specialized functionality encapsulation@before_wrapper@after_wrapperdefsome_function(value:str="original"):print(f"value = '{value}'")some_function()# BEFORE# value = 'original'# AFTERObserverTheobserver patternis generally used to inform one or more registered
objects (observers, subscribers, objects) about selected actions of an
observed object (observable, publisher, subject).The time of activation is set toAFTERby default, i.e. the observable
performs its own activity and then activates all registered observers in the
specified order. This setting can be adjusted to before, after, both or even
no activation at all via the parameteractivateofObservable.This implementation provides several ways to decorate a function or class
as an observable or observer.Observable DecorationObserver DecorationClass DecorationInstance DecorationObservable DecorationThe simplest and at the same time the most pythonic variant of decoration
is todecorate only the observed entities as anObservable.This is possible because all observer pattern functionalities are concentrated
in theObservable.BaseClass = BaseObservableof the observable decorator,
while theObserver.BaseClass = BaseObserverof the observer decorator
remains empty here. If necessary, it is possible to inherit from both
BaseClasses to modify their functionalities.# *** example_observer.py - observable decorationfromdecoratory.observerimportObservablefromdecoratory.basicimportFdefperson(say:str="Hello?"):print(f"{person.__name__}says '{say}'")@Observable(observers=F(person,'Hey, dog!'))defdog(act:str="Woof!"):print(f"{dog.__name__}acts '{act}'")# Case 1: Observable decoration#    ---> Person as an observer to observable dogperson()# person says 'Hello?'    (person acting)dog()# dog acts 'Woof!'        (dog acting)# person says 'Hey, dog!' (observer to dog)Obviously, the addressed observer, the person, must be declared before
the observed dog. With the simpler decoration@Observable(observers=person)the person would always respond with their
default action and say'Hello?'. The usage ofFenables the transfer
of individual parameters to the observer.Due to hierarchies in stacked observer patterns, a more detailed management
of observed vs. observing objects may be necessary.# *** example_observer.py - observable decorationdefperson(say:str="Hello?"):print(f"{person.__name__}says '{say}'")@Observable(observers=F(person,'Hey, cat!'))defcat(act:str="Meow!"):print(f"{cat.__name__}acts '{act}'")@Observable(observers=[F(cat,'Roar!'),F(person,'Hey, dog!')])defdog(act:str="Woof!"):print(f"{dog.__name__}acts '{act}'")# Case 2: Stacked observable decoration#    ---> Cat observes dog, person observes cat and dogperson()# person says 'Hello?'    (person acting)cat()# cat acts 'Meow!'        (cat acting)# person says 'Hey, cat!' (observer to cat)dog()# dog acts 'Woof!'        (dog acting)# cat acts 'Roar!'        (observer to dog)# person says 'Hey, cat!' (observer to cat)# person says 'Hey, dog!' (observer to dog)Person is an observer, but not an observable, so the call toperson()reflects only personâ€™s own activity.
Cat is an observable that is observed by person and therefore the activitycat()triggers a follow-up activity by person.
Callingdog()results in three activities at the observers, becausedog()is observed by theobserved cat, which informs the person about
its own activity.The order of reactions is determined by the order in the list in which
the cat observes the dog prior to the person. If this order is reversed:# *** example_observer.py - observable decoration@Observable(observers=[F(person,'Hey, dog!'),F(cat,'Roar!')])defdog(act:str="Woof!"):print(f"{dog.__name__}acts '{act}'")# Case 3: Stacked observable decoration#    ---> Cat observes dog, person observes dog and catdog()# dog acts 'Woof!'        (dog acting)# person says 'Hey, dog!' (observer to dog)# cat acts 'Roar!'        (observer to dog)# person says 'Hey, cat!' (observer to cat)Again, callingdog()results in three activities at the observers,
but here person reacts first as an observer to dog and later again as an
observer to cat.If this behavior is not desired,dog()can instead address theoriginal catusing thecat.substitute.callee, i.e.# *** example_observer.py - observable decoration@Observable(observers=[F(cat.substitute.callee,'Roar!'),F(person,'Hey, dog!')])defdog(act:str="Woof!"):print(f"{dog.__name__}acts '{act}'")# Case 4: Stacked observable decoration#    ---> Original cat observes dog, person observes dog and catdog()# dog acts 'Woof!'        (dog acting)# cat acts 'Roar!'        (observer to dog)# person says 'Hey, dog!' (observer to dog)In this case, cat acts before person because of the order of the observer
list and because theoriginal catobserves dog theHey, cat!statement
of person is missing.Observer DecorationIn this reversed decoration scheme, the observer decorator collects its
observables. This seems more elaborate at first glance, but some prefer to
explicitly designate theObserverandObservableroles in their code.Because an observer decoration uses observable methods, all
observable(s) must always bedeclared and decoratedbefore their
observer(s).1. Rule:DeclareObservables before Observers2. Rule:Decorating as@Observablebefore using in an@ObserverThus, the initial exampleCase 1fromObservable Decorationtranslates to:# *** example_observer.py - observer decorationfromdecoratory.observerimportObserver,Observablefromdecoratory.basicimportX@Observabledefdog(act:str="Woof!"):# 1. Rule: declare dog before person!print(f"{dog.__name__}acts '{act}'")@Observer(observables=X(dog,'Hey, dog!'))defperson(say:str="Hello?"):print(f"{person.__name__}says '{say}'")# Case 1: Observer decoration#    ---> Person as an observer to observable dogperson()# person says 'Hello?'dog()# dog acts 'Woof!'        (dog acting)# person says 'Hey, dog!' (observer to dog)The use of thesemantic cross-functionXfromdecoratory.basicinstead ofFindicates thatdogis the observable, but theXarguments apply for the observerperson.For multiple decorations, theorder of decorationis also relevant here.
The situationCase 2fromObservable Decorationwith person,
dog and cat would then look like:# *** example_observer.py - observer decoration@Observable# 2. Rule: dog before cat & persondefdog(act:str="Woof!"):# 1. Rule: dog before cat & personprint(f"{dog.__name__}acts '{act}'")@Observer(observables=X(dog,'Roar!'))@Observable# 2. Rule: observable cat before persondefcat(act:str="Meow!"):# 1. Rule: cat before personprint(f"{cat.__name__}acts '{act}'")@Observer(observables=[X(dog,'Hey, dog!'),X(cat.substitute.callee,say='Hey, cat!')])defperson(say:str="Hello?"):print(f"{person.__name__}says '{say}'")# Case 2: Stacked observer decoration#    ---> Cat observes dog, person observes cat and dogperson()# person says 'Hello?'    (person acting)cat()# cat acts 'Meow!'        (cat acting)# person says 'Hey, cat!' (observer to cat)dog()# dog acts 'Woof!'        (dog acting)# cat acts 'Roar!'        (observer to dog)# person says 'Hey, cat!' (observer to cat)# person says 'Hey, dog!' (observer to dog)Here, cat becomes an observer but its calleecat.substitute.calleeis an
observable which can be observed by person! Thisobserved catobserves
the dog, reacts and triggers the person.To reproduceCase 4from above, simply swap the order of the decorations
at the cat and the dog then is observed by theoriginal cat.# *** example_observer.py - observer decoration@Observable# 2. Rule: dog before cat & persondefdog(act:str="Woof!"):# 1. Rule: dog before cat & personprint(f"{dog.__name__}acts '{act}'")@Observable# 2. Rule: cat before person@Observer(observables=X(dog,'Roar!'))defcat(act:str="Meow!"):# 1. Rule: cat before personprint(f"{cat.__name__}acts '{act}'")@Observer(observables=[X(dog,'Hey, dog!'),X(cat,say='Hey, cat!')])defperson(say:str="Hello?"):print(f"{person.__name__}says '{say}'")# Case 3: Stacked observer decoration#    ---> Cat observes dog, person observes cat and dogperson()# person says 'Hello?'    (person acting)cat()# cat acts 'Meow!'        (cat acting)# person says 'Hey, cat!' (observer to cat)dog()# dog acts 'Woof!'        (dog acting)# cat acts 'Roar!'        (observer to dog)# person says 'Hey, dog!' (observer to dog)Now, both dog and cat end up being observables, observed by the person. But the
cat observing the dog is theoriginal cat, which does not inform the person
about its activities, and so personâ€™s statementHey, cat!is missing.Class DecorationBoth techniques,Observable DecorationandObserver Decoration,
are static, in the sense, decorations are done e.g. in @-notation evaluated
at compile time. They are applied tostatic functions.Decoration of a classby default addresses decoration of theclass initializer, this means@ObservableclassDog:def__init__(self):pass# Some code ...should be understood asclassDog:@Observabledef__init__(self):pass# Some code ...But this behavior is insidious, e.g.# *** example_observer.py - class decorationfromdecoratory.observerimportObservableclassPerson:def__init__(self,name:str="Jane Doe"):print(f"{name}arrived.")@Observable(observers=Person)classDog:def__init__(self,name:str="Teddy"):print(f"Dog{name}arrived.")# Case 1: Dog is an observable to Personprs=Person()# Jane Doe arrived.dog=Dog()# Dog Teddy arrived.# Jane Doe arrived.The instantiation ofDoginduced an instantiation ofPerson.Hint— Take care when decorating a class initializerNotifying the__init__method of an observer results in a new
instance! This means calling the observable induces instantiation of
a new observer object, surely in not any case this is the desired
behavior …So the decoration should not address a class but one (or more) target
methods of the class. As already mentioned, this is easy if this callback
function is a@staticmethodor@classmethod.# *** example_observer.py - class decorationclassPerson:def__init__(self,name:str="Jane Doe"):print(f"{name}arrived.")@staticmethoddefaction1(act:str="Hello?"):print(f"Person says{act}")@classmethoddefaction2(cls,act:str="What's up?"):print(f"Person says{act}")@Observable(observers=[Person.action1,Person.action2])classDog:def__init__(self,name:str="Teddy"):print(f"Dog{name}arrived.")# Case 2: Dog is an observable to Person.actionprs=Person()# Jane Doe arrived.dog=Dog()# Dog Teddy arrived.# Person says Hello?# Person says What's up?This is how it usually works:one action of the observable, here it’s
the instantiation ofDog, triggersone to many actions at each observer,
herePerson.But often an instance method is also interesting as a callback function:If aparticular instanceprs =Person(name="JohnDoe")of a
person is meant, a decoration like@Observable(observers=prs.action)with theinstance methodcan be applied toDog.Forany instanceof a person@Observable(observers=Person().action)works.Even a list ofFstructures would be possible to optionally submit
different parameters.# *** example_observer.py - class decorationfromdecoratory.observerimportObservablefromdecoratory.basicimportFclassPerson:def__init__(self,name:str="Jane Doe"):self.name=nameprint(f"{name}arrived.")defaction(self,act:str="Hello?"):print(f"{self.name}says{act}")prs1=Person()# Jane Doe arrived.prs2=Person("John Doe")# John Doe arrived.@Observable(observers=[prs1.action,F(prs2.action,"What's up?")])classDog:def__init__(self,name:str="Teddy"):print(f"Dog{name}arrived.")# Case 3: Dog is an observable to actions of various person instances.dog=Dog()# Dog Teddy arrived.# Jane Doe says Hello?# John Doe says What's up?But here,one action of the observable, the instantiation ofDog, triggersone to many actions at each selected resp. instantiated observer,Person.
In such situations, a latedynamic decorationcould be a good idea.So far, instantiatingDogresulted in an information and induced
action atPerson. IfDoghas its own actions that need to be
selectively monitored, each of the selected actions can of course be decorated
individually as anObservable. For the sake of a better overview, this
can also be done on the class itself.# *** example_observer.py - class decorationclassPerson:def__init__(self,name:str="Jane Doe"):self.name=nameprint(f"{name}arrived.")@classmethoddefactionA(cls,act:str="Hello?"):print(f"Person says{act}")defactionB(self,act:str="Hello?"):print(f"{self.name}says{act}")@Observable(methods=["action1","action2"],observers=[Person.actionA,Person("Any Doe").actionB])classDog:def__init__(self,name:str="Teddy"):self.name=nameprint(f"Dog{name}arrived.")@staticmethoddefaction1(act:str="Woof!"):print(f"Dog acts{act}")defaction2(self,act:str="Brrr!"):print(f"{self.name}acts{act}")# Case 4: Dog is an observable with selected actions.# Any Doe arrived.prs=Person()# Jane Doe arrived.dog=Dog()# Dog Teddy arrived.dog.action1()# Dog acts Woof!        (@staticmethod)# Person says Hello?    (@classmethod)# Any Doe says Hello?   (Instance 'Any')Dog.action2(dog)# Teddy acts Brrr!      (Instance 'Teddy')# Person says Hello?    (@classmethod)# Any Doe says Hello?   (Instance 'Any')The last lineDog.action2(dog)provides the instance ofTeddyasthe
first argumentself. This works because internally theclass methodDog.action2was registered instead of an instance method that didn’t
exist at compile time. On the other hand, the calldog.action2()would
fail because thisinstance methodwas not registered. But, if this is what
is to be achieved, an instance method can first be created and registered,
just as seen above inClass Decoration, Case 3.Instance DecorationThe classic way to exchange information between objects with the observer
pattern is through the active use of theregister,dispatch, andunregisterinterface methods that an observable exposes. Information can
be given to the right recipients at relevant places in the code. For this,
the classes are not decorated anddynamic decorationcomes into play. Dynamic decoration is used often also in connection with
getter/setter/property constructions since data changes take place
meaningfully over these methods.Consider the following two example classes:# *** example_observer.py - instance decorationclassNote:# Observer without decoration!definfo(self,thing):print(f"Note.info: val ={thing.a}")classThing:# Observable without decoration!def__init__(self,a=0):# Initializer, defining variabe 'a'self._a=adefinc(self):# Instance method, modifying 'a'self._a+=1defget_a(self):# Getter, setter, property,returnself._a# modifying variable 'a'defset_a(self,value):self._a=valuea=property(get_a,set_a)Initially, all these classes are undecorated and typical actions might be:# *** example_observer.py - instance decorationfromdecoratory.observerimportObservablefromdecoratory.basicimportF# (1) Setup instancesnti=Note()# Note instancetgi=Thing()# Thing instance# (2) Dynamic decoration of some methods: Late bindingtgi.inc=Observable(tgi.inc)# Late method decorationThing.set_a=Observable(Thing.set_a)# Late property decorationThing.a=property(Thing.get_a,Thing.set_a)# (3) Register the observer (Note) with the observable (Thing)tgi.inc.observable.register(F(nti.info,tgi))tgi.set_a.observable.register(F(nti.info,thing=tgi))# Case 1: Change self.a = 0 using inc()tgi.inc()# Note.info: val = 1# Case 2: Change self.a = 1 using setter via propertytgi.a=2# Note.info: val = 2# Case 3: Notification from inc() to nti.info() about Thing(3)tgi.inc.observable.dispatch(nti.info,Thing(3))# Note.info: val = 3# Case 4: Notification from set_a() to nti.info() about Thing(4)tgi.set_a.observable.dispatch(nti.info,Thing(4))# Note.info: val = 4# Case 5: Print the current value of tgi.aprint(f"a ={tgi.a}")# a = 2     (no changes by notification)# Case 6: Print list of all observersprint(tgi.inc.observable.observers(classbased=True))# ---> {'Note': ['F(info, <__main__.Thing object at ..)']}print(tgi.set_a.observable.observers(classbased=True))# ---> {'Note': ['F(info, thing=<__main__.Thing object at ..)']}# Case 7: Unregister nti.info from tgitgi.inc.observable.unregister(nti.info)print(tgi.inc.observable.observers(classbased=True))# {}In contrast toClass Decoration, thisInstance Decorationinstantiates the native classes (1), thendecorates the relevant instance methods (2), and thenregisters the observers with the associated observables (3).This method of instance decoration is certainly the most flexible.
However, it bears the risk of losing track of all dependencies.~~~Home~~~Contents~~~Singleton~~~Multiton~~~Wrapper~~~Observer~~~