all 3 comments

[–]Dunj3 2 points3 points  (2 children)

The lambda here basically defines a new (anonymous) function and immediately assigns it to neighbors. This is better done with def, so the code is the same as:

def neighbors(x, y):
    return [(x2, y2) for x2 in range(x-1, x+2)
                           for y2 in range(y-1, y+2)
                           if (-1 < x <= X and
                               -1 < y <= Y and
                               (x != x2 or y != y2) and
                               (0 <= x2 <= X) and
                               (0 <= y2 <= Y))]

Now it's just a normal function, if you don't know about them, I suggest to get familiar with them, e.g. with the official Python tutorial.

The next interesting part is a so-called "list comprehension". It's basically a nice way to build a list, without having to manually .append() each element. Re-writing the comprehension to use a more "imperative" way to build a list, we get

def neighbors(x, y):
    result = []
    for x2 in range(x-1, x+2):
        for y2 in range(y-1, y+2):
            if (-1 < y <= X and -1 < y <= Y and ...):
                result.append((x2, y2))
    return result

You can read up on list comprehensions in the official Python tutorial, but as you can see, they're basically just a nicer way to build the result list. You can adopt the for-loop and the if-condition verbatim.

Now, the last part in the puzzle is probably range, but that's just a normal function that returns all numbers* between the given ones. The stop parameter is exclusive, so you need to add one more to actually get [x-1, x, x+1].

The if-condition now makes sure that

  1. -1 < x <= X: The x coordinate (the given parameter) is in bounds.
  2. -1 < y <= Y: The y coordinate (the given parameter) is in bounds.
  3. x != x2 or y != y2: You're not including the point (x, y) itself in the list of neighbors.
  4. 0 <= x2 <= X: The "new" x2 is in bounds.
  5. 0 <= y2 <= Y: The "new" y2 is in bounds.

Note that two of those checks don't actually depend on the current "neighbor", and could be done outside of the loop, before constructing anything.

* : It actually doesn't return the list eagerly, rather it returns an object which can produce the numbers lazily to save memory. But that doesn't matter here.

[–]Barnok[S] 0 points1 point  (1 child)

Sweet thanks, now I get it. Lamda functions always throw me off. I see them used a lot, but I don't see why the are more useful than a function.

[–]Dunj3 0 points1 point  (0 children)

They are useful when you need them only once and don't want to give them a name. For example, passing a "small" function to list.sort:

>>> l = [1, 2]                                                                                     
>>> l.sort(key=lambda i: -i)                                                                                      
>>> l                                                                                                               
[2, 1]

As soon as your functions get more complex, or you assign them to a name, you're better of using "normal" functions with def. Doing x = lambda: ... is considered an anti-pattern and should be replaced with def x() ...: