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.
The most basic tool to help you is the DelegationAspect, it’s 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 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
With DelegationAspect you can narrow it down to:
from pytilities.delegation import DelegationAspect
class Player(object):
def __init__(self):
self._destructible = Destructible()
# make a new Aspect that will delegate any access to take_damage and health
# to the object it is applied to, to the _destructible attribute on that
# class
#
# first parameter: a descriptor that returns the object to delegate to
# second parameter: which attributes to delegate
delegation_aspect = DelegationAspect(AttributeDescriptor('_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.
Note: For those wondering, AttributeDescriptor and various other special descriptors will be explained in the next part of the guide.
Instead of specifying the attributes to delegate directly, you may want to use the in_profile and profile_carrier decorators to generate delegation Profile instances for you instead. A Profile is roughly equivalent to specifying a group of attributes (it’s actually far more powerful).
A lengthy example to explain the decorators:
from pytilities.delegation import profile_carrier, in_profile
# profile_carrier is required by in_profile decorators,
# it stores generated profiles in Destructible.attribute_profiles
# which is a dict of profile name to Profile instance
@profile_carrier
class Dispatcher(object): # an event dispatcher (cfr observer pattern)
# not included in any profile
def __remove_handlers(self, event, owner):
pass # omitted actual code for clarity
# not included in any profile
def foo(self):
pass # omitted actual code for clarity
# included in "public" profile
@in_profile("public")
def add_handler(self, event_name, handler, owner = None):
pass # omitted actual code for clarity
# included in "default" profile
@in_profile()
def dispatch(self, event_name, *args, **keyword_args):
pass # omitted actual code for clarity
@in_profile("public")
def remove_handlers(self, event_name=None, owner=None):
pass # omitted actual code for clarity
@in_profile("public")
def remove_handler(self, event_name, handler, owner = None):
pass # omitted actual code for clarity
@in_profile("public")
def event(self, event_name, owner = None):
pass # omitted actual code for clarity
@in_profile("public")
def has_event(self, event_name):
pass # omitted actual code for clarity
@in_profile("public")
@property
def events(self):
pass # omitted actual code for clarity
@in_profile()
def register_events(self, *event_names):
pass # omitted actual code for clarity
# advanced profile usage:
profiles = Dispatcher.attribute_profiles
# have default include all attributes of public
# if we didn't, default would only contain 'dispatch' and 'register_events'
profiles['default'] |= profiles['public']
# Profiles support most set operators
With all the above defined you could now make one of your classes look like an event dispatcher, e.g.:
from pytilities.delegation import DelegationAspect
class Player(object):
def __init__(self):
self._dispatcher = Dispatcher()
delegation_aspect = DelegationAspect(AttributeDescriptor('_destructible'),
Dispatcher.attribute_profiles['public'])
delegation_aspect.apply(Player)
player = Player()
player.events # this now returns all events that player might send out
player.dispatch('walked') # this will give an attribute not found error as
# it's not included in the public profile
Profiles also allow you to delegate to different named attributes, e.g.:
# in this example we'll try to map the top left coordinates of a rectangle to
# a vector (x,y coord)
class Rectangle(object):
# has a Vector instance for its top left (and another for its bottom
# right)
class Vector(object):
# has x, y properties
profile = Profile()
profile.add_mappings('rwd', **{'left' : 'x', 'top' : 'y'})
delegation_aspect = DelegationAspect(AttributeDescriptor('top_left'), profile)
delegation_aspect.apply(Rectangle)
r = Rectangle()
r.top = 1 # this now sets its top_left.y to 1
Profile also allows you to map just read access, or write access, or delete access, e.g.:
# The following example is rather abstract;
# to be honest, I forgot why you might need this
class Delegate(object):
@getter
def x(self):
print('delegated')
class Vector(object):
def x(self):
self.delegate = Delegate()
self._x = 0
@getter
def x(self):
print('original')
return self._x
@x.setter
def x(self, value):
print('original')
self._x = value
profile = Profile()
# delegate only read access
profile.add_mappings('r', 'x') # note you can also use in_profile(modifiers='r')
delegation_aspect = DelegationAspect(AttributeDescriptor('delegate'), profile)
delegation_aspect.apply(Vector)
v = Vector()
print(v.x) # prints delegated
v.x = 1 # prints original