Some Caveats when Overriding IDL_Object Methods
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