7

I understand that there are magic methods in python that can be overwritten by classes to control the way certain built in functions treat the members of these classes. For example, the behavior of len() and str() can be overwritten via magic methods __len__() and __str__():

class EmptySet(object):
    def __len__(self):
        return 0

    def __str__(self):
        return '[]'

>>> e = EmptySet()
>>> str(e)
[]

>>> len(e)
0

There are also __cmp__() and __ge__(), __le__() etc methods to control how these objects can be compared and how a list of them should be ordered by list.sort(). My question is not about customizing the ordering of objects in a list but about sorting the object itself. Suppose the set weren't empty and I want to use sorted() to sort it:

class SetOfTwo(object):
    def __init__(self, a , b):
        el_0 = a
        el_1 = b

    def __len__(self):
        return 2

    def __str__(self):
        return '[{}, {}]'.format(el_0, el_1)

Is there a magic method I can implement to have sorted() flip the elements if they aren't in order? I'm picturing the following behavior:

>>> s = SetOfTwo(2, 1)
>>> str(s)
[2, 1]

>>> t = sorted(s)
>>> str(t)
[1, 2]

>>> type(t)
>>> SetOfTwo
Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
gregrf
  • 211
  • 3
  • 8
  • 2
    Sets are inherently unordered. See [this question](https://stackoverflow.com/questions/9792664/set-changes-element-order). As for your actual question (nomenclature aside) just define your own sort method on the class and use that instead. Or pass in a lamda. – Jared Smith Feb 19 '18 at 14:25
  • 3
    @JaredSmith I don't think this question is really about sets. – khelwood Feb 19 '18 at 14:28
  • 1
    you can try OrderedSet – caot Feb 19 '18 at 14:28
  • 1
    I apologize for using an inherently unordered collection in my toy example. Hopefully the question which is about whether there is a magic method for sorted() in python is still clear. – gregrf Feb 19 '18 at 14:30
  • Basically, if `el_0` is less than `el_1`, you swap them? – Arnav Borborah Feb 19 '18 at 14:36
  • 1
    Python’s [`sorted()`](https://docs.python.org/3/library/functions.html#sorted) function takes an iterable and returns a list. So if your collection supports iteration it can be passed to `sorted()`. The ordering of the elements is based on their natural ordering or on the key function passed to `sorted()`. You will get back a list, not your custom type. – Steven Rumbalski Feb 19 '18 at 14:39
  • The question isn't about sets. Is there an OOP way in python to override `sorted()` as I would `len()` or this not a thing? – gregrf Feb 19 '18 at 14:40
  • 1
    You’re missing the point. `sorted()` acts on *elements* not on *collections*. It does not preserve the collection type. It is implemented this way so it can accept a wide range of collections, both ordered and unordered. It only cares that it’s arguments are itereable. It then guarantees ordering by returning a simple ordered collection, i.e. a list. – Steven Rumbalski Feb 19 '18 at 14:47
  • @Steven Rumbalski, this is the essence of my question. So you are saying the answer is, no, because sorted() always returns a list so there can't be a magic method in a class to override it's behavior. If that's true, that's all the answer I was looking for. – gregrf Feb 19 '18 at 14:51
  • So there is no magic method you can add to your custom collection because the only contract that `sorted` pays attention to is iteration. If you want sorted to do more complicated ordering you can supply additional arguments like `key` for a custom key function and `reverse=True`. – Steven Rumbalski Feb 19 '18 at 14:52

4 Answers4

5

You should definitely read the official documentation of how to emulate container types. Basically a class supposed to work as a container (list, dict etc.) needs to implement methods to set or get members __getitem__(), __setitem__() and iterate over items __iter__() and to get the number of items - method __len__(). This is the minimum. But you can also add the ability to delete items and other operations.

The behaviour of sorted() built-in function is to iterate over elements of your container and compare them using methods you mentioned __cmp__(), __ge__(), __le__() which should be defined for items and not the container as you know already. Then a new list instance is created with items sorted and this new instance is returned. You can pass it to the constructor of your custom container then or you can wriap sorted() with a custom function which will return the desired class instance.

ElmoVanKielmo
  • 10,907
  • 2
  • 32
  • 46
  • 1
    That link to emulating container types is helpful. Basically, if there were a magic method for `sorted()`, it would appear there in official documentation as `__sorted__()`. I almost asked this question about `reversed()` instead and that page does list a `__reversed__()` magic method. So there is a magic method to implement your own behavior of `reversed()` but not `sorted()`. That's not obvious until you read the right part of the docs. – gregrf Feb 19 '18 at 14:59
  • This is because reversing a collection doesn't care about values of items but sorting it depends on these values. – ElmoVanKielmo Feb 19 '18 at 15:12
  • it seems to me the only important difference is that `reversed()` looks for a `__reversed__()` method in the container its reversing whereas `sorted()` doesn't look for a `__sorted__()` method. – gregrf Feb 19 '18 at 15:17
  • This is the technical difference - you are correct. But the reason for developers of Python to implement it this way, I explained in my last comment. One can imagine a class which orders items somehow without comparing them but it couldn't be called "sorting". – ElmoVanKielmo Feb 19 '18 at 15:27
  • There are other reasons to want to override sorting. For instance I might want to add an issorted flag that skips the sorting algorithm and returns a copy of the object if the object were known to have been sorted previously. – gregrf Feb 19 '18 at 15:37
  • So, what I need to write now, won't fit in a comment. Therefore I will edit the answer. Wait a bit. – ElmoVanKielmo Feb 19 '18 at 16:08
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/165432/discussion-between-gregrf-and-elmovankielmo). – gregrf Feb 19 '18 at 16:51
1

As some have said in the comments, sets are unordered but I don't think your question is really about sets.

Python uses the data model methods you mentioned, ge, le, and cmp to determine how a class behaves when sorted() is called on it. You can see how I try to call it here, but Python objects and asks me to implement <.

>>> class a(object):
...   pass
...
>>> b = a()
>>> c = a()
>>> d = [b, c]
>>> sorted(d)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'a' and 'a'

Hope this helps. Aslo, as other people said, it's a good idea to subclass something in collections.abc. I'd read Item 28 in effective python that talks about this to get a good idea.

stoksc
  • 324
  • 3
  • 12
1

len() and str() are functions who take an object as parameter and return an integer (resp. string). The object can personalize the way the len is calculated, or the string is generated, via the __len__() and __str__() magic methods.

Similarly, sorted() is a function that takes a list (or any iterable) of objects and returns a list of the sorted objects. The objects can personalize the way they get compared through the __lt__() magic method.

Some confusion arises when we think of `sorted(my_list) as a function that "sorts the list", rather than "sorts the elements of the list".

You don't want to sort your objects (i.e. make an ordered list of objects), but only sort some data in their internal representation. So you need an instance method on your object that will update that internal representation. You can name it as you wish, .sort() if you'd like, but you will have to call it on your one object, and it will not be involved in comparing objects.

Thierry Lathuille
  • 23,663
  • 10
  • 44
  • 50
  • Can you explain how to sort anything without comaparing? – ElmoVanKielmo Feb 20 '18 at 08:34
  • @ElmoVanKielmo Maybe what I meant is more clearly explained with an example: in the question, the OP wanted to sort the `el_0` and `el_1` attributes inside a `SetOfTwo` object (which he expressed as 'sort the object itself'). He can have a method that does it, and he will of course need to compare `el_0` and `el_1` in it. But all of this is totally unrelated to the sorting of a bunch of `SetOfTwo` objects (as by `sorted()`), which involves comparing a `SetOfTwo` with another `SetOfTwo`. This (also necessary) comparison between objects can be done through the `__lt__` method of the object. – Thierry Lathuille Feb 20 '18 at 11:10
1

You have to implement comparison operator magic methods. Python will automatically take care of the sort and sorted.

class Edge:
    def __init__(self,source, dest, weight=float('inf')):
        self.source = source
        self.dest = dest
        self.weight = weight        
    
    def __repr__(self):
        return f"Edge: ({self.source}, {self.dest}, {self.weight})"
    
    def __lt__(self, other):
        return self.weight < other.weight    
e1 = Edge(0, 1, 2)
e2 = Edge(1, 2, 3)
e3 = Edge(2, 3, 10)
e4 = Edge(2, 4, 10)
e5 = Edge(2, 4, 0)

l = [e1, e3, e4, e2, e5]
print(l)
print(e3 > e2)
print(e3 == e4)
print(sorted(l))
Durai
  • 505
  • 4
  • 12