Reduce list manipulation for QGIS layer tree

I am developing a plugin for the GIS software, QGIS. The code below reads the number of layers in various groups (as shown in the image) and adds them to a QTableWidget:

For each group, I want to count the number of layers, divide 1 by this number and then add the result of this to each of the layers in each group in the table.

Taking "Group1" as an example:

1. Count the number of layers (in this case 3).
2. Calculate 1 / 3 = 0.33....
3. Insert 0.33... to the first three layers in the table.
4. Repeat for the remaining groups.

So the table looks like this:

However, there's a fair bit of list manipulation involved which seems unnecessary so was hoping to see if there's a way to reduce this.

Here is the code:

# Define QTableWidget
qTable = self.dockwidget.tableWidget
# Define group
root = QgsProject.instance().layerTreeRoot()
main_group = root.findGroup('Main group')

def refresh_table():
# Define parameters for QTableWidget
qTable.setRowCount(0)
# Define list to contain all layer names
layer_data = []
# Define list to contain the number of layers
layer_count = []

# Find all groups in main_group
for group in main_group.children():
# Define list to count number of layers in each group
layer_list = []
# Find all layers in each group
for child in group.children():
node = root.findLayer(child.layer().id())
try:
# If layer is visible, add them to lists
if node.isVisible() == Qt.Checked:
layer_data.append(child.layerName())
layer_list.append(child.layerName())
except AttributeError:
pass
# Insert the number of layers in each group to layer_count list
layer_count.append(len(layer_list))

# Get total number of layers
layer_data_count = len(layer_data)
try:
# Create new list for layer_count but ignore any zeros
new_layer_count = [x for x in layer_count if x != 0]
except ValueError:
pass

# List manipulation
# Calculate the number of layers in each group and divide 1 by this number
value_list = [1 / float(x) for x in new_layer_count]
# Format the list to one decimal place
formatted_value_list = ['%.1f' % elem for elem in value_list]
# Convert the values in list to float
formatted_value_list_to_float = [float(x) for x in formatted_value_list]
# Create final list containing each layer and the values of their group
final_value_list = [x for n,x in zip(new_layer_count,formatted_value_list_to_float) for _ in range(n)]

# Set number of rows/columns
nb_row = len(layer_data)
nb_col = 2
qTable.setRowCount(nb_row)
qTable.setColumnCount(nb_col)
# Hide row index number

# Insert layer names and values
for row in range(layer_data_count):
for col in [0]:
item = QTableWidgetItem(str(layer_data[row]))
qTable.setItem(row,col,item)
# Make first column non-editable
item.setFlags(QtCore.Qt.ItemIsEnabled)
for col in [1]:
item = QTableWidgetItem(str(final_value_list[row]))
qTable.setItem(row,col,item)


Your lists manipulations are indeed messy. It seems like you are trying to perform multiple things at once and thus you end up mixing variables that serve different purposes into the same loops.

Instead, you should try to extract logical steps to perform, such as:

• extract informations you need from the internals of QGis to a similar but simpler representation;
• compute the data you need out of this simplified representation;
• format the data you computed for presentation;
• use the formatted data to update your visual presentation.

Here the second and third step are simple enough they can be made at once.

But first, we will need an helper function to simplify the inner working of the first double for loop:

def is_visible_layer(layer, root):
node = root.findLayer(layer.id())
try:
return node.isVisible() == Qt.Checked
except AttributeError:
return False


Now I’m not QGis expert, but I’m wondering if the root is really necessary to get the meta-information about the node. Isn't it possible to write it like:

def is_visible_layer(layer):
try:
return layer.isVisible() == Qt.Checked
except AttributeError:
return False


?

This let us write the for loop like:

for group in main_group.children():
layer_list = []
for child in group.children():
if is_visible_layer(child.layer(), root):
layer_data.append(child.layerName())
layer_list.append(child.layerName())
layer_count.append(len(layer_list))


Which can be simplified using list-comprehensions:

for group in main_group.children():
layer_list = [child.layerName() for child in group.children() if is_visible_layer(child.layer(), root)]
layer_data.extend(layer_list)
layer_count.append(len(layer_list))


Now recall what I said about performing several things at once? Here you already try to count things while extracting the informations from the QGis internals. Let's get that for later and simplify the loop once again to build a list of list of layer names:

layers = [
[
child.layerName()
for child in group.children()
if is_visible_layer(child.layer(), root)
]
for group in root.findGroup(main_group).children()
]


and then, only after you can start counting. And as it is much easier using this structure, you can start converting the list of list into a single list of couples that will mimic the table layout:

table = [
(layer, len(group))
for group in layers
for layer in group
]


Turning this list of couples into an N × 2 table is then really easy. The whole code should look like:

def is_visible_layer(layer, root):
node = root.findLayer(layer.id())
try:
return node.isVisible() == Qt.Checked
except AttributeError:
return False

def refresh_table(root, qTable, main_group='Main group'):
# Get names of relevant layers, as a list of list of names
layers = [
[
child.layerName()
for child in group.children()
if is_visible_layer(child.layer(), root)
]
for group in root.findGroup(main_group).children()
]

# Convert the layer list to the table layout i.e. a list of couples
table = [
(layer, len(group))
for group in layers
for layer in group
]

# Configure output table
qTable.setRowCount(len(table))
qTable.setColumnCount(2)

# Insert data into table
for row, (layer, count) in enumerate(table):
name_item = QTableWidgetItem(layer)
name_item.setFlags(QtCore.Qt.ItemIsEnabled)
count_item = QTableWidgetItem('{:.1f}'.format(1./count))
qTable.setItem(row, 0, name_item)
qTable.setItem(row, 1, count_item)


Call it like

refresh_table(
QgsProject.instance().layerTreeRoot(),
self.dockwidget.tableWidget)

• Many thanks for your answer, I'll get this implemented at some point and report back :) – Joseph Mar 16 '17 at 14:32
• Thanks, that fixed that error. Now received another one: for group in root.findGroup(main_group).children() AttributeError: 'QgsLayerTreeLayer' object has no attribute 'findGroup'. I will get back to this issue later as it's more a QGIS error than pure python. But in the meantime, I'll accept your answer. Many thanks :) – Joseph Mar 17 '17 at 14:24