pyqgis – Formatting Attribute Map for changeAttributeValues Method in PyQGIS

field-mappingfields-attributesgeopackagepyqgisqgsvectorlayer

I have a PyQGIS script being run from within QGIS that updates attribute values in a GeoPackage, based on a mapping. It currently iterates through fields and features one by one, updating the values using QgsVectorLayer.changeAttributeValue.

This seems inefficient, particularly if there are many features. I'm looking to update it to use QgsVectorDataProvider.changeAttributeValues: https://qgis.github.io/pyqgis/3.28/core/QgsVectorDataProvider.html#qgis.core.QgsVectorDataProvider.changeAttributeValues

The changeAttributeValues method takes as a parameter "attr_map (object) – a map containing changed attributes". Unfortunately, the documentation doesn't set out the format of the map, and a Google search hasn't yielded any useful information.

The aim is to update the code to create the attribute map, and then run one operation using changeAttributeValues to update all changes at once. I am specifically looking for the format of the attribute map, though code is of course welcome.

The dummy existing code is as follows:

outfile = "c:/temp/output.gpkg"

oldFieldNames = ["oldField1", "oldField2", "oldField3"]
newFieldNames = ["newField1", "newField2", "newField3"]
mapping = [
    {0: "orange", 1: "apple", 2: "pineapple"},
    {0: "red", 1: "yellow", 2: "green", 3: "blue"},
    {0: "dog", 1: "cat", 2: "goat", 3: "cow", 4: "fish"},
]

vl = QgsVectorLayer(outfile, 'temp', 'ogr') 
pr = vl.dataProvider()

for i in range(len(oldFieldNames)):
    newFieldIndex = pr.fieldNameIndex(newFieldNames[i])
    with edit(vl):
        for feature in vl.getFeatures():
            vl.changeAttributeValue(feature.id(), newFieldIndex, mapping[i][feature[oldFieldNames[i]]])

If the initial layer attributes are as follows:

fid oldField1 oldField2 oldField3 newField1 newField2 newField3
1 0 3 4
2 1 2 3

then the final result after mapping should be:

fid oldField1 oldField2 oldField3 newField1 newField2 newField3
1 0 3 4 orange blue fish
2 1 2 3 apple green cow

As an aside, I have found quite regularly with the QGIS Python API documentation, that there is not enough detail to know the exact format of parameters required for some methods.

Best Answer

As @BERA says, the attribute map is a dictionary of dictionaries, with the first key as the feature ID, and the second key as the field index.

e.g.

attrMap = {
    featureId1: {fieldIndex1: value1, fieldIndex2: value2, fieldIndex3: value3}, 
    featureId2: {fieldIndex1: value4, fieldIndex2: value5}, 
    ...}

Updated code using changeAttributeValues (there may be more efficient ways of iterating):

outfile = "c:/temp/output.gpkg"

oldFieldNames = ["oldField1", "oldField2", "oldField3"]
newFieldNames = ["newField1", "newField2", "newField3"]
mapping = [
    {0: "orange", 1: "apple", 2: "pineapple"},
    {0: "red", 1: "yellow", 2: "green", 3: "blue"},
    {0: "dog", 1: "cat", 2: "goat", 3: "cow", 4: "fish"},
]

vl = QgsVectorLayer(outfile, 'temp', 'ogr') 
pr = vl.dataProvider()

attrMap = {}
newFieldIndices = []
for i in range(len(newFieldNames)):
    newFieldIndices.append(pr.fieldNameIndex(newFieldNames[i]))

for feature in vl.getFeatures():
    featureAttrMap = {}
    for i in range(len(oldFieldNames)):
        featureAttrMap[newFieldIndices[i]] = mapping[i][feature[oldFieldNames[i]]]
    attrMap[feature.id()] = featureAttrMap

# print(attrMap)
pr.changeAttributeValues(attrMap)

The attrMap data for the example above is:

attrMap = {
    1: {4: 'orange', 5: 'blue', 6: 'fish'}, 
    2: {4: 'apple', 5: 'green', 6: 'cow'}
}