QGIS 3 – Update or Change Attribute Field Value on the Fly in QGIS 3

qgisqgis-3qgis-custom-function

I'm using QGIS 3.2.
I have 4 fields ("kk1"-"kk4") where at least one of the fields contains a number (real).
I want to calculate the mean of these 1-4 fields and save the result (the average) in another field called "mkviskrans".
I calculate the average (based on this answer) of these 1-4 fields using this function in the Function Editor window in the Layer Properties:

@qgsfunction(args=-1, group='Custom')
def mean_col(cols, feature, parent):
  vals = [feature[i] for i in cols if feature[i] is not None]
  return sum(vals)/float(len(vals))

and calling the function from the Expression window in the widget's Default value option in Layer Properties:

mean_col('kk1','kk2','kk3','kk4')

UPDATE: I tried return 2.3instead of return sum(vals)/float(len(vals)). In this case the attribute table is updated automatically with the value 2.3. This indicates that the values typed in by the user in "kk1"-"kk4" actually have to be updated/evaluated/"commited" before returning the mean value, i.e.the division-expression will always return "None"? I have tried to update the feature/field values of "kk1"-"kk4" before return, but without any luck so far.

enter image description here
However, the mean value does not automatically appear in the "mkviskrans" "Text Edit" widget (alias "Midlere kvistkransavstand (dm)"):
enter image description here
Nor in the attribute table:
enter image description here
When I click in a cell in the attribute table, then the mean value appears:
enter image description here
Is it possible to get/show/save the value "on the fly"? I have tried different variants of update and/or commit within the function:

@qgsfunction(args=-1, group='Custom')
def mean_col(cols, feature, parent):
  layer = qgis.utils.iface.activeLayer()
  fieldIndex = layer.fields().indexFromName( 'mkviskrans' )
  vals = [feature[i] for i in cols if feature[i] is not None]
  """return sum(vals)/float(len(vals))"""
  mv = sum(vals)/float(len(vals))
  layer.changeAttributeValue(feature.id(), fieldIndex, mv)
  layer.commitChanges()

If I save the edits without clicking in a cell in the attribute table, the average (the field "mkvistkrans") will not contain any mean values.

(I may of course calculate the mean values after the field work, but I want the user to see the mean value "on the fly" – if possible.)

Best Answer

NULL and None

Fun fact: NULL is not None

With this in mind it's easy to fix the code

@qgsfunction(args=-1, group='Custom')
def mean_col(cols, feature, parent):
  vals = [feature[i] for i in cols if feature[i] != NULL]
  return sum(vals)/float(len(vals))

Do not write to layers in expressions

Additional advice: never write to layers in expressions. Expressions are often executed in threads and in different contexts and using layer.changeAttributeValue() and similar functions may result in undefined behavior (read: crash).

Using values instead of attribute names

Another interesting experiment for educational purpose.

Instead of providing column names to the function, you can also provide values.

@qgsfunction(args='auto', group='Custom')
def mean_value(vals, feature, parent):
    vals = [val for val in vals if val != NULL]
    return sum(vals)/float(len(vals))

And then call it with

mean_value("kk1", "kk2", "kk3", "kk4")

The interesting result is: it will return NULL as soon as at least one column is NULL. How comes? QGIS assumes the result for an expression function is NULL as soon as at least one parameter is NULL for performance reasons. It's possible to control this behavior, but unfortunately the @qgsfunction decorator does not support this to date (QGIS 3.2).

To workaround this behavior we can either provide a full-blown QgsExpressionFunction implementation which is out of scope for this answer. Or just take a little detour and use an array instead.

@qgsfunction(args='auto', group='Custom')
def mean_value(vals, feature, parent):
    vals = [val for val in vals if val != NULL]
    return sum(vals)/float(len(vals))

and then call it with

mean_value(array("kk1", "kk2", "kk3", "kk4"))
Related Question