Tekhartha의 인공지능 기술블로그

Anytree(파이썬 트리 라이브리리)

|


작업을 하다 보면 트리 형식으로 구현해야 할 때가 꽤 생긴다. C라면 한땀한땀 구조체로 구현해서 사용해야겠지만, 파이썬 같은 high-level의 언어는 트리 라이브러리를 지원한다. 이번 포스팅에서는 파이썬의 꽤 강력한 트리 라이브러리인 “Anytree”를 다룬다. 이 라이브러리는 트리의 탐색, 노드의 추가/삭제 등을 간편하게 구현할 수 있게 만들어 놓았다. 뒤에 보면 알겠지만 메소드들이 매우 직관적이고 쓰기 쉽게 만들어 놓았다. 게다가 트리를 시각화해서 출력하는 기능까지 제공한다.


[Installation]

일단, 공식 documentation은 여기를 들어가서 볼 수 있다.

간단히 pip install anytree 또는 conda install anytree로 설치할 수 있다.

[Library Import]

from anytree import Node, RenderTree
from anytree.exporter import DotExporter

위의 코드로 anytree를 import해온다.

[Making Nodes]

이제부터는 Node()라는 함수를 통해서 트리의 노드를 정의내릴 수 있다. documentation에 있는 코드를 기준으로 설명하면,

udo = Node("Udo")
marc = Node("Marc", parent=udo)
lian = Node("Lian", parent=marc)
dan = Node("Dan", parent=udo)
jet = Node("Jet", parent=dan)
jan = Node("Jan", parent=dan)
joe = Node("Joe", parent=dan)

위와 같이 7개의 노드로 구성되고 root가 udo인 트리를 만들었다.

Node는 위와 같은 형식으로 만든다. 먼저 쌍따옴표로 Node의 name을 정하고(이는 나중에 udo.name으로 접근할 수 있다) 그 뒤에는 parent를 어떤 노드로 할 것인지 정한다. 이렇게 설정하면 자동적으로 marc의 부모 노드는 udo, udo의 자식 노드는 marc라고 인식하게 된다. 그러니까 노드를 잇고 끊을 때는 parent만 신경써주면 된다는 뜻이다.

한 가지 주의할 점. Node들을 연결할 때 해당 노드를 가리키는 변수로 연결해야 한다. 그러니까 udo에 연결해야 하지, Node(“Udo”) 에 연결하면 안 된다는 거다. 모든 노드는 변수처럼 선언되어야 사용할 수 있다.

Node에는 parent라는 argument 말고도 사용자가 직접 argument를 추가할 수도 있다.

data = Node("Data", data_index = 4, data_spec = "Key", mat = [1,2,3])

이런 식으로 내 마음대로 Node에다가 데이터를 집어넣을 수 있다. 데이터의 형식은 아무래도 상관없다.

다만, 모든 트리의 argument들은 다 똑같아야 한다. 위처럼 Node를 또 만들 때 mat이 없는 Node라면 mat = [] 또는 mat = None 으로 설정해줘야 한다는 말이다.

[Visualization]

for pre, fill, node in RenderTree(udo):
    print("%s%s" % (pre, node.name))

위와 같은 코드를 사용하면 udo를 root로 하는 트리를 시각화해서 볼 수 있다.

Udo
├── Marc
│   └── Lian
└── Dan
    ├── Jet
    ├── Jan
    └── Joe

이런 식으로 아름답게 시각화되어 출력된다. 직접 실험해본 결과 엥간히 큰 규모의 트리도 txt 파일에 저장시키면 다 시각화시켜서 볼 수 있다. 사실상 이 라이브러리의 가장 강력한 기능이 아닐까 한다.

[Methods of Node]

Node를 선언하고 .을 덧붙이면 메소드들을 볼 수 있는데, 메소드들은 다음과 같다 :

  • ancestors : 해당 노드의 조상 노드들을 모두 반환한다(tuple). 바로 위만 반환하는 게 아니라 그 위, 그 위…root까지 모두 반환한다.
  • anchestors : ancestors와 같음. 사용하지 말 것.
  • children : 해당 노드의 자식 노드들을 모두 반환한다(tuple). 자신에게 직접 연결된 자식 노드만.
  • depth : 해당 노드의 깊이를 반환한다.(int) root Node는 0을 반환
  • descendants : 해당 노드의 자손 노드들을 모두 반환한다(tuple). 자식의 자식의 자식…까지 전부 반환
  • height : 해당 노드의 높이를 반환한다.(int) leaf Node는 0을 반환
  • is_leaf : 해당 노드가 leaf인지 아닌지를 반환한다(bool).
  • is_root : 해당 노드가 root인지 아닌지를 반환한다(bool).
  • name : 해당 노드의 이름을 반환한다(str). Node 정의할 때 큰따옴표 안에 있던 그것.
  • parent : 해당 노드의 부모 노드를 반환한다.(Node)
  • path : root부터 해당 노드까지의 path를 반환한다.(tuple)
  • root : 해당 노드가 속한 트리의 root Node를 반환한다.(Node)
  • separator : 해당 노드의 구분자를 출력한다. 별로 쓸 일은 없다.
  • siblings : 해당 노드와 같은 부모를 가진 노드들을 출력한다(tuple)

사실상 노드에서 알아내야 하는 모든 정보들을 메소드들로부터 알 수 있다. 매우 파워풀.

[Attatch / Detach]

트리에 노드를 추가 / 삭제하는 방법 또한 매우 간단하다.

  • 추가할 땐 노드를 새로 정의하면서 parent argument에 parent로 삼고 싶은 노드를 써 주면 된다. 이러면 알아서 부모 노드는 자식을 알아본다.
  • 삭제할 땐 해당 노드의 parent를 None으로 설정하면 끝난다. 예를 들어 data라는 Node를 트리에서 제거하고 싶으면 data.parent = None 이라는 코드 한 줄로 끝난다. 만약 이 노드가 leaf 노드가 아니라면, data 노드를 root로 하는 새로운 트리가 만들어진 것이다. 메모리 문제 때문에 트리에서 떼낸 Node를 제거하고 싶다면 del data 코드를 추가해주자.


이 정도면 자유롭게 라이브러리를 통해 tree를 사용할 수 있을 것이다.

실제 사용할 때는 Node들을 모아 놓은 list를 하나 만들거나 해서 함께 관리해 주는 게 좀더 효율적이다.

Comments