0

As I understand, one can call a magic method of an object in either of two ways. Firstly like this, where the magic method is called like any regular method:

x = (1, 2, 3, 4)
print(x.__len__())
#prints 4

And secondly, like this, where the magic method is called "specially":

x = (1, 2, 3, 4)
print((len(x)))
#prints 4

As we can see, both are equivalent. However, when I call the magic method reversed() upon a tuple with the first way, I am met with an error:

x = (1, 2, 3, 4)
x.__reversed__()
#causes an attribute error

Yet the "special" way of calling the reversed method works perfectly fine:

x = (1, 2, 3, 4)
reversed(x)
#returns a reversed object

Why does this occur? Have I misunderstood something about dunder methods?

Dude156
  • 158
  • 1
  • 12
  • Similar: [Is there any case where `len(someObj)` does not call `someObj`'s `__len__` function?](https://stackoverflow.com/q/496009/4518341) – wjandrea May 01 '20 at 20:45
  • 3
    Conventions say that you should not consider any method starting with an underscore as part of the public API of an object. Why? The observation in your question is a great example. – Klaus D. May 01 '20 at 20:52

3 Answers3

7

Most magic methods are not equivalent to whatever other thing they look like they should be equivalent to. __add__ is not equivalent to +. __iadd__ is not equivalent to +=. __iter__ is not equivalent to iter. Neither __getattribute__ nor __getattr__ is equivalent to getattr (or ordinary attribute access).

Continuing the pattern, __reversed__ is not equivalent to reversed. If reversed finds a __reversed__ method, it'll use the method, but if not, it'll just make an iterator that accesses the argument from back to front with ordinary indexing. The sequence needs no __reversed__ method, and tuple does not have one.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Ah, that makes sense. But what if I call iter on an object that doesn't have an __iter__ method? Wouldn't things go funky then? – Dude156 May 01 '20 at 20:55
  • 1
    @Dude156: `__iter__` has the same fallback `__reversed__` does, but indexing front to back instead of back to front. – user2357112 May 01 '20 at 20:58
  • But I mean if we call `iter()` when we don't have an `__iter__` for that object, how would the method know how to make an iterator...? – Dude156 May 01 '20 at 20:59
2

Your assumption that there must be an __reversed__ method is wrong:

reversed(seq)

Return a reverse iterator. seq must be an object which has a __reversed__() method or supports the sequence protocol (the __len__() method and the __getitem__() method with integer arguments starting at 0).

MatsLindh
  • 49,529
  • 4
  • 53
  • 84
1

Because reversed does one of the following depending on the argument:

  1. Argument is an object with __reversed__ magic method and reversed just calls it and returns given iterator.
>>> class A:
...     def __reversed__(self):
...         yield 17
...         yield 42
>>> a = A()
>>> reversed(a)
<generator object A.__reversed__ at 0x7fa1283e8e40>
>>> list(reversed(a))
[17, 42]
  1. Argument is a sequence (like tuple or str) that has __len__ and __getitem__ and __reversed__ creates an intermediate object that yields elements of the sequence from the last to the first.
>>> t = (42, 17)
>>> reversed(t)
<reversed object at 0x7fa12840fc40>
>>> list(reversed(t))
[17, 42]

From docs

reversed(seq) Return a reverse iterator. seq must be an object which has a __reversed__() method or supports the sequence protocol (the __len__() method and the __getitem__() method with integer arguments starting at `0).

More generally you probably should not call dunder methods (because it is very dangerous way, especially in production code) and use special functions/classes for accessing them like reversed/len.

Azat Ibrakov
  • 9,998
  • 9
  • 38
  • 50