@@ -244,6 +244,8 @@ def array_ufunc(self, ufunc: np.ufunc, method: str, *inputs: Any, **kwargs: Any)
244
244
245
245
cls = type (self )
246
246
247
+ kwargs = _standardize_out_kwarg (** kwargs )
248
+
247
249
# for backwards compatibility check and potentially fallback for non-aligned frames
248
250
result = _maybe_fallback (ufunc , method , * inputs , ** kwargs )
249
251
if result is not NotImplemented :
@@ -318,6 +320,11 @@ def array_ufunc(self, ufunc: np.ufunc, method: str, *inputs: Any, **kwargs: Any)
318
320
def reconstruct (result ):
319
321
if lib .is_scalar (result ):
320
322
return result
323
+
324
+ if isinstance (result , tuple ):
325
+ # np.modf, np.frexp, np.divmod
326
+ return tuple (reconstruct (x ) for x in result )
327
+
321
328
if result .ndim != self .ndim :
322
329
if method == "outer" :
323
330
if self .ndim == 2 :
@@ -349,6 +356,13 @@ def reconstruct(result):
349
356
result = result .__finalize__ (self )
350
357
return result
351
358
359
+ if "out" in kwargs :
360
+ result = _dispatch_ufunc_with_out (self , ufunc , method , * inputs , ** kwargs )
361
+ return reconstruct (result )
362
+
363
+ # We still get here with kwargs `axis` for e.g. np.maximum.accumulate
364
+ # and `dtype` and `keepdims` for np.ptp
365
+
352
366
if self .ndim > 1 and (len (inputs ) > 1 or ufunc .nout > 1 ):
353
367
# Just give up on preserving types in the complex case.
354
368
# In theory we could preserve them for them.
@@ -375,8 +389,66 @@ def reconstruct(result):
375
389
# Those can have an axis keyword and thus can't be called block-by-block
376
390
result = getattr (ufunc , method )(np .asarray (inputs [0 ]), ** kwargs )
377
391
378
- if ufunc .nout > 1 :
379
- result = tuple (reconstruct (x ) for x in result )
380
- else :
381
- result = reconstruct (result )
392
+ result = reconstruct (result )
382
393
return result
394
+
395
+
396
+ def _standardize_out_kwarg (** kwargs ) -> dict :
397
+ """
398
+ If kwargs contain "out1" and "out2", replace that with a tuple "out"
399
+
400
+ np.divmod, np.modf, np.frexp can have either `out=(out1, out2)` or
401
+ `out1=out1, out2=out2)`
402
+ """
403
+ if "out" not in kwargs and "out1" in kwargs and "out2" in kwargs :
404
+ out1 = kwargs .pop ("out1" )
405
+ out2 = kwargs .pop ("out2" )
406
+ out = (out1 , out2 )
407
+ kwargs ["out" ] = out
408
+ return kwargs
409
+
410
+
411
+ def _dispatch_ufunc_with_out (self , ufunc : np .ufunc , method : str , * inputs , ** kwargs ):
412
+ """
413
+ If we have an `out` keyword, then call the ufunc without `out` and then
414
+ set the result into the given `out`.
415
+ """
416
+
417
+ # Note: we assume _standardize_out_kwarg has already been called.
418
+ out = kwargs .pop ("out" )
419
+ where = kwargs .pop ("where" , None )
420
+
421
+ result = getattr (ufunc , method )(* inputs , ** kwargs )
422
+
423
+ if result is NotImplemented :
424
+ return NotImplemented
425
+
426
+ if isinstance (result , tuple ):
427
+ # i.e. np.divmod, np.modf, np.frexp
428
+ if not isinstance (out , tuple ) or len (out ) != len (result ):
429
+ raise NotImplementedError
430
+
431
+ for arr , res in zip (out , result ):
432
+ _assign_where (arr , res , where )
433
+
434
+ return out
435
+
436
+ if isinstance (out , tuple ):
437
+ if len (out ) == 1 :
438
+ out = out [0 ]
439
+ else :
440
+ raise NotImplementedError
441
+
442
+ _assign_where (out , result , where )
443
+ return out
444
+
445
+
446
+ def _assign_where (out , result , where ) -> None :
447
+ """
448
+ Set a ufunc result into 'out', masking with a 'where' argument if necessary.
449
+ """
450
+ if where is None :
451
+ # no 'where' arg passed to ufunc
452
+ out [:] = result
453
+ else :
454
+ np .putmask (out , where , result )
0 commit comments