mlv
mlv

Reputation: 630

Getting a class to act as a tuple

I'm trying to have a class act in every way like a tuple that's an attribute of the class, so len(instance) would be the same as len(instance.tup), instance[3] would return instance.tup[3], etc. Here's the class:

class mytup(object):
     def __init__(self, a):
         self.tup = tuple(a)
     def __getattr__(self, nm):
         f = types.MethodType(lambda self:getattr(self.tup, nm)(), self, type(self))
         f.__func__.func_name = nm
         setattr(self, nm, f)
         return f

I can

mt = mytup(range(10))

But if I try to:

In [253]: len(mt)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-253-67688b907b8a> in <module>()
----> 1 len(mt)

TypeError: object of type 'mytup' has no len()

mt does in fact have a __len__ that I can call:

In [254]: mt.__len__
Out[254]: <bound method mytup.__len__ of <__main__.mytup object at 0x2e85150>>
In [255]: mt.__len__()
Out[255]: 10

(I even renamed it __len__). As near as I can tell, this should look just as if I did:

def __len__(self, *a):
    return self.tup.__len__(*a)

But python won't let me len(mt) (or mt[2] or mt [1:5] for that matter).

Upvotes: 6

Views: 2907

Answers (3)

martineau
martineau

Reputation: 123541

New-style classes look-up "special methods"—those that start and end with two underscore characters—on an instance's class not the instance involved, so when len() is called it tries to call typeof(mt).__len__(). So the proper way to do what you want would be to use one of the Abstract Base Classes for Containers in the collections module (since Python 3.3)

import collections.abc

class MyTuple(collections.abc.Sequence):
    def __init__(self, a):
        self.tup = tuple(a)

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

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

mt = MyTuple(range(10))
print(len(mt))  # -> 10
print(mt[4])  # -> 4

Upvotes: 3

juanpa.arrivillaga
juanpa.arrivillaga

Reputation: 96286

The reason this isn't working as you have hoped is because doing:

setattr(self, nm, f)

Is not equivalent to

def __len__(self, *a):
    return self.tup.__len__(*a)

In the latter case, your method is a property of the class because it is defined in class scope. It would be the equivlanet of setattr(cls, nm, f). If you check MyTup.__dict__ you will see it there. However, in the former case, __len__ is a property of the instance. So it will be in my_instance.__dict__. len checks the class for a __len__ method, and doesn't find one. Hence the error. Your __getattr__ is never actually called, and even if it were, it wouldn't allow you to use len. You can use an_instanec.__len__ diretly, though.

Upvotes: 0

Uriel
Uriel

Reputation: 16224

len does not use __getattr__ to get the __len__ function - it calls __len__ directly.

Calling x.__len__ is like calling getattr(x, '__len__') - which will return the x.__len__ method object.

len works behind the scene, so it can access this method directly, without invoking the __getattr__ helper.

  • Try to add a print statement in your __getattr__ to see what is printed when calling len (hint: nothing).

Upvotes: 0

Related Questions