Skip to content

Instantly share code, notes, and snippets.

@dubslow
Last active December 15, 2019 16:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dubslow/b8996308fc6af2437bef436fa28e86fa to your computer and use it in GitHub Desktop.
Save dubslow/b8996308fc6af2437bef436fa28e86fa to your computer and use it in GitHub Desktop.
Python recipe to "automatically delegate method calls to a proxy attribute", aka "customized inheritance"
# Following class and decorator totally jacked from Jacob Zimmerman, with a few tweaks/renames
# https://programmingideaswithjake.wordpress.com/2015/05/23/python-decorator-for-simplifying-delegate-pattern/
# (I couldn't get the default copying of the entire baseclass dict to work, because that delegates __new__,
# which chokes on the non-subclass subclass. So just remove it for now)
# What this does: allows you to "subclass" an arbitrary baseclass by delegating certain methods/fields on
# your 'subclass' to the delegate-attribute without boilerplate for each individual method/field
# Equally valid interpretation of the same semantics: "subclass" a baseclass, but allows you to exclude
# certain methods from being used on the "subclass".
# Possible use cases include: making a read-only/immutable "subclass" of the builtin/superspeed dict
# Usage: define a attribute name on your subclass that is used as the proxy/delegator, and pass the methods
# you want available (or ignored) from the proxy on the main class into the decorator. See example below.
# (Your subclass is in charge of ensuring the delegator attribute exists and is a suitable instance of the
# baseclass, but everything else is automatic)
###########################################################################################################
class _DelegatedAttribute:
def __init__(self, delegator_name, attr_name, baseclass):
self.attr_name = attr_name
self.delegator_name = delegator_name
self.baseclass = baseclass
def __get__(self, instance, klass):
if instance is None:
# klass.DelegatedAttr() -> baseclass.attr
return getattr(self.baseclass, self.attr_name)
else:
# instance.DelegatedAttr() -> instance.delegate.attr
return getattr(self.delegator(instance), self.attr_name)
def __set__(self, instance, value):
# instance.delegate.attr = value
setattr(self.delegator(instance), self.attr_name, value)
def __delete__(self, instance):
delattr(self.delegator(instance), self.attr_name)
def delegator(self, instance):
# minor syntactic sugar to help remove "getattr" spam (marginal utility)
return getattr(instance, self.delegator_name)
def __str__(self):
return ""
def custom_inherit(baseclass, delegator='delegate', include=None, exclude=None):
'''A decorator to customize inheritance of the decorated class from the
given baseclass. `delegator` is the name of the attribute on the subclass
through which delegation is done; `include` and `exclude` are a whitelist
and blacklist of attrs to include from baseclass.__dict__, providing the
main customization hooks.'''
# `autoincl` is a boolean describing whether or not to include all of baseclass.__dict__
# turn include and exclude into sets, if they aren't already
if not isinstance(include, set):
include = set(include) if include else set()
if not isinstance(exclude, set):
exclude = set(exclude) if exclude else set()
# delegated_attrs = set(baseclass.__dict__.keys()) if autoincl else set()
# Couldn't get the above line to work, because delegating __new__ fails miserably
delegated_attrs = set()
attributes = include | delegated_attrs - exclude
def wrapper(subclass):
## create property for storing the delegate
#setattr(subclass, delegator, None)
# ^ Initializing the delegator is the duty of the subclass itself, this
# decorator is only a tool to create attrs that go through it
# don't bother adding attributes that the class already has
attrs = attributes - set(subclass.__dict__.keys())
# set all the attributes
for attr in attrs:
setattr(subclass, attr, _DelegatedAttribute(delegator, attr, baseclass))
return subclass
return wrapper
###########################################################################################################
# Example time!
# Create a read-only builtin dict by only delegating the immutable methods
@custom_inherit(dict, delegator='_dict_proxy',
include=['__len__', '__getitem__', '__contains__', 'get', 'items', 'keys', 'values', '__str__'])
class ImmutableCDict:
def __init__(self, *args, other=None, **kwargs):
if other:
self._dict_proxy = other.copy()
else:
self._dict_proxy = dict(*args, **kwargs)
# attr matches name passed to decorator
def my_other_custom_behaviors(self, *args, **kwargs):
pass
# Use:
'''
>>> d = ImmutableCDict([('a', 1), ('b', 2)], c=3, d=4)
>>> d
<__main__.ImmutableCDict object at 0x7f4db36c3748>
>>> print(d)
{'b': 2, 'a': 1, 'd': 4, 'c': 3}
>>> print(d.keys())
dict_keys(['b', 'a', 'd', 'c'])
>>> print(d.values())
dict_values([2, 1, 4, 3])
>>> 'c' in d
True
>>> 'e' in d
False
>>> d['b'] *= 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'ImmutableCDict' object does not support item assignment
>>> d['e'] = object()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'ImmutableCDict' object does not support item assignment
>>> 2 * d['d']
8
>>> d.get('a')
1
>>> d.get('e', None)
>>> d.pop('c')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'ImmutableCDict' object has no attribute 'pop'
>>> d.update({'e': object()})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'ImmutableCDict' object has no attribute 'update'
>>> d._dict_proxy['e'] = object() # The private proxy is still usable
>>> d['e']
<object object at 0x7f4db37d50f0>
>>> d.my_other_custom_behaviors()
>>> isinstance(d, dict) # No good way to "fix" this, but also it's not really true either, dict methods fail on d
False
'''
@fish2000
Copy link

Hey if you want things like isinstance(dict, baseclass) to work, you perhaps can do something apropos:

collections.abc.MutableMapping.register(baseclass)

… in the wrapper function before you return. As your current setup stands, you’d have to pass in the collections.abc.MutableMapping bit alongside baseclass in the decorator initializer, or retool things around the collections.abc type tower – or implement your own __subclasshook__ logic (which the latter would probably be the most fun). Indeed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment