Я хочу преобразовать файл Newick в иерархический объект (похожий на то, что было опубликовано в этом сообщении) в Python.
Мой ввод - это файл Newick, например:
(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F:0.9
Исходный пост анализирует строковый символ по символу. Чтобы также сохранить длины ветвей, я изменил файл JavaScript (отсюда) следующим образом:
var newick = '// (A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F:0.9',
stack = [],
child,
root = [],
node = root;
var na = "";
newick.split('').reverse().forEach(function(n) {
switch(n) {
case ')':
// ')' => begin child node
if (na != "") {
node.push(child = { name: na });
na = "";
}
stack.push(node);
child.children = [];
node = child.children;
break;
case '(':
// '(' => end of child node
if (na != "") {
node.push(child = { name: na });
na = "";
}
node = stack.pop();
// console.log(node);
break;
case ',':
// ',' => separator (ignored)
if (na != "") {
node.push(child = { name: na });
na = "";
}
break;
default:
// assume all other characters are node names
// node.push(child = { name: n });
na += n;
break;
}
});
console.log(node);
Теперь я хочу перевести этот код на Python.
Здесь моя попытка (я знаю, что это неверно):
class Node:
def __init__(self):
self.Name = ""
self.Value = 0
self.Children = []
newick = "(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"
stack = []
# root = []
# node = []
for i in list(reversed(newick)):
if i == ')':
if na != "":
node = Node()
node.Name = na
child.append(node)
na = ""
stack.append(node)
# insert logic
child = node.Children
# child.append(child)
elif i == '(':
if (na != ""):
child = Node()
child.Name = na
node.append(child)
na = ""
node = stack.pop()
elif i == ',':
if (na != ""):
node = Node()
node.Name = na
node.append(child)
na = ""
else:
na += n
Поскольку я совершенно не знаком с JavaScript, мне не удается "перевести" код на Python. В частности, я не понял следующие строки:
child.children = [];
node = child.children;
Как я могу правильно написать это в Python, чтобы также извлечь длины?
Некоторые комментарии к версии JavaScript:
if (na != '')...
), что легко избежать.node
как имя переменной для массива. Читаемость улучшается при использовании множественного слова для массивов (или списков в Python). Из-за последней точки, код необходимо сначала исправить, прежде чем делать перевод на Python. Он должен поддерживать разделение атрибутов name/length, позволяя любому из них быть необязательным. Кроме того, он может назначать значения идентификатора каждому созданному узлу и добавлять свойство parentid
для ссылки на родительский элемент.
Я лично предпочитаю кодирование с рекурсией вместо использования переменной стека. Кроме того, с помощью API регулярных выражений вы можете легко ввести токен для облегчения разбора:
function parse(newick) {
let nextid = 0;
const regex = /([^:;,()\s]*)(?:\s*:\s*([\d.]+)\s*)?([,);])|(\S)/g;
newick += ";"
return (function recurse(parentid = -1) {
const children = [];
let name, length, delim, ch, all, id = nextid++;;
[all, name, length, delim, ch] = regex.exec(newick);
if (ch == "(") {
while ("(,".includes(ch)) {
[node, ch] = recurse(id);
children.push(node);
}
[all, name, length, delim, ch] = regex.exec(newick);
}
return [{id, name, length: +length, parentid, children}, delim];
})()[0];
}
// Example use:
console.log(parse("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"));
.as-console-wrapper { max-height: 100% !important; top: 0; }
import re
def parse(newick):
tokens = re.findall(r"([^:;,()\s]*)(?:\s*:\s*([\d.]+)\s*)?([,);])|(\S)", newick+";")
def recurse(nextid = 0, parentid = -1): # one node
thisid = nextid;
children = []
name, length, delim, ch = tokens.pop(0)
if ch == "(":
while ch in "(,":
node, ch, nextid = recurse(nextid+1, thisid)
children.append(node)
name, length, delim, ch = tokens.pop(0)
return {"id": thisid, "name": name, "length": float(length) if length else None,
"parentid": parentid, "children": children}, delim, nextid
return recurse()[0]
# Example use:
print(parse("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"))
О назначении node = child.children
в вашем JavaScript-коде: это перемещает "указатель" (то есть node
) на один уровень глубже в создаваемом дереве, так что на следующей итерации алгоритма на новый уровень добавляются новые узлы, С node = stack.pop()
этот указатель отслеживает один уровень вверх в дереве.
Вот параграф синтаксического анализа для этой входной строки. Он использует pyparsing nestedExpr
parser builder с определенным аргументом содержимого, так что результаты представляют собой пары пар ключ-значение, а не просто строки (которые по умолчанию).
import pyparsing as pp
# suppress punctuation literals from parsed output
pp.ParserElement.inlineLiteralsUsing(pp.Suppress)
ident = pp.Word(pp.alphas)
value = pp.pyparsing_common.real
element = pp.Group(ident + ':' + value)
parser = pp.OneOrMore(pp.nestedExpr(content=pp.delimitedList(element) + pp.Optional(','))
| pp.delimitedList(element))
tests = """
(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F:0.9
"""
parsed_results = parser.parseString(tests)
import pprint
pprint.pprint(parsed_results.asList(), width=20)
дает:
[[['A', 0.1],
['B', 0.2],
[['C', 0.3],
['D', 0.4]],
['E', 0.5]],
['F', 0.9]]
Обратите внимание, что выражение pyparsing для синтаксического анализа также преобразует время анализа в Python-float.
Следующий код может не быть точным переводом кода javascript, но он работает так, как ожидалось. Были некоторые проблемы, такие как "n", которые не определены. Я также добавил разбор имени узла в имя и значение и родительское поле.
Вам следует рассмотреть возможность использования уже существующих парсеров, таких как https://biopython.org/wiki/Phylo, поскольку они уже предоставляют вам инфраструктуру и алгоритмы работы с деревьями.
class Node:
# Added parsing of the "na" variable to name and value.
# Added a parent field
def __init__(self, name_val):
name, val_str = name_val[::-1].split(":")
self.name = name
self.value = float(val_str)
self.children = []
self.parent = None
# Method to get the depth of the node (for printing)
def get_depth(self):
current_node = self
depth = 0
while current_node.parent:
current_node = current_node.parent
depth += 1
return depth
# String representation
def __str__(self):
return "{}:{}".format(self.name, self.value)
newick = "(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"
root = None
# na was not defined before.
na = ""
stack = []
for i in list(reversed(newick)):
if i == ')':
if na != "":
node = Node(na)
na = ""
if len(stack):
stack[-1].children.append(node)
node.parent = stack[-1]
else:
root = node
stack.append(node)
elif i == '(':
if (na != ""):
node = Node(na)
na = ""
stack[-1].children.append(node)
node.parent = stack[-1]
stack.pop()
elif i == ',':
if (na != ""):
node = Node(na)
na = ""
stack[-1].children.append(node)
node.parent = stack[-1]
else:
# n was not defined before, changed to i.
na += i
# Just to print the parsed tree.
print_stack = [root]
while len(print_stack):
node = print_stack.pop()
print(" " * node.get_depth(), node)
print_stack.extend(node.children)
Вывод бит печати в конце следующий:
F:0.9
A:0.1
B:0.2
E:0.5
C:0.3
D:0.4
G:0.8