X
10786 Rate this article:
No rating

Some Caveats when Overriding IDL_Object Methods

Anonym

Barrett’s post last week about using FOREACH with Hash objects reminded me of some problems I’ve had using FOREACH with the ENVIRasterSeries class.  This class inherits from IDL_Object and overrides the _overlodForeach() method, which changes the behavior of the loop.

Let’s use some code to explain this better.  First we can create a raster series using the data shipped with ENVI and the BuildTimeSeries ENVITask:

  e = ENVI(/headless)
 
  dataDir = FilePath('', SUBDIR=['data','time_series'], $
                     ROOT_DIR=e.ROOT_DIR)
  files = File_Search(dataDir, 'AirTemp*.dat')
 
  oTask = ENVITask('BuildTimeSeries')
  oTask.INPUT_RASTER_URI = files
  oTask.OUTPUT_RASTERSERIES_URI = e.GetTemporaryFilename('series')
  oTask.Execute
 
  oSeries = oTask.OUTPUT_RASTERSERIES

If we then use a FOREACH loop on the raster series object, one would logically expect one iteration of the loop with the series itself being the loop element, but instead it will perform 12 iterations for the 12 ENVIRasters that are in the raster series:

  foreach iter, oSeries do begin
    help, iter
  endforeach

Outputs

ITER            ENVIRASTER <249906>
ITER            ENVIRASTER <249960>
ITER            ENVIRASTER <250014>
ITER            ENVIRASTER <250068>
ITER            ENVIRASTER <250122>
ITER            ENVIRASTER <250176>
ITER            ENVIRASTER <250230>
ITER            ENVIRASTER <250284>
ITER            ENVIRASTER <250338>
ITER            ENVIRASTER <250392>
ITER            ENVIRASTER <250446>
ITER            ENVIRASTER <250500>

 

The ENVIRasterSeries class was designed this way so that you could use the more efficient FOREACH loop instead of a FOR loop over the size of the series with calls to Set and Raster:

  for i = 0, oSeries.Count-1 do begin
    oSeries.Set, i
    iter = oSeries.Raster
    help, iter
  endfor

Now let’s say you have a function that can take in an array of ENVIRasterSeries objects, and you want to use a FOREACH loop over the array, so that the loop element is the series in the array:

  foreach series, oSeriesArray do begin
    ; do stuff with each ENVIRasterSeries object
  endforeach

This will fail if a scalar ENVIRasterSeries is passed in, since it will instead result in the iteration over the rasters in the series and not the single series.  Fortunately we can take advantage of IDL’s array promotion capabilities.  If we wrap the oSeriesArray variable in square brackets, then we get the behavior we desire.  When a scalar is passed in, it becomes a 1-element array so the FOREACH loop will iterate over the array one time.  When an N-element array is passed in, it temporarily gets promoted to an Nx1 2-D array, which immediately collapses back into an N-element 1-D array.  Note that this technique won’t work in all cases, however, if you have a List of Hashs (from JSON_Parse(), perhaps), then wrapping the List in square brackets will create a 1-element array of Lists and the FOREACH loop will iterate once and have the List as the loop element, not the Hashs in the List as expected.  But in general this trick will work when you don’t know if you will get an array or a scalar.

  foreach series, [oSeriesArray] do begin
    ; do stuff with each ENVIRasterSeries object
  endforeach

Another IDL_Object override that can cause problems is _overloadSize(), as it can make N_Elements() appear to lie to you.  The List and Hash classes override this method, so that calling N_Elements() on them will tell you how many items are in the List or Hash, not how many List or Hash objects you have.  While an array of Lists sounds like a concept you’ll never encounter, an array of Hashs is possible, as it is what JSON_Parse(/TOARRAY) will return.

Let’s say we are writing a class that has a property that is a Hash, backed by a member variable to store the Hash.  The common pattern for SetProperty is to use N_Elements() on the keyword variable to determine if the method was called with that property name or not:

  if (N_Elements(myProp) gt 0) then begin
    self.myProp = myProp
  endif

The flaw here is that if an empty Hash is passed in the N_Elements() function will return 0 and we won’t update the property with the new value.  What we can do is call the base class implementation of _overloadSize(), which will return the number of Hash objects, not the number of elements in the Hash:

IDL> h = hash('foo', 1, 'bar', 2, 'baz', 3)
IDL> n_elements(h)
           3
IDL> h.IDL_Object::_overloadSize()
           1