3

I am searching for the pythonic way to do the following:

I have a list of keys and a list of objects.

For any key, something should be done with the first object that fits to that key.

If no object fits to no key, so nothing has be done at all, something different should be done instead.

I implemented this as follows and it is working properly:

didSomething = False
for key in keys:
    for obj in objects:
        if <obj fits to key>:
            doSomething(obj, key)
            didSomething = True
            break
if not didSomething:
    doSomethingDifferent()

But normally, if there is only one for-loop, you don't need such a temporary boolean to check whether something has been done or not. You can use a for-else statement instead. But this does not work with 2 for-loops, did it?

I have the feeling that there should be some better way to do this but i don't get it. Do you have any ideas or is there no improvement?

Thank you :)

Max16hr
  • 438
  • 2
  • 5
  • 20
  • The problem with a for else is that you never have a possibility of a break from your loop over the keys, so you won't be able to use that as the signal for `do_something_different`. So this seems fine as it is. – juanpa.arrivillaga Apr 04 '18 at 15:55
  • Did you want the `break` to break out of the outer loop as well as the inner one? If so, that’s not possible (without a flag and two `break`s), and that’s your real problem; if you _did_ have a way to break out of the outer loop, that _would_ skip the outer `else`. If you didn’t want that, on the other hand, then an `else` is meaningless—you never break out of the outer loop—so there’s something wrong with your design, not a limitation of the language. – abarnert Apr 04 '18 at 16:16
  • @abarnert: I want to break only the inner loop to check the other keys as well. – Max16hr Apr 04 '18 at 16:20
  • In that case, what do you expect the `else` to mean? I don’t understand what semantics you’re looking for. – abarnert Apr 04 '18 at 16:25
  • @abarnert: I just want to check, whether something has been done at all within the for-loops. If not, something different should be done. If there is only 1 for-loop the for-else statemant is exactly what will do this. But i need 2 loops. So i am searching for something like an outer for-else statement for the inner for-loop^^ – Max16hr Apr 04 '18 at 16:27
  • The equivalent of `for…break…else` for nested loops is what’s in my answer, but the equivalent of `for…don’t break anywhere…if check some arbitrary condition` is just checking the same arbitrary condition; there is no way to simplify it because it doesn’t really have anything to do with looping; it’s just “did I call this function one or more times?” You’re not testing “did I break out of this loop”, but “did I ever (or always) break out of any inner loops in anything inside this loop”. – abarnert Apr 04 '18 at 16:28
  • @abarnert: This is what i want, yes! Did I call the function one or more times or not? So it seems i have to use the temporary variable. – Max16hr Apr 04 '18 at 16:43
  • Yeah, in that case, it’s really not about the outer loop, so there’s no way to express it in the syntax of the outer loop. (Which means my answer and all of the others are basically useless; I’ll delete mine or edit it.) – abarnert Apr 04 '18 at 16:48
  • @abarnert: Thank you anyway! :) – Max16hr Apr 04 '18 at 16:49
  • I think a different refactoring _might_ be useful here—but it might also be a terrible idea, depending on what your real problem is. It's a bit too much to explain in a comment, so see my edited answer. – abarnert Apr 04 '18 at 17:04

4 Answers4

3

This doesn't really fit into the for/else paradigm, because you don't want to break the outer loop. So just use a variable to track whether something was done, as in your original code.

Instead of the second loop, use a single expression that finds the first matching object. See Python: Find in list for ways to do this.

didSomething = false
for key in keys:
    found = next((obj for obj in objects if <obj fits to key>), None)
    if found:
        doSomething(found, key)
        didSomething = true
if not didSomething:
    doSomethingDifferent()
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • 3
    I think the intent of the code is to run `doSomething` for *every* key that matches, not just the first one. `doSomethingDifferent` should run if no keys match. – Blender Apr 04 '18 at 15:59
  • @Blender: Exactly. I cannot break the outer loop at the first match. – Max16hr Apr 04 '18 at 16:22
  • 1
    Then this doesn't really fit the `for/else` model, which can only be used when you break out of the loop. – Barmar Apr 04 '18 at 18:14
3

Whenever you find yourself needing to break out of a nested loop, it’s usually hard to think through the details, and when you finish figuring it out, the answer is usually just that it’s impossible (or at least only possible with an explicit flag variable or an exception or something else that obscures your logic).

There's an easy answer to that (which I'll include below in case anyone finding this question by search has that problem), but that's not actually your problem. What you want to check is not "did I complete the loop normally", because you always complete the loop normally. What you want to check is "did I do something (in this case, call doSomething) one or more times".

That isn't really about the outer loop, unlike breaking out of the outer loop (which obviously is), so there's no syntax for it. You need to keep track of whether you did something one or more times, and the way you're already doing that is probably the simplest way.


In some cases, you can rearrange things to flatten or invert the loop, so you end up doing one thing with all of the currently-outer values one time and breaking out of that loop, in which case it is about looping again. But if that twists your logic up so much that it's no longer clear what's going on, that's not going to be an improvement. For example:

fits = set()
for key in keys:
    for obj in objects:
        if <obj fits to key>:
            fits.add((obj, key))
for obj, key in fits:
    do_something(obj, key)
if not fits:
    do_something_else()

This can be simplified:

fits = {(obj, key) for key in keys for obj in objects if <obj fits to key>}
for obj, key in fits:
    do_something(obj, key)
if not fits:
    do_something_else()

But, either way, notice that the way I avoided storing a flag saying whether you ever found a fit was by storing a set of all of the fits you found. For some problems, that's an improvement. But if that set could be very large, it's a terrible idea. And if that set just conceptually doesn't mean anything in your problem, it might obscure the logic instead of simplifying it.


If your problem were breaking out of a nested loop (which it isn't, but, again, it might be for someone else who finds this question by search), there’s always an easy answer to that: just take the whole nest of loops and refactor it into a function. Then you can break out at any level by just using return. If you didn’t return anywhere, the code after the loops will get run, while if you did return, it will—just like an else.

So:

def fits():
    for key in keys:
        for obj in objects:
            if <obj fits to key>:
                doSomething(obj, key)
                return
    doSomethingDifferent()

fits()

I’m not sure whether breaking out if both loops is what you want. If it is, this does exactly what you want. If not, it doesn’t, but then I’m not sure what semantics you were looking for with the else–when it should get run—so I don’t know how to explain how to do that.

Once you’ve done this, you may find the abstraction generalizes to more than use in your code, so you can turn the function into something that takes parameters instead of using closure or global variables, and that returns a value or raises instead of calling one of two functions, and so on. But sometimes, this trivial local function is all you need.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Thanks for that advice! I will think about using some inner functions. But this way will not work, because you leave if the first key fits to an object. But then i have to check the other keys as well and that would not happen with the return statement, For example, if i have 5 keys, it is possible to call doSomething() up to 5 times. But only if it was called 0 times, i want to call DoSomethingDifferent(). – Max16hr Apr 04 '18 at 16:39
  • 1
    Thank you very much for that detailed answer! So i accept that there is no hidden python function that could prevent me from using a temporary variable to store a flag :D Your way to store the set looks a bit more compact but imo, it is not easy to read. Thats why i think, I will keep it as it is :) – Max16hr Apr 05 '18 at 11:42
  • 1
    @Max16hr Yeah, that’s another good reason not to use the set comprehension in some cases, besides the two I gave. Whenever you think “me in 6 months might find it hard to read this code”, come up with something more verbose/explicit/whatever that doesn’t have that problem, even if it feels less clever. – abarnert Apr 05 '18 at 15:17
2

There's no real way to simplify your code. It is, however, kind of confusing the way it's written. I would actually make it more verbose to make sure it's read properly:

def fit_objects_to_keys(objects, keys):
    for key in keys:
        for obj in objects:
            if <obj fits to key>:
                yield obj, key
                break

none_fit = True

for obj, key in fit_objects_to_keys(keys, objects):
    doSomething(obj, key)
    none_fit = False

if none_fit:
    doSomethingDifferent()

You may be able to simplify it further if you explain what <obj fits to key> actually does.

Blender
  • 289,723
  • 53
  • 439
  • 496
  • Probably you are right and it is not possible to simplify. I want to reduce 3 lines (temp=false, temp=true, if temp) into 1 line (else). So i don't think a new function will simplify the code :D – Max16hr Apr 04 '18 at 16:34
  • 1
    @Max16hr: A few extra lines won't hurt if they make it significantly more readable (given the conflicting answers to your question, you can see how your code is easy to misread on first glance). After all, humans will be ones reading the code, Python will just be evaluating it. – Blender Apr 04 '18 at 16:40
1

I agree with the comment that your code is fine as it is - but if you must flatten multiple for-loops into one (so that you can use the 'else' feature, for example, or the number of for-loops is itself variable), this is actually possible:

import itertools
for key, obj in itertools.product(keys, objects):
    if <obj fits to key>:
        doSomething(obj, key)
        break
else:
    doSomethingDifferent()
jasonharper
  • 9,450
  • 2
  • 18
  • 42