grantjenks / python-sortedcontainers

Python Sorted Container Types: Sorted List, Sorted Dict, and Sorted Set
http://www.grantjenks.com/docs/sortedcontainers/
Other
3.51k stars 203 forks source link

Return NotImplemented for SortedSet.__and__ #219

Open SimpleArt opened 1 year ago

SimpleArt commented 1 year ago

Binary operators such as __eq__, __and__, etc. should return NotImplemented in the event that the given argument is an unsupported type rather than raising an error. This allows the other object to patch in its own implementation.

Roughly speaking, it means code should be written like this:

from typing import Iterable

class SortedSet:

    def __and__(self, other):
        if isinstance(other, Iterable):  # Or `hasattr(type(other), "__iter__")`.
            return self.intersection(other)
        else:
            return NotImplemented  # `self & other` then checks `other.__and__(self)`.

This allows cases such as this:

class Interval:  # Not a set e.g. does not contain `__iter__`.

    def __and__(self, other):
        if isinstance(other, Interval):
            ...
        elif isinstance(other, Iterable):
            return {x for x in other if x in self}
        else:
            return NotImplemented

    def __contains__(self, other):
        ...

sorted_set & interval  # Expected set, got TypeError.
grantjenks commented 1 year ago

I'm familiar with return NotImplemented and agree it would be helpful. Here's the code:

    def intersection(self, *iterables):
        """Return the intersection of two or more sets as a new sorted set.

        The `intersection` method also corresponds to operator ``&``.

        ``ss.__and__(iterable)`` <==> ``ss & iterable``

        The intersection is all values that are in this sorted set and each of
        the other `iterables`.

        >>> ss = SortedSet([1, 2, 3, 4, 5])
        >>> ss.intersection([4, 5, 6, 7])
        SortedSet([4, 5])

        :param iterables: iterable arguments
        :return: new sorted set

        """
        intersect = self._set.intersection(*iterables)
        return self._fromset(intersect, key=self._key)

    __and__ = intersection
    __rand__ = __and__

SortedSet.__and__ calls self._set.intersection(*iterables) and that's what raises the TypeError.

It may be easy enough to fix with:

    def __and__(self, other):
        """Return the intersection of two sets as a new sorted set.

        ``ss.__and__(iterable)`` <==> ``ss & iterable``
        """
        intersect = self._set & other
        return self._fromset(intersect, key=self._key)

But it's disappointing the __and__ = intersection trick doesn't work.

SimpleArt commented 1 year ago

I think maybe it ought to be:

    def __and__(self, other):
        """Return the intersection of two sets as a new sorted set.

        ``ss.__and__(iterable)`` <==> ``ss & iterable``
        """
        intersect = self._set.__and__(other)
        if intersect is NotImplemented:
            return NotImplemented
        return self._fromset(intersect, key=self._key)
SimpleArt commented 1 year ago

Actually I don't believe set & iterable works, only set & set works, so it'd have to be either set.intersection(iterable) with a try block or an if check beforehand.