ArcPy - Check a field type and execute depending on the type

8911
12
03-13-2015 11:00 AM
JohnSemanchik
New Contributor II

Hello,

I'm not sure if this is the right place to send this so please correct me if I am wrong. 

I had a script that converted data from a clients site to a usable GIS format until they changed the output.  I've tried two different directions with no luck.  Due to how an earlier part of the script works, CSV to Table, and the nature of the data being converted; the 'total_length' field that's imported might end up being a string or an integer (TableToTable conversion will not work).  I need it to look at the field type and if it's already an integer then populate the 'length_val' field with the number.  If it's a string then strip it down to the number (typically the greater than, less than or tilde off the front) and populate the 'length_val' field. 

In other words:

If 'total_length' = STRING, then send only the number to 'length_val'

Otherwise, if 'total_length' = INTEGER, then calculate the field. 

I've been using update cursors to do a lot of the rest of the work, but that doesn't have to be the case.  Any assistance would be great. 

Thanks,

John

Row[3] = 'total_length'

Row[17] = 'length_val'

---First Attempt---

with arcpy.da.UpdateCursor(aoi_table_gdb,aoi_fields) as cursor:

    totlen = "total_length"  #<--- This was just an idea

    fields = arcpy.ListFields(aoi_table_gdb)

    for field in fields:

        if field.name = '"'+ totlen +'"' and field.type = String:

            for row in cursor:

                if row[3] == None:

                    row[17] = None

                elif row[3].find(str('~ ')) > -1:

                    row[17] = row[3].lstrip(str('~ '))

                elif row[3].find(str('> ')) > -1:

                    row[17] = row[3].lstrip(str('> '))

                elif row[3].find(str('< ')) > -1:

                    row[17] = row[3].lstrip(str('< '))

                else:

                    row[17] = row[3]

        else:

            for row in cursor:

                row[17] = row[3]

        cursor.updateRow(row)

---Second Attempt---

with arcpy.da.UpdateCursor(aoi_table_gdb,aoi_fields) as cursor:

    for row in cursor:

        #Length Value

        if row[3] == None:

            row[17] = None

            arcpy.AddWarning("A. Row: " + str(row[1]) + " - " + str(row[3]))

        elif row[3].isdigit():

            row[17] = row[3]

            arcpy.AddWarning("B. Row: " + str(row[1]) + " - " + str(row[3]))

        else:

            length = filter(unicode.isdigit, row[3])

            row[17] = length

            arcpy.AddWarning("C. Row: " + str(row[1]) + " - " + str(row[3]))

Tags (1)
0 Kudos
12 Replies
DarrenWiens2
MVP Honored Contributor

In your first script, the line should be:

if field.name == 'total_length' and field.type == 'String':

There are other logic issues in the first script, too. For example, it is only looking for one field, but does the operation once per field per row.

0 Kudos
JohnSemanchik
New Contributor II

Awesome!  Thank you.  I'll give it a try. 

0 Kudos
JoshuaBixby
MVP Esteemed Contributor

Type checking, e.g., field.type == 'String', might be the ArcGIS way but it isn't very Pythonic.  This is one of those cases where learning programming through ArcPy teaches less-than-idiomatic Python.

As the expression goes, and is summarized by @herge over at Stackoverflow (What's the canonical way to check for type in python?😞

The most Pythonic way to check the type of an object is... not to check it.

Since Python encourages Duck Typing, you should just try to use the object's methods the way you want to use them. So if your function is looking for a writable file object, don't check that it's a subclass of file, just try to use its .write() method!

Of course, sometimes these nice abstractions break down and isinstance(obj, cls) is what you need. But use sparingly.

It is for this reason that I suggested type casting your integers to strings and then process them and your strings containing integers the same way.

BlakeTerhune
MVP Regular Contributor

In addition to the points Darren Wiens made, the rows returned by the update cursor are a tuple. In Python, a tuple is what's called an immutable object; it cannot be changed. You have to convert the row from a tuple to a list before you can change the values. Then you can pass the altered row list back to the cursor's updateRow() method (which accepts a tuple or list).

You can also create the update cursor with only the fields you are interested in so you don't have to loop through all the fields unnecessarily.

This code assumes the data type of the length_val field is integer. It also assumes that if total_length was a string, that it would never have more than one number.

aoi_fields = ["total_length", "length_val"]
with arcpy.da.UpdateCursor(aoi_table_gdb, aoi_fields) as cursor:
    for row in cursor:
        rowList = list(row)
        if isinstance(rowList[0], int) or isinstance(rowList[1], float):
            rowList[1] = int(rowList[0])
        elif isinstance(rowList[1], basestring):
            # Pull out only the number from the string
            rowList[1] = int(filter(str.isdigit, rowList[0]))
        else:
            rowList[1] = None
        cursor.updateRow(rowList)

I used this thread for extracting the number from the string.

Alternatively, you could use Calculate Field to do this (instead of an arcpy.da cursor).

DarrenWiens2
MVP Honored Contributor

Just a small note that update cursors return rows in a list not tuple  (search cursors return rows in a tuple), so you can skip that step.

BlakeTerhune
MVP Regular Contributor

Even though the documentation says tuple, it does return a list. Thanks for the clarification!

0 Kudos
DarrenWiens2
MVP Honored Contributor

Blake T, sorry to harp on this, but I'm not seeing the reference to the update cursor returning rows as a tuple. There are a few places that may take a tuple as input (e.g. fields), and outputs some properties as tuples (e.g. SHAPE@XY), but not rows. Perhaps you're thinking of the tuple row returned by next(), but this is hardly ever used anymore.

0 Kudos
BlakeTerhune
MVP Regular Contributor

Yeah, I was looking at the return value for the next() method. I see the second second sentence at the very beginning clearly says:

Returns an iterator of lists.

But down at the bottom under the Method overview for next(), it says:

Returns the next row as a tuple.

I tested both the next() method and just iterating through the whole cursor in a for loop and both give the row as a list (like you said). Esri's documentation for the next() method is just wrong.

JoshuaBixby
MVP Esteemed Contributor

They likely wrote the documentation for the Search Cursor first, and it is correct for that cursor because it does return tuples, and then someone got sloppy and didn't update the next() portion for the update cursor.

Little mistakes like this happen, it is human nature, but what amazes me is how ridiculous it gets when you try to get Esri to correct them, i.e., open a Support Case, submit a documentation bug, wait for development to look at it (which could be months), and then maybe they will address it in a future version.  The software may, may, be agile but not the business processes!