views:

51

answers:

2

I'm doing some collision detection and I would very much like to use the same function in two different contexts. In one context, I would like for it to be something like

def detect_collisions(item, others):
    return any(collides(item, other) for other in others)

and in another, I would like it to be

def get_collisions(item, others):
    return [other for other in others if collides(item, other)]

I really hate the idea of writing two functions here. Just keeping their names straight is one turnoff and complicating the interface to the collision detecting is another. so I was thinking:

def peek(gen):
    try:
        first = next(gen)
    except StopIteration:
        return False
    else:
        return it.chain((first,), gen)

def get_collisions(item, others):
    get_collisions.all = peek(other for other in others if collides(item, other))
    return get_collisions.all

Now when I just want to do a check, I can say:

if get_collisions(item, others):
    # aw snap

or

if not get_collisions(item, others):
    # w00t

and in the other context where I actually want to examine them, I can do:

if get_collisions(item, others):
    for collision in get_collisions.all:
        # fix it

and in both cases, I don't do any more processing than I need to.

I recognize that this is more code than the first two functions but it also has the advantage of:

  1. Keeping my interface to the collision detection as a tree with the node at the top level instead of a mid level. This seems simpler.

  2. Hooking myself up with a handy peek function. If I use it one other time, then I'm actually writing less code. (In response to YAGNI, if I have it, I will)

So. If you were the proverbial homicidal maniac that knows where I live, would I be expecting a visit from you if I wrote the above code? If so, how would you approach this situation?

+4  A: 

Just make get_collisions return a generator:

def get_collisions(item, others):
    return (other for other in others if collides(item, other))

Then, if you want to do a check:

for collision in get_collisions(item, others):
    print 'Collision!'
    break
else:
    print 'No collisions!'
Piet Delport
that's one thing I thought of but specifically detecting an empty generator is tedious enough to want to have wrapped away in a function like `peek`. I would want to know if it's empty or not and that would involve a flag (ugh!) or reinventing `peek` every time I want to check.
aaronasterling
It would work if I only ever wanted to take action if there were collisions though. +1
aaronasterling
You would just detect the empty case with an `else` clause: i added that to my answer, for reference. A function like `peek` should never be needed in Python: using it is probably a sign that something is wrong.
Piet Delport
good eye. I always ask the dumbest questions on here. I knew that. Thanks for talking me out of it.
aaronasterling
actually, sorry for the accepted psyche out but setting up a loop to throw a break to dodge an else clause on the loop that shouldn't be a loop to do what I want every time I want to do it is just too tedious. It obfuscates the intent of the code.
aaronasterling
Why do you find it tedious? The only addition is a `break`, compared to the other approach's addition of an entire `if` or `for` (plus the helper).
Piet Delport
Also, it may help to stop thinking of `for` as a looping-only construct. `while` is a looping/repetition construct, and so is C's `for`, but Python's use of `for` is more like a map: the idea is not repetition based on a condition, but rather mapping a code block across a collection of items. In this particular case, the idea is to search for and break on the first match, or otherwise, handle the case of no matches being found. This *should* describe the intent of the computation (a collision *search*) more clearly than a relatively opaque boolean flag.
Piet Delport
It's not true that it only add's the `break`. If I only want to make sure it's empty, I have to setup the `for`, `break` and `else` as opposed to simple `if`. It's true that it adds a second statement in the case that I want to actually access the collisions but at least it's a semantically meaningful line as opposed to the convolution I described above.
aaronasterling
with regards to the second point, I've already abstracted away the _actual_ search into a function. That's were the search is actually happening and where the sort of logic you describe is appropriate. The client code _is_ interested in a simple boolean: _did_ you find one or _didn't_ you. It then has the option to get nosy but isn't required too. Also, 'map a block of code across a collection of items' sounds tremendously like 'loop'. And when I think of it like that, I'm more inclined to want to use map or a gen-exp to do it.
aaronasterling
A: 

This is very similar to what we were discussing in "pythonic way to rewrite an assignment in an if statement", but this version only handles one positional or one keyword argument per call so one can always retrieve the actual valued cached (not just whether is a True value in the Pythonian boolean sense or not).

I never really cared for your proposal to accept multiple keywords and have differently named functions depending on whether you wanted all the results put through any() or all() -- but liked the idea of using keyword arguments which would allow a single function to be used in two or more spots simultaneously. Here's what I ended up with:

# can be called with a single unnamed value or a single named value
def cache(*args, **kwargs):
    if len(args)+len(kwargs) == 1:
        if args:
            name, value = 'value', args[0]  # default attr name 'value'
        else:
            name, value = kwargs.items()[0]
    else:
        raise NotImplementedError('"cache" calls require either a single value argument '
                                  'or a name=value argument identifying an attribute.')
    setattr(cache, name, value)
    return value

# add a sub-function to clear the cache attributes (optional and a little weird)
cache.clear = lambda: cache.func_dict.clear()

# you could then use it either of these two ways
if get_collisions(item, others):
    # no cached value

if cache(collisions=get_collisions(item, others)):
    for collision in cache.collisions:
        # fix them

By putting all the ugly details in a separate function, it doesn't affect the code in get_collisions() one way or the other, and is also available for use elsewhere.

martineau