Python Tips and Tricks

This page is a place to find Python code snippits that do something useful (we'll focus on Python 2.7 since that's what Indigo uses as of Indigo 7). Python is a great language but many of its features are often overlooked. Here's a place to find real-world examples.

The developer section (in which this page resides) is open to anyone who wants to contribute. We've turned back on the self-signup, so if you don't already have an account on the wiki just sign up for one. To get write access to this section, send a request to support@indigodomo.com and we'll update your permissions. Be sure to include your wiki user name. When adding, please title your snippit inside a header 3 (4 equals signs on each side of the title - use the last H icon in the button bar above to select Header 3) so that they will all appear on the table of contents on the right.

Convert List of Integers in a String to Python List of Ints

The map() function rules:

>> str_of_ints = "1, 2, 3"
>>> map(int, str_of_ints.split(","))
[1, 2, 3]

Convert List of Primitives in a String to a Python List of Primitives

As a followup to the previous snippit, you can even supply your own function in place of int in the map() call above. For instance, this function will look for ints, floats, booleans, and return the correct conversion:

def convert_from_str(in_string):
    # Test for an int
    try:
        int_value = int(in_string)
        return int_value
    except:
    	pass
    # Test for a float
    try:
    	float_value = float(in_string)
    	return float_value
    except:
    	pass
    # Test for a boolean
    if in_string.lower() in ("false", "off", "closed"):
        return False
    elif in_string.lower() in ("true", "on", "open"):
        return True
    # If it's not one of the above, then just return the string
    return in_string

Then call it from map():

>>> str_to_map = "False, true, cLoSeD, 1.0, 3, Some Text"
>>> map(convert_from_str, [x.strip() for x in str_to_map.split(",")])
[False, True, False, 1.0, 3, 'Some Text']

(I stripped any extra spaces from the beginning and end of each element in this example.)

Different Ways to Test Multiple Flags at Once

# Different ways to test multiple
# flags at once in Python
x, y, z = 0, 1, 0

if x == 1 or y == 1 or z == 1:
    print('passed')

if 1 in (x, y, z):
    print('passed')

# These only test for truthiness:
if x or y or z:
    print('passed')

if any((x, y, z)):
    print('passed')

Courtesy of Real Python's Python Tricks email.

Sorting a dictionary

Python dictionaries are inherently unsorted. Sometimes you want to sort them (for instance to iterate in key or value order). Here's the easiest way to do that. By key:

>>> dict_unordered = {'a': 4, 'c': 2, 'b': 3, 'd': 1}
>>> sorted(dict_unordered.items(), key=lambda x: x[0])
[('a', 4), ('b', 3), ('c', 2), ('d', 1)]

Or, by value:

>>> dict_unordered = {'a': 4, 'c': 2, 'b': 3, 'd': 1}
>>> sorted(dict_unordered.items(), key=lambda x: x[1])
[('d', 1), ('c', 2), ('b', 3), ('a', 4)]

Both return a list of tuples, each one with the first item being the key and the second the value. To iterate:

>>> for k, v in sorted(dict_unordered.items(), key=lambda x: x[1]):
...    print("{}: {}".format(k, v))
...    
d: 1
c: 2
b: 3
a: 4

Idea courtesy of Real Python's Python Tricks email.

Using Ordered Dictionaries

Python dictionaries are inherently unsorted. However, with Python 2.7 (and 3+) there is a new OrderedDict object. It functions exactly like a dict except that it stores the objects in the order in which they were inserted.

In the Sorting a Dictionary snippit, we see how to sort a dictionary. However, the results are transient since it's just converting them to a list of tuples. If you need to use the sorted contents of the dictionary multiple times, then you can convert the sorted dict elements in an OrderedDict object once then just use that.

Using the dictionary from the above snippit:

>>> from collections import OrderedDict
>>> dict_unordered = {'a': 4, 'c': 2, 'b': 3, 'd': 1}
>>> dict_sorted_by_key = OrderedDict(sorted(dict_unordered.items(), key=lambda x: x[0]))
>>> dict_sorted_by_value = OrderedDict(sorted(dict_unordered.items(), key=lambda x: x[1]))

After the code above, you can use the two new sorted OrderedDicts exactly the same way you would use any other dict. And if you iterate, you'll notice that you'll see the results ordered correctly:

>>> for k, v in dict_sorted_by_key.items():
...    print("{}: {}".format(k, v))
...    
a: 4
b: 3
c: 2
d: 1
>>> for k, v in dict_sorted_by_value.items():
...    print("{}: {}".format(k, v))
...    
d: 1
c: 2
b: 3
a: 4

Pretty Printing Python Collections

Python has the pprint module to assist with printing collection objects (dicts, lists, etc.). Unfortunately, it doesn't really do a great job. Here's a code snippit that illustrates using JSON to pretty print:

# The standard string repr for dicts is hard to read:
>>> my_mapping = {'a': 23, 'b': 42, 'c': 0xc0ffee}
>>> my_mapping
{'b': 42, 'c': 12648430. 'a': 23}  # 😞

# The "json" module can do a much better job:
>>> import json
>>> print(json.dumps(my_mapping, indent=4, sort_keys=True))
{
    "a": 23,
    "b": 42,
    "c": 12648430
}

# Note this only works with dicts containing
# primitive types (check out the "pprint" module):
>>> json.dumps({all: 'yup'})
TypeError: keys must be a string

Courtesy of Real Python's Python Tricks email.

Namedtuples

Named tuples are a sort of short-hand for creating a read-only class:

# Why Python is Great: Namedtuples
# Using namedtuple is way shorter than
# defining a class manually:
>>> from collections import namedtuple
>>> Car = namedtuple('Car', 'color mileage')

# Our new "Car" class works as expected:
>>> my_car = Car('red', 3812.4)
>>> my_car.color
'red'
>>> my_car.mileage
3812.4

# We get a nice string repr for free:
>>> my_car
Car(color='red' , mileage=3812.4)

# Like tuples, namedtuples are immutable:
>>> my_car.color = 'blue'
AttributeError: "can't set attribute"

One other use-case is if you want to create constants (read-only variables) which is often useful if you're creating a library/module that others will use. Python doesn't have the concept of a read-only primitive, which can lead to unexpected results if you want a value to never change. But using named tuples you can since tuples are immutable (read-only):

>>> Constants = namedtuple('Constants', ['CONST1', 'CONST2'])
>>> MY_CONSTANTS = Constants('value of const1', 'value of const2')
>>> MY_CONSTANTS.CONST1
'value of const1'
>>> MY_CONSTANTS.CONST2
'value of const2'
>>> MY_CONSTANTS.CONST2 = 'illegally trying to change the value'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

(Python convention is to put values that are considered constant in UPPERCASE).

Courtesy of Real Python's Python Tricks email.

Allowing a method definition to collect an unspecified number of arguments

Python method calls define the parameters that are to be passed in by callers:

def fixed(one, two, three):
    print("one: {}, two: {}, three: {}".format(one, two, three))

When you call it, you must pass in 3 arguments or you get an error:

>>> fixed(1)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: fixed() takes exactly 3 arguments (1 given)

When you pass in the correct number, it works:

>>> fixed(1,2,3)
one: 1, two: 2, three: 3

You may specify defaults as one way to define optional parameters:

def fixed_with_optionals(one, two=2, three=3):
    print("one: {}, two: {}, three: {}".format(one, two, three))

So it works with just one param:

>>> fixed_with_optionals(1)
one: 1, two: 2, three: 3

But this still requires that you define the complete possible list of arguments up-front. What if you wanted your method to collect up any additional arguments? For instance, depending on some of the defined parameters, there might be a difference in what other arguments are passed in?

You can do this using *args and **kwargs in your method definition. In this example, you're allowing the caller to pass in 1 to N number of positional arguments:

def extra_positionals(one, *args):
    print("one: {}".format(one))
    print("the rest of the positional args")
    for arg in args:
        print(arg)

Will result in this:

>>> extra_positionals(1, 2, 3)
one: 1
the rest of the positional args
2
3

If you'd rather allow for named parameters, you can do that too:

def extra_named(one, **kwargs):
    print("one: {}".format(one))
    print("the rest of the named args")
    for k, v in kwargs.items():
        print("{}: {}".format(k, v))

Call it with any number of named args:

>>> extra_named(1, two=2, three=3)
one: 1
the rest of the named args
two: 2
three: 3

These options are often useful if you're building a library that others will use. Rather than defining a lot of similarly named methods, you can just define one and then use the fixed params to determine what the rest of the params should be.

Execute actions in a Trigger only on certain days of the week

A new user recently asked how to execute actions in a Trigger only if it's either a weekday or a weekend day. Here's a simple script that illustrates how to do that in a Python script action:

from datetime import date

# Days in Python go from Monday (0) through Sunday (6)
WEEKDAY = [0, 1, 2, 3, 4] # this is a list of weekdays
WEEKEND = [5, 6] # this is a list of weekend days

# Here's an example of executing actions only on a weekday
if date.today().weekday() in WEEKDAY:
    # It's a week day, so execute actions here
    # You could directly execute actions if they're simple:
    indigo.device.turnOn(12345)  # 12345 is the ID of some on/off device
    # Or, if you have complicated actions, you would create an Action Group
    # with all the actions in it and execute the group from here:
    indigo.actionGroup.execute(67890) # 67890 is the ID of the Action Group
    
# Here's an example of executing actions only on a weekend
if date.today().weekday() in WEEKEND:
    # It's a week day, so execute actions here
    # You could directly execute actions if they're simple:
    indigo.device.turnOn(12345)  # 12345 is the ID of some on/off device
    # Or, if you have complicated actions, you would create an Action Group
    # with all the actions in it and execute the group from here:
    indigo.actionGroup.execute(67890) # 67890 is the ID of the Action Group

Obviously, this could be easily made more specific for certain days of the week, etc.

developer/python_tips_and_tricks.txt · Last modified: 2019/01/25 22:53 (external edit)
 

© Perceptive Automation, LLC. · Privacy