2007. 11. 19. 13:53

map, grep 그리고 //g의 함정

문제는 펄스터디 모임중 다음 코드에서 시작되었다.
#!/usr/bin/perl
@str = qw(test test1 test28 test88 test102 test9209);
@grep_ret = grep m/test[0-9]{2}/g, @str;
print("grep_ret : @grep_ret\n");

@str = qw(test test1 test28 test88 test102 test9209);
@map_ret = map m/test[0-9]{2}/g, @str;
print("map_ret : @map_ret\n");
결과 :
grep_ret : test28 test88 test102 test9209
map_ret : test28 test88 test10 test92


이 코드는  map과 grep의 차이를 잘 보여주는 코드이다. 그런데 문제는 두번째 @str을 주석처리 하자 map의 결과가 사라졌다는 것이다.
#!/usr/bin/perl
@str = qw(test test1 test28 test88 test102 test9209);
@grep_ret = grep m/test[0-9]{2}/g, @str;
print("grep_ret : @grep_ret\n");
#@str = qw(test test1 test28 test88 test102 test9209);
@map_ret = map m/test[0-9]{2}/g, @str;
print("map_ret : @map_ret\n");
결과 :
grep_ret : test28 test88 test102 test9209
map_ret :
처음에는 매우 의아한 결과라고 생각했지만 찬찬히 살펴보자 //g의 문제라는 것을 알았다. 첫 grep 에서 g에의해서  매치 포지션이 뒤로 이동해 버린 것이 문제였다.  이를 증명 및 해결하기 위해서 다음 코드를 사용했다.

#!/usr/bin/perl
@str = qw(test test1 test28 test88 test102 test9209);
@grep_ret = grep m/test[0-9]{2}/g, @str;
print("grep_ret : @grep_ret\n");
map{pos=0}@str;
@map_ret = map m/test[0-9]{2}/g, @str;
print("map_ret : @map_ret\n");
결과 :
grep_ret : test28 test88 test102 test9209
map_ret : test28 test88 test10 test92
pos를 이용해서 매치 포지션을 0으로 바꾸자 우리가 원하던대로 코드가 작동했다.
이에 대해 몇가지 얘기들이 있었고 본인이 잘못알고 있던 것을 아는 척하다가(grep에서 반환값이 리스트라는. 엉터리 정보를 주장했다.) grep과 map의 순서를 바꾸자 더 황당한 일이 벌어졌다.
#!/usr/bin/perl
@str = qw(test test1 test28 test88 test102 test9209);
        @map_ret = map m/test[0-9]{2}/g, @str;
print("map_ret : @map_ret\n");
  @grep_ret = grep m/test[0-9]{2}/g, @str;
 print("grep_ret : @grep_ret\n");
 @map_ret = map m/test[0-9]{2}/g, @str;
 print("map_ret : @map_ret\n");
결과 :
map_ret : test28 test88 test10 test92
grep_ret : test28 test88 test102 test9209
map_ret :
이럴수가, 왜 map은 grep에 영향을 주지 않았는데 grep는 map에 영향을 주는 걸까.

이제 문제를 일으킨 //g와 map, grep의 특성들을 자세히 살펴보자.
//g 는 하나의 문자열에 대해서 여러번(global) 매치를 실행하기 때문에 매치가 이루어질때마다 매치가 이루어진 다음의 위치를 메모리에 저장한다. 이 위치는 pos함수로 확인할 수 있고 또 pos에 값을 대입해서 이 위치를 수정할 수 있다. 그런데 매치를 다 찾아서 더이상 매치가 없는 문자열에 다사  //g를 적용하면 어떻게 될까. 예를 통해 확인해보자.
#!/usr/bin/perl
use strict;
my $str="test12test34";
my $count;
$count = $str=~/test/g; print $count, "\t", pos $str, "\n";
$count = $str=~/test/g; print $count, "\t", pos $str, "\n";
$count = $str=~/test/g; print $count, "\t", pos $str, "\n";
$count = $str=~/test/g; print $count, "\t", pos $str, "\n";

결과:
1       4          # 매치가 되었다는 뜻의 1과 다음 매치위치인 "1"의 위치 4가 출력되었다.
1       10        # 마찬가지로 1과 다음 매치위치인 "3"의 위치인 10이 출력되었다.
                    # 더이상 매치할 것이 없기때문에 실패의 의미인  undef가 출력되었고 pos역시 undef.
1       4          # 바로 이거다. 한번더 //g가 호출되면 처음부터 다시 매치를 시작한다.

//g를 리스트에 저장하면 어떻게 될까.
#!/usr/bin/perl
use strict;
my $str="test12test34";
my @result;
@result = $str=~/test/g; print @result, "\t", pos $str, "\n";
@result = $str=~/test/g; print @result, "\t", pos $str, "\n";

결과
testtest
testtest

//g는 모든 매치를 실행해서 매치된 내용을 @list에 집어넣었고 매치가 모두 이루어졌기 때문에 pos는 undef를 반환했다. 앞의 결과와 비슷하게  pos가 undef가 된후 다시 매치를 실행하자 처음부터 매치가 이루어진다.
그렇다면 앞의 코드를 다시 인용해 모든 문제를 짚어보자.

#!/usr/bin/perl
@str = qw(test test1 test28 test88 test102 test9209);
        @map_ret = map m/test[0-9]{2}/g, @str;
print("map_ret : @map_ret\n");
  @grep_ret = grep m/test[0-9]{2}/g, @str;
 print("grep_ret : @grep_ret\n");
 @map_ret = map m/test[0-9]{2}/g, @str;
 print("map_ret : @map_ret\n");
결과 :
map_ret : test28 test88 test10 test92
grep_ret : test28 test88 test102 test9209
map_ret :
첫번째  map에서 map은 list를 반환한다. 따라서 //g는 리스트 컨텍스트로 작동하고 모든 매치된 내용을 리턴 한 후에 매치 포지션을 초기화 시킨다. 따라서 grep은 아무 문제없이 작동한다. 하지만 grep은 스칼라 데이터를 반환하기 때문에 매치 위치를 뒤로 옮겨버립니다.(사실 이말은 틀린 말이네요. grep의 반환값이 아니라 grep이 입력된 리스트 요소들에 대해서  True/False를  판단하기 위해서  마지막  expression의  스칼라 값을  사용한다는 것이 맞겠지요. 예를 들어 극단적으로 grep{(1,2,3,0)}@InputLIst 의 경우에 첫번째 스터디에서 공부했던 것 처럼 (1,2,3,0) 의 마지막 값인 0을 사용해 모두 False라고 판단하게 됩니다.) 따라서 마지막 map에서는 이동된 매치 포지션을 가지고 매치를 실행하게 되고 더이상 매치 할 것이 없으므로 undef만 줄기차게 반환하게 되겠지요. 다음 예제를 보면 좀더 분명해 지리라 봅니다. 매치의 정규표현식과 데이터를 살짝 바꿔봤습니다.
#!/usr/bin/perl
@str = qw(1and2 3and4and5 6 7 8 9 nodigit);
        @map_ret = map m/\d/g, @str;
print("map_ret : @map_ret\n");
  @grep_ret = grep m/\d/g, @str;
 print("grep_ret : @grep_ret\n");
 @map_ret = map m/\d/g, @str;
 print("map_ret : @map_ret\n");
결과 :
map_ret : 1 2 3 4 5 6 7 8 9
grep_ret : 1and2 3and4and5 6 7 8 9
map_ret : 2 4 5
두번째 grep에서 매치 위치를 옮긴 후에도 매치될 내용이 남아 있는 요소 에 대해서는 마지막 map에서 모두 매치해서 반환해 버립니다.

몇가지 요소가 복합적으로 얽혀있는데다가 쭈루룩 쓴 글이라 좀 복잡하네요.
요약하자면
1. //g는 매치가 이루어질때마다 다음 매치를 위해 매치위치를 옮긴다.
2. 매치 위치는 pos를 이용해 알아내거나 수정할 수 있다.
3. 스칼라문맥에서 //g는 호출될때마다 매치성공여부를 반환하고 매치위치를 옮긴다. 매치가 다 이루어진 후에는 //g가 한번더 호출되면 undef를 반환하고 매치 위치를 undef로 설정한다(매치 위치가 문자열의 처음으로 재설정 되는 것과 같은 효과). 따라서 다음 //g 는 문자열의 처음부터 다시 시작된다.
4. 리스트 문맥에서 //g는 가능한 모든 매치를 실행하여 매치된 값들의 리스트를 반환한다. 그리고 매치 위치도 undef로 바꾼다. 따라서 리스트 문맥 이후에 //g는 문자열의 처음부터 매치를 시작한다.
5. grep에서 //g 스칼라 문맥이고 map에서 //g는 리스트 문맥이다. 따라서 grep에서는 매치 위치가 바뀌에 되고 map이후에는 매치위치가 문자열의 처음이 된다.

끝입니다.^^

---
글을 올리고 나서 너무 미진한 부분이 많이 보여서 수정및 추가를 해야하나 하고 고민하고 있었는데 마침 aero님께서 제 고민을 한방에 날려주셨습니다. 감사. 역시 고수의 글은 무섭군요!!





'PerlMania' 카테고리의 다른 글

3번째 펄마니아 스터디 모임  (5) 2007.11.18
펄마니아 스터디 후기  (9) 2007.11.04