Что такое AXON. Примеры использования.


In [1]:
import axon
from pprint import pprint
from collections import namedtuple
import io

Данная статья является продолжением Что такое AXON. Если первая статья была посвящена знакомству с нотацией AXON, то это посвящена вариантам использования на основе библиотеки pyaxon.

Загрузка объектов из потока

Сообщение в нотации AXON это поток объектов или поток пар ключ:объект в текстовом формате. Это позволяет не загружать предварительно в память весь поток, но загружать объекты из потока по очереди.

Для этого имеются две функции iload для итеративной загрузки из файла и iloads для итеративной загрузки из текста.

Две другие функции load и load – для полной загрузки объектов из файла и из текста, соответственно. В результате получается список закруженных объектов (list) или словарь (ordered dict).

Поток пар key:value


In [2]:
text = """
RU: {capital:"Москва"}
US: {capital:"Вашингтон"}
GB: {capital:"Берлин"}
""" 
for val in axon.iloads(text):
    print(val)


('RU', {'capital': 'Москва'})
('US', {'capital': 'Вашингтон'})
('GB', {'capital': 'Берлин'})

Поток словарей (dict)


In [3]:
text = """
{FR:"Paris" RU:"Moscow"}
{CN:"Beigin" JP:"Tokio"}
{US:"Washington" CA:"Ottava"}
"""
for val in axon.iloads(text):
    print(val)


{'FR': 'Paris', 'RU': 'Moscow'}
{'JP': 'Tokio', 'CN': 'Beigin'}
{'CA': 'Ottava', 'US': 'Washington'}

Поток словарей (ordered dict)


In [4]:
text = """
# последовательность словарей
# с сохранением порядка ключей
[RU:"Москва" FR:"Париж"]
[CN:"Пекин" JP:"Токио"]
[US:"Вашингтон" CA:"Оттава"]
"""
for val in axon.iloads(text):
    print(val)


OrderedDict([('RU', 'Москва'), ('FR', 'Париж')])
OrderedDict([('CN', 'Пекин'), ('JP', 'Токио')])
OrderedDict([('US', 'Вашингтон'), ('CA', 'Оттава')])

Поток кортежей (tuple)


In [5]:
text = """
# кортежи экономят память в python,
# если мутируемость не нужна
("RU" "Moscow")
("US" "Washington")
("GB" "London")
"""
for val in axon.iloads(text):
    print(val)


('RU', 'Moscow')
('US', 'Washington')
('GB', 'London')

Поток списков (list)


In [6]:
text = """
["RU" "Moscow"]
["US" "Washington"]
["GB" "London"]
"""
for val in axon.iloads(text):
    print(val)


['RU', 'Moscow']
['US', 'Washington']
['GB', 'London']

Поток элементов/объектов


In [7]:
text = """
# это AXON без форматирования
country{id:"RU" capital:"Moscow"}
country{id:"USA" capital:"Washington"}
country{id:"GB" capital:"London"}
"""
for val in axon.iloads(text):
    print(val)


country{id: 'RU', capital: 'Moscow'}
country{id: 'USA', capital: 'Washington'}
country{id: 'GB', capital: 'London'}

Если вместо элементов нужны объекты определенного типа, то нужно зарегистрировать фабричные функции:


In [8]:
Country = namedtuple("Country", "id capital")

@axon.factory("country")
def factory_country(attrs, vals):
    return Country(**attrs)

text = """
# это AXON без форматирования
country{id:"RU" capital:"Moscow"}
country{id:"USA" capital:"Washington"}
country{id:"GB" capital:"London"}
"""
for val in axon.iloads(text, mode='strict'):
    print(val)


Country(id='RU', capital='Moscow')
Country(id='USA', capital='Washington')
Country(id='GB', capital='London')

Для того чтобы обратно преобразовать в AXON необходимо зарегистрировать функции для редуцирования:


In [9]:
@axon.reduce(Country)
def reduce_Country(ob):
    return axon.node("country", {'id':ob.id, 'capital':ob.capital})

vals = axon.loads(text, mode='strict')
print(axon.dumps(vals))


country{id:"RU" capital:"Moscow"}
country{id:"USA" capital:"Washington"}
country{id:"GB" capital:"London"}

Метки и ссылки

Проблема копирования объектов возникает при загрузке объектов с зависимостями. Для примера рассмотрим пример с обычным графом.


In [10]:
text = """
graph {
    nodes: [
       &n1 node {id:1 val:4}
       &n2 node {id:2 val:7}
       &n3 node {id:3 val:2}
       &n4 node {id:4 val:5}
    ]
    edges: [
        edge {val:12 *n1 *n2}
        edge {val:8 *n1 *n4}
        edge {val:-2 *n2 *n3}
        edge {val:5 *n3 *n4}
    ]
}
"""

Метки объектов имеют префикс &, а ссылки префикс *.


In [11]:
vals = axon.loads(text)

Убеждаемся, что копирования не происходит:


In [12]:
graph = vals[0]
nodes, edges = graph.nodes, graph.edges
print(id(nodes[0]) == id(edges[0][0]) == id(edges[1][0]))
print(id(nodes[1]) == id(edges[0][1]) == id(edges[2][0]))
print(id(nodes[2]) == id(edges[2][1]) == id(edges[3][0]))
print(id(nodes[3]) == id(edges[1][1]) == id(edges[3][1]))


True
True
True
True

In [13]:
print("Дамп в компактной форме:")
print("=======================")
print(axon.dumps(vals))


Дамп в компактной форме:
=======================
graph{nodes:[node{id:1 val:4} node{id:2 val:7} node{id:3 val:2} node{id:4 val:5}] edges:[edge{val:12 node{id:1 val:4} node{id:2 val:7}} edge{val:8 node{id:1 val:4} node{id:4 val:5}} edge{val:-2 node{id:2 val:7} node{id:3 val:2}} edge{val:5 node{id:3 val:2} node{id:4 val:5}}]}

XML -> AXON

Пусть нужно загрузить сторонние данные из XML документа и вставить в AXON. Для работы возьмем библиотеку xml.etree из стандартной библиотеки python.


In [14]:
from xml.etree import ElementTree

Зарегистрируем функции для редуцирования при преобразования в AXON формат.


In [15]:
@axon.reduce(ElementTree.Element)
def element_reduce(elem):
    children = elem.getchildren()
    if elem.attrib:
        if children:
            return axon.node(elem.tag, elem.attrib, children)
        else:
            return axon.node(elem.tag, elem.attrib, None)
    elif children:
            return axon.node(elem.tag, None, children)
    else:
        return axon.node(elem.tag)
        
@axon.reduce(ElementTree.ElementTree)
def etree_reduce(element):
    return element_reduce(element.getroot())

Возьмем XML документ (без пространств имен).


In [16]:
xml_doc = """
<persons>
<person firstname="Елена" lastname="Иванова" />
<person firstname="Анна" lastname="Сидорова" />
</persons>
"""
xml = ElementTree.fromstring(xml_doc)

In [17]:
axon_text = """
person {firstname:"Иван" lastname:"Иванов"}
person {firstname:"Николай" lastname:"Сидоров"}
"""

Загружаем и объединяем:


In [18]:
vals = axon.loads(axon_text)
vals.extend(xml.getchildren())
axon_text2 = axon.dumps(vals)
print(axon_text2)


person{firstname:"Иван" lastname:"Иванов"}
person{firstname:"Николай" lastname:"Сидоров"}
person{firstname:"Елена" lastname:"Иванова"}
person{firstname:"Анна" lastname:"Сидорова"}

Можно загрузить из AXON в XML. Для этого зарегистрируем построитель объектов. Изначально в библиотеке имеются три построителя:

  • safe (безопасный в том смысле, что для представления используются только встроенные типы),
  • strict (используются только зарегистрированные пользователем фабричные функции, иначе генерируется исключение) и
  • mixed (как в strict, только если нет зарегистрированной фабричной функции, то используются встроенные объекты).

In [19]:
def update_attribs(d):
    for key, val in d.items():
        d[key] = str(val)

class ElementTreeBuilder(axon.Builder):
    def node(self, name, attrs, vals):
        if attrs:
            attrs = dict(attrs)
            update_attribs(attrs)
        else:
            attrs = {}
        e = ElementTree.Element(name, attrs)
        if vals:
            e.extend(vals)
        return e

axon.register_builder('etree', ElementTreeBuilder())

Теперь можно загрузить и получить дерево XML документа.


In [20]:
vals = axon.loads(axon_text2, mode='etree')
root = ElementTree.Element("persons")
root.extend(vals)
ElementTree.dump(root)


<persons><person firstname="Иван" lastname="Иванов" /><person firstname="Николай" lastname="Сидоров" /><person firstname="Елена" lastname="Иванова" /><person firstname="Анна" lastname="Сидорова" /></persons>

Сравнение производительности

AXON и JSON

Скорость загрузки/дампа для AXON ожидаемо ниже чем для AXON, так как структура объектной нотации AXON сложнее JSON.

Для теста используется файл из 69933 записей (list of dicts). Запись имеет вид:

{"auc":233524008,"item":98799,"owner":"???","ownerRealm":"???","bid":8500000,"buyout":8500000,"quantity":1,"timeLeft":"VERY_LONG","rand":0,"seed":1639860480}

In [21]:
import json

In [22]:
# fp = io.open('data/auctions.json', 'rt')
# vals = json.load(fp)
# fp.close()
# axon.dump('data/auctions.axon', vals)

In [23]:
def load_and_iterate_json(path):
    fp = io.open(path, 'rt')
    vals = json.load(fp)
    print(len(vals))
    fp.close()
    for val in vals:
        pass
    
def iload_and_iterate_axon(path):
     for val in axon.iload(path):
         pass    
    
def load_and_iterate_axon(path):
    vals = axon.load(path)
    for val in vals:
        pass

In [24]:
%time load_and_iterate_json('data/auctions.json')
%time load_and_iterate_axon('data/auctions.axon')
%time iload_and_iterate_axon('data/auctions.axon')


69933
CPU times: user 302 ms, sys: 54.8 ms, total: 357 ms
Wall time: 586 ms
CPU times: user 425 ms, sys: 55.2 ms, total: 480 ms
Wall time: 516 ms
CPU times: user 381 ms, sys: 6.77 ms, total: 388 ms
Wall time: 389 ms

In [25]:
import random
test_vals_1 = []
for i in range(100000):
    test_vals_1.append([random.randint(0, 1000000), random.random(), random.randint(0, 1000000), random.random()])
    
fp = io.open('data/test_1.json', 'wt')    
json.dump(test_vals_1, fp)
fp.close()
axon.dump('data/test_1.axon', test_vals_1)

In [26]:
%time load_and_iterate_json('data/test_1.json')
%time load_and_iterate_axon('data/test_1.axon')
%time iload_and_iterate_axon('data/test_1.axon')


100000
CPU times: user 194 ms, sys: 20.6 ms, total: 215 ms
Wall time: 215 ms
CPU times: user 290 ms, sys: 14 ms, total: 304 ms
Wall time: 302 ms
CPU times: user 292 ms, sys: 3.9 ms, total: 296 ms
Wall time: 296 ms

In [27]:
import random
test_vals_2 = []
for i in range(100000):
    test_vals_2.append({'a':random.randint(0, 1000000), 'b':random.random(), 'c':random.randint(0, 1000000), 'd':random.random()})
    
fp = io.open('data/test_2.json', 'wt')    
json.dump(test_vals_2, fp)
fp.close()
axon.dump('data/test_2.axon', test_vals_2)

In [28]:
%time load_and_iterate_json('data/test_2.json')
%time load_and_iterate_axon('data/test_2.axon')
%time iload_and_iterate_axon('data/test_2.axon')


100000
CPU times: user 199 ms, sys: 35.6 ms, total: 235 ms
Wall time: 237 ms
CPU times: user 290 ms, sys: 21.1 ms, total: 311 ms
Wall time: 311 ms
CPU times: user 305 ms, sys: 4.78 ms, total: 310 ms
Wall time: 310 ms

In [ ]: