메인 컨텐츠로 가기

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

Python으로 간결하고 테스트 가능하며 수준 높은 코드 쓰기

Noah Gift , Founder, GiftCS, LLC
Photo of Noah Gift

Noah Gift is the co-author of Python For UNIX and Linux System Administration by O'Reilly, and is also working on Google App Engine In Action for Manning. He is an author, speaker, consultant, and community leader, writing for publications such as Red Hat Magazine, O'Reilly, and MacTech. His consulting company's website is http://www.giftcs.com, and much of his writing can be found at http://noahgift.com. You can also follow Noah on Twitter.

He has a Master's degree in CIS from Cal State Los Angeles and a B.S. in Nutritional Science from Cal Poly San Luis Obispo. He is an Apple- and LPI-certified sysadmin, and has worked at companies such as Caltech, Disney Feature Animation, Sony Imageworks, Turner Studios, and Weta Digital. In his free time, he enjoys spending time with his wife, Leah, and their son, Liam, composing for the piano, running marathons, and exercising religiously.

요약:  고상하고 강력한 Python 언어를 포함한 어떤 언어로 코드를 쓰든 끔직할 정도로 나쁜 코드를 쓸 수 있습니다. 본 기사에서는 테스트에 대한 생각의 차이가 실제로 어떻게 극적이라 할 정도로 다른 Python 코드를 만들어내는지 살펴봅니다. 마지막으로, 그 차이점을 과학적으로 평가하는 방법을 알아봅시다.

원문 게재일:  2010 년 9 월 28 일 번역 게재일:   2011 년 2 월 07 일
난이도:  중급 원문:  보기 PDF:  A4 and Letter (62KB | 15 pages)Get Adobe® Reader®
페이지뷰:  4746 회
의견:  


소개

소프트웨어 작성은 인간이 시도할 수 있는 가장 복잡하고 어려운 시도 중 하나이다. AWK 프로그래밍 언어와 "K 및 R C"의 공동 저자인 Brian Kernigan은 소프트웨어 도구(Software Tools)라는 책에서 소프트웨어 개발의 진짜 본질을 "복잡도의 제어가 소프트웨어 개발의 정수"라고 한마디로 요약했다. 실제 소프트웨어 개발의 냉정한 현실은 소프트웨어를 작성할 때 의도적이든 그렇지 않든 너무 복잡해지고 유지 관리 능력, 테스트 능력 및 품질을 경시하는 일이 자주 발생한다는 점이다. 이런 불행한 현실에 따른 최종적인 결과는 유지 관리하기가 점점 더 어려워지고 비용도 많이 드는데다가, 처음에는 가끔 오류가 발생하다가 나중에는 어떻게 해볼 수 없을 정도로 처참하게 실패작으로 끝날 수 있는 소프트웨어가 된다는 것이다.

훌륭한 코드를 작성하기 위한 프로세스의 첫 단계는 개인이나 팀이 소프트웨어를 개발하는 방식에 대한 전체적인 사고 과정을 재검토하는 것이다. 실패하거나 문제가 있는 소프트웨어 개발 프로젝트를 잘 살펴보면, 소프트웨어 개발의 초점이 가능한 어떤 방식으로든 문제 해결에 맞춰져 있어, 발생한 문제에 대처한다는 소극적인 사고방식으로 소프트웨어가 개발된 경우가 많다는 점을 알 수 있다. 성공적인 소프트웨어 프로젝트의 개발자는 당면한 문제 해결 방법에 대해 고민할 뿐 아니라, 그 문제를 해결하는 데 관련되는 프로세스에 대해서도 깊이 생각한다.

성공적인 소프트웨어 개발자라면 소프트웨어 작업 결과를 지속적으로 입증할 수 있도록 손쉽게 자동화되는 방식으로 테스트를 실행할 방법을 고안할 것이다. 이런 개발자들은 불필요한 복잡도가 지닌 위험성을 확실히 인식하고 있다. 그들은 자신의 접근 방식에 겸손하고, 비판적인 의견에 귀를 기울이며, 개발 과정의 모든 단계에서 리팩토링을 예상한다. 또한, 자신이 개발한 소프트웨어를 적절히 테스트하고 읽고 유지 관리할 수 있게 하려면 어떻게 해야 할지 늘 깊이 생각한다. Python은 언어이자 커뮤니티로서 효과적이면서도 간결하고 유지 관리하기 쉬운 코드를 작성하겠다는 욕구가 큰 원동력이 되고 있지만, 여전히 정확히 그 반대되는 결과를 초래하기 십상인 점도 간과하지 말자. 본 기사에서는 이 문제를 정면으로 다루고 Python으로 간결하고 테스트 가능하며 수준 높은 코드를 작성하는 방법을 탐구해본다.

간결한 코드 작성을 위한 가상의 문제점

이런 스타일의 개발을 잘 보여주기 위한 최선의 방법은 가상의 문제점을 해결해보는 것이다. 자신이 어떤 회사의 백엔드 웹 개발자로서, 사용자의 검토 생성을 허용하고 그런 검토 내용의 작은 스니펫을 보여주고 강조할 방법을 제시할 필요가 있다고 가정해보자. 이 문제에 접근하는 한 가지 방법은 텍스트 및 쿼리 매개변수의 스니펫을 가져와서 쿼리 매개변수가 강조 표시되어 문자 수가 제한된 스니펫을 다시 리턴하는 큰 함수를 작성하는 것이다. 그러면 문제 해결에 필요한 모든 논리가 한 "메가" 함수에 포함되고, 원하는 결과를 얻을 때까지 단순히 스크립트를 계속 실행하기만 하면 된다. 이 형식은 아래의 코드 예제와 같아 보일 것이며, 인쇄 명령문이나 로깅 명령문 및 대화식 쉘의 조합으로 개발되는 경우가 많다.


조잡한 코드
def my_mega_function(snippet, query)
    """This takes a snippet of text, and a query parameter and returns """

    #Logic goes here, and often runs on for several hundred lines
    #There are often deeply nested conditional statements and loops
    #Function could reach several hundred, if not thousands of lines
    
    return result

Python, Perl 또는 Ruby와 같은 동적 언어를 사용하면 (대개는 대화식으로) 올바른 결과로 보이는 결과를 얻을 때까지 해당 문제점을 단순히 계속 공략하고 그런 결과를 얻으면 끝내는 방법으로 소프트웨어를 개발하기 쉽다. 물론, 이런 방법으로 개발하고 싶은 마음이 들겠지만, 불행히도 이런 접근 방식으로 개발하면 위험으로 가득 찬 잘못된 소프트웨어가 탄생하는 경우가 있다. 여기서 말하는 위험 중 많은 부분은 테스트 가능한 솔루션으로 디자인하지 못한다는 점에 있고, 일부는 작성한 소프트웨어의 복잡도를 적절히 제어하지 못한다는 점에도 있다.

이런 함수가 어떻게 효과적이라고 말할 수 있겠는가? 개발 중에 해당 함수를 마지막으로 실행했을 때 올바로 작동했으므로 함수가 올바로 작동한다고 믿을 수 있겠지만, 그 함수의 논리나 구문에 포착하기 어려운 오류가 포함되어 있지 않다고 확신하는가? 코드를 변경해야 하는 경우 어떤 일이 벌어질까? 코드를 변경한 함수가 계속 올바로 작동하는가? 그리고 어떻게 그렇다는 사실을 알 수 있는가? 다른 개발자가 그 코드의 유지 관리를 맡고 필요할 때는 코드를 변경해야 하는 경우는 어떤가? 그 개발자는 자신이 코드를 변경하더라도 미묘한 문제로 인해 작동이 중단되지 않으리라고 어떻게 알 수 있을까? 그 개발자가 이 코드가 어떤 일을 하는지 얼마나 쉽게 이해할 수 있을까?

간단히 답하자면, 자신이 개발한 소프트웨어라도 철저히 테스트하지 않으면 올바로 작동할지 알 수 없다는 것이다. 다양한 각도에서 충분히 많이 추측하면서 개발하면 결국 올바로 작동하는 것으로 보이는 소프트웨어를 만들 수 있겠지만, 그 소프트웨어가 언제나 확실히 올바로 작동한다고 말할 수 있는 사람은 아무도 없다. 바로 이 점이 골치 아픈 부분으로, 필자는 소프트웨어를 작성하는 동시에 이런 식으로 작성된 소프트웨어를 디버그할 수 있게 도움을 주었다. 다행히도, 이런 조건은 손쉽게 회피할 수 있다. 테스트 중심 개발(Test Driven Development)의 경우와 같이 자신의 논리를 코드로 작성하기 전이나 작성하는 동안 테스트 방법을 구현하는 것이 코드가 작성된 방식을 실제로 구체화한다. 그렇게 하면 테스트, 코드 이해 및 유지 관리가 쉬운 확장 가능한 모듈식 코드로 연결된다. 경험이 풍부한 개발자로서는 테스트를 염두에 두고 소프트웨어를 개발했을 때와 그렇지 않았을 때 이 점이 바로 분명하게 이해된다. 잘 훈련되고 경험이 많은 개발자의 눈에는 소프트웨어 자체가 전혀 다른 모습으로 보인다.

필자의 말을 있는 그대로 받아들이거나 직접 자기 눈으로 코드를 검사하지 않고도 이런 두 가지 다른 스타일 사이의 차이점을 과학적으로 평가할 수 있는 방법이 있다. 첫 번째 방법은 테스트 대상 코드 행을 실제로 평가하는 것이다. Nose는 코드 검사와 같이 테스트 및 플러그인의 일괄처리를 자동으로 실행하는 손쉬운 방법을 포함하는 Python 장치 테스트 프레임워크의 인기 확장 기능이다. 개발 중에 코드 검사를 평가하면 매우 복잡하게 중첩된 논리로 임시적 방법으로 빌드되는 큰 함수들로 구성된 코드에 대해 100% 테스트 범위에 이르기가 거의 불가능하다는 사실이 금방 분명해진다.

차이점을 평가하는 두 번째 방법은 정적 분석 도구를 사용하는 것이다. 일반적인 코드 수준부터 중복 코드 또는 복잡도와 같은 특정 지표까지, Python 개발자를 위해 다양한 지표를 평가하는 여러 가지 Python 도구가 많이 사용된다. Pygenie 또는 pymetrics로 코드의 사이클로매틱 복잡도를 평가할 수 있다(참고 자료 참조).

다음은 비교적 간단하고 "간결한" 코드에서 pygenie를 실행할 때 어떤 결과가 나오는지 보여주는 예제이다.


사이클로매틱 복잡도의 pygenie 출력 결과
% python pygenie.py complexity --verbose highlight spy
File: /Users/ngift/Documents/src/highlight.py
Type Name                                                                   Complexity 
----------------------------------------------------------------------------------------
M    HighlightDocumentOperations._create_snippit                                  3
M    HighlightDocumentOperations._reconstruct_document_string                     3
M    HighlightDocumentOperations._doc_to_sentences                                2
M    HighlightDocumentOperations._querystring_to_dict                             2
M    HighlightDocumentOperations._word_frequency_sort                             2
M    HighlightDocumentOperations.highlight_doc                                    2
X    /Users/ngift/Documents/src/highlight.py 1          
C    HighlightDocumentOperations                                                  1
M    HighlightDocumentOperations.__init__                                         1
M    HighlightDocumentOperations._custom_highlight_tag                            1
M    HighlightDocumentOperations._score_sentences                                 1
M    HighlightDocumentOperations._multiple_string_replace                         1

사이클로매틱 복잡도란?

사이클로매틱 복잡도는 1976년에 Thomas J. McCabe가 프로그램의 복잡도를 결정하기 위해 개발한 소프트웨어 지표이다. 이 지표를 사용하면 소스 코드를 통해 선형적으로 독립적인 경로 또는 분기의 수를 판단할 수 있다. McCabe에 따르면, 메소드의 복잡도를 10 미만으로 유지하는 것이 최선이라고 한다. 이는 인간의 기억력에 관한 연구에서 인간이 단기 기억력으로 기억할 수 있는 항목의 수가 7±2개인 것으로 밝혀졌기 때문에 중요하다.

어떤 개발자가 선형적으로 독립적인 경로가 50개나 되는 코드를 작성하고 있다면, 이 수는 그 메소드에서 발생하고 있는 일을 계속 추적할 때 단기 기억력의 용량을 대략 5배 이상 초과하는 것이 된다. 인간의 모든 단기 기억에 부담을 주지 않는 간단한 메소드가 사용하기 더 쉽고 오류가 덜 발생하는 것으로 입증되었다. Enerjy가 2008년에 수행한 한 연구 결과, 사이클로매틱 복잡도와 불완전성 간에는 강력한 상관관계가 있는 것으로 밝혀졌다. 복잡도가 11인 클래스는 오류 발생 확률이 0.28이었지만, 복잡도가 74인 클래스에서는 그 확률이 무려 0.98로 상승했다.

예제에서 알 수 있듯이, 모든 메소드가 극히 간단하고 복잡도도 10 미만이므로, McCabe의 연구에 따라 바람직한 수준이라 할 수 있다. 필자는 복잡도가 무려 140을 넘고 그 길이만도 1,200행을 훌쩍 넘었지만 테스트조차 하지 않은 "메가" 함수를 본 적이 있다. 이와 같은 코드를 테스트하기란 말 그대로 불가능하다는 정도로만 얘기하겠다. 실제로도 이런 코드가 작동하는지 알 길이 없고 코드를 리팩토링하는 것도 불가능하다. 코드 작성자가 테스트를 염두에 두고 100%의 테스트 범위로 같은 논리를 코드로 작성했다면, 그처럼 높은 수준의 복잡도를 가진 코드를 작성하지는 않았을 것이다.

간결한 코드 작성을 위한 가상의 해결책

이제 장치 테스트와 기능 테스트가 수반되는 완전한 소스 코드 예제를 살펴보고 소스 코드가 실제로 하는 일과 왜 이 코드가 간결한 것으로 간주되는지 알아보자. 엄격한 지표를 이용한 간결한 코드의 한 가지 합리적인 정의는 100% 테스트 범위에 근접하고 모든 클래스와 메소드에 대해 10 미만의 사이클로매틱 복잡도를 가지며 pylint로 10.0의 등급에 근접한 점수를 얻는다는 요구사항을 충족하는 코드를 간결한 코드라고 정의하는 것이다. 다음은 nose를 사용하여 highlight 모듈에서 장치 테스트 및 doctest 범위를 테스트하는 예제이다.


범위 보고를 포함한 nosetests 실행: 100% 범위
% nosetests -v --with-coverage --cover-package=highlight --with-doctest\
     --cover-erase --exe

Doctest: highlight.HighlightDocumentOperations._custom_highlight_tag ... ok
test_functional.test_snippit_algorithm ... ok
test_custom_highlight_tag (test_highlight.TestHighlight) ... ok
Consumes the generator, and then verifies the result[0] ... ok
Verifies highlighted text is what we expect ... ok
test_multi_string_replace (test_highlight.TestHighlight) ... ok
Verifies the yielded results are what is expected ... ok

Name        Stmts   Exec  Cover   Missing
-----------------------------------------
highlight      71     71   100%   
----------------------------------------------------------------------
Ran 7 tests in 4.223s

OK

위 스니펫에서 알 수 있듯이, nosetests 명령이 여러 가지 옵션으로 실행되었고, highlight spy 스크립트용으로 100% 테스트 범위가 있었다. 실제로 지적해야 할 유일한 사항은 --cover-package=highlight가 지정된 모듈에서 nose에게 범위 보고서만 표시하도록 알리는 방법이라는 점이다. 이것은 범위 보고서의 출력을 범위 보고를 위해 관찰하려는 모듈 또는 패키지로 격리하는 데 매우 유용하다. 본 기사에서 소스 코드를 다운로드하고 범위 보고 메커니즘이 실제로 어떻게 작동하는지 알아보기 위해 테스트 중 몇 가지를 주석 처리하는 방법을 시도해볼 수 있을 것이다.


highlight spy
#/usr/bin/python
# -*- coding: utf-8 -*-

"""
:mod:`highlight` -- Highlight Methods
===================================

.. module:: highlight
   :platform: Unix, Windows
   :synopsis: highlight document snippets that match a query.
.. moduleauthor:: Noah Gift

 
Requirements::
    1.  You will need to install the ntlk library to run this code.
        http://www.nltk.org/download
    2.  You will need to download the data for the ntlk:
        See http://www.nltk.org/data::
        
        import nltk
        nltk.download()

"""

import re
import logging

import nltk

#Globals
logging.basicConfig()
LOG = logging.getLogger("highlight")
LOG.setLevel(logging.INFO)

class HighlightDocumentOperations(object):

    """Highlight Operations for a Document"""
    
    def __init__(self, document=None, query=None):
        """
        Kwargs:
            document (str):
            query (str):
            
        """
        self._document = document
        self._query = query
    
    @staticmethod
    def _custom_highlight_tag(phrase,
                              start="<strong>",
                              end="</strong>"):
        
        """Injects an open and close highlight tag after a word

        Args:
            phrase (str) - A word or phrase.
        Kwargs:
        start (str) - An opening tag.  Defaults to <strong>
        end (str) - A closing tag.  Defaults to </strong>
        Returns:
            (str) word or phrase with custom opening and closing tags
            
        >>> h = HighlightDocumentOperations()
        >>> h._custom_highlight_tag("foo")
        'foo'
        >>>
        
        """
        tagged_phrase = "{0}{1}{2}".format(start, phrase, end)
        return tagged_phrase
    
    def _doc_to_sentences(self):
        """Takes a string document and converts it into a list of sentences
        
        Unfortunately, this approach might be a tad naive for production
        because some segments that are split on a period are really an
        abbreviation, and to make things even more complicated, an
        abbreviation can also be the end of a sentence::
            http://nltk.googlecode.com/svn/trunk/doc/book/ch03.html
        
        Returns:
            (generator) A generator object of a tokenized sentence tuple,
            with the list position of sentence as the first portion of
            the tuple, such as:  (0, "This was the first sentence")
        
        """
        
        tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
        sentences = tokenizer.tokenize(self._document)
        for sentence in enumerate(sentences):
            yield sentence

    @staticmethod    
    def _score_sentences(sentence, querydict):
        """Creates a scoring system for each sentence by substitution analysis
        
        Tokenizes each sentence, counts characters
        in sentence, and pass it back as nested tuple
    
        Returns:
            (tuple) - (score (int), (count (int), position (int),
                   raw sentence (str))
            
        """
        
        position, sentence = sentence
        count = len(sentence)
        regex = re.compile('|'.join(map(re.escape, querydict)))
        score = len(re.findall(regex, sentence))
        processed_score = (score, (count, position, sentence))
        return processed_score
    
    def _querystring_to_dict(self, split_token="+"):
        """Converts query parameters into a dictionary
        
        Returns:
            (dict)- dparams, a dictionary of query parameters
            
        """
        
        params = self._query.split(split_token)
        dparams = dict([(key, self._custom_highlight_tag(key)) for\
                    key in params])
        return dparams
    
    @staticmethod
    def _word_frequency_sort(sentences):
        """Sorts sentences by score frequency, yields sorted result
        
        This will yield the highest score count items first.
        
        Args:
            sentences (list) - a nested tuple inside of list
            [(0, (90, 3, "The crust/dough was just way too effin' dry for me.
            Yes, I know what 'cornmeal' is, thanks."))]

        """

        sentences.sort()
        while sentences:
            yield sentences.pop()

    def _create_snippit(self, sentences, max_characters=175):
        """Creates a snippet from a sentence while keeping it under max_chars 
        
        Returns a sorted list with max characters.  The sort is an attempt
        to rebuild the original document structure as close as possible,
        with the new sorting by scoring and the limitation of max_chars.
        
        Args:
            sentences (generator) - sorted object to turn into a snippit
            max_characters (int) - optional max characters of snippit
           
        Returns:
            snippit (list) - returns a sorted list with a nested tuple that
            has the first index holding the original position of the list::
            
            [(0, (90, 3, "The crust/dough was just way too effin' dry for me.
            Yes, I know what 'cornmeal' is, thanks."))]
            
        """
        
        snippit = []
        total = 0
        for sentence in self._word_frequency_sort(sentences):
            LOG.debug("Creating snippit", sentence)
            score, (count, position, raw_sentence) = sentence
            total += count
            if total < max_characters:
                #position now gets converted to index 0 for sorting later
                snippit.append(((position), score, count, raw_sentence))
        
        #try to reassemble document by original order by doing a simple sort
        snippit.sort()
        return snippit
    
    @staticmethod
    def _multiple_string_replace(string_to_replace, dict_patterns):
        """Performs a multiple replace in a string with dict pattern.
        
        Borrowed from Python Cookbook.
        
        Args:
            string_to_replace (str) - String to be multi-replaced
            dict_patterns (dict) - A dict full of patterns
            
        Returns:
            (str) - Multiple replaced string.
        
        """
        
        regex = re.compile('|'.join(map(re.escape, dict_patterns)))
        def one_xlat(match):
            """Closure that is called repeatedly during multi-substitution.
            
            Args:
                match (SRE_Match object)
            Returns:
                partial string substitution (str)
            
            """
            
            return dict_patterns[match.group(0)]
        
        return regex.sub(one_xlat, string_to_replace)
    
    def _reconstruct_document_string(self, snippit, querydict):
        """Reconstructs string snippit, build tags, and return string
        
        A helper function for highlight_doc.
        
        Args:
            string_to_replace (list) - A list of nested tuples, containing
            this pattern::
            
            [(0, (90, 3, "The crust/dough was just way too effin' dry for me.
            Yes, I know what 'cornmeal' is, thanks."))]
            
            dict_patterns (dict) - A dict full of patterns
        
        Returns:
            (str) The most relevant snippet with the query terms highlighted.
        
        """
        
        snip = []
        for entry in snippit:
            score = entry[1]
            sent = entry[3]
            #if we have matches, now do the multi-replace
            if score:
                sent = self._multiple_string_replace(sent,
                                                    querydict)
            snip.append(sent)
        highlighted_snip = " ".join(snip)
        
        return highlighted_snip
        
    def highlight_doc(self):
        """Finds the most relevant snippit with the query terms highlighted
        
        Returns:
            (str) The most relevant snippet with the query terms highlighted.
        
        """
        
        #tokenize to sentences, and convert query to a dict
        sentences = self._doc_to_sentences()
        querydict = self._querystring_to_dict()
        
        #process and score sentences
         scored_sentences = []
        for sentence in sentences:
            scored = self._score_sentences(sentence, querydict)
            scored_sentences.append(scored)
        
        #fit into max characters, and sort by original position
        snippit = self._create_snippit(scored_sentences)
        #assemble back into string
        highlighted_snip = self._reconstruct_document_string(snippit,
                                                             querydict)

        return highlighted_snip


test_highlight.py
#/usr/bin/python
# -*- coding: utf-8 -*-
"""
Tests this query searches a document, highlights a snippit and returns it
http://www.example.com/search?find_desc=deep+dish+pizza&ns=1&rpp=10&find_loc=\
                                                        San+Francisco%2C+CA

Contains both unit and functional tests.

"""


import unittest
from highlight import HighlightDocumentOperations

class TestHighlight(unittest.TestCase):
    
    def setUp(self):
        
        self.document = """
Review for their take-out only.
Tried their large Classic (sausage, mushroom, peppers and onions) deep dish;\
and their large Pesto Chicken thin crust pizzas.
Pizza = I've had better.  The crust/dough was just way too effin' dry for me.\
Yes, I know what 'cornmeal' is, thanks.  But it's way too dry.\
I'm not talking about the bottom of the pizza...I'm talking about the dough \
that's in between the sauce and bottom of the pie...it was like cardboard, sorry!
Wings = spicy and good.   Bleu cheese dressing only...hmmm, but no alternative\
of ranch dressing, at all.  Service = friendly enough at the counters.  
Decor = freakin' dark.  I'm not sure how people can see their food.  
Parking = a real pain.  Good luck.        
        
        """
        self.query = "deep+dish+pizza"
        self.hdo = HighlightDocumentOperations(self.document, self.query)
        
    def test_custom_highlight_tag(self):
        
        actual = self.hdo._custom_highlight_tag("foo",
                                            start="[BAR]",
                                            end="[ENDBAR]")
        expected = "[BAR]foo[ENDBAR]"
        self.assertEqual(actual,expected)
    
    def test_query_string_to_dict(self):
        """Verifies the yielded results are what is expected"""
        
        result = self.hdo._querystring_to_dict()
        expected = {"deep": "deep",
                    "dish": "dish",
                    "pizza":"pizza"}
        
        self.assertEqual(result,expected)
    
    def test_multi_string_replace(self):
        
        query = """pizza = I've had better"""
        expected = """pizza = I've had better"""
        query_dict = self.hdo._querystring_to_dict()
        result = self.hdo._multiple_string_replace(query, query_dict)
        self.assertEqual(expected, result)
        
    def test_doc_to_sentences(self):
        """Consumes the generator, and then verifies the result[0]"""
        
        results = []
        expected = (0,'\nReview for their take-out only.')
        
        for sentence in self.hdo._doc_to_sentences():
            results.append(sentence)
        self.assertEqual(results[0], expected)
 
    def test_highlight(self):
        """Verifies highlighted text is what we expect"""
        
        expected = """Tried their large Classic (sausage, mushroom, peppers and onions)\
deep dish;and their large Pesto Chicken thin crust \
pizzas."""
 
        actual = self.hdo.highlight_doc()
        self.assertEqual(expected, actual)
    
    def tearDown(self):
        
        del self.query
        del self.hdo
        del self.document

if __name__ == '__main__':
    unittest.main()


test_functional_highlight.py
"""Functional Test That Performs Some Basic Sanity Checks"""

from highlight import HighlightDocumentOperations


def test_snippit_algorithm():
    document1 = """
        This place has awesome deep dish pizza.
        I have been getting delivery through Waiters on wheels for years.
        It is classic, deep dish  Chicago style pizza.
        Now I found out they also have half-baked to pick-up and cook at home.
        This is a great benefit. I am having it tonight. Yum.
        """
    document2 = """Review for their take-out only.
Tried their large Classic (sausage, mushroom, peppers and onions) deep dish;\
and their large Pesto Chicken thin crust pizzas.
Pizza = I've had better.  The crust/dough was just way too effin' dry for me.\
Yes, I know what 'cornmeal' is, thanks.  But it's way too dry.\
I'm not talking about the bottom of the pizza...I'm talking about the dough \
that's in between the sauce and bottom of the pie...it was like cardboard, sorry!
Wings = spicy and good.   Bleu cheese dressing only...hmmm, but no alternative\
of ranch dressing, at all.  Service = friendly enough at the counters.  
Decor = freakin' dark.  I'm not sure how people can see their food.  
Parking = a real pain.  Good luck."""
    
    h1 = HighlightDocumentOperations(document1, "deep+dish+pizza")
    actual = h1.highlight_doc()
    print "Raw Document1: %s" % document1
    print " Formatted Document1: %s" % actual
    assert  len(actual) < 500
    assert "<strong>" in actual

    h2 = HighlightDocumentOperations(document2, "deep+dish+pizza")
    actual = h2.highlight_doc()
    print "Raw Document2: %s" % document2
    print " Formatted Document2: %s" % actual
    assert  len(actual) < 500
    assert "<strong>" in actual

    
if __name__ == "__main__":
    test_snippit_algorithm()

위 코드 샘플에 관해, 이 코드를 실행하려면 자연어 툴킷(Natural Language Toolkit) 소스를 다운로드하고 지시사항에 따라 nltk 데이터를 다운로드해야 한다. 본 기사는 표시된 코드 샘플에 관한 것이 아니라 코드 샘플의 작성 방법과 코드 테스트 방법에 관한 것이므로, 해당 코드가 실제로 하는 작업을 자세히 설명하지는 않을 것이다. 그 대신, 소스 코드에서 정적 코드 분석 도구인 pylint를 실행하는 것으로 마무리하자.


Pylint
% pylint highlight spy 
No config file found, using default configuration
************* Module highlight
E: 89:HighlightDocumentOperations._doc_to_sentences: Instance of 'unicode' has no 
    'tokenize' member (but some types could not be inferred)
E: 89:HighlightDocumentOperations._doc_to_sentences: Instance of 'ContextFreeGrammar' 
    has no 'tokenize' member (but some types could not be inferred)
W:108:HighlightDocumentOperations._score_sentences: Used builtin function 'map'
W:192:HighlightDocumentOperations._multiple_string_replace: Used builtin function 'map'
R: 34:HighlightDocumentOperations: Too few public methods (1/2)

Report
======
69 statements analysed.

Global evaluation
-----------------
Your code has been rated at 8.12/10 (previous run: 8.12/10)

이 코드는 10점 중 8.12점을 받았으며 몇몇 항목 때문에 점수가 깎였다. Pylint는 구성 가능하므로, 아마 프로젝트에 관한 요구를 충족시키려면 pylint를 구성해야 할 것이다. 공식 pylint 문서를 참조할 수 있다(참고자료 참조). 이 특정 예제의 경우, 외부 라이브러리 nltk로 인해 발생할 수 있는 두 개의 오류가 89행에 있고, pylint에 대한 구성 변경에 의해 변경될 수 있었던 두 개의 경고가 있다. 일반적으로, 소스 코드에서 pylint 오류를 절대 허용하고 싶지 않겠지만, 때로는 위 예제에서와 같이 실제로 수행되는 의사 결정을 해야 할 때도 있다. Pylint가 완벽한 도구는 아니지만, 필자는 pylint가 실제 환경에서 매우 유용하다는 점을 깨달았다.

결론

본 기사에서는 단지 테스트를 염두에 둔다는 것이 소프트웨어 구조에 어떤 영향을 미치고 테스트에 대한 고려가 부족할 때 프로젝트에 얼마나 치명적인 해를 끼칠 수 있는지 살펴보았다. 우리는 기능 및 장치 테스트를 모두 포함하고 nose를 이용한 코드 검사 분석과 두 가지 정적 분석 도구, 즉 pylint와 pygenie에 대해 테스트를 실행한 완성된 코드 예제를 검토했다. 이 기사에서 다루기에는 지면이 부족했던 한 가지는 지속적 통합 테스트의 형태로 이런 테스트를 자동화하는 방법에 관한 내용이었다. 다행히도, 이런 자동화는 오픈 소스 Java™ Continuous Integration System인 Hudson을 사용하면 꽤 간단히 구현할 수 있다. Hudson 문서를 읽어보고(참고자료 참조) 정적 코드 분석을 포함한 모든 테스트를 실행하는 프로젝트를 위한 자동 테스트를 설정하는 방법을 여러 가지로 시험해보면 많은 도움이 될 것이다.

마지막으로, 테스트가 만병통치약은 아니며, 정적 분석 도구도 마찬가지다. 소프트웨어 개발은 힘든 작업이다. 성공 가능성을 높이려면 항상 실제 목표가 무엇인지 잊지 말아야 한다. 문제 해결만이 아니라, 개발한 소프트웨어가 올바로 작동하는지 입증할 수 있는 것을 고안하는 것도 염두에 두어야 한다. 이 전제에 동의한다면 곧 지나치게 복잡한 코드, 사용자를 무시한 거만한 디자인, Python의 능력을 평가절하하는 태도가 이 목표에 직접적인 방해가 된다는 점을 인정한다는 의미이다.

본 기사에 대한 기술 검토를 해주신 Imagemovers Digital의 Kennedy Behrman에게 감사한 마음을 전한다.



다운로드 하십시오

설명이름크기다운로드 방식
Zip fileclean_code_sample.zip5.4KBHTTP

다운로드 방식에 대한 정보


참고자료

교육

토론

필자소개

Photo of Noah Gift

Noah Gift is the co-author of Python For UNIX and Linux System Administration by O'Reilly, and is also working on Google App Engine In Action for Manning. He is an author, speaker, consultant, and community leader, writing for publications such as Red Hat Magazine, O'Reilly, and MacTech. His consulting company's website is http://www.giftcs.com, and much of his writing can be found at http://noahgift.com. You can also follow Noah on Twitter.

He has a Master's degree in CIS from Cal State Los Angeles and a B.S. in Nutritional Science from Cal Poly San Luis Obispo. He is an Apple- and LPI-certified sysadmin, and has worked at companies such as Caltech, Disney Feature Animation, Sony Imageworks, Turner Studios, and Weta Digital. In his free time, he enjoys spending time with his wife, Leah, and their son, Liam, composing for the piano, running marathons, and exercising religiously.

잘못된 도움말 신고

부정사용 신고

감사합니다. 이 항목은 운영자가 관심을 표시했습니다.


잘못된 도움말 신고

부정사용 신고

제출실패 신고. 나중에 다시 실행해주세요.


디벨로퍼웍스 로그인


IBM ID가 필요하세요?
IBM ID를 잊으셨습니까?


비밀번호를 잊으셨습니까?
비밀번호 변경

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

화면상에 보여지는 닉네임을 정하세요.

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

3개의 &이나 대쉬를 포함해주시고 31글자내로 제한해주세요.


developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


아티클 순위

의견

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=20
Zone=AIX와 UNIX
ArticleID=623286
ArticleTitle=Python으로 간결하고 테스트 가능하며 수준 높은 코드 쓰기
publish-date=09282010
author1-email=noah.gift@giftcs.com
author1-email-cc=

태그

Help
검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오.

태그를 더 많이 보거나 적게 보기 위해 슬라이더 막대를 사용하십시오.

인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다.

내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.

검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오. 인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다. 내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.