소프트웨어 작성은 인간이 시도할 수 있는 가장 복잡하고 어려운 시도 중 하나이다. 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 |
예제에서 알 수 있듯이, 모든 메소드가 극히 간단하고 복잡도도 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 file | clean_code_sample.zip | 5.4KB | HTTP |
교육
- "In pursuit of
code quality: Monitoring cyclomatic complexity"(Andrew Glover, developerWorks, 2006년 3월): 간단한 코드 지표와 Java 기반 도구를 사용하여
사이클로매틱 복잡도를 모니터하는 방법을 알아보자.
-
Expert Python
programming book
-
Foundations of Agile Python development book
-
Python
testing: Beginner's guide book
-
What killed smalltalk could
kill Ruby, too
-
Automating
software testing using program analysis
-
Software Tools
book
-
Hudson continuous integration
-
Quotations on simplicity of design
-
PEP 8: Python style guide
-
Python style guide checker
-
Measuring
cyclomatic complexity of Python code
-
Pylint: Static code analysis tool
-
Clone digger: Duplicate code detection
utility
-
Microsoft
research case study on test driven development
-
Hudson violations plugins
-
Pymetrics Python complexity
rating tool
-
Test driven development
-
Nose extends unittest to make testing easier
-
Ned Batchelder's coverage module
-
Idomatic Python
-
Intermediate and advanced
software carpentry
-
Twill source code, an
example of "clean" code
-
Python cookbook
-
Cyclomatic complexity study by Enerjy
-
The
magic number plus or minus two
-
Pythoscope unittest generator
-
Natural Language Toolkit
토론
- Twitter의 developerWorks 페이지를 살펴보자.
- My developerWorks 커뮤니티에 참여하자.
-
다음과 같은 AIX 및 UNIX 포럼에 참여하자.
- AIX Forum
- AIX Forum for developers
- Cluster Systems Management
- IBM Support Assistant Forum
- Performance Tools Forum
- Virtualization Forum
- 기타 AIX and UNIX Forums

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.