Evandro Coan
Evandro Coan

Reputation: 9476

How to add __len__ to an object without __len__ on its data type definition?

According to the documentation, this does not work because of this:

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary. That behaviour is the reason why the following code raises an exception:

>>> class C:
...     pass
...
>>> c = C()
>>> c.__len__ = lambda: 5
>>> len(c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'C' has no len()

https://docs.python.org/3/reference/datamodel.html#special-method-lookup

I had tried this on a function generator, which does not have __len__, but I knew beforehand its length, then, I tried monkey patch it with something like c.__len__ = lambda: 5, but it kept saying the generator object had no length.

This is the generator:

def get_sections(loaded_config_file):
    for module_file, config_parser in loaded_config_file.items():
        for section in config_parser.sections():
            yield section, module_file, config_parser

I was passing the generator (which has no length) to this other function (yet, another generator), which requires the iterable length by calling len():

def sequence_timer(sequence, info_frequency=0):
    i = 0
    start = time.time()
    if_counter = start
    length = len(sequence)
    for elem in sequence:
        now = time.time()
        if now - if_counter < info_frequency:
            yield elem, None
        else:
            pi = ProgressInfo(now - start, float(i)/length)
            if_counter += info_frequency
            yield elem, pi
        i += 1

https://github.com/arp2600/Etc/blob/60c5af803faecb2d14b5dd3041254ef00a5a79a9/etc.py

Then, when trying to add the __len__ attribute to get_sections, hence the error:

get_sections.__len__ = lambda: calculated_length
for stuff, progress in sequence_timer( get_sections ):
    section, module_file, config_parser = stuff

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

Upvotes: 4

Views: 1382

Answers (2)

John Jiang
John Jiang

Reputation: 955

You can do it in the following way

class A:
    def __init__(self):
        self.x = 1

a = A()
def b_fn(self):
    return self.x

def __len__(self):
    return 5

a.b = b_fn.__get__(a)
# A can be also obtained as type(a). 
A.__len__ = __len__.__get__(A)
a.b()
len(a)

5

Upvotes: 1

ShadowRanger
ShadowRanger

Reputation: 155546

You can't add it to an existing object, so make your own wrapper class that has a class level definition you control:

class KnownLengthIterator:
    def __init__(self, it, length):
        self.it = it
        self.length = int(length)

    def __len__(self):
        return self.length

    def __iter__(self):
        yield from self.it

Now you just change your invalid attempt to set a length of:

get_sections.__len__ = lambda: calculated_length

to a valid rewrapping that makes get_sections continue to be a valid generator (yield from will delegate all iteration behaviors to the wrapped generator), while exposing a length too:

get_sections = KnownLengthIterator(get_sections, calculated_length)

No other code needs to change.

Upvotes: 5

Related Questions