-
Notifications
You must be signed in to change notification settings - Fork 105
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
Callback: step-parameter accepts list of steps #1542
base: master
Are you sure you want to change the base?
Conversation
Checking updated PR...
Comment last updated at 2020-03-06 16:45:15 UTC |
The casting of the items should probably be changed to |
Looks good overall. I think the correct way in python is to try-catch iter(step) though, see e.g. https://stackoverflow.com/questions/1952464/in-python-how-do-i-determine-if-an-object-is-iterable |
If one inputs e.g. |
Do you think the code should be refactored into a method that is called in all relevant places? def _setupShouldEvaluateAtStep(self, step):
try:
self.step = frozenset(int(i) for i in step)
self.should_evaluate_at_step = lambda i: i in self.step
except TypeError:
self.step = int(step)
self.should_evaluate_at_step = lambda i: i % self.step == 0 I think 1) it makes the inits more readable and 2) it makes it easier to keep track of changes in a single place |
One could even think of putting the whole On that exact note: do I get it wrong, or does the |
Something like this maybe? from functools import wraps
def call_if_should_be_evaluated(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if self.should_evaluate_at_step(self.iter):
func(self, *args, **kwargs)
self.iter += 1
return wrapper |
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.
I like the general idea to support varying step size in callbacks. The current version, however, combines two different ways of determining when to evaluate the callback: either after a number of steps or at given steps. IMO they should either be two separate mutually exclusive arguments (not great), or the list of steps should also mean the steps between consecutive evaluations. In other words, step=1
whould then be equivalent to step=[1, 1, ...]
. The iterations at which to evaluate would be given by np.cumsum([0] + list(steps))
, and conversely, steps can be calculated from iterations using np.diff
.
That would raise the question what should happen when the list of steps is exhausted. One option would be to keep going with the last step value in the list (then step=1
is the same as step=[1]
). Alternatively, one could stop after the last step has been used. What's your preference? Also ping @adler-j @aringh
Implementation-wise I think the best solution would be to use a generator that is stored in the instance (instead of storing a function, the step and the current iteration separately). For instance:
class Callback(object):
def __init__(self, ..., step=1):
try:
int(step)
except TypeError:
step_iter = iter(step)
else:
def step_iter():
while True:
yield step
def should_evaluate():
yield True # always run at first iteration
while True:
cur_step = next(step_iter)
for _ in range(cur_step - 1):
yield False
yield True
self.should_evaluate = should_evaluate
# This is implemented by subclasses
def __call__(self):
should_eval = next(self.should_evaluate)
if should_eval:
# run logic
This solution would also immediately support iterators instead of finite collections.
@@ -1034,6 +1045,14 @@ def __repr__(self): | |||
return '{}({})'.format(self.__class__.__name__, | |||
inner_str) | |||
|
|||
def _setupShouldEvaluateAtStep(self, step): |
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 use a different function name style?
def _setupShouldEvaluateAtStep(self, step): | ||
try: | ||
self.step = frozenset(int(i) for i in step) | ||
self.should_evaluate_at_step = lambda i: i in self.step |
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.
Attributes should only be set in __init__
if possible, not in "free" functions like this.
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.
I wanted to refactor the whole repetition and thought about returning self, so that the user does see that self is modified inside of this function. Otherwise there is a lot of code repetition and the inits are quite bloated if the user wants to take a look at them.
I also thought about a metaclass that will automatically setup the the init and call, but I guess that is a bit of an overkill
Is there a reason why this must be done?
Another consideration could be I do understand that there is a big difference in the interpretation of integers and lists in my implementation. In which cases / with which Callbacks would the user opt for variable step sizes instead of fixed? I would argue that they want to perform tasks at specific steps. If I want to save images at step 5, 50 and 100 I would need to write Isn't the question "What happens after exhaustion" without an obvious answer a hint that the notion of steps between iterations is flawed in this context? I for myself think a list of steps at iterations is the more logical interpretation, even if there is a difference in the interpretation of at/between for Ints and lists. |
Only reason is that this happens for integer
Yes, I fully understand and agree. We should support "starting late".
Right. In that case I tend towards the default to stop when the list is exhausted. The other cases can then be supported by wrapping the list in a generator that cycles, repeats, or does anything you like.
This is not very intuitive or easy to handle, agreed. To fix it, we could offer an alternative constructor that takes a list of evaluation points instead of steps, e.g., class Callback(object):
def __init__(self, ...):
# as usual
@classmethod
def from_iterations(cls, its, *args, **kwargs):
its = iter(its)
def step_iter():
next_it = next(its)
while True:
try:
cur_it, next_it = next_it, next(its)
except StopIteration:
return
step = next_it - cur_it
assert step > 0
yield step
return cls(*args, step=step_iter, **kwargs) And construction then works as Side note: If I were to start again from scratch, I'd implement the callbacks with plain Python generators and scrap the classes. Alternative constructors would then just be conversion functions.
I'd say no, because the callback cannot know when it will stop and thus has to assume that it will run indefinitely. It has to somehow deal with an unbounded number of iterations, and just stopping after the last iteration in its list is only one possible solution. I'd argue, on the contrary, that extending a list of steps to infinity is much more obvious than extending a list of evaluation points. |
Any new thoughts on this stuff? |
Sorry for not replying, but I needed to help my grandmother for the last few weeks... Your proposal needs BTW: I totally agree with you about stoping after exhaustion. It was just an additional example on what strange things could be thought of ;) |
Don't worry.
Sounds like a good suggestion. We tend to be liberal about input values. Maybe it's good to clarify then that duplicates will not lead to the callback running multiple times.
Hm, makes me realize that my suggested implementation has a bug 😬, namely that the first step in the generator should be from To improve usability, I'd suggest pulling out the "iterations-to-steps" conversion into a public function (a function returning a generator, 🤯). That gives the opportunity to "extend" that generator in interesting ways, e.g., wrap it in a new generator that yields all values and then the last one indefinitely: def repeat_last(gen):
for val in gen:
yield val
while True:
yield val Or other stuff from |
PR to #1541
It most probably isn't PEP8 compliant (there is no char-count in the Web-Editor...) and the naming can be debated as well