Pytilities delegation utilities help in applying the concept of delegation to your code, it makes delegation less tedious so you are less tempted to use inheritance (delegation tends to be more flexible, but most languages have better support for easy inheriting rather than easy delegating).
Delegation is done using the DelegationAspect, its usage and use is best shown by example: Imagine you are making a game. You have a Player, an Enemy and a Box class. These all have health, can take damage and can be destroyed. We decide to centralise this code in a Destructible class. As Player, Enemy and Box will have other characteristics as well we decide not to simply inherit from the Destructible but give ourselves the extra flexibility by giving each of them a Destructible instance and delegate calls on the public object to it.
Normally, you’d have to write:
class Player(object):
def __init__(self):
self._destructible = Destructible()
def take_damage(self, amount):
self._destructible.take_damage(amount)
@getter
def health(value):
return self._destructible.health
@health.setter
def health(self, value):
self._destructible.health = value
# repeat above code for Enemy and Box
When Destructible’s interface changes, you have to change this code too... how tedious. With DelegationAspect you can narrow it down to:
from pytilities.delegation import DelegationAspect
class Player(object):
def __init__(self):
self._destructible = Destructible()
delegation_aspect = DelegationAspect(property(lambda s: s._destructible),
('take_damage', 'health'))
delegation_aspect.apply(Player)
# do the same for Enemy and Player, note you can reuse the aspect for all 3
Using DelegationAspect, the code is more maintainable, and still readable even though it’s shorter.
The first parameter to DelegationAspect is a descriptor that returns the object to which to delegate. The descriptor is passed the instance on which it is ran (in this case: a player instance).
The second parameter specifies the names of the attributes to delegate. Instead of an iterable of names, you can provide a ‘*’ to indicate all attributes should be delegated.
DelegationAspect‘s has a second overload with as second parameter a Mapping of (access, source_attribute_name) to target_attribute_name’s. This allows for advanced delegation, especially in combination with pytilities.dictionary.FunctionMap. With this you can delegate only ‘get’ access, all attributes that start with ‘a’ (to give a silly example), delegate to from source attribute ‘x’ to target attribute ‘y’.
Access indicates on which types of access delegation should occur. Possible values are:
In order to delegate to differently named attributes:
# Note: Rectangle and Vector implementation omitted
mapping = {
('get', 'left') : 'x',
('set', 'left') : 'x',
('delete', 'left') : 'x',
('get', 'top') : 'y',
('set', 'top') : 'y',
('delete', 'top') : 'y',
}
delegation_aspect = DelegationAspect(property(lambda s: s._top_left),
mapping)
delegation_aspect.apply(Rectangle)
top_left = Vector(x=1, y=2)
bottom_right = Vector(x=3, y=4)
r = Rectangle(top_left, bottom_right)
r.top # returns 2
r.left # returns 1
In order to delegate all attributes that start with an ‘a’:
from pytilities.delegation import DelegationAspect
from pytilities.dictionary import FunctionMap
def mapper(key):
access, source_attribute_name = key
if source_attribute_name.startswith('a'):
# delegate to attrib with same name on target
return source_attribute_name
return None # do not delegate
mapping = FunctionMap(mapper)
delegation_aspect = DelegationAspect(
property(lambda s: s._target), mapping,
True) # undefined_keys: set to True if mapping can't return a valid set
# of keys, as is the case with FunctionMap
Instead of specifying the attributes to delegate directly, you can use the mapped and mapped_class decorator to generate it for you.
Here’s an example:
from pytilities.delegation import mapped_class, mapped
# mapped_class is required by the mapped decorator (it just doesn't work
# without it)
@mapped_class
class Dispatcher(object): # an event dispatcher (cfr observer pattern)
# fill mappings with empty dicts (any MutableMapping will do)
public_mapping = {}
protected_mapping = {}
def __remove_handlers(self, event, owner):
'omitted code'
def foo(self):
'omitted code'
@mapped(public_mapping)
def add_handler(self, event_name, handler, owner = None):
'omitted code'
@mapped(protected_mapping)
def dispatch(self, event_name, *args, **keyword_args):
'omitted code'
@mapped(public_mapping)
def remove_handlers(self, event_name=None, owner=None):
'omitted code'
# the 'get', 'set' is the access to delegate on. It wasn't necessary in
# this case though, just showing it can be done
@mapped(public_mapping, 'get', 'set')
def remove_handler(self, event_name, handler, owner = None):
'omitted code'
@mapped(public_mapping)
def event(self, event_name, owner = None):
'omitted code'
@mapped(public_mapping)
def has_event(self, event_name):
'omitted code'
@mapped(public_mapping)
@property
def events(self):
'omitted code'
@mapped(protected_mapping)
def register_events(self, *event_names):
'omitted code'
# include all mappings of public_mapping in protected_mapping
Dispatcher.protected_mapping.update(Dispatcher.public_mapping)
# value of public_mapping:
# {
# ('get', 'add_handler'): 'add_handler',
# ('set', 'add_handler'): 'add_handler',
# ('delete', 'add_handler'): 'add_handler',
#
# ('get', 'remove_handlers'): 'remove_handlers',
# ('set', 'remove_handlers'): 'remove_handlers',
# ('delete', 'remove_handlers'): 'remove_handlers',
#
# ('get', 'remove_handler'): 'remove_handler'
# ('set', 'remove_handler'): 'remove_handler',
#
# ('get', 'event'): 'event',
# ('set', 'event'): 'event',
# ('delete', 'event'): 'event',
#
# ('get', 'has_event'): 'has_event',
# ('set', 'has_event'): 'has_event',
# ('delete', 'has_event'): 'has_event',
#
# ('get', 'events'): 'events',
# ('set', 'events'): 'events',
# ('delete', 'events'): 'events',
# }
# value of protected_mapping:
# {
# ('get', 'add_handler'): 'add_handler',
# ('set', 'add_handler'): 'add_handler',
# ('delete', 'add_handler'): 'add_handler',
#
# ('get', 'dispatch'): 'dispatch',
# ('set', 'dispatch'): 'dispatch',
# ('delete', 'dispatch'): 'dispatch',
#
# ('get', 'remove_handlers'): 'remove_handlers',
# ('set', 'remove_handlers'): 'remove_handlers',
# ('delete', 'remove_handlers'): 'remove_handlers',
#
# ('get', 'remove_handler'): 'remove_handler'
# ('set', 'remove_handler'): 'remove_handler',
#
# ('get', 'event'): 'event',
# ('set', 'event'): 'event',
# ('delete', 'event'): 'event',
#
# ('get', 'has_event'): 'has_event',
# ('set', 'has_event'): 'has_event',
# ('delete', 'has_event'): 'has_event',
#
# ('get', 'events'): 'events',
# ('set', 'events'): 'events',
# ('delete', 'events'): 'events',
#
# ('get', 'register_events'): 'register_events',
# ('set', 'register_events'): 'register_events',
# ('delete', 'register_events'): 'register_events',
# }
Public_mapping and protected_mapping are valid mappings to pass to a DelegationAspect:
delegation_aspect = DelegationAspect(property(lambda s: s._dispatcher),
Dispatcher.public_mapping)
One of the added benefits is that you’ll only ever have to change the mappings on your class when delegation needs to change, as opposed to changing any class that delegates to it.
Note that the keys of the generated mappings could also be used as keys for advice to give in AOP Aspects. (Just replace the values with advice)