AutoNetOps

Cover Image for Python Tricks I use a lot

Python Tricks I use a lot

·

11 min read

This article explores some of Python's essential functions—enumerate(), reversed(), zip(), map(), filter(), sorted(), and lambda—showing how they can be used to enhance scripts, simplify data processing, and improve overall network management. This is a quick and handy cheat sheet I use whenever I need a refresher on some Python skills.

Let’s get to it.

enumerate

When working with iterators, it's often necessary to keep track of the number of iterations. Python simplifies this task with the built-in function enumerate(). The enumerate() function adds a counter to an iterable and returns it as an enumerating object. This object can be used directly in loops or converted into a list of tuples using the list() function.

Example:

import requests

# List of network devices
network_devices = [
    '10.0.0.1',
    '10.0.0.2',
    '10.0.0.3',
]

# Function to ping a device
def ping_device(device_ip):
    try:
        response = requests.get(f'http://{device_ip}', timeout=1)
        return response.status_code == 200
    except requests.RequestException:
        return False

# Enumerate through the list of devices
for index, device_ip in enumerate(network_devices, start=1):
    is_reachable = ping_device(device_ip)
    status = "reachable" if is_reachable else "unreachable"
    print(f"Device {index}: {device_ip} is {status}")

### RESULT
Device 1: 10.0.0.1 is reachable 
Device 2: 10.0.0.2 is unreachable 
Device 3: 10.0.0.3 is unreachable
obj = enumerate(network_devices, start=1)
list(obj)

##RESULT
[(1, '10.0.0.1'), (2, '10.0.0.2'), (3, '10.0.0.3')]

reversed

In order to reverse an iterator:

network_devices = [
    '10.0.0.1',
    '10.0.0.2',
    '10.0.0.3',
]

for ip in reversed(network_devices)
    print(ip)

## OUTPUT
10.0.0.3
10.0.0.2
10.0.0.1

zip

The zip() function returns an iterator of tuples, where each tuple contains elements from the input iterators paired by their index. The first tuple contains the first elements from each iterator, the second tuple contains the second elements, and so on.

If the input iterators have different lengths, the resulting iterator will stop at the shortest input, truncating any excess elements from the longer iterators.

# Example of using the zip function in Python

# Two lists to be zipped
list1 = ['a', 'b', 'c', 'd']
list2 = [1, 2, 3, 4]

# Using zip to combine lists
zipped_list = zip(list1, list2)

# Converting the zipped object to a list and printing it
print(list(zipped_list))
""" 
OUTPUT: 
[('a', 1), ('b', 2), ('c', 3), ('d', 4)]
"""

# If lists are of different lengths, 
# zip stops creating pairs when the shortest list is exhausted
list3 = ['w', 'x', 'y']
zipped_list_short = zip(list1, list3)
print(list(zipped_list_short))
""" 
OUTPUT: 
[('a', 'w'), ('b', 'x'), ('c', 'y')]
"""

# Unzipping the lists
unzip1, unzip2 = zip(*list(zipped_list))
print("Unzipped list1:", unzip1)
print("Unzipped list2:", unzip2)

Map, Filter, Sorted, Lambda

map(), filter(), and sorted() functions in Python. These built-in functions are essential for performing functional programming tasks, enabling concise and efficient data processing. Understanding these functions will enhance your ability to automate network engineering tasks effectively.

Lambda

A lambda function in Python is an anonymous function defined using the keyword lambda instead of the def keyword used for regular functions. Lambda functions are typically used for short, simple operations that can be expressed in a single line of code. They are often passed as arguments to higher-order functions like the map(), filter(), or sorted(), where a function is needed for short-lived, inline operations.

Syntax: lambda arguments: expression

Unlike regular functions, lambda functions can only contain a single expression and implicitly return its result. This makes them ideal for simple tasks, like transforming or filtering data, without the overhead of writing a full function definition.

The power of lambda is best demonstrated when used as an anonymous function inside another function.

x = lambda a : a * a
x(2)
# OUTPUT 4

# Convert to IP Address
import ipaddress
x = lambda ip: ipaddress.ipaddress(ip)
x('192.168.0.1')
# OUTPUT: IPv4Address('192.168.0.1')
x(3232235521)
# OUTPUT: IPv4Address('192.168.0.1')

Map

The map() function applies a specified function to each item of an iterable (like a list, tuple, etc.) and returns a map object (which is an iterator) containing the results.

Syntax: map(function, iterable, …)

  • function: A function that takes one or more arguments and returns a value. This can be a regular function or a lambda (anonymous) function.

  • iterable: One or more iterable objects (e.g., list, tuple, dictionary).

Example:

devices = ['router1', 'switch1', 'firewall1', 'router2']

# Make Upercase
devices = map(lambda d: d.upper(), devices)
devices
## Out: <map at 0x7fba4d6fda80>
list(devices)
## OUT: ['ROUTER1', 'SWITCH1', 'FIREWALL1', 'ROUTER2']

Filter

The filter() function is used to construct an iterator from elements of an iterable (like a list or tuple) that satisfy a specific condition. The condition is specified by a function passed as the first argument to filter(). This function is applied to each element of the iterable, and only the elements for which the function returns True are included in the resulting iterator. Essentially, it filters out elements that do not satisfy a given condition.

Syntax: filter(function, iterable)

Used to extract elements that meet specific criteria from an iterable.

# Example: Filtering active network devices using filter and lambda 

devices = [ 
{'hostname': 'Router1', 'status': 'active', 'ip': '192.168.1.1'}, 
{'hostname': 'Switch1', 'status': 'inactive', 'ip': '192.168.1.2'}, 
{'hostname': 'Firewall1', 'status': 'active', 'ip': '192.168.1.3'}, 
{'hostname': 'Router2', 'status': 'inactive', 'ip': '192.168.1.4'}, 
]

active_devices = list(filter(lambda dev: dev['status'] == 'active'), devices)

"""
[{'hostname': 'Router1', 'status': 'active', 'ip': '192.168.1.1'},
 {'hostname': 'Firewall1', 'status': 'active', 'ip': '192.168.1.3'}]
"""

Sorted

The sorted() function returns a new sorted list from the items in an iterable. Unlike the list.sort() method, which sorts a list in place, sorted() does not modify the original iterable.

Syntax: sorted(iterable, *, key=None, reverse=False)

  • iterable: An iterable object to be sorted.

  • key (optional): A function that serves as a key for the sort comparison. This is often a lambda function.

  • reverse (optional): A boolean value. If True, the list elements are sorted as if each comparison were reversed (i.e., in descending order).

Usage

  • Can sort any iterable (lists, tuples, dictionaries, etc.).

  • Useful for sorting based on specific attributes or criteria using the key parameter.

# Sort a list of network interfaces based on their bandwidth utilization
# to identify the most and least utilized interfaces.

interfaces = [
    {'name': 'Gig0/1', 'utilization': 75},
    {'name': 'Gig0/2', 'utilization': 50},
    {'name': 'Gig0/3', 'utilization': 90},
    {'name': 'Gig0/4', 'utilization': 20},
]

interfaces_sorted = sorted(interfaces, key=lambda iface : iface['utilization'], reverse=False)

"""
[{'name': 'Gig0/4', 'utilization': 20},
 {'name': 'Gig0/2', 'utilization': 50},
 {'name': 'Gig0/1', 'utilization': 75},
 {'name': 'Gig0/3', 'utilization': 90}]
"""

Summary of Functions

FunctionPurposeReturns
map()Applies a function to each item of an iterableA map object (iterator) with the transformed items
filter()Filters items of an iterable based on a function's conditionA filter object (iterator) with the items that satisfy the condition
sorted()Sorts the items of an iterableA new sorted list of the items

Best Practices for Using map(), filter(), and sorted()

  1. Use lambda for Conciseness:
    When the function logic is simple and used only once, lambda functions can make your code more concise and readable.

  2. Combine with List Comprehensions:
    While map() and filter() are powerful, Python's list comprehensions often provide a more readable and Pythonic way to achieve the same results.

  3. Ensure Readability:
    While functional programming constructs are powerful, prioritize code readability. If a map() or filter() expression becomes too complex, it might be better to use a regular for loop or a named function for clarity.

Applying These Functions to Network Engineering Automation

In the context of network engineering automation, these functions can be instrumental in processing and manipulating data related to network devices, configurations, and logs. Here are a few scenarios where map(), filter(), and sorted() can be applied:

  1. Processing Device Configurations:

    • map(): Apply configuration changes to a list of devices.

    • filter(): Select devices that require updates based on their current configuration status.

    • sorted(): Order devices based on their priority or location for systematic updates.

  2. Analyzing Network Logs:

    • map(): Extract specific fields from raw log data.

    • filter(): Identify logs that indicate errors or security breaches.

    • sorted(): Arrange logs chronologically or by severity for easier analysis.

  3. Inventory Management:

    • map(): Generate summaries or reports from device inventories.

    • filter(): Identify devices that are inactive or need maintenance.

    • sorted(): Organize devices based on their roles, locations, or other attributes.

By mastering these functions, you can create more efficient and effective automation scripts, reducing manual effort and minimizing errors in managing complex network infrastructures.

Dictionaries

Dictionaries are powerful data structures in Python that store key-value pairs, making them ideal for managing configurations, mappings, and various network-related data. Mastering dictionary manipulation can significantly enhance your ability to automate and manage network infrastructures efficiently. This section explores essential dictionary operations and advanced tricks tailored for network engineering automation.

Merging

# Python Tricks for Dictionaries in Network Automation

# 1. Merge Multiple Dictionaries
config1 = {'hostname': 'router1', 'ip': '192.168.1.1'}
config2 = {'username': 'admin', 'password': 'admin123'}

merged_config = {**config1, **config2}
print(merged_config)
"""
    {'hostname': 'router1', 
    'ip': '192.168.1.1', 
    'username': 'admin', 
    'password': 'admin123'}
"""

Simplifying Key Management with defaultdict

The defaultdict from the collections module simplifies dictionary operations by providing default values for missing keys, eliminating the need to check for key existence explicitly.

from collections import defaultdict

interface_status = defaultdict(lambda: 'down')

interface_status['Gig0/1'] = 'up'
print(interface_status['Gig0/1'])  # Output: up
print(interface_status['Gig0/2'])  # Output: down (default value)

# 3. Dictionary Comprehensions for Filtering
interfaces = {'Gig0/1': 'up', 'Gig0/2': 'down', 'Gig0/3': 'up'}
up_interfaces = {iface: status for iface, status in interfaces.items() if status == 'up'}
print(up_interfaces)

## Output: {'Gig0/1': 'up', 'Gig0/3': 'up'}

defaultdict automatically assigns a default value ('down' in this case) to any key that doesn't exist in the dictionary. This is particularly useful for tracking the status of network interfaces without initializing each key manually.

Filtering Dictionaries with Comprehensions

Dictionary comprehensions provide a concise way to create dictionaries by filtering and transforming existing ones based on specific conditions.

Example: Dictionary Comprehensions for Filtering

# Dictionary Comprehensions for Filtering
interfaces = {'Gig0/1': 'up', 'Gig0/2': 'down', 'Gig0/3': 'up'}
up_interfaces = {iface: status for iface, status in interfaces.items() if status == 'up'}
print(up_interfaces)

Getting info

Mandatory vs Optional Items

device_info = {'hostname': 'router1', 'vendor': 'cisco'}

# Mandatory Item. Will return Error if trying to access a not existing key
device_type = device_info['device_type']
# KeyError: 'device_type'


# Using get() Method to Safely Access Keys with falback return value
device_type = device_info.get('device_type', 'unknown')
print(device_type)
# Output: 'unknown'

Inverting

# Inverting a Dictionary
vlan_mappings = {10: 'Sales', 20: 'Engineering', 30: 'HR'}
name_to_vlan = {v: k for k, v in vlan_mappings.items()}
print(name_to_vlan)

"""
{'Sales': 10, 'Engineering': 20, 'HR': 30}
"""

The dictionary comprehension iterates over vlan_mappings.items() and swaps each key-value pair, resulting in a new dictionary name_to_vlan where VLAN names are keys and VLAN IDs are values.

Updating Dictionaries

Updating dictionaries with new key-value pairs or modifying existing ones is a frequent operation in network automation.

Example: Updating Dictionaries with .update()

# Updating Dictionaries with .update()
config1 = {'hostname': 'router1', 'ip': '192.168.1.1'}
config2 = {'ip': '192.168.1.2', 'location': 'Data Center'}

config1.update(config2)

print(config1)
## Output: {'hostname': 'router1', 'ip': '192.168.1.2', 'location': 'Data Center'}

Removing Items from Dictionaries

Efficiently removing items from dictionaries helps maintain accurate and up-to-date configurations.

Example: Removing Items with .pop()

# Removing Items with .pop()
device_info = {'hostname': 'router1', 'vendor': 'cisco', 'ip': '192.168.1.1'}

# Remove and return the value of 'vendor'
vendor = device_info.pop('vendor')
print(vendor)        
## Output: cisco
print(device_info)   
## Output: {'hostname': 'router1', 'ip': '192.168.1.1'}

The .pop() method removes the key 'vendor' from device_info and returns its value. If the key doesn't exist, it raises a KeyError unless a default value is provided.

Using setdefault() for Default Values

The setdefault() method is useful for initializing dictionary keys with default values if they don't already exist.

Example: dict_element.setdefault()

# Using setdefault() for Default Values
device = {'Router1': 0}

# Initialize the count for 'Router1' if it doesn't exist
device.setdefault('Router2', 0)
device.setdefault('Router1', 10)

print(device_stats) ## {'Router1': 1, 'Router2': 0}

setdefault() ensures that 'Router2' is a key in device_stats with a value of 0, since the key does not exist, it creates it. It also updates the value of ‘Router1’ since the key already exists.

Summary of Dictionary Functions and Techniques

Function/TechniquePurposeReturns
{**dict1, **dict2}Merges two or more dictionaries into a new one.A new merged dictionary
defaultdictProvides default values for missing keys, simplifying key management.A dictionary with default values
Dictionary ComprehensionsCreates new dictionaries by filtering and transforming existing ones.A new dictionary based on the comprehension criteria
.update()Updates a dictionary with key-value pairs from another dictionary or iterable.Modifies the original dictionary
.pop()Removes a key and returns its value, optionally providing a default if the key is missing.The value associated with the removed key
.setdefault()Sets a default value for a key if it doesn't exist and returns the value.The value of the key after setting the default
.keys(), .values(), .items()Accesses the keys, values, or key-value pairs of a dictionary for iteration.Iterables of keys, values, or key-value pairs

Conclusion

By enhancing your understanding of Python's enumerate(), reversed(), zip(), map(), filter(), sorted(), and dictionary operations, you can significantly improve your network engineering automation scripts. These functions enable more concise, readable, and efficient code, ultimately leading to more streamlined and reliable network management.

Remember to balance the use of functional programming constructs with readability and maintainability. When in doubt, opting for clarity with regular loops or named functions can often be more beneficial, especially in complex automation tasks.