2019년 9월 20일 금요일

'Quick-and-Dirty' Perl 스크립트 한 줄의 작동원리 알아보기

여러 개의 염기서열을 담고 있는 sequence.fasta에서 특정 ID에 해당하는 서열만을 뽑아내는 한줄짜리 Perl 스크립트를 설명해 보고자 한다. 추출할 서열의 ID 목록은 ids.txt에 존재한다고 가정하자.

$ perl -ne 'if(/^>(\S+)/){$c=$i{$1}}$c?print:chomp;$i{$_}=1 if @ARGV' ids.txt sequences.fasta

이 예제는 Short command lines for manupulation of FASTQ and FASTA sequence files에서 가져온 것이고, 내가 작성하여 위키 페이지에 올렸던 문서에 포함되어 있다. 솔직하게 말하자면 교육 또는 학습 목적으로는 대단히 바람직하지 않은 코드이다. 길이가 매우 짧고 어쨌든 실행은 되니까 그저 복사해서 붙여놓고 아무런 의문을 제기하지 않고 써도 된다. 물론 나는 이보다는 조금 더 긴 Perl 스크립트를 별도로 만들어서 사용한다.

마침 이 코드의 정규표현식에 관한 질문이 들어와서 블로그에 상세하게 작동 원리 전체를 설명해 보기로 하였다. 겨우 한 줄에 불과하지만 이해하기는 쉽지 않다. 한참 다른 정보를 찾아서 고민하고 테스트를 거듭하여 겨우 감을 잡을 수 있었다.

먼제 Perl 인터프리터의 명령행 옵션인 -e와 -n을 알아야 한다. -e는 잘 알려진 것으로, 스크립트를 파일로 만들지 않고 명령행에서 직접 입력하여 사용하기 위한 것이다. -n은 -e 코드를 while loop로 둘러싸는 것이다.  Perl의 명령행 옵션에 관한 상세한 설명과 예제는 Perl Command-Line Options 또는 7 of the most useful Perl command line options를 참조하기 바란다.

그러면 본격적으로 이 고약한 코드를 해부해 보자. 코드는 다음과 같이 세 개의 블록으로 나눌 수 있다. 각각을 b-I, b-II, 그리고 b-III이라 부르기로 하자. perl -n 옵션을 주었으므로 ids.txt와 sequences.fasta의 모든 줄에 대해서 다음이 순차적으로 실행되는 implicit loop를 돈다.


ids.txt에 seq1, seq2(실제로는 한 줄에 한 ID이므로 "seq1\n", "seq2\n"라고 생각해야 됨)라는 내용이 담겨있다고 가정하자. 이 두 줄에 대해서 루프를 돌 때는 b-I은 건너뛴다. 왜냐하면 첫 줄이 '>'로 시작하지 않기 때문이다. 뒤이어서 b-II에서는 무슨 일이 벌어지는가? A? : command 1 : command 2는 if..the..else와 매우 흡사하다. '?'는 conditional operator라 부른다(참고). A라는 조건을 판별하여 참이면 command 1을, 거짓이면 command 2를 실행하라는 뜻이다. $c?라고 물어보았으니 $c가 1이면 print 함수를 , 그렇지 않으면 chomp 함수를 실행하게 되는데, 함수의 인수가 없다고 걱정할 것은 없다. 자동으로 $_ 변수(현재 읽어들여 처리 중인 라인)을 인수로 사용하기 때문이다. chomp 함수는 인수를 명시적으로 나타내지 않으면 $_의 뒤에 붙은 "\n"을 털어낸다. 이어서 이 코드의 세번째 블록(b-III)으로 진행되어 $i{seq1}=1가 선언되고, 다음번 줄에 대해서 반복을 돌면서 $i{seq2}=1가 실행된다. 만약 b-II에서 chomp를 실행하지 않았다면 $i의 key는 "seq1\n"이 되고 만다. 이는 "seq1"과는 다른 문자열로 간주됨에 유의하라. 결과적으로 ids.txt를 이루는 두 줄에 대해서 코드 전체(b-I/II/III)가 반복되고, %i hash에는 추출해야 할 서열의 ID가 key로서 기록된다.

ids.txt에 대해서 loop를 돌았으니 다음으로는 sequences.fasta 파일을 이루는 각 줄에 대해서 b-I/II/III을 반복하여 실행할 차례가 되었다. sequences.fasta의 첫 줄은 '>sequece_id' 형태이므로 block-I에서 패턴 매칭문을 만난다. /^>(\S+)/의 의미는 '>'로 시작하는 라인에서 '>'의 바로 다음에 오는 공백이 아닌 문자 여러개에 해당하는 패턴을 찾아서 $1 변수에 넣으라는 뜻이다. if {} 블록 내부의 $c=$i{$1}는 그러면 무슨 의미인가? 만약 지금 읽어들인 서열 ID가 ids.txt에 존재하는 것이라면 %i hash의 key에 정의된 상태가 된다. 따라서 $c=1이 된다. 만약 지금 막 읽어들인 서열 ID가 ids.txt에 없다면 $c는 아직 미정의 상태이다.

다음으로는 b-II로 진행한다. 조건연산자 '?'가 $c의 참 혹은 거짓 여부를 묻는다. $c가 정의된 상태라면 '>sequence_id'를 표준출력으로 인쇄한다. 정확히 말하자면 라인의 끝을 나타내는 "\n"까지 $_에 포함된 상태이니 정상적으로 한 줄이 다 인쇄되고 다음 라인을 인쇄할 준비를 마친다. 다음으로 b-III을 만난다. $i{">sequence_id\n"}=1이 실행되지만 이는 이후의 동작에 영향을 미치지 않는다.

이번에는 sequences.fasta의 두번째 줄을 실행할 순서이다. '>'로 시작하는 줄이 아니므로 b-I은 통과한다. b-II에서는 어떻게 될까? $c는 여전히 1의 값을 갖고 있으므로 print 문이 실행된다. b-III을 만나게 되지만 $i{"ATCGACG..\n"}=1을 실행하게 되고 역시 결과에는 영향을 미치지 않는다. 즉, 추출하고자 선택된 sequence의 ID 및 실제 염기서열 라인만 계속 표준출력으로 인쇄가 된다.

그러다가 ids.txt에 정의되지 않은 ID의 서열을 드디어 만나게 된다. '>'로 시작하는 라인을 만났으니 일단 b-I으로는 들어가는데, 이에 맞는 $i{서열ID}는 존재하지 않는다. 따라서 $c는 정의되지 않는다. 따라서 이어서 진행되는 b-II에서는 chomp를 만나게 된다. 이는 마찬가지로 출력물에 영향을 미치지 않는다. 이 동작은 다시 '>'로 시작하는 줄을 만나게 될 때까지 반복된다.

솔직하게 말해서 나의 코드 분석이 100% 맞는다고 자신은 못하겠다. 겨우 한 줄에 불과한 코드를 이렇게 긴 글로 설명해야 한다면, Perl 스크립트 작성 학습용 샘플로는 적합하지 않다. 물론 사용하는 데에는 아무런 문제가 없다. 1996년부터 2000년까지 소위 '난해한 Perl 코드 콘테스트(Obfuscated Perl Contest)'가 있었다고 하니 500바이트 내외의 Perl 코드가 불러일으키는 난잡함, 그러나 그 속에서 풍겨나는 익살과 아름다움을 감상하고 싶다면 위키피디아 혹은 다음의 수상작 페이지를 방문해 보라.


댓글 없음: