python-sortedcontainers icon indicating copy to clipboard operation
python-sortedcontainers copied to clipboard

Return NotImplemented for SortedSet.__and__

Open SimpleArt opened this issue 2 years ago • 3 comments
trafficstars

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.

SimpleArt avatar Mar 11 '23 06:03 SimpleArt

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.

grantjenks avatar Mar 13 '23 06:03 grantjenks

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 avatar Mar 13 '23 20:03 SimpleArt

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.

SimpleArt avatar Mar 13 '23 20:03 SimpleArt