Adding modified methods from another class without subclassing

J

John O'Hagan

I have a class like this:

class MySeq():
def __init__(self, *seq, c=12):
self.__c = c
self.__pc = sorted(set([i % __c for i in seq]))
self.order = ([[self.__pc.index(i % __c), i // __c] for i in seq])
#other calculated attributes

@property
def pitches(self):
return [self.__pc[i[0]] + i[1] * self.__c for i in self.order]

#other methods

The "pitches" attribute initially reconstructs the "seq" arguments but can be modified by writing to the "order" attribute.

The "pitches" attribute represents the instances and as such I found myself adding a lot of methods like:


def __getitem__(self, index):
return self.pitches[index]

def __len__(self):
return len(self.pitches)

def __iter__(self):
return iter(self.pitches)

def __repr__(self):
return str(self.pitches)

and so on, and calling a lot of list methods on the "pitches" attribute of MySeq instances elsewhere. I thought of making MySeq a subclass of list with "pitches" as its contents, but then I would have had to override a lot of methods case-by-case, for example to ensure that any alterations to "pitches" were reflected in the other calculated attributes.

So I wrote this function which takes a method, modifies it to apply to an instance attribute, and takes care of any quirks:

def listmeth_to_attribute(meth, attr):
def new_meth(inst, *args):
#ensure comparison operators work:
args = [getattr(i, attr) if isinstance(i, inst.__class__)
else i for i in args]
reference = getattr(inst, attr)
test = reference[:]
result = meth(test, *args)
#ensure instance is reinitialised
#if attribute has been changed:
if test != reference:
inst.__init__(*test)
#ensure slices are of same class
if isinstance(result, meth.__objclass__):
result = inst.__class__(*result)
return result
return new_meth

and this decorator to apply this function to all the list methods and add them to MySeq:

def add_mod_methods(source_cls, modfunc, modfunc_args, *overrides):
"""Overides = any methods in target to override from source"""
def decorator(target_cls):
for name, meth in vars(source_cls).items():
if name not in dir(target_cls) or name in overrides:
setattr(target_cls, name, modfunc(meth, *modfunc_args))
return target_cls
return decorator

a kind of DIY single inheritance, used like this:

@add_mod_methods(list, listmeth_to_attribute, ('pitches',), '__repr__')
class MySeq():
......

Now I can call list methods transparently on MySeq instances, like subclassing but without all the overriding. If this works it will simplify a lot of code in my project.

But the fact that I haven't seen this approach before increases the likelihood it may not be a good idea. I can almost hear the screams of "No, don't do that!" or the sound of me slapping my forehead when someone says "Why don't you just...". So before I put it in, I'd appreciate any comments, warnings, criticisms, alternatives etc..

Regards,

John
 
S

Steven D'Aprano

The "pitches" attribute represents the instances and as such I found
myself adding a lot of methods like:

def __getitem__(self, index):
return self.pitches[index]

def __len__(self):
return len(self.pitches)


Looks like a call for (semi-)automatic delegation!

Try something like this:


# Untested
class MySeq(object):
methods_to_delegate = ('__getitem__', '__len__', ...)
pitches = ... # make sure pitches is defined
def __getattr__(self, name):
if name in self.__class__.methods_to_delegate:
return getattr(self.pitches, name)
return super(MySeq, object).__getattr__(self, name)
# will likely raise AttributeError



But the fact that I haven't seen this approach before increases the
likelihood it may not be a good idea. I can almost hear the screams of
"No, don't do that!"

The general technique is called delegation and is perfectly legitimate.

http://c2.com/cgi/wiki?WhatIsDelegation
http://en.wikipedia.org/wiki/Delegation_(programming)
 
J

John O'Hagan

The "pitches" attribute represents the instances and as such I found
myself adding a lot of methods like:

def __getitem__(self, index):
return self.pitches[index]

def __len__(self):
return len(self.pitches)


Looks like a call for (semi-)automatic delegation!

Try something like this:


# Untested
class MySeq(object):
methods_to_delegate = ('__getitem__', '__len__', ...)
pitches = ... # make sure pitches is defined
def __getattr__(self, name):
if name in self.__class__.methods_to_delegate:
return getattr(self.pitches, name)
return super(MySeq, object).__getattr__(self, name)
# will likely raise AttributeError

Thanks, this looks promising. I didn't know about __getattr__ or delegation. This example doesn't seem to work as is for special methods beginning with "__" (e.g.: "TypeError: object of type 'MyList' has no len()"). It seems that __getattr__ is not called for special methods. Also, it doesn't immediately suggest to me a way of modifying method calls (maybe __setattr__?). But it's certainly a neater way to get methods to operate on the attribute. I'm looking into it, and delegation generally.

However, I don't understand what the super call is doing. If the method isn't delegated, shouldn't it just fall back to getattr(self, name)?

Thanks and regards,

John
 
J

John O'Hagan

On Mon, 22 Aug 2011 15:27:36 +1000
Looks like a call for (semi-)automatic delegation!

Try something like this:


# Untested
class MySeq(object):
methods_to_delegate = ('__getitem__', '__len__', ...)
pitches = ... # make sure pitches is defined
def __getattr__(self, name):
if name in self.__class__.methods_to_delegate:
return getattr(self.pitches, name)
return super(MySeq, object).__getattr__(self, name)
# will likely raise AttributeError
[..]

However, I don't understand what the super call is doing. If the method isn't delegated, shouldn't it just fall back to getattr(self, name)?

On reading the __getattr__ docs properly, I see that AttributeError is what should generally happen.
 
S

Steven D'Aprano

# Untested
class MySeq(object):
methods_to_delegate = ('__getitem__', '__len__', ...)
pitches = ...  # make sure pitches is defined
def __getattr__(self, name):
if name in self.__class__.methods_to_delegate:
return getattr(self.pitches, name)
return super(MySeq, object).__getattr__(self, name)
# will likely raise AttributeError
[..]

However, I don't understand what the super call is doing. If the method
isn't delegated, shouldn't it just fall back to getattr(self, name)?

On reading the __getattr__ docs properly, I see that AttributeError is
what should generally happen.

Which is what the call to super will accomplish, but if the behaviour ever
changes (including the error message given), you won't have to change your
class.
 
S

Steven D'Aprano

On Mon, 22 Aug 2011 15:27:36 +1000


Thanks, this looks promising. I didn't know about __getattr__ or
delegation. This example doesn't seem to work as is for special methods
beginning with "__" (e.g.: "TypeError: object of type 'MyList' has no
len()"). It seems that __getattr__ is not called for special methods.

Ah yes, that would be a problem.

This recipe may help.

http://code.activestate.com/recipes/252151-generalized-delegates-and-proxies/

Also, it doesn't immediately suggest to me a way of modifying method calls
(maybe __setattr__?).

What do you mean, "modifying method calls"?

__getattr__ doesn't know whether the method retrieved modifies the instance
or not. That's irrelevant.

__setattr__ is called when you say

instance.attribute = value


But it's certainly a neater way to get methods to
operate on the attribute. I'm looking into it, and delegation generally.

However, I don't understand what the super call is doing. If the method
isn't delegated, shouldn't it just fall back to getattr(self, name)?

getattr(self, name) will just call self.__getattr__(name) again, which will
call getattr, and so on... leading to RecursionError.
 
J

John O'Hagan

On Mon, 22 Aug 2011 15:27:36 +1000
# Untested
class MySeq(object):
methods_to_delegate = ('__getitem__', '__len__', ...)
pitches = ... # make sure pitches is defined
def __getattr__(self, name):
if name in self.__class__.methods_to_delegate:
return getattr(self.pitches, name)
return super(MySeq, object).__getattr__(self, name)
# will likely raise AttributeError
[...]

Also, it doesn't immediately suggest to me a way of modifying method calls
(maybe __setattr__?).

What do you mean, "modifying method calls"?

That was a rather badly-worded reference to a function in my original post which modified the action of list methods in ways specific to the new class. As I said, I re-read the docs on __getattr__, and now __setattr__, and I get what they do now.

Thanks for your pointers, I'll get back to work.

Regards,

John
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,962
Messages
2,570,134
Members
46,690
Latest member
MacGyver

Latest Threads

Top