Last active
December 15, 2019 16:53
-
-
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"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
''' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey if you want things like
isinstance(dict, baseclass)
to work, you perhaps can do something apropos:… in the wrapper function before you
return
. As your current setup stands, you’d have to pass in thecollections.abc.MutableMapping
bit alongsidebaseclass
in the decorator initializer, or retool things around thecollections.abc
type tower – or implement your own__subclasshook__
logic (which the latter would probably be the most fun). Indeed!