views:

346

answers:

11

Is there a more pythonic way of doing this? I am trying to find the eight neighbours of an integer coordinate lying within an extent. I am interested in reducing its verbosity without sacrificing execution speed.

def fringe8((px, py), (x1, y1, x2, y2)):
    f = [(px - 1, py - 1),
         (px - 1, py),
         (px - 1, py + 1),
         (px, py - 1),
         (px, py + 1),
         (px + 1, py - 1),
         (px + 1, py),
         (px + 1, py + 1)]
    f_inrange = []
    for fx, fy in f:
        if fx < x1:  continue
        if fx >= x2: continue
        if fy < y1:  continue
        if fy >= y2: continue
        f_inrange.append((fx, fy))
    return f_inrange
+5  A: 

This list comprehension should work

f_inrange = [(px+i,py+j) for i in (-1,0,1) for j in (-1,0,1) \
             if (i != j or i != 0) and x1 <= px+i < x2 and \
             y2 > py+j >= y1]

though it is kind of lengthy.

Justin Peel
+1 cool ;-) you could shorten it by writing the condition as `x1 <= px + i < x2` and similarly for y.
David Zaslavsky
I'm not sure, this comprehension looks pretty hard to read..
Xavier Ho
True, although it looks better once you've done enough (some would say too much) Python.
David Zaslavsky
@David Yes, I realized that not long after I wrote it and put that in. I sometimes forget about that great feature of Python.
Justin Peel
+6  A: 

Here's a thought:

def fringe8((px, py), (x1, y1, x2, y2)):
    f = [(px + dx, py + dy) for (dx, dy) in itertools.product((-1,0,1),repeat=2) if not (dx == dy == 0)]
    f_inrange = [(fx, fy) for (fx, fy) in f if x1 <= fx < x2 and y1 <= fy < y2]
    return f_inrange
David Zaslavsky
Actually, good idea, David. I'm stealing your second last line into my answer. `;]`
Xavier Ho
Wow, tricky. `if not (dx == dy == 0)`. My eyes have been opened. Nice!
Xavier Ho
Don't you just love Python sometimes? Although, I think your `(dx,dy) != (0,0)` makes as much if not more sense.
David Zaslavsky
+11  A: 

Here's my take on the code cleanup:

Edit: I've taken David's code into my answer to make it even more compact (and faster execution time).

>>> from itertools import product
>>>
>>> def fringe8((px, py), (x1, y1, x2, y2)):
...     f = [(px+dx, py+dy) for (dx, dy) in product((-1,0,1),(-1,0,1)) if (dx, dy) != (0, 0)]
...     f_inrange = [(fx, fy) for (fx, fy) in f if x1 <= fx < x2 and y1 <= fy < y2]
...     return f_inrange
...   
>>> fringe8((2, 2), (1, 1, 3, 3))
[(1, 1), (1, 2), (2, 1)]

Edit: If you're not too comfortable with list comprehensions, feel free to break it down to a for loop. The conditions proposed here, and others' answers, are all much more concise for you to use.

After all, one the goals of list comprehesion is to make it easier to read, and not verbose.

Edit again: Please also look at Ryan Ginstrom's answer.

Xavier Ho
`itertools.product()` could also be used instead of the double `for..in`: `itertools.product((-1,0,1),(-1,0,1))`
Amber
Great idea, Amber!
Xavier Ho
@Amber: good point, I was actually just editing that into my answer while you posted your comment ;-)
David Zaslavsky
oh, and +1 for synthesizing the best of all our answers
David Zaslavsky
Thanks! (I've upvoted for both of you as well.) Although I didn't take any from Justin `:]`.
Xavier Ho
I noted your [distaste with my newbish python](http://stackoverflow.com/questions/2838965/i-dont-like-python-functions-that-take-two-or-more-iterables-is-it-a-good-idea) and am using named tuples now :)
fmark
@fmark: Sorry for not having told you earlier. :]
Xavier Ho
+2  A: 
def fringe8((px, py), (x1, y1, x2, y2)):
    return [(x, y) for x,y 
            in itertools.product(   [xi+px for xi in [-1,0,1]],
                                    [yi+py for yi in [-1,0,1]] )
            if (x1<=x<x2) and (y1<=y<y2) and not ((x==px) and (y==py))]

I don't know if that's more pythonic, but it's certainly another way to do it.

Andrew McGregor
maybe skip for loop completely, `[px-1,px,px+1]`?
aaa
+2  A: 
def fringe8((px, py), (x1, y1, x2, y2)):
  return [(fx, fy) for fx in [px - x for x in [-1, 0, 1]] if (fx >= x1 and fx < x2) 
  for fy in [py - y for y in [-1, 0, 1]]  if fy >= y1 and fy < y2 
  and not(fx == px and fy == py)]
Jakub Hampl
A: 

here is my recycle :

def fringe8((px, py), (x1, y1, x2, y2)):
    return [(x, y) for x,y in itertools.product((px-1, px , px+1),
                                                (py-1, py , py+1))
            if (x1<=x<x2) and (y1<=y<y2) and (x,y) != (px,py)]
aaa
+8  A: 

This is a reworking of Xavier Ho's answer. I think that it's made a little more clear by using intermediate steps.

from itertools import product

def fringe8((px, py), (x1, y1, x2, y2)):

    nonzero = (pair for pair in product((-1,0,1),(-1,0,1)) if pair != (0, 0))
    f = ((px+dx, py+dy) for (dx,dy) in nonzero)

    def in_range((fx, fy)):
        return x1 <= fx < x2 and y1 <= fy < y2
    return [pair for pair in f if in_range(pair)]

print fringe8((2, 2), (1, 1, 3, 3))
Ryan Ginstrom
Awesome! Good job of refactoring.
Xavier Ho
I like this one too. I'm using a combination of yours and Xavier's answers.
fmark
This is where StackOverflow really shines. A place of collaborated power and knowledge. Thanks everyone!
Xavier Ho
@Xavier: True indeed, I was even compelled to write a blog entry about it (and when some random person blogs about something, you _know_ it's important... ;-P)
David Zaslavsky
@David: Haha, I was going to, too! Except I need to restructure my blog first so it's more code friendly.
Xavier Ho
A: 

The name of the function could be more informative. I know what the code does, but I have no idea what it is intended for.

msw
+3  A: 

Here's my effort. It constructs the interesting ranges directly, so doesn't test redundant points (except for the pesky center point). I added an extra argument r than can return a larger square around the point too.

def fringe((px, py), (x1, y1, x2, y2), r=1):
    return [(x, y)
            for x in xrange(max(px - r, x1), min(px + r + 1, x2))
            for y in xrange(max(py - r, y1), min(py + r + 1, y2))
            if x != px or y != py]
Paul Hankin
+1  A: 

I don't know from pythonicness, but I see that this function is doing two things: a) finding adjacent points; and b) restricting a list of points to those that are in range. So I would split it up, because a) it's clearer; b) sometimes I might want one of those behaviors without the other; c) it's easier to test; and d) it's easier to name. I won't attempt to convert this advice into python - I'm sure my attempt wouldn't be pretty - but that would be my starting point.

Carl Manaster
+1  A: 

I like Carl's suggestion. Also you can avoid repeated construction of the neighbor offsets.

from itertools import product

NEIGHBOR_OFFSETS = [pair for pair in product((-1,0,1), (-1,0,1))
    if pair != (0, 0)]

def clip(points, (x1, y1, x2, y2)):
    return [(px, py) for px, py in points if 
        x1 <= px < x2 and y1 <= py < y2]

def neighbors(px, py):
    return [(px+dx, py+dy) for dx, dy in NEIGHBOR_OFFSETS]

def fringe8(point, rect):
    return clip(neighbors(*point), rect)
Haukur Hreinsson
Very nice decomposition of separate functions
fmark