Создать минимально связанный ориентированный ациклический граф

У меня есть ориентированный ациклический простой граф в NetworkX.

Теперь для каждого ребра у этого ребра есть «источник» и «цель». Если существует путь от «источника» к «цели» помимо этого края, то я хочу удалить это ребро.

  1. Есть ли в NetworkX встроенная функция для этого?

Я действительно не хочу изобретать колесо заново.

  1. [необязательно] Только в случае, если ответ на 1 - «нет», тогда какой алгоритм является наиболее эффективным для достижения этого (для довольно плотного графа)?

Вот пример DAG, который нужно очистить:

  • Узлы:

    ['termsequence', 'maximumdegree', 'emptymultigraph', 'minimum', 'multiset', 'walk', 'nonemptymultigraph', 'euleriantrail', 'nonnullmultigraph', 'cycle', 'loop', 'abwalk', 'endvertices', 'simplegraph', 'vertex', 'multipletrails', 'edge', 'set', 'stroll', 'union', 'trailcondition', 'nullmultigraph', 'trivialmultigraph', 'sequence', 'multiplepaths', 'path', 'degreevertex', 'onedgesonvertices', 'nontrivialmultigraph', 'adjacentedges', 'adjacentvertices', 'simpleedge', 'maximum', 'multipleloops', 'length', 'circuit', 'class', 'euleriangraph', 'incident', 'minimumdegree', 'orderedpair', 'unique', 'closedwalk', 'multipleedges', 'pathcondition', 'multigraph', 'trail']
    
  • Края бывают:

    [('termsequence', 'endvertices'), ('emptymultigraph', 'nonemptymultigraph'), ('minimum', 'minimumdegree'), ('multiset', 'trailcondition'), ('multiset', 'pathcondition'), ('multiset', 'multigraph'), ('walk', 'length'), ('walk', 'closedwalk'), ('walk', 'abwalk'), ('walk', 'trail'), ('walk', 'endvertices'), ('euleriantrail', 'euleriangraph'), ('loop', 'simplegraph'), ('loop', 'degreevertex'), ('loop', 'simpleedge'), ('loop', 'multipleloops'), ('endvertices', 'abwalk'), ('vertex', 'adjacentvertices'), ('vertex', 'onedgesonvertices'), ('vertex', 'walk'), ('vertex', 'adjacentedges'), ('vertex', 'multipleedges'), ('vertex', 'edge'), ('vertex', 'multipleloops'), ('vertex', 'degreevertex'), ('vertex', 'incident'), ('edge', 'adjacentvertices'), ('edge', 'onedgesonvertices'), ('edge', 'multipleedges'), ('edge', 'simpleedge'), ('edge', 'adjacentedges'), ('edge', 'loop'), ('edge', 'trailcondition'), ('edge', 'pathcondition'), ('edge', 'walk'), ('edge', 'incident'), ('set', 'onedgesonvertices'), ('set', 'edge'), ('union', 'multiplepaths'), ('union', 'multipletrails'), ('trailcondition', 'trail'), ('nullmultigraph', 'nonnullmultigraph'), ('sequence', 'walk'), ('sequence', 'endvertices'), ('path', 'cycle'), ('path', 'multiplepaths'), ('degreevertex', 'maximumdegree'), ('degreevertex', 'minimumdegree'), ('onedgesonvertices', 'multigraph'), ('maximum', 'maximumdegree'), ('circuit', 'euleriangraph'), ('class', 'multiplepaths'), ('class', 'multipletrails'), ('incident', 'adjacentedges'), ('incident', 'degreevertex'), ('incident', 'onedgesonvertices'), ('orderedpair', 'multigraph'), ('closedwalk', 'circuit'), ('closedwalk', 'cycle'), ('closedwalk', 'stroll'), ('pathcondition', 'path'), ('multigraph', 'euleriangraph'), ('multigraph', 'nullmultigraph'), ('multigraph', 'trivialmultigraph'), ('multigraph', 'nontrivialmultigraph'), ('multigraph', 'emptymultigraph'), ('multigraph', 'euleriantrail'), ('multigraph', 'simplegraph'), ('trail', 'path'), ('trail', 'circuit'), ('trail', 'multipletrails')]
    

person mareoraft    schedule 21.08.2015    source источник
comment
Вы уверены, что это не НП? Другими словами, есть ли какой-нибудь известный вам алгоритм, который решает эту проблему за полиномиальное время?   -  person Sait    schedule 21.08.2015
comment
@Joel Я отредактировал вопрос. Это DAG (без направленных циклов).   -  person mareoraft    schedule 21.08.2015
comment
@Sait Время выполнения, в худшем случае, будет равняться количеству ребер, умноженному на время выполнения, чтобы найти направленный путь между парой узлов (я не знаю, что это конкретно. Я хотел бы узнать!)   -  person mareoraft    schedule 21.08.2015
comment
@PatrickMaupin Я добавил пример входного графа. Дайте мне знать, если он слишком большой. Спасибо!   -  person mareoraft    schedule 25.08.2015
comment
Обратите внимание: в моем коде было глупое замедление. Я продолжал пересчитывать G.out_degree (node). Я изменил свой код так, чтобы вначале был создан dict. Это приводит к улучшению на порядок.   -  person Joel    schedule 30.08.2015
comment
Я добавил код в свой ответ, который обеспечивает дополнительную оптимизацию. Например, для группы DAG с максимальным подключением сложность снижается с O(V**3) до O(V**2), поэтому для полностью подключенного графа с 4000 узлов время выполнения сокращается примерно с 90 секунд (с помощью кода Джоэла или моего базового алгоритма) до примерно 5 секунд. .   -  person Patrick Maupin    schedule 30.08.2015


Ответы (3)


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

Похоже, вам действительно нужен способ эффективного удаления всех избыточных ребер. Это означает, что вам нужен способ делать их все сразу. Это другой вопрос, но вот ответ на него. Я не верю, что в networkx есть что-то для этого встроенное, но найти работоспособный алгоритм несложно.

Идея состоит в том, что, поскольку это группа DAG, некоторые узлы не имеют внешних ребер. Начните с них и обработайте их. После того, как все они обработаны, у подмножества их родителей нет дочерних элементов, которые не были обработаны. Пройдите через этих родителей. Повторение. На каждом этапе набор необработанных узлов представляет собой DAG, и мы обрабатываем «конечные узлы» этого DAG. Гарантированно завершится (если исходная сеть конечна).

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

import networkx as nx
from collections import defaultdict

def remove_redundant_edges(G):
    processed_child_count = defaultdict(int)  #when all of a nodes children are processed, we'll add it to nodes_to_process
    descendants = defaultdict(set)            #all descendants of a node (including children)
    out_degree = {node:G.out_degree(node) for node in G.nodes_iter()}
    nodes_to_process = [node for node in G.nodes_iter() if out_degree[node]==0] #initially it's all nodes without children
    while nodes_to_process:
        next_nodes = []
        for node in nodes_to_process:
            '''when we enter this loop, the descendants of a node are known, except for direct children.'''
            for child in G.neighbors(node):
                if child in descendants[node]:  #if the child is already an indirect descendant, delete the edge
                    G.remove_edge(node,child)
                else:                                    #otherwise add it to the descendants
                    descendants[node].add(child)
            for predecessor in G.predecessors(node):             #update all parents' indirect descendants
                descendants[predecessor].update(descendants[node])  
                processed_child_count[predecessor]+=1            #we have processed one more child of this parent
                if processed_child_count[predecessor] == out_degree[predecessor]:  #if all children processed, add to list for next iteration.
                    next_nodes.append(predecessor)
        nodes_to_process=next_nodes

Чтобы проверить это:

G=nx.DiGraph()
G.add_nodes_from(['termsequence', 'maximumdegree', 'emptymultigraph', 'minimum', 'multiset', 'walk', 'nonemptymultigraph', 'euleriantrail', 'nonnullmultigraph', 'cycle', 'loop', 'abwalk', 'endvertices', 'simplegraph', 'vertex', 'multipletrails', 'edge', 'set', 'stroll', 'union', 'trailcondition', 'nullmultigraph', 'trivialmultigraph', 'sequence', 'multiplepaths', 'path', 'degreevertex', 'onedgesonvertices', 'nontrivialmultigraph', 'adjacentedges', 'adjacentvertices', 'simpleedge', 'maximum', 'multipleloops', 'length', 'circuit', 'class', 'euleriangraph', 'incident', 'minimumdegree', 'orderedpair', 'unique', 'closedwalk', 'multipleedges', 'pathcondition', 'multigraph', 'trail'])
G.add_edges_from([('termsequence', 'endvertices'), ('emptymultigraph', 'nonemptymultigraph'), ('minimum', 'minimumdegree'), ('multiset', 'trailcondition'), ('multiset', 'pathcondition'), ('multiset', 'multigraph'), ('walk', 'length'), ('walk', 'closedwalk'), ('walk', 'abwalk'), ('walk', 'trail'), ('walk', 'endvertices'), ('euleriantrail', 'euleriangraph'), ('loop', 'simplegraph'), ('loop', 'degreevertex'), ('loop', 'simpleedge'), ('loop', 'multipleloops'), ('endvertices', 'abwalk'), ('vertex', 'adjacentvertices'), ('vertex', 'onedgesonvertices'), ('vertex', 'walk'), ('vertex', 'adjacentedges'), ('vertex', 'multipleedges'), ('vertex', 'edge'), ('vertex', 'multipleloops'), ('vertex', 'degreevertex'), ('vertex', 'incident'), ('edge', 'adjacentvertices'), ('edge', 'onedgesonvertices'), ('edge', 'multipleedges'), ('edge', 'simpleedge'), ('edge', 'adjacentedges'), ('edge', 'loop'), ('edge', 'trailcondition'), ('edge', 'pathcondition'), ('edge', 'walk'), ('edge', 'incident'), ('set', 'onedgesonvertices'), ('set', 'edge'), ('union', 'multiplepaths'), ('union', 'multipletrails'), ('trailcondition', 'trail'), ('nullmultigraph', 'nonnullmultigraph'), ('sequence', 'walk'), ('sequence', 'endvertices'), ('path', 'cycle'), ('path', 'multiplepaths'), ('degreevertex', 'maximumdegree'), ('degreevertex', 'minimumdegree'), ('onedgesonvertices', 'multigraph'), ('maximum', 'maximumdegree'), ('circuit', 'euleriangraph'), ('class', 'multiplepaths'), ('class', 'multipletrails'), ('incident', 'adjacentedges'), ('incident', 'degreevertex'), ('incident', 'onedgesonvertices'), ('orderedpair', 'multigraph'), ('closedwalk', 'circuit'), ('closedwalk', 'cycle'), ('closedwalk', 'stroll'), ('pathcondition', 'path'), ('multigraph', 'euleriangraph'), ('multigraph', 'nullmultigraph'), ('multigraph', 'trivialmultigraph'), ('multigraph', 'nontrivialmultigraph'), ('multigraph', 'emptymultigraph'), ('multigraph', 'euleriantrail'), ('multigraph', 'simplegraph'), ('trail', 'path'), ('trail', 'circuit'), ('trail', 'multipletrails')])

print G.size()
>71
print G.order()
>47
descendants = {}  #for testing below
for node in G.nodes():
    descendants[node] = nx.descendants(G,node)

remove_redundant_edges(G)  #this removes the edges

print G.size()  #lots of edges gone
>56
print G.order() #no nodes changed.
>47
newdescendants = {}  #for comparison with above
for node in G.nodes():
    newdescendants[node] = nx.descendants(G,node)

for node in G.nodes():  
    if descendants[node] != newdescendants[node]:
        print 'descendants changed!!'   #all nodes have the same descendants
    for child in G.neighbors(node):  
        if len(list(nx.all_simple_paths(G,node, child)))>1:
            print 'bad edge'  #no alternate path exists from a node to its child.

Это будет эффективно: он должен обработать каждый узел в начале, чтобы увидеть, является ли он «конечным» узлом. Затем он обрабатывает каждое ребро, достигающее их, и проверяет, были ли обработаны все дочерние элементы этого родителя. Затем он смотрит на этих родителей и повторяет.

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

person Joel    schedule 26.08.2015
comment
Думаю, это лучшее решение. Мне жаль, что у меня не было времени протестировать все решения на очень большом графике, но мне придется вернуться к этому позже. - person mareoraft; 27.08.2015

Вот простой универсальный алгоритм. Алгоритм можно запускать вперед или назад. Этот ответ и ответ Джоэла по сути двойственны - его бег назад, а этот - вперед:

def remove_redundant_edges(G):
    """
    Remove redundant edges from a DAG using networkx (nx).
    An edge is redundant if there is an alternate path
    from its start node to its destination node.

    This algorithm could work front to back, or back to front.
    We choose to work front to back.

    The main persistent variable (in addition to the graph
    itself) is indirect_pred_dict, which is a dictionary with
    one entry per graph node.  Each entry is a set of indirect
    predecessors of this node.

    The algorithmic complexity of the code on a worst-case
    fully-connected graph is O(V**3), where V is the number
    of nodes.
    """

    indirect_pred_dict = collections.defaultdict(set)
    for node in nx.topological_sort(G):
        indirect_pred = indirect_pred_dict[node]
        direct_pred = G.predecessors(node)
        for pred in direct_pred:
            if pred in indirect_pred:
                G.remove_edge(pred, node)
        indirect_pred.update(direct_pred)
        for succ in G.successors(node):
            indirect_pred_dict[succ] |= indirect_pred

Анализ сложности и большие оптимизации

Для минимально связного графа, где каждый узел связан только с одним ребром, сложность равна O(V+E). Однако даже для простого линейного графа (где каждый узел имеет входящее и исходящее ребро) сложность равна O(V*E), а для максимально связного графа (что является наихудшим случаем, когда каждый узел подключен к каждому нижележащему узлу на графе) сложность O(V**3). В этом случае количество операций следует за последовательностью A000292, которая равна n * (n+1) * (n+2) / 6, где n - количество узлов (V). минус 3.

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

def remove_redundant_edges(G, optimize_dense=True, optimize_chains=True,
                              optimize_tree=False,  optimize_funnel=False):
    """
    Remove redundant edges from a DAG using networkx (nx).
    An edge is redundant if there is an alternate path
    from its start node to its destination node.

    This algorithm could work equally well front to back,
    or back to front. We choose to work front to back.

    The main persistent variable (in addition to the graph
    itself) is indirect_pred_dict, which is a dictionary with
    one entry per graph node.  Each entry is a set of indirect
    predecessors of this node.

    The main processing algorithm uses this dictionary to
    iteratively calculate indirect predecessors and direct
    predecessors for every node, and prune the direct
    predecessors edges if they are also accessible indirectly.
    The algorithmic complexity is O(V**3), where V is the
    number of nodes in the graph.

    There are also several graph shape-specific optimizations
    provided.  These optimizations could actually increase
    run-times, especially for small graphs that are not amenable
    to the optimizations, so if your execution time is slow,
    you should test different optimization combinations.

    But for the right graph shape, these optimizations can
    provide dramatic improvements.  For the fully connected
    graph (which is worst-case), optimize_dense reduces the
    algorithmic complexity from O(V**3) to O(V**2).

    For a completely linear graph, any of the optimize_tree,
    optimize_chains, or optimize_funnel options would decrease
    complexity from O(V**2) to O(V).

    If the optimize_dense option is set to True, then an
    optimization phase is before the main algorithm.  This
    optimization phase works by looking for matches between
    each node's successors and that same node's successor's
    successors (by only looking one level ahead at a time).

    If the optimize_tree option is set true, then a phase is
    run that will optimize trees by working right-to-left and
    recursively removing leaf nodes with a single predecessor.
    This will also optimize linear graphs, which are degenerate
    trees.

    If the optimize_funnel option is set true, then funnels
    (inverted trees) will be optimized.

    If the optimize_chains option is set true, then chains
    (linear sections) will be optimized by sharing the
    indirect_pred_dict sets.  This works because Python
    checks to see if two sets are the same instance before
    combining them.

    For a completely linear graph, optimize_funnel or optimize_tree
    execute more quickly than optimize_chains.  Nonetheless,
    optimize_chains option is enabled by default, because
    it is a balanced algorithm that works in more cases than
    the other two.
    """

    ordered = nx.topological_sort(G)

    if optimize_dense:
        succs= dict((node, set(G.successors(node))) for node in ordered)
        for node in ordered:
            my_succs = succs.pop(node)
            kill = set()
            while my_succs:
                succ = my_succs.pop()
                if succ not in kill:
                    check = succs[succ]
                    kill.update(x for x in my_succs if x in check)
            for succ in kill:
                G.remove_edge(node, succ)

    indirect_pred_dict = dict((node, set()) for node in ordered)

    if optimize_tree:
        remaining_nodes = set(ordered)
        for node in reversed(ordered):
            if G.in_degree(node) == 1:
                if not (set(G.successors(node)) & remaining_nodes):
                    remaining_nodes.remove(node)
        ordered = [node for node in ordered if node in remaining_nodes]

    if optimize_funnel:
        remaining_nodes = set(ordered)
        for node in ordered:
            if G.out_degree(node) == 1:
                if not (set(G.predecessors(node)) & remaining_nodes):
                    remaining_nodes.remove(node)
        ordered = [node for node in ordered if node in remaining_nodes]

    if optimize_chains:
        # This relies on Python optimizing the set |= operation
        # by seeing if the objects are identical.
        for node in ordered:
            succs = G.successors(node)
            if len(succs) == 1 and len(G.predecessors(succs[0])) == 1:
                indirect_pred_dict[succs[0]] = indirect_pred_dict[node]

    for node in ordered:
        indirect_pred = indirect_pred_dict.pop(node)
        direct_pred = G.predecessors(node)
        for pred in direct_pred:
            if pred in indirect_pred:
                G.remove_edge(pred, node)
        indirect_pred.update(direct_pred)
        for succ in G.successors(node):
            indirect_pred_dict[succ] |= indirect_pred

Я не анализировал, возможно ли построить плотный, но не максимально связанный граф, который выполняется со сложностью выше O(V**2) с включенной опцией optimize_dense, но у меня нет причин a priori для считаю, что это невозможно. Оптимизация работает лучше всего для максимально связного графа и ничего не даст, например, в случае, когда каждый узел разделяет наследников со своим внуком, а не со своими дочерними элементами, и я не анализировал время выполнения этого случая.

Пример испытательного стенда

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

import collections
import networkx as nx

def makegraph(numnodes):
    """
    Make a fully-connected graph given a number of nodes
    """
    edges = []
    for i in range(numnodes):
        for j in range(i+1, numnodes):
            edges.append((i, j))
    return nx.DiGraph(edges)

def remove_redundant_edges(G):
    ops = 0
    indirect_pred_dict = collections.defaultdict(set)
    for node in nx.topological_sort(G):
        indirect_pred = indirect_pred_dict[node]
        direct_pred = G.predecessors(node)
        for pred in direct_pred:
            if pred in indirect_pred:
                G.remove_edge(pred, node)
        indirect_pred.update(direct_pred)
        for succ in G.successors(node):
            indirect_pred_dict[succ] |= indirect_pred
            ops += len(indirect_pred)
    return ops

def test_1(f, numnodes):
    G = makegraph(numnodes)
    e1 = nx.number_of_edges(G)
    ops = f(G)
    e2 = nx.number_of_edges(G)
    return ops, e1, e2

for numnodes in range(30):
    a = test_1(remove_redundant_edges, numnodes)
    print numnodes, a[0]
person Patrick Maupin    schedule 27.08.2015

да.

Вы хотите использовать all_simple_paths [документацию ] (который предоставляет генератор всех простых путей между ними). Закройте, как только он найдет второй, чтобы он не рассчитал их все.

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

def multiple_paths(G,source,target):
    '''returns True if there are multiple_paths, False otherwise'''
    path_generator = nx.all_simple_paths(G, source=source, target=target)
    counter = 0
    for path in path_generator: #test to see if there are multiple paths
        counter += 1
        if counter >1:  
            break  #instead of breaking, could have return True
    if counter >1:  #counter == 2
        return True
    else:  #counter == 0 or 1
        return False

import networkx as nx
G=nx.DiGraph()
G.add_edges_from([(0,1), (1,2), (1,3), (0,3), (2,3)])
multiple_paths(G,0,1)
> False
multiple_paths(G,0,2) 
> False
multiple_paths(G,0,3)
> True

for edge in G.edges_iter():  #let's do what you're trying to do
    if multiple_paths(G, edge[0], edge[1]):
        G.remove_edge(edge[0],edge[1])

G.edges()
> [(0, 1), (1, 2), (2, 3)]

Обратите внимание: вы также можете удалить край, а затем запустить has_path, чтобы проверить, есть ли еще путь. Если нет, то добавьте край обратно.

import networkx as nx
G=nx.DiGraph()
G.add_edges_from([(0,1), (1,2), (1,3), (0,3), (2,3)])
for edge in G.edges_iter():
    G.remove_edge(edge[0],edge[1])
    if not nx.has_path(G,edge[0],edge[1]):
        G.add_edge(edge[0],edge[1])

G.edges()
> [(0, 1), (1, 2), (2, 3)]

вы должны быть осторожны, если есть какие-либо данные о ребрах, и мне не нравится возможность удалить ребро и затем добавить его обратно - это открывает шанс для некоторых трудных для поиска ошибок.

person Joel    schedule 25.08.2015
comment
Есть идеи о времени выполнения этих двух стратегий и их сравнении? В частности, как вы думаете, has_path и path_generator (практически) одинаковы во времени? Я предполагаю, что path_generator ищет пути только по одному, поскольку цикл for запрашивает их. - person mareoraft; 25.08.2015
comment
Проверяя документацию, has_path основан на shortest_path, который сам основан на bidirectional_shortest_path. Я считаю, что это будет работать быстрее, чем all_simple_paths, который, по-видимому, использует поиск в глубину. Преимущество двунаправленного пути можно представить, представив две точки в трехмерном пространстве. Чтобы расширить воздушный шар вокруг них, пока он не достигнет другого (что соответствует исследованию всех путей, выходящих из одного), требуется много воздуха. Гораздо меньше воздуха, чтобы расширять воздушные шары вокруг них обоих, пока они не соприкоснутся. Это может быть медленнее, если нет пути ... Точно не знаю. - person Joel; 25.08.2015
comment
Похоже, что следующий комментарий, который я написал, не появился. Я бы порекомендовал запустить несколько примеров с обоими подходами для нескольких типовых сетей. Я ожидал, что has_path будет быстрее, если есть путь, но all_simple_paths будет быстрее, если его нет (однако я не тестировал). То, что быстрее, может зависеть от структуры и размера сети нетривиальным образом. - person Joel; 26.08.2015
comment
Это неполный ответ, и теперь у вас есть полный ответ, который совершенно другой - зачем вам, чтобы люди возились с этим - они пришли сюда не для этого ... - person Patrick Maupin; 30.08.2015
comment
@PatrickMaupin Как вы заметили - это совершенно другой ответ. В этом есть ценность. Вы ошибаетесь, говоря, что это неполный ответ. Он предоставляет именно ту команду, которую запрашивал OP. Тот факт, что у меня есть совершенно другой ответ, который обеспечивает то, что на самом деле хотел OP, не означает, что кто-то, пытающийся ответить на очень похожий вопрос в будущем, не захочет это увидеть. Скажите честно, если бы вы хотели сделать это на одном ребре (возможно, даже не в группе DAG), вы бы хотели другое решение, которое я дал, или это? - person Joel; 30.08.2015
comment
Позвольте мне раскрыть то, что я имел в виду под неполным ответом - вы не упаковали все это в одну функцию удаления всех узлов. И если бы вы сделали это, это было бы невероятно медленно. И нет, я лично не думаю, что это полезно в этом вопросе, и я даже не думаю, что это Pythonic - например, вы используете уродливый цикл for вместо itertools.slice. - person Patrick Maupin; 30.08.2015