A Case for No-Argument ArcPy Geometry Constructors

1367
5
06-12-2017 09:14 AM
Labels (1)
JoshuaBixby
MVP Esteemed Contributor
3 5 1,367

My previous blog post, /blogs/tilting/2017/06/10/a-case-of-missing-prefixes-esris-geometries?sr=search&searchId=c6def5e0-70..., was unplanned.  I started writing this blog post and the next one, and I realized the background information I wanted to include didn't quite fit in either, so I wrote the background information up separately.

Over the years, I have found the ArcPy Geometry constructors to be quirky, especially with edge or niche cases like empty geometries.  As as engaged user, who believes providing feedback to developers is important, I have submitted several bugs and enhancement requests over time relating to geometry constructors.  Right off the top, I can't say how many have been implemented, possibly just one, but it wasn't one high up my wish list.

A couple years back I submitted the Support Empty Geometries with ArcPy Geometry Class Constructors idea.  Seeing how few users work with empty geometries, I can't say I am surprised with the whopping 2 votes it has received so far.  As‌ David Wynne from Esri points out in the comments, empty geometries are actually supported with the ArcPy Geometry constructors, it is just the syntax is undocumented.  Given empty geometries are supported by the constructors, I think my original idea should be merged under Pythonic ArcPy Geometries‌ since what I am really after is a more intuitive and idiomatic way to create empty geometries using ArcPy Geometry constructors.  I would merge my two ideas if I could, but I can't myself, and I don't want to just delete the idea.

Using the ArcPy Polygon class as an example, let's take a look at a variety of ways one might try to construct an empty geometry using the ArcPy Geometry constructors:

>>> ctor_stmts = (
...     '',
...     'None',
...     'arcpy.Array()',
...     'arcpy.Array(None)',
...     'arcpy.Point()',
...     'arcpy.Array([])',
...     'arcpy.Array([None])',
...     'arcpy.Array(arcpy.Array())',
...     'arcpy.Array(arcpy.Array(None))',
...     'arcpy.Array(arcpy.Array([]))'
... )
... 
>>> for i, stmt in enumerate(ctor_stmts):
...     stmt = "arcpy.Polygon({})".format(stmt)
...     try:
...         print("{:0>2}. {:<46}: {}".format(i+1, stmt, eval(stmt).WKT))
...     except Exception as err:
...         print("{:0>2}. {:<46}: {}....".format(i+1, stmt, err.message[:35]))
...         
01. arcpy.Polygon()                               : Object: CreateObject cannot create ....
02. arcpy.Polygon(None)                           : Object: CreateObject cannot create ....
03. arcpy.Polygon(arcpy.Array())                  : Object: CreateObject cannot create ....
04. arcpy.Polygon(arcpy.Array(None))              : MULTIPOLYGON EMPTY
05. arcpy.Polygon(arcpy.Point())                  : MULTIPOLYGON EMPTY
06. arcpy.Polygon(arcpy.Array([]))                : Object: CreateObject cannot create ....
07. arcpy.Polygon(arcpy.Array([None]))            : MULTIPOLYGON EMPTY
08. arcpy.Polygon(arcpy.Array(arcpy.Array()))     : MULTIPOLYGON EMPTY
09. arcpy.Polygon(arcpy.Array(arcpy.Array(None))) : MULTIPOLYGON EMPTY
10. arcpy.Polygon(arcpy.Array(arcpy.Array([])))   : MULTIPOLYGON EMPTY
>>> ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Before sharing any thoughts or comments, I think it is important to refresh ourselves on exactly what the ArcPy Polygon documentation states:

Syntax

 Polygon  (inputs, {spatial_reference}, {has_z}, {has_m})
ParameterExplanationData Type
inputs

The coordinates used to create the object. The data type can be either Point

or Array objects.

Object

A few observations before comments:

  • Examples 01 & 02:
    • The documentation states the inputs need to be Point or Array objects, which neither of these examples provide.
  • Examples 03 & 06:
    • These two examples are the same, e.g., repr(arcpy.Array()) == repr(arcpy.Array([]))Since Esri doesn't implement equivalence with ArcPy Arrays, I used the objects' __repr__ to show equality in this case.
    • Calling arcpy.Array() creates an empty ArcPy Array:  <Array []>.  Empty ArcPy Arrays are valid objects, and the documentation states ArcPy Array objects can be passed to the ArcPy Polygon constructor.
  • Examples 04 & 07:
  • Example 05:
    • The documentation states a Point object can be used as an input.
    • Calling arcpy.Point() doesn't create an empty point, like calling arcpy.Array(), instead it creates a 0, 0 point: <Point (0.0, 0.0, #, #)>.  Although not demonstrated above, further testing shows any point works, e.g., arcpy.Polygon(arcpy.Point(10000.5, -4500.3)) also creates an empty multi-polygon.
  • Examples 08 & 10:
    • Similar to Examples 03 & 06 and Examples 04 & 07, these two examples are the same.
    • Calling arcpy.Array(arcpy.Array()) creates an empty ArcPy Array nested within an ArcPy Array.  The ArcPy Array documentation does state an ArcPy Array is a valid input to the ArcPy Array constructor.

There are several oddities with the results from the polygon constructor test above.  I think Examples 03 & 06 are most likely to trip users up.  The documentation clearly states ArcPy Array objects are valid inputs, and empty ArcPy Arrays are valid objects, so why does passing an empty ArcPy Array to the ArcPy Polygon constructor generate an error?  Although Examples 04 & 07 work, it does seem odd that an ArcPy Array containing a Python None is somehow substantively different than an empty ArcPy Array when building an empty geometry.

Example 05 is a surprise and makes the least sense to me.  The documentation clearly states a Point object is a valid input to the ArcPy Polygon constructor, but what does a polygon with a single point look like?  An empty multi-polygon, according to Esri.  What takes the strangeness up another level for me is that there is no empty ArcPy Point, and that any ArcPy Point that is passed to the ArcPy Polygon constructor creates an empty geometry.

Using dis — Disassembler for Python bytecode to look at the bytecode for each of the constructor statements above, it appeared Example 05 would be the highest performing constructor statement.  Running timeit — Measure execution time of small code snippets on the constructor statements showed Example 05 was the quickest at generating empty geometries, not by much, but still the quickest.  This only adds to the mystery of the single-point polygon.

Examples 08 & 10 will be touched on in a subsequent blog post, so I won't dive into my thoughts on them here.

So how do the results from this test fit into the idea/request/plea for more Pythonic ArcPy Geometries‌?  For me, the answer is that Example 01 should be the primary way to create ArcPy empty geometries, i.e., passing no arguments to an ArcPy Geometry constructor should generate the equivalent empty geometry type.  For objects that support the concept of emptiness, it is fairly common in Python to have passing no arguments to the constructor create an empty object.  Let's look at some examples:

>>> # Common built-in data structures
>>> list()
[]
>>> set()
set([])
>>> dict()
{}
>>> str()
''
>>> 
>>> # Some SciPy data structures now bundled with ArcGIS
>>> import pandas
>>> pandas.DataFrame()
Empty DataFrame
Columns: []
Index: []
>>> import matplotlib.pylab
>>> matplotlib.pylab.plot()
[]
>>> ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

What is more Pythonic for creating an empty geometry, arcpy.Polygon(arcpy.Array(None)) or arcpy.Polygon() ?

UPDATE 06/2018:

For the sake of completeness, I want to include the results of running the constructor tests on the other ArcPy Geometry classes:

>>> geometry_classes = ["point", "multipoint", "polyline"]
>>> ctor_stmts = (
    '',
    'None',
    'arcpy.Array()',
    'arcpy.Array(None)',
    'arcpy.Point()',
    'arcpy.Array([])',
    'arcpy.Array([None])',
    'arcpy.Array(arcpy.Array())',
    'arcpy.Array(arcpy.Array(None))',
    'arcpy.Array(arcpy.Array([]))'
)
>>> for geometry in geometry_classes:
...     for i, stmt in enumerate(ctor_stmts):
...         stmt = "arcpy.Geometry('{}',{})".format(geometry, stmt)
...         try:
...             print("{:0>2}. {:<60}: {}".format(i+1, stmt, eval(stmt).WKT))
...         except Exception as err:
...             print("{:0>2}. {:<60}: {}....".format(i+1, stmt, err.message[:20]))
...     print("")
...     
01. arcpy.Geometry('point',)                                    : Object: CreateObject....
02. arcpy.Geometry('point',None)                                : Object: CreateObject....
03. arcpy.Geometry('point',arcpy.Array())                       : Object: CreateObject....
04. arcpy.Geometry('point',arcpy.Array(None))                   : Object: CreateObject....
05. arcpy.Geometry('point',arcpy.Point())                       : POINT (0 0)
06. arcpy.Geometry('point',arcpy.Array([]))                     : Object: CreateObject....
07. arcpy.Geometry('point',arcpy.Array([None]))                 : Object: CreateObject....
08. arcpy.Geometry('point',arcpy.Array(arcpy.Array()))          : Object: CreateObject....
09. arcpy.Geometry('point',arcpy.Array(arcpy.Array(None)))      : Object: CreateObject....
10. arcpy.Geometry('point',arcpy.Array(arcpy.Array([])))        : Object: CreateObject....

01. arcpy.Geometry('multipoint',)                               : Object: CreateObject....
02. arcpy.Geometry('multipoint',None)                           : Object: CreateObject....
03. arcpy.Geometry('multipoint',arcpy.Array())                  : Object: CreateObject....
04. arcpy.Geometry('multipoint',arcpy.Array(None))              : MULTIPOINT EMPTY
05. arcpy.Geometry('multipoint',arcpy.Point())                  : MULTIPOINT ((0 0))
06. arcpy.Geometry('multipoint',arcpy.Array([]))                : Object: CreateObject....
07. arcpy.Geometry('multipoint',arcpy.Array([None]))            : MULTIPOINT EMPTY
08. arcpy.Geometry('multipoint',arcpy.Array(arcpy.Array()))     : MULTIPOINT EMPTY
09. arcpy.Geometry('multipoint',arcpy.Array(arcpy.Array(None))) : MULTIPOINT EMPTY
10. arcpy.Geometry('multipoint',arcpy.Array(arcpy.Array([])))   : MULTIPOINT EMPTY

01. arcpy.Geometry('polyline',)                                 : Object: CreateObject....
02. arcpy.Geometry('polyline',None)                             : Object: CreateObject....
03. arcpy.Geometry('polyline',arcpy.Array())                    : Object: CreateObject....
04. arcpy.Geometry('polyline',arcpy.Array(None))                : MULTILINESTRING EMPTY
05. arcpy.Geometry('polyline',arcpy.Point())                    : MULTILINESTRING EMPTY
06. arcpy.Geometry('polyline',arcpy.Array([]))                  : Object: CreateObject....
07. arcpy.Geometry('polyline',arcpy.Array([None]))              : MULTILINESTRING EMPTY
08. arcpy.Geometry('polyline',arcpy.Array(arcpy.Array()))       : MULTILINESTRING EMPTY
09. arcpy.Geometry('polyline',arcpy.Array(arcpy.Array(None)))   : MULTILINESTRING EMPTY
10. arcpy.Geometry('polyline',arcpy.Array(arcpy.Array([])))     : MULTILINESTRING EMPTY

>>> ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

arcpy.Polyline() behaves the same as arcpy.Polygon() while arcpy.Multipoint() has one difference with passing arcpy.Point()

5 Comments
About the Author
I am currently a Geospatial Systems Engineer within the Geospatial Branch of the Forest Service's Chief Information Office (CIO). The Geospatial Branch of the CIO is responsible for managing the geospatial platform (ArcGIS Desktop, ArcGIS Enterprise, ArcGIS Online) for thousands of users across the Forest Service. My position is hosted on the Superior National Forest. The Superior NF comprises 3 million acres in northeastern MN and includes the million-acre Boundary Waters Canoe Area Wilderness (BWCAW).