ENH: make apply_function aware of channel index#12206
ENH: make apply_function aware of channel index#12206larsoner merged 19 commits intomne-tools:mainfrom
Conversation
|
uses initial commit only for |
|
I can also extend the code to support |
Co-authored-by: Mathieu Scheltienne <mathieu.scheltienne@gmail.com>
|
quick question for you @drammock : discussing with @wmvanvliet and @britta-wstnr we couldnt think of a good reason why this should be intentional, so I implemented this functionality analogous to |
32e379e to
6d35e19
Compare
|
I like the functionality you're adding here! What I found a little surprising, though, is that we allow functions that by themselves accept parameters passed by position, but then we insist on specific parameter names. I have never seen this anywhere in any other Python project I've worked on. If we want to enforce a parameter name, we should probably insist on a keyword-only argument |
|
I mean seriously, if we have to use inspection just to find out how to call a function in an API that we define, doesn't it sound like a hack to you immediately? To me it does... |
Would you be OK if we force the signature to be one of:
I'm not sure I follow this one, anyway the function signature can not be different from the one mentionned above. |
I'm not sure we're talking about the same thing. I'm not proposing to silently modify the function signature. |
Yes, this would work if we want to go with kw-args Personally I'd prefer to pass by position but the above would be acceptable for me too |
|
... would still require inspection though ... but I think it keeps it to minimum at least I'd prefer to have a solution that can do without any introspection ideally I can see why you don't like @cbrnr's proposal so much though OTOH this is how we've been doing things in MNE-Python in many places for many years So I don't understand why this is immediately off the table now? I'd love to hear @agramfort weigh in if he has time |
|
Also I'd like to say: thanks for everyone contributing here, be it code or by sharing their opinion in this discussion! 🤗 |
|
After a discussion with @britta-wstnr, @larsoner and @dominikwelke we are all in favor of the current implementation (as is I think @wmvanvliet and @mscheltienne also are?). Here is the rationale as I see it:
I agree that this is unusual. But to me the question from the UX perspective is: do we compel the user to write their one-off channel-modifying function to follow best-practices around APIs? Or do we say "its first arg must be the data, and it can have 1 or 2 others (ch_name and/or ch_idx) if you want/need them." To me this is a perfect use case for |
|
I am still not in favor, but whatever. |
|
I see your arguments but I disagree that this makes it easier for users. Like you said, it's unusual, hence likely to cause confusion or at least additional cognitive load. It sets MNE's behavior apart from the rest of the Python ecosystem by introducing some kind of meta language feature, and thereby adds inconsistencies without any need to do so. My 2 ct |
|
This is not a veto. But I want it to be noted that I think this implementation is a bad idea. |
For transparency, my perspective rests on the assumptions that
Personally I had been coding in Python a few years before even seeing an |
Exactly. That's why it should be avoided, because as soon as users start gaining experience and using other libraries like scikit-learn, they'll experience confusion due to this inconsistency. Also, we're being inconsistent within MNE itself. We don't do this stuff anywhere else even within MNE AFAIK, which IMHO is reason enough to seriously reconsider this decision.
We're asking users to define their own function that does NumPy data manipulation but don't trust they'll be able to write a function signature or know any Python basics? This doesn't make sense, in my opinion – it's a contradiction in itself.
kw-only arguments are relatively new and advanced stuff. Hence I'm not a big fan of having them here – I only brought them up because they'd be the clean solution to the idea of passing parameters by name. If I could choose, I'd define a fixed signature for the callback, and pass things by position. This is what happens with Matplotlib callbacks, with scikit-learn scorers etc.
I'm just saying by trying to make things easier for absolute beginners, you're adding confusion to anyone wiling to advance or already having an advanced level of Python proficiency. And adding inconsistent behavior inside MNE-Python itself. We're adopting SPECs and Black coding styles and whatnot, but when it comes to functions, we start our very own special thing because MNE-Python is so special. Sorry, err… no. Just no. I mean do it if you want to, but I don't like it. At all. I do agree that Python is too difficult for beginners, though. |
|
Thanks for the meeting summaries and explanations, @drammock I know it's difficult to keep everyone involved & informed here while we're working and discussing so asynchronously. I appreciate your effort. |
|
@dominikwelke I think you should be okay to proceed with the |
I think this is an assumption without evidence. They might not notice or remember the difference, they might be confused, or they might think (with curiosity or even admiration) "huh, I wonder how MNE-Python pulled that off?"
Depends on what you mean by "this stuff". We do use
to me this isn't about trusting the user, it's about making life easier for them. We do this all over the place by allowing list/tuple/array, for example, or by allowing them to pass
Wait, are they a "Python basic" or are they "advanced stuff"?
Let me give an analogy. I want to use a function that someone else wrote. I see it has a signature Now turn the perspective around. The "user" in this case is not me, it is MNE-Python's code. It needs to call a function that someone else wrote. It cannot read that function's docstring, and it cannot (easily) read that function's source code to figure out how it works. But what can it do? It can look at the param names, and say "aha! I have a To me that is a case of our code handling realistic, real-world user input (callables) flexibly, smartly and gracefully. It is not us "starting our very own special thing". With that, I'm going to sign off of this discussion. My goal here is not necessarily to change your vote @hoechenberger, but hopefully lead you to feel less vehemently opposed to the will of the majority. You're welcome to have the last word for posterity, if you want it. |
|
You're entirely ignoring the possibility of passing parameters by position, or @cbrnr's proposal of adding a switch to control the behavior, like we do elsewhere. I don't know why you're providing an example of good vs bad parameter naming, it just seems out of place. |
|
Given that this is a pretty niche bit of functionality, could we perhaps YAGNI a bit and only give the channel index, not its name? So either |
|
just applied @mscheltienne 's suggestions. let me know what to do next
this would work, but yes - for the user it would be more dificult if they wanted to use if this was to be the desired route, i would rather consider only offering |
|
I understand the decision has already been made, but for the record, I would also be fine with just having either 1 or 3 positional parameters. So either |
drammock
left a comment
There was a problem hiding this comment.
this looks pretty good! a couple of suggested simplifications/cleanups below. Reviewing this makes me realize how much code duplication there is in these three methods; maybe after this one is merged you could try (if you want to) to refactor some of that duplication into a helper function that all three methods could use?
mne/epochs.py
Outdated
| if dtype is not None and dtype != self._data.dtype: | ||
| self._data = self._data.astype(dtype) | ||
|
|
||
| args = getfullargspec(fun)[0] + getfullargspec(fun)[4] |
There was a problem hiding this comment.
let's take advantage of the fact that getfullargspec returns a named tuple, to make this line a bit more transparent about what we're doing:
| args = getfullargspec(fun)[0] + getfullargspec(fun)[4] | |
| args = getfullargspec(fun).args + getfullargspec(fun).kwonlyargs |
| channel_wise=True, | ||
| verbose=None, | ||
| **kwargs, |
|
hi @drammock and all |
larsoner
left a comment
There was a problem hiding this comment.
Thanks for sticking with it @dominikwelke, looks good and implements what we (eventually!) converged on, so in it goes!
Co-authored-by: Mathieu Scheltienne <mathieu.scheltienne@gmail.com>
Fixes #10034.
this PR makes the index of currently processed channel available within custom functions applied to data.
this is needed for processing channels differently during parallel processing, as requested in #10034 .