Most objects in Blender have multiple lists of data in their structure. For example, inside a “mesh” object there are list with vertices of this mesh, the list of mesh edges, and the list of mesh polygons. And in the structure of each mesh vertex there is, for example, a list with its coordinates along the X, Y and Z axes.
To access such internal data lists, we usually have to create loops of several levels of nesting. However, in the Python language, which is used in the Blender API, there is a convenient ability to form such nested loops in a single line – list comprehension.
Let’s consider a simple example: we have several selected polygons on the mesh, and we need to find the maximum coordinate along the Z axis among the vertices of these polygons.
In common way, we will first create a loop through the selected polygons:
1 2 |
for polygon in bpy.context.object.data.polygons: if polygon.select: |
Create another loop based on the vertexes belonging to the selected polygons inside it:
1 |
for vertex_id in polygon.vertices: |
And only inside it, we will be able to access the coordinates of each vertex. This results in the following code structure consisting of four levels of nesting:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
z_co = [] for polygon in bpy.context.object.data.polygons: if polygon.select: for vertex_id in polygon.vertices: z_co.append(bpy.context.object.data.vertices[vertex_id].co.z) print(z_co) # [0.2421875, 0.1171875, 0.15625, ...] print(max(z_co)) # 0.375 |
But using the list comprehension, we can write this entire expression in a single line.
This is how list comprehension in Python works: one level of loop that produces a list is written to a string in square brackets. On the left is the resulting expression for each iteration of the loop, on the right, if necessary, an additional condition.
new_list = [expression for item in iterable (if condition)]
So the first and second levels of our code can be represented by the following one-line expression:
1 2 3 |
[polygon.vertices for polygon in bpy.context.object.data.polygons if polygon.select] # [bpy.data.meshes['Suzanne'].polygons[49].vertices, bpy.data.meshes['Suzanne'].polygons[51].vertices, ...] |
Having expanded only the first two levels into a string using list comprehension, we get a list of lists – each element of the new list is itself a list with vertex coordinates.
We could loop through the resulting list again:
1 2 3 4 5 6 7 |
z_co = [] for polygon_vertices in [polygon.vertices for polygon in bpy.context.object.data.polygons if polygon.select]: for vertex_id in polygon_vertices: z_co.append(bpy.context.object.data.vertices[vertex_id].co.z) print(z_co) |
The level of nesting has become slightly smaller, but there is still chance for further compression, since list comprehension in Python supports several levels of nesting.
Each subsequent level is written from left to right, with an optional conditions. The expression on the left can operate on variables from the last (most often), or from each of the levels.
Let’s continue turning our code into a one-liner.
On the left, we specify the final expression to obtain the Z coordinate of the vertex. Next, from left to right, we write expressions from each level of code and conditions, if exists.
We will get the following line of code:
1 2 3 4 5 6 7 |
z_co = [bpy.context.object.data.vertices[vertex_id].co.z for polygon in bpy.context.object.data.polygons if polygon.select for vertex_id in polygon.vertices] print(z_co) # [0.2421875, 0.1171875, 0.15625, ...] print(max(z_co)) # 0.375 |
As a result, we received exactly the same list of values as when using multi-level loops.
Let’s look at another example: we have several selected vertices on a mesh, and we need to get a list of edges coming out of these vertices that are not selected.
To get edges associated with vertices, we can use the bmesh object.
1 2 3 4 |
bm = bmesh.new() bm.from_mesh(bpy.context.object.data) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() |
First, let’s get a list of unselected edges in the usual way, using nested loops:
1 2 3 4 5 6 7 8 9 10 11 |
non_selected_edges = [] for vertex in bm.verts: if vertex.select: for edge in vertex.link_edges: if not edge.select: non_selected_edges.append(edge) print(non_selected_edges) # [<BMEdge(0x00000243184671F0), index=6>, ...] |
And next, get this list using list comprehension, writing all the code in one line.
Specify the final expression – what exactly we get in the list, on the left. In our case, this is edge. Next, from left to right, we write each level with optionally a condition.
1 2 3 4 5 |
non_selected_edges = [edge for vertex in bm.verts if vertex.select for edge in vertex.link_edges if not edge.select] print(non_selected_edges) # [<BMEdge(0x00000243184671F0), index=6>, ...] |
We received the same list of edges we needed, using only one line of code.