How to automate __special_methods__ delegation in Python?

Let spam be an instance of some spam class and suppose spam.ham is an object of some built-in type, for example, dict . Although spam not a subclass of dict , I would like its instances to have the same API as a regular dict (i.e. the same methods with the same signatures), but I want to not type bazillion template form methods:

  def apimethod(self, this, that): return self.ham.apimethod(this, that) 

I tried the following:

 class Spam(object): def __init__(self): self.ham = dict() def __getattr__(self, attr): return getattr(self.ham, attr) 

... but it works for "regular" methods like keys and items , but not for special methods like __setitem__ , __getitem__ and __len__ :

 >>> spam = Spam() >>> spam.keys() [] >>> spam['eggs'] = 42 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Spam' object does not support item assignment >>> spam.ham['eggs'] = 42 >>> foo.items() [('eggs', 42)] >>> spam['eggs'] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Spam' object is not subscritable >>> len(spam) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Spam' object has no len() 


All the special methods that I tried gave similar errors.

How can I automate the definition of special methods (so that they get a link to the delegate)?

Clarification: I'm not necessarily looking for solutions that use a standard method search sequence. My goal is to minimize boilerplate code.

Thanks!

+6
source share
4 answers

This may not be very useful if you need a solution that also prohibits metaclasses, but here is the solution I came across:

 def _wrapper(func): def _wrapped(self, *args, **kwargs): return getattr(self.ham, func)(*args, **kwargs) return _wrapped class DictMeta(type): def __new__(cls, name, bases, dct): default_attrs = dir(object) for attr in dir(dict): if attr not in default_attrs: dct[attr] = _wrapper(attr) return type.__new__(cls, name, bases, dct) class Spam(object): __metaclass__ = DictMeta def __init__(self): self.ham = dict() 

It seems you need to do what you are looking for:

 >>> spam = Spam() >>> spam['eggs'] = 42 >>> spam.items() [('eggs', 42)] >>> len(spam) 1 >>> spam.ham {'eggs': 42} 

If on Python 3.x use class Spam(object, metaclass=DictMeta) and remove the __metaclass__ line from the Spam body.

+5
source

It looks like work for ... a metaclass!

 def make_method(p, m): def method(self, *a, **k): return getattr(getattr(self, p),m)(*a, **k) return method class Proxier(type): def __new__(cls, name, bases, dict): objs = dict.get('proxyobjs', []) if objs: old_init = dict.get('__init__', lambda self: None) def new_init(self, *a, **k): for (n,v) in objs.iteritems(): setattr(self, n, v()) old_init(self, *a, **k) dict['__init__'] = new_init meths = dict.get('proxymethods', {}) for (proxyname, methnames) in meths.iteritems(): for methname in methnames: dict[methname] = make_method(proxyname, methname) return super(Proxier, cls).__new__(cls, name, bases, dict) class Spam(object): __metaclass__ = Proxier proxyobjs = {'ham': dict, 'eggs': list, } proxymethods = {'ham': ('__setitem__', '__getitem__', '__delitem__'), 'eggs': ('__contains__', 'append') } 

It works!

 In [28]: s = Spam() In [29]: s[4] = 'hi' In [30]: s.append(3) In [31]: 3 in s Out[31]: True In [32]: 4 in s Out[32]: False In [33]: s[4] Out[33]: 'hi' 

Note that you must indicate which parts of the interface you are using (otherwise, why not just inherit?). So we have __contains__ from list and __getitem__ from dict , and __iter__ from none. (And there is only one way to change the base list using append , but not extend or __delitem__ .) So (like Martian), I'm not sure how useful this is.

+1
source

Attribute access for special methods does not obey normal attribute access rules, basically these methods MUST exist at the class level, read http://docs.python.org/reference/datamodel.html#special-method-lookup-for-new -style-classes

So, you need to add all these methods manually or add them to the class programmatically, and the best way to do this is through the metaclass. Also note that I am not adding all methods to the dict , but only special methods, because rest can be easily redirected through __getattr__

 def redirect(methodname): def _redirect(self, *args, **kwargs): print "redirecting",methodname method = getattr(self.ham, methodname) return method(*args, **kwargs) return _redirect class DictRedirect(object): def __new__(cls, name, bases, attrs): # re-create all special methods from dict dict_attr_names = set(dir(dict)) common_names = set(dir(cls)) for methodname in dict_attr_names-common_names: if not methodname.startswith('__'): continue attrs[methodname] = redirect(methodname) return type(name, bases, attrs) class Spam(object): __metaclass__ = DictRedirect def __init__(self): self.ham = dict() def __getattr__(self, name): return getattr(self.ham, name) spam = Spam() spam['eggs'] = 'yolk' print 'keys =',spam.keys() print spam['eggs'] 

exit:

 redirecting __setitem__ keys = ['eggs'] redirecting __getitem__ yolk 

Disclaimer: IMO is too much magic and should be avoided except as fun :)

+1
source

Not sure if __getattribute__ will help, but the reason is that special methods are looked up in the class and not in the instance: http://docs.python.org/reference/datamodel.html#special-method-lookup-for-new- style-classes , such as, for example, you need to look for special methods like __getattr__ and __getattribute__ .

The proxy, as it seems, is asking me problems without thinking carefully, for example, how things should look like __dict__ and __class__ , and about possible method conflicts if your wrapper has some methods and, of course, there are other problems .

Re: is-a vs. has-a:

If you just duplicate the entire interface of the contained element, for me it looks like an anti-template, like what inheritance is for. What if you have two - relations to two dict objects?

In is related, it usually selects useful methods that often export them under different names to make a reasonable API. So instead of Spam.append(item) , you will have Spam.addBot(bot) .

0
source

Source: https://habr.com/ru/post/903960/


All Articles