☜ 제 16 장 기능적 프로그래밍 | """ Dive Into Python """ 다이빙 파이썬 |
제 18 장 수행성능 조율 ☞ |
복수형에 대하여 연구해 보겠습니다. 또 기타 다른 함수와 고급 정규 표현식 그리고 발생자를 돌려주는 함수도 언급하고 싶습니다. 발생자(Generators)는 파이썬 2.3에서 새로 도입되었습니다. 그러나 먼저 복수형을 만드는 방법에 관하여 연구해 봅시다.
아직 제 7 장 정규표현식(Regular Expressions)을 읽지 않았다면 지금이 좋은 때가 된 것 같군요. 이 장에서는 여러분이 정규 표현식을 기본적으로 알고 있다고 가정하고, 곧바로 보다 고급스런 사용법을 익혀 보겠습니다.
영어는 수 많은 언어로부터 빌려 온 정신분열적인 언어로서, 단수형을 복수형으로 바꾸는 규칙은 복잡하고 다양합니다. 먼저 규칙이 있고 다음 그런 규칙에 대한 예외가 있으며 그리고 그 예외에 대한 예외가 있습니다.
영어로-말하는 나라에서 자랐거나 정규 교육 환경에서 영어를 배웠다면 아마도 기본적인 규칙을 잘 아실 것입니다:
(물론, 예외가 많습니다. “Man”은 “men”이 되고 “woman”은 “women”이 되지만 “human”은 “humans”가 됩니다. “Mouse”는 “mice”가 되고 “louse”는 “lice”가 되지만 “house”는 “houses”가 됩니다. “Knife”는 “knives”가 되고 “wife”는 “wives”가 되지만 “lowlife”는 “lowlifes”가 됩니다. 따로 복수형을 가진 단어인 “sheep”와 “deer” 그리고 “haiku”는 더 이상 언급하지 않겠습니다.)
물론 다른 언어들은 완전히 다릅니다.
명사를 복수형으로 만드는 모듈을 디자인해 봅시다. 먼저 영어 명사로 시작하고 그저 이 네 가지 규칙만으로 시작하지만 반드시 규칙을 더 추가할 필요가 있다는 사실을 꼭 명심합시다. 그리고 결국 언어를 더 추가할 필요가 있을 것입니다.
그래서 단어들을 유심히 살펴보시면 적어도 영어로 구성된 문자열일 뿐입니다. 다양하게 조합된 문자를 찾아서, 각각을 다르게 처리해야 한다는 규칙이 있습니다. 이런 작업은 아무래도 정규 표현식으로 처리해야 할 듯 싶습니다.
import re def plural(noun): if re.search('[sxz]$', noun): ① return re.sub('$', 'es', noun) ② elif re.search('[^aeioudgkprt]h$', noun): return re.sub('$', 'es', noun) elif re.search('[^aeiou]y$', noun): return re.sub('y$', 'ies', noun) else: return noun + 's'
① | 좋습니다. 이것은 정규 표현식입니다. 그러나 제 7장, 정규 표현식에서 보지 못했던 구문을 사용합니다. 각괄호는 “정확하게 이 문자중 하나에 부합한다는 뜻입니다”. 그래서 [sxz]는 “s나 x 또는 z이지만 그 중에 하나”라는 뜻입니다. $는 알고 계실 겁니다; 문자열의 끝에 일치합니다. 그래서 noun이 s나 x 또는 z로 끝나는지 점검합니다. |
② | 이 re.sub 함수는 정규 표현식 기반의 문자열 교체를 수행합니다. 좀 더 자세하게 살펴보겠습니다. |
>>> import re >>> re.search('[abc]', 'Mark') ① <_sre.SRE_Match object at 0x001C1FA8> >>> re.sub('[abc]', 'o', 'Mark') ② 'Mork' >>> re.sub('[abc]', 'o', 'rock') ③ 'rook' >>> re.sub('[abc]', 'o', 'caps') ④ 'oops'
import re def plural(noun): if re.search('[sxz]$', noun): return re.sub('$', 'es', noun) ① elif re.search('[^aeioudgkprt]h$', noun): ② return re.sub('$', 'es', noun) ③ elif re.search('[^aeiou]y$', noun): return re.sub('y$', 'ies', noun) else: return noun + 's'
① | plural 함수로 되돌아가 보겠습니다. 무엇을 하고 있습니까? 문자열의 끝을 es로 바꾸고 있습니다. 다른 말로 하면 문자열에 es를 추가합니다. 문자열 결합으로 같은 일을 달성할 수 있습니다. 예를 들면 noun + 'es'와 같이 말입니다. 그러나 나는 일관성있게 무엇에나 정규 표현식을 사용합니다. 그 이유는 이 장의 후반부에서 가르쳐 드리겠습니다. |
② | 자세하게 살펴보세요. 이는 또다른 변형입니다. 각괄호 안의 첫 문자인 ^는 뭔가 특별한 뜻이 있습니다: 부인이라는 뜻이 있는데, [^abc]는 “ a만 제외하고 어떤 문자이든 한 문자나 b 또는 c”라는 뜻입니다. 그래서 [^aeioudgkprt]는 a를 제외하고 아무 문자 하나, e, i, o, u, d, g, k, p, r, 또는 t라는 뜻입니다. 다음 그 문자 다음에 h가 따르고, 그 뒤에 문자열의 끝이 따라야 합니다. H로 끝나고 발음이 되는 단어를 찾고 있습니다. |
③ | 여기도 같은 패턴입니다: Y로 끝나는 단어에 일치하며, Y 앞의 문자는 a, e, i, o, 또는 u가 아닙니다. Y로 끝나면 I처럼 소리나는 단어를 찾고 있습니다. |
>>> import re >>> re.search('[^aeiou]y$', 'vacancy') ① <_sre.SRE_Match object at 0x001C1FA8> >>> re.search('[^aeiou]y$', 'boy') ② >>> >>> re.search('[^aeiou]y$', 'day') >>> >>> re.search('[^aeiou]y$', 'pita') ③ >>>
>>> re.sub('y$', 'ies', 'vacancy') ① 'vacancies' >>> re.sub('y$', 'ies', 'agency') 'agencies' >>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy') ② 'vacancies'
① | 이 정규 표현식은 vacancy를 vacancies로 바꾸고 agency를 agencies로 바꾸는데, 이것이 바로 원하던 바입니다. 주의하세요. boy도 boies로 바뀔 것 같지만 함수 안에서 이런 일은 일어나지 않습니다. 왜냐하면 re.sub를 수행해도 되는지 알아 보기 위해 먼저 re.search를 실행했기 때문입니다. |
② | 지나가는 길에, 지적하고 싶은 것이 있습니다. (하나는 규칙이 적용되는지 알아보고 또 하나는 실제로 적용하는) 이 두 정규 표현식을 하나의 정규 표현식으로 결합하는 것도 가능합니다. 이렇게 보일 겁니다. 거의 비슷하게 보입니다: y 앞의 문자를 기억하기 위해, 섹션 7.6, “사례 연구: 전화 번호 해석하기” 에서 배운, 기억된 그룹(remembered group)을 사용하고 있습니다. 다음 교체 문자열에서, 새로운 구문인 \1를 사용하고 있는데, 이는 “이봐, 기억해 놓은 첫 그룹 있지? 여기에다 두라구”라는 뜻입니다. 이 경우, y 앞에 c를 기억하고 있으며, 교체할 때 c 대신에 c를, 그리고 y 자리에 ies로 교체합니다. (기억된 그룹이 하나 이상이라면 \2와 \3 그리고 등등을 사용할 수 있습니다.) |
정규 표현식 교체는 아주 강력합니다. \1 구문은 더욱 더 강력합니다. 그러나 전체 연산을 하나의 정규 표현식으로 조합하면 또한 더 읽기 어려워지고, 처음에 기술한 복수화 규칙에 직접적으로 일치되지 않습니다. 원래는 “단어가 S나 X 또는 Z로 끝나면 ES를 추가하라”와 같은 규칙을 설계했었습니다. 이 함수를 살펴보면 두 줄의 코드로 “단어가 S나 X 또는 Z로 끝나면 ES를 추가하라”고 서술합니다. 이 보다 더 직접적일 수는 없습니다.
이제 추상화 수준을 추가해 보겠습니다. 규칙 목록을 정의하면서 시작했습니다: 이러면 저렇게 하고, 그렇지 않으면 다음 규칙으로 가라. 추상화 부분을 간단하게 만들기 위해 프로그램의 일부를 잠시 복잡하게 만들어 보겠습니다.
import re def match_sxz(noun): return re.search('[sxz]$', noun) def apply_sxz(noun): return re.sub('$', 'es', noun) def match_h(noun): return re.search('[^aeioudgkprt]h$', noun) def apply_h(noun): return re.sub('$', 'es', noun) def match_y(noun): return re.search('[^aeiou]y$', noun) def apply_y(noun): return re.sub('y$', 'ies', noun) def match_default(noun): return 1 def apply_default(noun): return noun + 's' rules = ((match_sxz, apply_sxz), (match_h, apply_h), (match_y, apply_y), (match_default, apply_default) ) ① def plural(noun): for matchesRule, applyRule in rules: ② if matchesRule(noun): ③ return applyRule(noun) ④
① | 이 버전은 더 복잡해 보입니다 (분명히 더 깁니다). 그러나 정확하게 같은 일을 합니다: 네 가지 다른 규칙을 순서대로 일치시켜 보고 부합되면 적절한 정규 표현식을 적용합니다. 차이점은 일치 규칙과 적용 규칙마다 따로따로 자신만의 함수에 정의된다는 것입니다. 그리고 그 함수들은 이 rules 변수에 나열되는데, 이 변수는 터플로 구성된 터플입니다. |
② | for 회돌이를 사용하면 rules 터플로부터 일치 규칙과 적용 규칙을 한 번에 두 개(일치규칙, 적용규칙)를 끌어낼 수 있습니다. for 회돌이의 첫 회전에서, matchesRule는 match_sxz를 얻고, applyRule는 apply_sxz를 얻습니다. 두 번째 돌때 (여기까지 성공했다고 간주한다면), matchesRule에는 match_h가 할당되고, applyRule은 apply_h가 할당됩니다. |
③ | 파이썬에서는 모든 것이, 함수조차도 객체라는 사실을 기억하세요. rules에는 실체 함수가 담겨 있습니다; 함수의 이름이 아니라 실제 함수입니다. for 회돌이에서 할당되면 matchesRule와 applyRule은 실제로 호출할 수 있는 함수입니다. 그래서 for 회돌이의 첫 회전에서, 이는 matches_sxz(noun)을 호출한 것과 동등합니다. |
④ | for 회돌이의 첫 회전에서, 이는 apply_sxz(noun)를 호출한 것과 동등합니다. 등등. |
이렇게 추가된 추상화 수준이 이해가 가지 않는다면 과연 같은지 함수를 풀어서 알아 보겠습니다. 이 for 회돌이는 다음과 동등합니다:
def plural(noun): if match_sxz(noun): return apply_sxz(noun) if match_h(noun): return apply_h(noun) if match_y(noun): return apply_y(noun) if match_default(noun): return apply_default(noun)
여기에서 이점은 plural 함수가 이제 단순해진다는 것입니다. 다른 곳에 정의된 규칙 리스트를 받아서, 일반적인 형태로 그 규칙들을 반복합니다. 일치 규칙을 얻으면; 일치되었는가? 그러면 적용 규칙을 호출합니다. 규칙은 어느 곳이든 어떤 방식으로든 정의될 수 있습니다. plural 함수는 그에 개의치 않습니다.
이제, 이 추상화 수준을 추가한 가치가 있는가? 음, 아직은 아닙니다. 함수에 새로운 규칙을 추가해야 한다고 생각해 봅시다. 자, 앞 예제에서, if 서술문을 plural 함수에 추가할 필요가 있었습니다. 이 예제에서는 두가지 함수 match_foo와 apply_foo를 추가할 필요가 있고, 다음 rules 리스트를 갱신하여 새로운 일치 함수와 적용 함수가 다른 규칙에 상대적으로 호출되어야 할 순서를 지정할 필요가 있습니다.
이것이 바로 다음 섹션으로 나아갈 모퉁이 돌입니다. 다음 섹션으로 갑시다.
실제로는 일치마다 그에 맞게 이름을 붙여 따로 함수를 정의할 필요는 없습니다. 직접적으로 호출하지 않습니다; rules 리스트에 정의해 두고 그를 통하여 호출합니다. 그런 함수들을 익명화함으로써 규칙 정의를 날씬하게 만들어 보겠습니다.
import re rules = \ ( ( lambda word: re.search('[sxz]$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeioudgkprt]h$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeiou]y$', word), lambda word: re.sub('y$', 'ies', word) ), ( lambda word: re.search('$', word), lambda word: re.sub('$', 's', word) ) ) ① def plural(noun): for matchesRule, applyRule in rules: ② if matchesRule(noun): return applyRule(noun)
① | 이는 2 단계에서 정의한 것과 똑 같은 규칙 세트입니다. 유일한 차이점은 match_sxz와 apply_sxz 같이 이름붙여 함수를 정의하는 대신에, 그런 함수 정의를 직접적으로 rules 리스트 안에 “집어 넣는다”는 것입니다. 람다 함수를 사용해서 말입니다. |
② | plural 함수는 전혀 바뀌지 않았음에 주목하세요. 규칙 함수 세트를 반복하면서, 첫 규칙을 점검해서 참 값을 돌려주면 두 번째 규칙을 호출하고 그 값을 돌려줍니다. 말 그대로 위와 같습니다. 유일한 차이점은 규칙 함수가 람다 함수를 사용하여 익명으로 인라인으로 정의되었다는 것입니다. 그러나 plural 함수는 규칙이 어떻게 정의되어 있든지 상관하지 않습니다; 그저 규칙 리스트를 얻어서 맹목적으로 반복할 뿐입니다. |
이제 새로운 규칙을 얻으려면 규칙을 직접적으로 rules 리스트에 정의하면 됩니다: 하나는 일치 규칙이고 또 하나는 적용 규칙입니다. 그러나 규칙 함수를 이런 식으로 인라인으로 정의하면 여기에 불필요하게 중복이 있다는 사실이 더욱 확실하게 드러납니다. 네쌍의 함수가 있으며, 모두 같은 패턴을 따릅니다. match 함수는 re.search를 한 번만 호출하고, apply 함수는 re.sub를 한 번 호출합니다. 이런 유사점들을 공통 인수로 추출해 봅시다.
새로운 규칙을 더 쉽게 정의할 수 있도록 코드의 중복을 인수분해해 봅시다.
import re def buildMatchAndApplyFunctions((pattern, search, replace)): matchFunction = lambda word: re.search(pattern, word) ① applyFunction = lambda word: re.sub(search, replace, word) ② return (matchFunction, applyFunction) ③
① | buildMatchAndApplyFunctions 함수는 다른 함수들을 동적으로 구축합니다. 이 함수는 pattern과 search 그리고 replace를 받습니다 (실제로는 터플을 받지만 잠시 후에 더 자세히 설명합니다). lambda 구문을 사용하여 매개변수 하나 (word)를 받는 함수로 일치 함수를 구축할 수 있습니다. buildMatchAndApplyFunctions에 건넨 pattern 그리고 구축되어 있는 일치 함수에 건넨 word를 가지고 re.search를 호출합니다. |
② | 적용 함수를 구축하는 것도 비슷하게 작동합니다. 적용 함수는 매개변수를 하나 받아, buildMatchAndApplyFunctions 함수에 건넨 search 매개변수와 replace 매개변수 그리고 구축중인 적용 함수에 건넨 word를 가지고 re.sub를 호출하는 함수입니다. 바깥 매개변수의 값을 동적 함수 안에 사용하는 이 테크닉을 클로저(closures)라고 부릅니다. 구축하고 있는 적용 함수 안에 상수를 본질적으로 정의하고 있습니다: 하나의 매개변수(word)를 받지만 그와 더불어 적용 함수를 정의할 때 설정된 다른 두 값(search와 replace)에도 작용합니다. |
③ | 마지막으로, buildMatchAndApplyFunctions 함수는 값이 두 개인 터플을 하나 돌려줍니다: 방금 만든 두 개의 함수가 그것입니다. 그런 함수 안에 정의한 상수는 (matchFunction안의 pattern 그리고 applyFunction 안의 search와 replace) 그런 함수와 함께 존재하며, 심지어 buildMatchAndApplyFunctions으로부터 돌아온 후에도 존재합니다. 정말 멋집니다. |
이것이 정말 이해가 가지 않는다면 (그럴 겁니다. 괴이하지요), 사용법을 보시면 더 이해가 잘 가실 겁니다.
patterns = \ ( ('[sxz]$', '$', 'es'), ('[^aeioudgkprt]h$', '$', 'es'), ('(qu|[^aeiou])y$', 'y$', 'ies'), ('$', '$', 's') ) ① rules = map(buildMatchAndApplyFunctions, patterns) ②
① | 복수화 규칙은 이제 (함수가 아니라) 일련의 문자열로 정의됩니다. 첫 문자열은 re.search에서 이 규칙이 일치하는지 알아 보는데 사용할 정규 표현식입니다; 두 번째와 세번째 문자열은 검색과 치환 표현식으로서 re.sub에서 실제로 그 규칙을 적용하여 명사를 복수형으로 바꾸는데 사용합니다. |
② | 이 줄은 마법의 줄입니다. patterns에 문자열 리스트를 받아 그를 함수 리스트로 바꿉니다. 어떻게? 그 문자열들을 buildMatchAndApplyFunctions 함수에 짝지어 주면 되는데, 세 개의 문자열을 매개변수로 취하고 두 함수를 담은 터플을 돌려줍니다. 이는 rules가 결국 앞의 예제와 정확하게 똑 같다는 뜻입니다: 터플 리스트로서, 각 터플은 함수 쌍이며, 첫 함수는 match 함수로서 re.search를 호출하고 두 번째 함수는 apply 함수로서 re.sub를 호출합니다. |
맹세하건대 제가 지어낸 것이 아닙니다: rules는 결국 앞의 예제와 정확하게 똑 같은 함수 리스트입니다. rules 정의를 늘어놓아 보면 다음과 같습니다:
rules = \ ( ( lambda word: re.search('[sxz]$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeioudgkprt]h$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeiou]y$', word), lambda word: re.sub('y$', 'ies', word) ), ( lambda word: re.search('$', word), lambda word: re.sub('$', 's', word) ) )
def plural(noun): for matchesRule, applyRule in rules: ① if matchesRule(noun): return applyRule(noun)
① | rules 리스트는 앞 예제와 똑같기 때문에, plural 함수가 하나도 바뀌지 않아도 별로 놀랍지 않습니다. 기억하세요. 완전히 포괄적입니다; 규칙 함수 리스트를 받아서 순서대로 그를 호출합니다. 규칙이 어떻게 정의되어 있는지 신경쓰지 않습니다. 단계 2에서, 규칙은 별도로 이름붙은 함수로 정의되었습니다. 단계 3에서는 익명의 lambda 함수로 정의되었습니다. 이제 단계 4에서는 buildMatchAndApplyFunctions 함수를 날 문자열 리스트 위에 짝지어서 동적으로 구축됩니다. 아무 문제가 되지 않습니다; plural 함수는 여전히 똑같은 방식으로 작동합니다. |
이 정도로도 충분히 놀랍지 않았다면 buildMatchAndApplyFunctions 정의에 미묘하게 빼 놓은게 있다고 솔직히 고백해야겠습니다.
def buildMatchAndApplyFunctions((pattern, search, replace)): ①
① | 이중 괄호가 보이십니까? 이 함수는 실제로는 세 개의 매개변수를 받지 않습니다; 실제로는 한 개의 매개변수, 즉 세 개의 원소를 가진 터플을 하나 받습니다. 그러나 그 터플은 함수가 호출될 때 확장되고, 터플 안의 세 원소는 각각 다른 변수에 할당됩니다: pattern와 search 그리고 replace에 할당됩니다. 아직 잘 모르시겠다고요? 실제로 작동하는 모습을 살펴봅시다. |
>>> def foo((a, b, c)): ... print c ... print b ... print a >>> parameters = ('apple', 'bear', 'catnap') >>> foo(parameters) ① catnap bear apple
① | foo 함수를 적절하게 호출하려면 세 개의 원소를 가진 터플을 가지고 호출해야 합니다. 함수가 호출되면 원소들은 foo 안에 있는 지역 변수에 따로따로 할당됩니다. |
이제 되돌아가 왜 이 자동--터플-확장 트릭이 필요했는지 살펴보겠습니다. patterns는 터플이 담긴 리스트였습니다. 그리고 각 터플은 세개의 원소를 가집니다. map(buildMatchAndApplyFunctions, patterns)를 호출하면 그것은 buildMatchAndApplyFunctions가 세걔의 매개변수를 가지고 호출되지 않는다는 뜻입니다. map을 사용하여 리스트를 함수에 짝지으면 언제나 그 함수는 매개변수 하나로 호출됩니다: 리스트의 각 원소가 매개변수가 됩니다. patterns의 경우, 리스트의 각 원소는 터플입니다. 그래서 buildMatchAndApplyFunctions는 언제나 그 터플을 가지고 호출됩니다. 그리고 자동-터플-확장 트릭을 buildMatchAndApplyFunctions의 정의에 사용하면 그 터플의 원소들을 이름붙은 변수에 할당하여 그 변수들을 가지고 작업할 수 있습니다.
중복된 코드를 모두 공통인수로 추출했으며 충분하게 추상화 수준을 추가하여 복수화 규칙을 문자열 리스트로 정의할 수 있었습니다. 다음 논리적 단계는 이런 문자열들을 받아서 별도의 파일에 두는 것입니다. 규칙을 사용하는 코드와 별도로 규칙을 파일에 따로 관리할 수 있습니다.
먼저, 원하는 규칙을 텍스트 파일에 담아 봅시다. 데이터 구조는 평범해서, 그냥 공간문자로- (또는 탭문자로-) 나뉜 문자열로서 세 개의 컬럼으로 구성됩니다. 파일 이름은 rules.en이며; “en”은 영어를 뜻합니다. 이 규칙들은 영어 명사를 복수화하는 규칙입니다. 나중에 다른 언어에 다른 규칙을 추가할 수 있습니다.
이제 이 규칙 파일을 어떻게 사용할 수 있는지 살펴보겠습니다.
import re import string def buildRule((pattern, search, replace)): return lambda word: re.search(pattern, word) and re.sub(search, replace, word) ① def plural(noun, language='en'): ② lines = file('rules.%s' % language).readlines() ③ patterns = map(string.split, lines) ④ rules = map(buildRule, patterns) ⑤ for rule in rules: result = rule(noun) ⑥ if result: return result
① | 여기에서도 클로저 테크닉을 사용하고 있지만 (함수 밖에 정의된 변수를 사용하는 함수를 동적으로 구축하는 테크닉), 이제 따로 떨어진 match 함수와 apply 함수를 하나로 조합했습니다. (이렇게 바꾼 이유는 다음 섹션에서 알려 드리겠습니다.). 이렇게 하면 두 개의 함수를 가진 것과 똑 같은 일을 달성할 수 있지만 다르게 불러야 할 필요가 있습니다. 이에 관해서는 잠시뒤에 알려 드리겠습니다. |
② | 이제 plural 함수는 language라는 선택적인 두 번째 매개변수를 받습니다. 이 매개변수는 en이 기본값입니다. |
③ | language 매개변수를 사용하여 파일이름을 구성하고, 그 파일을 열어 내용을 리스트로 읽어들입니다. language가 en이면 rules.en 파일을 열고, 전체 내용을 읽어, 줄 단위로 분해하여 리스트로 돌려줍니다. 파일의 각 줄은 리스트의 원소 하나에 해당합니다. |
④ | 보셨다시피, 파일의 각 줄은 실제로 세 가지 값이 있습니다. 그러나 각 값은 공백으로 분리됩니다 (탭이든 공간문자이든, 아무 차이가 없습니다). string.split 함수를 이 리스트 위에 짝지으면 새로운 리스트가 탄생합니다. 이 리스트에서 각 원소는 세 개의 문자열로 구성된 터플입니다. 그래서 [sxz]$ $ es와 같은 줄은 ('[sxz]$', '$', 'es') 터플로 분해됩니다. 이는 patterns가 터플 리스트가 된다는 뜻이며, 즉 단계 4에 하드-코드한 것과 같이 말입니다. |
⑤ | patterns가 터플 리스트라면 rules는 buildRule를 호출할 때마다 동적으로 생성된 함수 리스트가 됩니다. buildRule(('[sxz]$', '$', 'es'))를 호출하면 한 개의 매개변수 word를 받는 함수를 돌려줍니다. 이렇게 돌려받은 함수를 호출하면 re.search('[sxz]$', word)와 re.sub('$', 'es', word)가 실행됩니다. |
⑥ | 이제 match 함수와 apply 함수를 조합하였으므로, 다른 방식으로 호출해야 합니다. 그냥 함수를 호출하고, 무언가 돌려주면 그것이 바로 복수형입니다; 아무것도 돌려주지 않으면 (None), 그 규칙이 부합하지 않은 것이고 또다른 규칙을 시도할 필요가 있습니다. |
그래서 여기에서 개선된 점은 복수화 규칙을 외부 파일로 완전히 분리해 넣었다는 것입니다. 그 파일을 코드와 따로 유지관리할 수 있을 뿐 아니라 같은 plural 함수라도 language 매개변수에 근거하여 서로 다른 규칙 파일을 사용할 수 있도록 이름짓기 체계를 설정하였습니다.
여기에서 단점은 plural 함수를 호출할 때마다 파일을 읽는다는 것입니다. “독자에게 연습문제로 남긴다”라는 문구 없이도 이 책을 집필할 수 있을 것이라고 생각했었습니다. 그러나, 여기에 숙제를 남깁니다: 규칙 파일이 호출과 호출 사이에 바뀌면 자동으로 재갱신되는 언어-종속적 규칙 파일을 위한 캐시 매커니즘을 구축하는 일을 독자 여러분께 숙제로 남겨 드리겠습니다. 재미있게 즐겨 보세요.
이제 발생자에 관하여 언급할 준비가 되었습니다.
import re def rules(language): for line in file('rules.%s' % language): pattern, search, replace = line.split() yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) def plural(noun, language='en'): for applyRule in rules(language): result = applyRule(noun) if result: return result
이는 발생자라고 부르는 테크닉을 사용합니다. 먼저 간단한 예제를 보여 드리고 나서 설명해 드리겠습니다.
>>> def make_counter(x): ... print 'entering make_counter' ... while 1: ... yield x ① ... print 'incrementing x' ... x = x + 1 ... >>> counter = make_counter(2) ② >>> counter ③ <generator object at 0x001C9C10> >>> counter.next() ④ entering make_counter 2 >>> counter.next() ⑤ incrementing x 3 >>> counter.next() ⑥ incrementing x 4
① | yield 키워드가 make_counter에 있다는 것은 이 함수가 보통 함수가 아니라는 뜻입니다. 이 함수는 특별한 함수로서 한 번에 값을 하나씩 발생시킵니다. 이를 재개가능한 함수라고 생각해도 좋습니다. 호출하면 발생자를 돌려주는데 x의 값을 연속적으로 발생시키는데 사용됩니다. |
② | make_counter 발생자의 실체를 만들려면 그냥 다른 함수처럼 호출하면 됩니다. 이는 실제로 함수 코드를 실행하지 않는다는 것을 주목하세요. 이 사실을 알 수 있는 이유는 make_counter의 첫 줄이 print 서술문인데, 지금까지 아무것도 출력되지 않았기 때문입니다. |
③ | make_counter 함수는 발생자 객체를 돌려줍니다. |
④ | 발생자 객체에 next() 메쏘드를 처음 호출하면 make_counter의 코드가 첫 yield 서술문까지 실행되고, 양보된 값을 돌려줍니다. 이 경우, 그 값은 2인데, 원래 make_counter(2) 호출해서 발생자를 만들었기 때문입니다. |
⑤ | 반복해서 next()를 그 발생자 객체에 요청하면 떠났던 바로 그 곳에서 재개되고 다음 yield 서술문을 만날 때가지 계속됩니다. 실행을 기다리는 다음 코드는 print 서술문인데, incrementing x를 인쇄합니다. 그 다음에 x = x + 1 서술문이 오고 실제로 증가시킵니다. 다음 다시 while 회돌이를 돌고, 제일 먼저 하는 일은 yield x인데, 이는 x의 현재 값 (지금은 3)을 돌려줍니다. |
⑥ | 두 번째로 counter.next()를 호출하면 다시 같은 일을 합니다. 그러나 이 번에는 x가 4입니다. 등등. make_counter는 무한 회돌이를 설정하기 때문에, 이론적으로 무한히 이렇게 할 수 있으며, 계속해서 x를 증가시키고 값을 뱉아 냅니다. 그러나 대신에 좀 더 생산적으로 발생자를 사용하는 법을 살펴보겠습니다. |
def fibonacci(max): a, b = 0, 1 ① while a < max: yield a ② a, b = b, a+b ③
① | 피보나치(Fibonacci) 수열은 앞의 두 숫자를 더한 숫자로 구성된 수열입니다. 0과 1에서 시작하여, 처음에는 서서히, 점점 더 빠르게 증가합니다. 이 수열을 시작하려면 두 개의 변수가 필요합니다: a는 0에서 시작하고, b는 1에서 시작합니다. |
② | a는 수열에서 현재 숫자입니다. 그래서 그것을 양보합니다. |
③ | b는 수열에서 다음 숫자입니다. 그래서 그것을 a에 할당하고, 또한 나중에 사용하려고 다음 값을 (a+b) 계산해서 그것을 b에 할당합니다. 이는 병행적으로 일어남을 주목하세요; a가 3이고 b가 5인 경우, a, b = b, a+b는 a에 5가 (b의 이전 값) 설정되고 b에 8이 (a와 b의 앞 값의 합) 설정됩니다. |
그래서 연속적으로 피보나치 숫자를 뱉아내는 함수가 완성되었습니다. 물론, 재귀로도 가능하지만 이런식으로 하는 것이 더 읽기 쉽습니다. 또한, for 회돌이와 더 잘 작동합니다.
>>> for n in fibonacci(1000): ① ... print n, ② 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
좋습니다. 다시 plural 함수로 돌아가 어떻게 사용하는지 살펴보겠습니다.
def rules(language): for line in file('rules.%s' % language): ① pattern, search, replace = line.split() ② yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) ③ def plural(noun, language='en'): for applyRule in rules(language): ④ result = applyRule(noun) if result: return result
① | for line in file(...)는 파일로부터 줄을, 한 번에 한 줄씩 읽는 상용구입니다. 이것이 작동하는 이유는 file이 실제로는 발생자를 돌려주기 때문인데, 이 발생자의 next() 메쏘드는 파일의 다음 줄을 돌려줍니다. 정말 멋집니다. 생각만 해도 감동의 눈물이 납니다. |
② | 여기에서 마법은 전혀 없습니다. 기억하세요. 규칙 파일에서 줄은 세 개의 값을 공백으로 나누어 보유합니다. 그래서 line.split()은 3개의 값으로 구성된 터플 하나를 돌려주고, 그 값을 3개의 지역 변수에 각각 할당합니다. |
③ | 그리고 양보합니다. 무엇을 양보합니까? 함수를 양보합니다. 람다(lambda)를 사용하여 동적으로 구축된 이 함수는 실제로는 클로저(closure)입니다 (지역 변수인 pattern과 search 그리고 replace를 상수로 사용합니다). 다시 말해, rules는 규칙 함수를 뱉아 내는 발생자입니다. |
④ | rules는 발생자이므로, 직접적으로 for 회돌이에 사용할 수 있습니다. for 회돌이를 처음 돌때, rules 함수를 호출하면 규칙 파일을 열고 그의 첫 줄을 읽어서, 규칙 파일에 정의된 첫 규칙을 일치시켜 적용하는 함수를 동적으로 구축하고, 동적으로 구축된 그 함수를 양보합니다. 두 번째로 for 회돌이를 돌 때, rules의 떠났던 그 곳에을 찾아서 (그 곳은 for line in file(...) 회돌이의 중앙이었습니다), 규칙 파일의 두 번째 줄을 읽어서, 규칙 파일에 정의된 두 번째 규칙을 일치시켜 적용하는 또다른 함수를 동적으로 구축하고, 그것을 양보합니다. 등등. |
단계 5에서 얻은 것은 무엇인가? 5 단계에서, 전체 규칙 파일을 읽어서 가능한 모든 규칙 리스트를 구축했습니다. 심지어 첫 규칙을 시도해 보기도 전에 말입니다. 이제 발생자를 사용하면 그 모든 것들을 게으르게 처리할 수 있습니다: 첫 규칙을 열어 일어보고 시도할 함수를 만듭니다. 그러나 그것이 작동한다면 나머지 파일을 읽지 않아도 되며 다른 함수는 만들지 않아도 됩니다.
이 장에서는 여러 다양한 고급 테크닉을 살펴보았습니다. 모두가 아무 곳에나 적절한 것은 아닙니다.
이제 다음과 같은 일들을 편안하게 할 수 있어야 합니다:
추상화층을 추가하는 법, 함수를 동적으로 구축하는 법, 클로저 구축하는 법, 그리고 발생자를 사용하는 법을 알면 코드가 더 단순해지고 더 읽기 쉬워지며 그리고 더 유연해집니다. 그러나 나중에 디버그 하기가 더 어려워질 수도 있습니다. 단순함과 파워 사이에 어느 정도로 균형을 유지할지는 여러분에게 달려 있습니다.
☜ 제 16 장 기능적 프로그래밍 | """ Dive Into Python """ 다이빙 파이썬 |
제 18 장 수행성능 조율 ☞ |