5

I am developing a Python toolbox, and came across a peculiar behavior: The tool objects does not remember any instance variables between function calls. Consider this code:

import arcpy

class Toolbox(object):

   def __init__(self):
      self.label = "Test"
      self.alias = "Test"
      self.tools = [Tool]

class Tool(object):

   def __init__(self):
      self.label = "Test tool"
      self.x = "INIT"

   def getParameterInfo(self):
      param0 = arcpy.Parameter(
         displayName = "Parameter",
         name = "parameter",
         datatype = "String",
         parameterType = "Required",
         direction = "Input")
      return [param0]

   def updateParameters(self, parameters):
      self.x = "UPDATE PARAMETERS"

   def updateMessages(self, parameters):
      parameters[0].setWarningMessage(self.x)
      self.x = "UPDATE MESSAGES"

   def execute(self, parameters, messages):
      arcpy.AddMessage(self.x)

The way I would expect this code to behave is this:

  • First __init__ is called, and x is set to "INIT".
  • Then updateParameters is called, and x is changed to "UPDATE PARAMETERS".
  • Then updateMessages is called. It displays a warning with the text "UPDATE PARAMETERS" and then change x to "UPDATE MESSAGES".
  • Finally, when the tool is executed it outputs "UPDATE MESSAGES".

However, when I run the tool both the warning and the execution output is "INIT". My conclusion is that ArcMap does not use the same instance of the class throughout, but instead creates a new instance for every function call (updateParameters, updateMessages, and execute).

Is my conclusion correct? If so, why does ArcMap behave that way? From an OOP perspective it seems very strange.

This behavior causes big problems for me. The first parameter of the tool I am developing is a text file. When the user has picked one I want to populate the other parameters with different default values depending on the content of the file. Since the file could be rather large I do not want to have to parse it every time any parameter changes, but now it seems like I have to since there is no way to know if the parameter has changed or not.

2
  • I made an edit to my question. You are right, it is spinning up a crazy amount of instances! This is not desirable behavior. – crmackey Aug 4 '15 at 12:56
  • 1
    To your question about if your conclusion is correct, I believe it is - I've had similar problems with toolboxes that have bitten me – nicksan Aug 5 '15 at 0:29
5

One thing I have noticed (with both add-ins or custom script tools/PYT's) is that when ArcMap or Catalog load, it will instantiate the classes contained within these custom tools as the application is loaded. I do not think these start a new instance of the class when the tool is ran (unless you refresh the PYT), but maybe I am wrong. Haven't tested this fully. It is interesting if it does create a new class instance whenever any method call is made.

To get around this, you may want to to include a function that can be called to parse your text file if a parameter is altered so that you can explicitly control when the text file is reloaded. A simple example:

def parseText(txt):
    with open(txt, 'r') as f:
        return f.readlines() 

So then, in your PYT you can call this function if the parameter has been changed by the user:

   def updateParameters(self, parameters):
      if parameters[0].altered:
          text_content = parseText(r'C:\path\to\your_file.txt')
          self.x = "UPDATE PARAMETERS"

You can also call this in the __init__ of the class to get an initial cache of the content. Python is pretty quick when parsing text files, even if they are large. If you are loading every line of text into a list and you are concerned about memory, perhaps you would be better off having the function return a generator.

Unfortunately, I do not think you can get around not reloading the text file if the user changes a parameter. Perhaps you can do all the control for the text file in the UpdateParameters with some if statements, like if parameters[0].value == 'this' and parameters[1].value == 'another thing' and handle the text file differently for whatever combination of input parameters you get.

I am with you though, I do not like the default behavior of the PYTs.

EDIT:

Wow, you are right. It is spinning up a TON of instances! Here's a simple way to track # of instances:

>>> class Test(object):
    i = 0
    def __init__(self):
        Test.i += 1


>>> t = Test()
>>> t2 = Test()
>>> t3 = Test()
>>> t3.i
3

And now if we apply that same logic to a PYT, try running this in ArcMap:

import arcpy
import pythonaddins

class Toolbox(object):

    def __init__(self):
      self.label = "Test"
      self.alias = "Test"
      self.tools = [Tool]

class Tool(object):
    i = 0
    def __init__(self):
        self.label = "Test tool"
        self.x = "INIT"
        Tool.i += 1

    def getParameterInfo(self):
        param0 = arcpy.Parameter(
        displayName = "Parameter",
        name = "parameter",
        datatype = "String",
        parameterType = "Required",
        direction = "Input")
        return [param0]

    def updateParameters(self, parameters):
        if parameters[0].altered:
            self.x = "UPDATE PARAMETERS"
            pythonaddins.MessageBox('# of instances: {}, x = {}'.format(self.i, self.x), 'test')

    def updateMessages(self, parameters):
        parameters[0].setWarningMessage(self.x)
        self.x = "UPDATE MESSAGES"
        pythonaddins.MessageBox('# of instances: {}, x = {}'.format(self.i, self.x), 'test')

    def execute(self, parameters, messages):

        arcpy.AddMessage('# of instances: {}, x = {}'.format(self.i, self.x))

I got 9 instances right off the bat! It spins up new ones every time I change a parameter, and it keeps climbing!

enter image description here

2
  • Thanks for the reply! Two questions: (1) If ArcMap does not create new instances, why are the values of the instance variables not remembered? (2) It is my understanding that the altered property is true when whenever the value has been changed by the user. So once it is changed by the user, it will still be True every time another parameter is changed? I guess you are right in that I should just make my code fast enough so that this isn't a problem anymore. – Anders Aug 4 '15 at 12:34
  • 1
    For your number (1), I think you must be right. The values should be remembered if it were the same instance; I am just not sure why Esri would implement it this way. From the base logic of their templates, it would seem it would all be contained within ONE instance. For (2), I believe it will call UpdateParameters every time a parameter is changed, so yes, it should be True every time something is changed. Another option could be to make an Add-In where this behavior is controlled by combo boxes. – crmackey Aug 4 '15 at 12:40
3

crmackey is right, it is making a ton of instances of the object. What you should do to get around this is create a global variable in getParameterInfo (because it is only called once), and then update or reference it in updateParameters or updateMessages.

For example:

class MyToolbox(object):
  self.label = 'my_toolbox'
  self.description = 'This is my toolbox.'
  self.canRunInBackground = False
def getParameterInfo(self):
  param0 = Parameter( ... )
  global myVar
  myVar = None
  return [param0]
def updateParameters(self, parameters):
  param0 = parameters[0]
  global myVar
  if param0.value and param0.value == myVar:
    ...
  myVar = param0.value
...
1
  • Thanks for posting, this is good to know. Is this documented somewhere that it is only called once? Or is there any documentation on why there so many instances being created? – crmackey Jan 6 '16 at 15:41
3

We have come across the same behavior in ArcGIS Desktop 10.6. The way we worked around this is by using a separate class for the "business logic" that shadows the methods of the .pyt (getParameters, updateParameters, updateMessages, execute). What we do however is to instantiate the business logic class as a singleton. This way it will only be instantiated one and will keep the state between the re-instantiation of the tool class.

The seperation of the business logic was done to make it more testable since we could not use the .pyt file with pytest.

_logic = None
class ShapefileLoader(object):
    def __init__(self):
        self.label = "Shapefile Loader"
        self.description = "Load a shapefile into the Parcel Fabric"
        self.canRunInBackground = False
        global _logic
        if _logic is None:
            _logic = ShapefileLoaderLogic.ShapefileLoaderLogic()
        self.logic = _logic

def getParameterInfo(self):
    """
    Define parameter definitions
    """
    params = self.logic.get_parameter_info()
    return params

This way the code can be unit tested and does not get instantiated lots of times.

Hope this helps.

0

I have been working on a Python Add-In that has a Toolbar with buttons that launch Tools in a Python Toolbox. What I found is that I can define global variables (as None) in a Python Toolbox (.PYT) file above the class definitions for the tools, then have Python global statements for those variables in the updateParameters(self, parameters) method, but NOT in the getParameterInfo(self) method for the Tools in the Python Toolbox. Any attempt to initialize a global variable in the getParameterInfo method is ignored, and the default None value is kept.

If you need to initialize a parameter in a Python Toolbox Tool using a global variable, you need to initialize it in updateParameters NOT getParameterInfo.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.