Repository navigation
Define a mixin for container classes with possibly masked values and use it in Time - #16844
Conversation
|
Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.
|
| mask = False | ||
| mask, val_data = get_mask_and_data(mask, val) | ||
| mask, val_data2 = get_mask_and_data(mask, val2) | ||
| data, masks = self._get_data_and_masks(val, val2) |
There was a problem hiding this comment.
These methods come with MaskableShapedLikeNDArray
| def masked(self): | ||
| return isinstance(self._time.jd1, Masked) | ||
|
|
||
| @property |
There was a problem hiding this comment.
These are now provided by MaskableShapedLikeNDArray.
| return val | ||
|
|
||
|
|
||
| def get_mask_and_data(mask, val): |
There was a problem hiding this comment.
This basically repeated code in masked, so is now superfluous.
| but also used in `~astropy.utils.masked.Masked`. | ||
| """ | ||
|
|
||
| @classmethod |
There was a problem hiding this comment.
All four methods are simply copied from Masked and MaskedNDArray.
|
@taldcroft - I realize you may be using your astropy time mostly for tables, but if you have a chance, I'd appreciate a look at this PR factoring out some of the |
|
@mhvk - I will have a look, but on first glance this will require my thinking hat so not this evening! |
taldcroft
left a comment
There was a problem hiding this comment.
Overall this looks good and I see now that it is basically an uncontroversial refactoring.
One thing for discussion is making the private utility class methods here into public functions. Unless I missed something, none of them actually use cls anywhere, and they are somewhat generic for masks and a masked protocol or np.ma.MaskedArray.
| mask = getattr(data, "mask", None) | ||
| if mask is not None: | ||
| try: | ||
| data = data.unmasked | ||
| except AttributeError: | ||
| if not isinstance(data, np.ma.MaskedArray): | ||
| raise | ||
| if data is np.ma.masked: | ||
| if allow_ma_masked: | ||
| data = None | ||
| else: | ||
| raise ValueError("cannot handle np.ma.masked here.") from None | ||
| else: | ||
| data = data.data |
There was a problem hiding this comment.
It's a bit out of scope (since this PR is mostly a refactor), but I have some suggestions on the code.
- Remove a big code indent with an early return.
- Avoid unnecessary re-use of
data. With a mindset of type-annotated code, redefining a variable seems like an idiom to avoid when possible. In this case I think the code is much more clear. - Improve exception messages
| mask = getattr(data, "mask", None) | |
| if mask is not None: | |
| try: | |
| data = data.unmasked | |
| except AttributeError: | |
| if not isinstance(data, np.ma.MaskedArray): | |
| raise | |
| if data is np.ma.masked: | |
| if allow_ma_masked: | |
| data = None | |
| else: | |
| raise ValueError("cannot handle np.ma.masked here.") from None | |
| else: | |
| data = data.data | |
| mask = getattr(data, "mask", None) | |
| if mask is None: | |
| return data, mask | |
| try: | |
| unmasked = data.unmasked | |
| except AttributeError: | |
| if not isinstance(data, np.ma.MaskedArray): | |
| raise TypeError( | |
| "data must have an 'unmasked' attribute or be np.ma.MaskedArray" | |
| ) from None | |
| if data is np.ma.masked: | |
| if allow_ma_masked: | |
| unmasked = None | |
| else: | |
| raise ValueError( | |
| "cannot handle np.ma.masked here, use allow_ma_masked=True" | |
| ) from None | |
| else: | |
| unmasked = data.data | |
| return unmasked, mask |
There was a problem hiding this comment.
I went with your suggestion to make this function public, and decided that, given that, I should also simplify it. So, I followed your suggestion to change the indentation, but removed the option of dealing with np.ma.masked (which should have included np.ma.nomask anyway).
| ---------- | ||
| data : array-like | ||
| Possibly masked item, judged by whether it has a ``mask`` attribute. | ||
| If so, checks for being an instance of `~astropy.utils.masked.Masked` |
There was a problem hiding this comment.
| If so, checks for being an instance of `~astropy.utils.masked.Masked` | |
| If so, checks for having an ``unmasked`` attribute |
There was a problem hiding this comment.
Adjusted text, thanks!
| return data, mask | ||
|
|
||
| @classmethod | ||
| def _get_data_and_masks(cls, *args): |
There was a problem hiding this comment.
I would suggest renaming this to _get_datas_and_masks. I really like the idiom of consistently using plural for list-like objects. This gives a strong type hint to the reader and makes iteration over the object obvious to code using the singular.
There was a problem hiding this comment.
Maybe also use arrays instead of args if this will be public. Overall in some of these functions I feel like array is a better name than data since that name narrows the type considerably. But that may be sliding into "perfect is the enemy of good" territory.
There was a problem hiding this comment.
Sorry, should have answered here: I ended up thinking that the plural was not needed as much (or at least it wasn't clear it was going to be needed a lot), so just left this on the class to be used by __array_ufunc__.
There was a problem hiding this comment.
p.s. Did go with array in the general get_data_and_mask
0a7f5d6 to
816d41f
Compare
|
Thanks, @taldcroft! I had been wondering about functions versus class methods, but your comment made clear functions is the answer. The only annoyance is that I couldn't stop myself from then using the function also in the |
816d41f to
f74be70
Compare
| copy : bool optional | ||
| Whether to ensure a copy is made. Only relevant if a single | ||
| input mask is not `None`, and ``out`` is not given. | ||
| """ |
| Parameters | ||
| ---------- | ||
| fill_value : object | ||
| Value to replace masked values with. Note that if this value is masked |
There was a problem hiding this comment.
Finish the sentence and add a Returns.
There was a problem hiding this comment.
How weird - that was already unfinished in Time... I guess it should have been that the mask is ignored, but to be honest, I feel that is such a weird corner case, it is better not even to mention it... But added a Returns section.
| np.logical_or(out, mask, out=out, where=where) | ||
| return out | ||
| @staticmethod | ||
| def _get_data_and_masks(arrays): |
There was a problem hiding this comment.
Since it is so easy with Copilot it's worth a docstring here.
"""
Extracts the data and masks from the given arrays.
Parameters
----------
arrays : iterable
An iterable of arrays.
Returns
-------
datas, masks
A tuple of extracted data arrays and mask arrays, respectively.
"""
There was a problem hiding this comment.
OK, thanks. Used slightly adjusted.
|
|
||
|
|
||
| def _get_data_and_masks(*args): | ||
| def _get_data_and_mask(array): |
There was a problem hiding this comment.
Optional, but maybe naming this _get_data_and_mask_as_array might help make the intent clear in the code that calls this.
There was a problem hiding this comment.
Yes, good suggestion -- as is, it is just confusing that the name is the same with just an underscore added. I made it _get_data_and_mask_array (saving the plural for the other routine).
f74be70 to
9746745
Compare
c9fc542 to
4922c27
Compare
|
@taldcroft - would you have time to do a final review for this refactoring of |
taldcroft
left a comment
There was a problem hiding this comment.
This looks good now from the code perspective.
Since it adds 3 items to the masked subpackage public API, there should be a changelog entry as such. It might also be worth mentioning that the Time base class now inherits from MaskableShapedLikeNDArray, mostly to advertise how this new formalism can be used elsewhere.
You may even want a What's New entry for this to advertise this nice addition. If you get the coordinates version through for 7.0 then definitely this will need highlighting.
…sses. As part of this, moves _combine_masks and some other class methods from Masked to a new MaskedMethods class so that they can be reused by MaskableShapedLikeNDArray as well.
4922c27 to
f7f212e
Compare
|
OK, I added a changelog entry following your suggestion. I really hope to have the changes to coordinates done before 7.0 - and that would definitely warrant a what's new in which I will mention the more general scheme one can now use. |
I've started implementing proper masks for representations, roughly following what is done for
Time. To help that, this PR defines a newMaskableShapedLikeNDArraymixin that provides some basics (likeunmaskedandfilled), and also some helper routines (built on top ofShapedLikeNDArray, which all container classes already use).In this PR, this is only used to somewhat simplify the implementation in
Time, but I will use it in follow-up for coordinates. I on purpose did not yet add a changelog entry for the new method, since I'd like to keep it semi-private until I'm sure it is the right approach also for coordinates -- the fact that it is now used inTimewill help ensure I keep the masking implementations consistent.