-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: patch NDArrayOperatorsMixin #2534
Conversation
Codecov Report
Additional details and impacted files
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would NumPy introduce __slots__
on a mixin? (Mixins are supposed to not be in charge of what data a class has, and therefore not define the __init__
and attributes. Defining __slots__
forces an exact set of data attributes, and therefore make the class not a mixin. #2533 is exactly the reason why mixins are okay for multiple inheritance and classes that define data are not.)
Since we have our own definition of NDArrayOperatorsMixin
,
awkward/src/awkward/_connect/numpy.py
Lines 315 to 401 in 7f9be54
try: | |
NDArrayOperatorsMixin = numpy.lib.mixins.NDArrayOperatorsMixin | |
except AttributeError: | |
from numpy.core import umath as um | |
def _disables_array_ufunc(obj): | |
try: | |
return obj.__array_ufunc__ is None | |
except AttributeError: | |
return False | |
def _binary_method(ufunc, name): | |
def func(self, other): | |
if _disables_array_ufunc(other): | |
return NotImplemented | |
return ufunc(self, other) | |
func.__name__ = f"__{name}__" | |
return func | |
def _reflected_binary_method(ufunc, name): | |
def func(self, other): | |
if _disables_array_ufunc(other): | |
return NotImplemented | |
return ufunc(other, self) | |
func.__name__ = f"__r{name}__" | |
return func | |
def _inplace_binary_method(ufunc, name): | |
def func(self, other): | |
return ufunc(self, other, out=(self,)) | |
func.__name__ = f"__i{name}__" | |
return func | |
def _numeric_methods(ufunc, name): | |
return ( | |
_binary_method(ufunc, name), | |
_reflected_binary_method(ufunc, name), | |
_inplace_binary_method(ufunc, name), | |
) | |
def _unary_method(ufunc, name): | |
def func(self): | |
return ufunc(self) | |
func.__name__ = f"__{name}__" | |
return func | |
class NDArrayOperatorsMixin: | |
__lt__ = _binary_method(um.less, "lt") | |
__le__ = _binary_method(um.less_equal, "le") | |
__eq__ = _binary_method(um.equal, "eq") | |
__ne__ = _binary_method(um.not_equal, "ne") | |
__gt__ = _binary_method(um.greater, "gt") | |
__ge__ = _binary_method(um.greater_equal, "ge") | |
__add__, __radd__, __iadd__ = _numeric_methods(um.add, "add") | |
__sub__, __rsub__, __isub__ = _numeric_methods(um.subtract, "sub") | |
__mul__, __rmul__, __imul__ = _numeric_methods(um.multiply, "mul") | |
__matmul__, __rmatmul__, __imatmul__ = _numeric_methods(um.matmul, "matmul") | |
__truediv__, __rtruediv__, __itruediv__ = _numeric_methods( | |
um.true_divide, "truediv" | |
) | |
__floordiv__, __rfloordiv__, __ifloordiv__ = _numeric_methods( | |
um.floor_divide, "floordiv" | |
) | |
__mod__, __rmod__, __imod__ = _numeric_methods(um.remainder, "mod") | |
if hasattr(um, "divmod"): | |
__divmod__ = _binary_method(um.divmod, "divmod") | |
__rdivmod__ = _reflected_binary_method(um.divmod, "divmod") | |
__pow__, __rpow__, __ipow__ = _numeric_methods(um.power, "pow") | |
__lshift__, __rlshift__, __ilshift__ = _numeric_methods(um.left_shift, "lshift") | |
__rshift__, __rrshift__, __irshift__ = _numeric_methods( | |
um.right_shift, "rshift" | |
) | |
__and__, __rand__, __iand__ = _numeric_methods(um.bitwise_and, "and") | |
__xor__, __rxor__, __ixor__ = _numeric_methods(um.bitwise_xor, "xor") | |
__or__, __ror__, __ior__ = _numeric_methods(um.bitwise_or, "or") | |
__neg__ = _unary_method(um.negative, "neg") | |
if hasattr(um, "positive"): | |
__pos__ = _unary_method(um.positive, "pos") | |
__abs__ = _unary_method(um.absolute, "abs") | |
__invert__ = _unary_method(um.invert, "invert") |
(though it was removed in #2115), I'd prefer to just use our definition and therefore control it. It means that we'll need to be on top of any new additional unary/binary operators (like @
), but we don't have to be on the defensive about upstream technicalities like this.
numpy issue numpy/numpy#22876 and PR numpy/numpy#23113 |
Oh, so my reading of this is that
By that logic, the NumPy developers concluded that there could be no downside to it. In #2533, we seem to have discovered a downside: Maybe a different solution to this is to define We don't want an infectious implementation requirement to spread to all downstream libraries that use Awkward, especially if the error message is cryptic, like
If defining If defining |
The way that the layout checker works is that it has a notion of type-supertype "compatibility", and it attempts to find a compatible common base. To generate our mixin classes for both records and arrays, we do something like class ArrayImpl(MixinType, ak.Array):
... The logic checking type-supertype compatibility for multiple inheritance walks down the Let's imagine that we add slots to Of the two approaches, it's probably safer to assume that the user doesn't define import awkward as ak
behavior = {}
class Point:
__slots__ = ()
class MyPoint(Point, ak.Array):
...
behavior["*", "Point"] = MyPoint
ak.operations.zip(
{
"x": ak.Array([1, 1]),
"y": ak.Array([1, 1]),
},
with_name="Point",
behavior=behavior,
) For the same reasons outlined above: I think, therefore, the safest immediate step is to drop the slots, by using our own mixin. For our ecosystem, I think there's lesser benefit to |
Let's go with that. I was also thinking that |
I've taken your above comment as approval for merging! |
Fixes #2533 by vendoring our own version of
NDArrayOperatorsMixin