Сообщить об ошибке.

Функция как значение ключа словаря в Python

Так как функции являются объектами Python, то можно заменить значения ключей словаря функциями и возвращать их, если ключи совпадают. Другими словами, использовать функцию в качестве значения ключа словаря в Python.

Подробнее о паттерне проектирования "Factory".

Пример своеобразной фабрики функций:

# словарь с функциями
calc = {
    "plus": lambda x, y: x + y,
    "minus": lambda x, y: x - y,
    "division": lambda x, y: x / y,
    # в качестве значения можно использовать 
    # встроенную функцию pow() или любую 
    # пользовательскую функцию 
    "power": pow
}

def action(match, dictionary, default="NO CALC"):
    """шаблон фабрики функций"""
    if match in dictionary:
        return dictionary[match]
    return lambda *x: default

>>> plus = action('plus', calc)
>>> minus = action('minus', calc)
>>> power = action('power', calc)
>>> square = action('square', calc)
>>> plus(5, 4)
# 9
>>> minus(5, 4)
# 1
>>> power(3, 3)
# 27
>>> square(1, 1)
'NO CALC'
>>> square(1)
'NO CALC'

Обратите внимание, что значениями ключей может быть обычная функция, определенная при помощи оператора def functuon(): ....

Если лень создавать шаблон Фабрики, то можно использовать словарь с функциями напрямую:

# словарь с функциями
calc = {
    "plus": lambda x, y: x + y,
    "minus": lambda x, y: x - y,
    "division": lambda x, y: x / y,
    "power": pow
}

# функция, которая будет вызываться при ошибке
def err(*args): return print(f"Ошибка calc{args}")

>>> calc.get('plus', err)(5, 4)
# 9
>>> calc.get('minus', err)(5, 4)
# 1
>>> calc.get('division', err)(5, 4)
# 3.0
>>> calc.get('pwr', err)(5, 4)
# Ошибка calc(5, 4)

Где можно использовать шаблон "Фабрика функций"?

Представьте ситуацию, что есть некий код, который по условию if/elif/else как-то обрабатывает данные, например, преобразует их в формат JSON или YAML или в список кортежей или делает что-то еще. Обратите внимание, что код обработки данных в каждом блоке if или elif может быть огромным. Как результат, он плохо читается и его трудно поддерживать, а также его нельзя использовать повторно. В таких ситуациях приходит на помощь фабрика функций.

Например есть такой код:

# test.py
import json
import xml.etree.ElementTree as et

# например поступают данные в виде словаря 
book = [
    {"book_id": "1", "title": "Книга-1", "autor": "Автор-1"},
    {"book_id": "2", "title": "Книга-2", "autor": "Автор-2"},
]

# и они обрабатываются следующим образом
def convertation(book, format):
    if format == 'JSON':
        # в каждом блоке может быть очень много кода
        for item in book:
            song_info = {
                'id': item['book_id'],
                'title': item['title'],
                'autor': item['autor']
            }
            print(json.dumps(song_info))
    elif format == 'XML':
        for item in book:
            autor_info = et.Element('book', attrib={'id': item['book_id']})
            title = et.SubElement(autor_info, 'title')
            title.text = item['title']
            autor = et.SubElement(autor_info, 'autor')
            autor.text = item['autor']
            print(et.tostring(autor_info, encoding='unicode'))
    else:
        raise ValueError(format)


# запускаем код в интерактивном режиме
# $ python3 -i test.py
>>> convertation(book, 'XML')

Согласно паттерна проектирования "Фабрика", сложный ветвящийся код необходимо разбить на части. А именно, вынести каждый блок, который преобразовывает данные определенным образом в отдельную функцию, и вызывать их например из словаря где ключами будут форматы, а значениями соответствующие функции.

Смотрим как это можно сделать:

# test.py
import json
import xml.etree.ElementTree as et

def to_json(book):
    """Преобразование в JSON"""
    for item in book:
        song_info = {
            'id': item['book_id'],
            'title': item['title'],
            'autor': item['autor']
        }
        print(json.dumps(song_info))


def to_xml(book):
    """Преобразование в XML"""
    for item in book:
        autor_info = et.Element('book', attrib={'id': item['book_id']})
        title = et.SubElement(autor_info, 'title')
        title.text = item['title']
        autor = et.SubElement(autor_info, 'autor')
        autor.text = item['autor']
        print(et.tostring(autor_info, encoding='unicode'))


def err(*args):
    """Сообщение об ошибке"""
    print('Формат не поддерживается.')


# словарь-фабрики функций
convertation = {
    'JSON': to_json,
    'XML': to_xml    
}

# поступают данные в виде словаря 
book = [
    {"book_id": "1", "title": "Книга-1", "autor": "Автор-1"},
    {"book_id": "2", "title": "Книга-2", "autor": "Автор-2"},
]


# запускаем код в интерактивном режиме
# $ python3 -i test.py
# и работаем с фабрикой
>>> convertation.get('JSON', err)(book)
>>> convertation.get('XML', err)(book)
>>> convertation.get('YAML', err)(book)

Теперь код легче читается, а функции, которые конвертируют данные можно использовать повторно. Также эти функции можно вынести в отдельный модуль и импортировать по необходимости и следовательно кода станет меньше и он станет проще для понимания.

Обратите внимание, что теперь добавление новых конверторов данных не составит труда. Стоит определить функцию и добавить ее в "Словарь-Фабрику" и все!

Но "словарь-фабрика" имеет свои недостатки, например, не видно какая произошла ошибка, чем она вызвана и т.д. Для нивелирования всех недостатков, связанных с применением словарей Python в качестве фабрик, лучше использовать функцию-интерфейс:

# test.py
import json
import xml.etree.ElementTree as et

def to_json(book):
    """Преобразование в JSON"""
    for item in book:
        song_info = {
            'id': item['book_id'],
            'title': item['title'],
            'autor': item['autor']
        }
        print(json.dumps(song_info))

def to_xml(book):
    """Преобразование в XML"""
    for item in book:
        autor_info = et.Element('book', attrib={'id': item['book_id']})
        title = et.SubElement(autor_info, 'title')
        title.text = item['title']
        autor = et.SubElement(autor_info, 'autor')
        autor.text = item['autor']
        print(et.tostring(autor_info, encoding='unicode'))

def convertation(song, format):
     """Интерфейс - принимает данные `book` и возвращает
     строковое представление согласно `format`
     """
     if format == 'JSON':
         return to_json(song)
     elif format == 'XML':
         return to_xml(song)
     else:
         raise ValueError(format)


# поступают данные в виде словаря 
book = [
    {"book_id": "1", "title": "Книга-1", "autor": "Автор-1"},
    {"book_id": "2", "title": "Книга-2", "autor": "Автор-2"},
]


# запускаем код в интерактивном режиме
# $ python3 -i test.py
# и работаем с интерфейсом
>>> convertation(book, 'JSON')
>>> convertation(book, 'XML')
>>> convertation(book, 'YAML')