Is there any way I can change the way list() function works on my class?

Viewed 254

I am writing a small class 'AP' ('Arithmetic Progression') for my school assignment. I have included an option for making the AP infinitely long.

class AP:
    def __init__(self, first_term: float, co_diff: float, n: int = inf) -> None:
        self.a1 = first_term
        self.d = co_diff
        self.term = 0
        self.n = n

    def __repr__(self) -> str:
        return f'AP({self.a1}, {self.d}, {self.n})'

    def __str__(self) -> str:
        output_str = f"{', '.join([str(term) for term in list(self.terms(term=6))])}, ...."
        if self.n != inf:
            return output_str + f', {(self.a1 + ((self.n - 1) * self.d))}'
        return output_str

    def terms(self, term: int):
        return AP(self.a1, self.d, n=term)

    def __iter__(self):
        return self

    def __next__(self):
        self.term += 1
        if self.term < self.n:
            return (self.a1 + ((self.term - 1) * self.d))
        raise StopIteration

    def __eq__(self, o: object) -> bool:
        if isinstance(o, AP):
            return str(self) == str(o)

But when someone wants to convert it into a list and if it is an infinite one, I want to raise an error like:

 ValueError: The list() function was called for an object of undefined length.

Same goes for sum() function as well. Are there any solutions to this?

2 Answers

AFAIK, Python has no notion of explicit conversion operator implemented in the source class (C++ has). The conversion is the responsibility of the destination class and currently list has no provision for infinity.

A possible workaround would be to hide list using a local function:

def list(it=()):
    import builtins

    if isinstance(it, AP) and it.n == inf:
        raise ValueError('The list() function was called for an object'
                         ' of undefined length.')
    return builtins.list(it)

The limit is that if you use sorted which also returns a list, your special function will not be used.

Another limit, if your class is declared in a specific module and used from other modules is that the special function has to be imported in the other modules with from APmod import list

A possible trick would be to use the __length_hint__ special method. It is used at least in CPython for all standard conversions of an iterable to a sequence type, and AFAIK should only be used there. You cannot return a value to tell list that the length would be infinite but you can raise the ValueError directly:

def __length_hint__(self):
    if self.n is inf:
        raise ValueError('This object has undefined length.')
    return self.n - 1

Example after adding this method to your class:

>>> ap = AP(1., 1.)
>>> ap
AP(1.0, 1.0, inf)
>>> next(ap)    # iteration is correct
1.0
>>> next(ap)
2.0
>>> next(ap)
3.0
>>> next(ap)
4.0
>>> list(ap)   # list raises ValueError
Traceback (most recent call last):
  File "<pyshell#75>", line 1, in <module>
    list(ap)
  File "<pyshell#68>", line 34, in __length_hint__
    raise ValueError('This object has undefined length.')
ValueError: This object has undefined length.
>>> tuple(ap)     # so does tuple
Traceback (most recent call last):
  File "<pyshell#76>", line 1, in <module>
    tuple(ap)
  File "<pyshell#68>", line 34, in __length_hint__
    raise ValueError('This object has undefined length.')
ValueError: This object has undefined length.
>>> sorted(ap)       # or sorted
Traceback (most recent call last):
  File "<pyshell#77>", line 1, in <module>
    sorted(ap)
  File "<pyshell#68>", line 34, in __length_hint__
    raise ValueError('This object has undefined length.')
ValueError: This object has undefined length.

But as long as you have a finite length, all is fine:

>>> ap = AP(1., 1., 4)
>>> list(ap)
[1.0, 2.0, 3.0]

By the way, you could use the same implementation for the special __len__ method to allow your class to be used by the len builtin function.

Related