프로젝트 구성하기

여기서 “구성”이란 어떻게 하면 목표한 바에 가장 부합하도록 프로젝트를 수행하기 위한 의사결정을 의미합니다. 우리는 깔끔하고 효율적인 코드라는 파이썬의 특성을 극대화 할 수 있는 방법을 고민할 필요가 있습니다. 일반적으로 “구성”이란 로직과 의존성이 깔끔한 코드를 만드는 것을 의미할 뿐만 아니라, 파일시스템에 어떻게 파일과 폴더들을 구성하느냐의 문제입니다.

어느 모듈에 어느 기능이 들어가야 할까요? 프로젝트에서 데이터는 어덯게 흘러가야 할까요? 어떤 특징과 기능이 통합되거나 분리되어야 할까요? 이러한 질문에 답함으로써 프로젝트의 계획을 시작할 수 있고, 더 나아가 최종적인 제품이 어떤 모습일지를 그릴 수 있습니다.

이 섹션에서 우리는 파이썬의 모듈과 임포트 시스템을 자세히 살펴볼 것입니다. 이 2가지가 프로젝트를 위한 강력한 구조를 만들기 위한 핵심적인 요소이기 때문입니다. 그런 다음, 확장하기 쉽고 확실하게 테스트 할 수 있는 코드를 짜기 위한 방법에 관하여 다양환 관점에서 검토할 것입니다.

저장소의 구조

중요합니다.

코딩 스타일, API 디자인, 자동화처럼 건강한 개발 사이클에 필수적인 요소들처럼, 저장소의 구조도 프로젝트의 아키텍처 에 결정적인 요소입니다.

당신이 만든 프로젝트의 저장소 웹페이지에 처음으로 상륙한 잠재적 사용자나 공헌자는 이런 것들을 보게 될 것입니다.

  • 프로젝트 이름

  • 프로젝트 설명

  • 파일들 한무더기

스크롤을 내리고 내리고 나서야 프로젝트의 README 파일을 발견할 수 있을 것입니다.

당신의 저장소가 엄청나게 많은 파일들과 디렉토리로 꼬여있다면, 방문자들은 금방 떠나버리고 말 것입니다. 아무리 아름다운 문서가 있다고 해도 말입니다.

원하는 바가 있다면 바로 그 원하는 바인 것처럼 행동하라.

물론 첫인상이 전부라는 말은 아닙니다. 당신과 동료들은 저장소에 올려진 프로젝트를 위해 수없이 많은 날들을 지세웠을 것입니다. 그러다보니 어느새 저장소의 구석구석에 이미 친숙해져있을 것입니다. 그래도 모양새는 중요합니다.

샘플 저장소

3줄 요약 필수(tl;dr): Kenneth Reitz 의 말입니다.

이 저장소는 깃허브에서 볼 수 있습니다.

README.rst
LICENSE
setup.py
requirements.txt
sample/__init__.py
sample/core.py
sample/helpers.py
docs/conf.py
docs/index.rst
tests/test_basic.py
tests/test_advanced.py

좀 더 자세히 봅시다.

실제 모듈

위치

./sample/ or ./sample.py

목적

관심있는 코드

모듈 패키지는 저장소의 핵심입니다. 숨겨놓으면 안됩니다:

./sample/

모듈 안에 단 하나의 파일밖에 없다면, 저장소의 최상위 폴더에 바로 두어도 좋습니다.

./sample.py

라이브러리를 애매한 소스나 파이썬 하위 디렉토리에 두면 안됩니다.

라이선스

위치

./LICENSE

목적

법적 공방

소스 코드를 외에 프로젝트 저장소의 가장 중요한 부분입니다. 라이선스 문서 전문과 저작권이 반드시 파일에 들어가야 합니다.

어떤 라이선스를 써야할지 모르겠다면 choosealicense.com 를 확인하세요.

라이선스 없이 코드가 배포되어도 상관없을 수도 있습니다. 하지만 그랬다가는 사람들이 당신의 코드를 사용하기를 꺼릴 것입니다.

Setup.py

위치

./setup.py

목적

패키지와 배포 관리

모듈 패키지가 저장소의 최상위 폴더에 있다면, Setup.py도 최상위 폴더에 두어야 합니다.

필요한 파일

위치

./requirements.txt

목적

개발 의존성

pip 필수 파일 은 반드시 저장소의 최상위 폴더에 두어야 합니다. 이 파일은 프로젝트에 기여하기 위한 작업들, 즉 테스트, 빌드, 문서 생성을 위해 필요한 의존성에 대하여 명시해야 합니다.

프로젝트에 개발 의존성이 없거나, 아니면 setup.py 를 통해 개발 환경을 설치하는 편을 좋아한다면 이 파일은 불필요합니다.

문서

위치

./docs/

목적

패키지 참조 문서

다른 경로에 문서 파일을 둘 이유가 없습니다.

테스트 도구

위치

./test_sample.py or ./tests

목적

패키지 통합 테스트와 단위 테스트

프로젝트를 시작할 때, 보통 하나의 파일에 간단한 테스트 도구를 만들 것입니다.

./test_sample.py

테스트 도구가 커지면 이런 폴더를 만들어서 테스트 파일을 옮기면 됩니다.

tests/test_basic.py
tests/test_advanced.py

물론 이런 테스트 모듈들은 프로젝트의 패키징된 모듈들을 임포트해서 테스트를 수행합니다. 테스트를 수행하는데에는 몇 가지 방법이 있습니다.

  • site-packages에 패키지를 설치하게 한다.

  • 패키지 경로를 잘 찾기 위해 간단한(하지만 명확한) 경로를 사용한다.

후자를 강력 추천합니다. 계속 변경되고 있는 코드를 테스트하기 위해 setup.py 를 실행해야만 한다면, 변경 중인 코드의 각 시점마다 독립된 환경 설정을 해야만 합니다.

각각의 테스트마다 별도의 임포트 설정을 주고 싶다면 tests/context.py 파일을 이렇게 작성하세요.

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import sample

그 다음, 각각의 테스트 모듈마다 이렇게 모듈을 임포트 합니다.

from .context import sample

이렇게 하면 설치 방법과 상관없이 항상 생각한 대로 동작할 것입니다.

어떤 사람들은 테스트 파일을 반드시 테스트 하려는 모듈 자체에 넣고 배포를 해야한다고 주장합니다. 하지만 저는 이에 동의하지 않습니다. 그렇게 하면 사용자 입장에서 볼 때 프로젝트의 모듈이 복잡해집니다. 그리고 추가적인 의존성 관계와 실행 환경이 필요한 테스트 도구가 많기 때문입니다.

Makefile

위치

./Makefile

목적

전반적인 관리 작업

Makefile은 지금 이 프로젝트와 다른 Pocoo 프로젝트에도 항상 있습니다. 왜 그럴까요? 이 프로젝트들은 C로 작성되지 않았기 때문입니다... 즉 make는 당신의 프로젝트를 관리하는 작업들을 알려주는 엄청나게 유용한 도구입니다.

Sample Makefile:

init:
    pip install -r requirements.txt

test:
    py.test tests

.PHONY: init test

다른 관리 작업 스크립트(manage.py 또는 fabfile.py) 들도 Makefile과 마찬가지로 저장소의 최상위 디렉토리에 두세요.

장고 어플리케이션에 대하여

장고 1.4 버전의 릴리즈 이후로 장고 어플리케이션에 새로운 유행이 불고 있습니다. 많은 개발자들이 자신들의 저장소를 지저분하게 구성하고 있습니다. 1.4 버전에 포함된 어플리케이션 템플릿 때문입니다.

어떻게 지저분하게 하길래? 음, 이런 개발자들은 자신의 정리 안 된 저장소에서 대체로 이런 명령을 실행합니다.

$ django-admin.py startproject samplesite

그 결과로 뽑히는 저장소의 구조는 이렇습니다:

README.rst
samplesite/manage.py
samplesite/samplesite/settings.py
samplesite/samplesite/wsgi.py
samplesite/samplesite/sampleapp/models.py

이렇게 하지 마세요.

상위 폴더와 하위 폴더의 이름이 같으니 개발 도구도 혼란스러워하고 개발자들도 혼란스러워하게 됩니다. 불필요한 폴더 감싸기는 (SVN 저장소를 그리워하는게 아니라면)도움이 안됩니다.

대신 이렇게 하면 좋습니다.

$ django-admin.py startproject samplesite .

여기에 주목하세요 “.”.

결과물은 이렇습니다:

README.rst
manage.py
samplesite/settings.py
samplesite/wsgi.py
samplesite/sampleapp/models.py

코드 구성이 핵심

파이썬의 임포트 방식과 잘 만들어진 모듈 덕분에 파이썬 프로젝트의 구성은 비교적 쉽습니다. 여기서 쉽다는 말은 제약이 많지 않고, 모듈을 불러오는 방식이 이해하기 쉽다는 뜻입니다. 따라서 프로젝트의 서로 다른 파트와 그 사이의 상호작용을 다듬는 아키텍처 작업만을 하면 됩니다.

프로젝트를 구성하기 쉽다는 말은 또한 엉망이 되기도 쉽다는 뜻입니다. 프로젝트가 엉망으로 구성되었음을 알 수 있는 신호를 몇 가지 찾아보았습니다.

  • 복잡한 순환 참조: 책상.누가 만들었나() 같은 질문에 답하려면 가구.py 의 탁자와 의자가 작업자.py 에서 목수를 불러와야 하는데, 반대로 목수.뭐하니() 같은 질문에 답하려면 목수가 책상과 의자를 불러와야만 하는 경우가 있습니다. 이것이 순환 참조입니다. 이런 경우에는 메소드나 펑션 안에 있는 임포트 구문을 사용하는 것 같은 난도질스러운 방법에 의존하는 수 밖에 없습니다.

  • 숨겨진 연결고리: 책상을 구현하는 코드의 모든 변경이 책상과 관련없는 다른 테스트들을 안돌아가게 합니다. 책상의 코드 변경이 목수의 코드를 무너뜨리기 때문입니다. 이럴 때 코드 변경을 하려면 아주 신중하게 수술을 해야합니다. 이 말인즉, 목수의 코드 안에 있는 책상의 코드를 변경하거나 그 반대의 경우에 아주 많은 경우의 수를 상정해야만 한다는 뜻입니다.

  • 전역 구문의 과다 사용: (높이, 너비, 타입, 나무) 같은 말로 대상을 명확하게 정의하지 않는 경우입니다. 목수가 언제든 아무나 바꿀 수 있는 전역 변수를 사용하기 시작하면, 원형 책상이 사각 책상으로 되어버리는 경우가 생깁니다. 어쩌다가 이런 일이 벌어졌는지를 조사하기 위해 꼼꼼히 살피다 보면, 엉뚱한 샘플 코드가 변수를 변경하고 책상의 모양을 바꾸고 있다는 사실을 발견할 것입니다.

  • 스파게티 코드: 복사 & 붙여넣기가 몇 페이지나 계속되고, 그 안에서 if조건문과 for반복문이 어지럽게 반복되며, 적절하게 나눠지지도 않은 코드를 스파게티 코드라고 합니다. 파이썬의 (가장 논란이 분분한 특징일)들여쓰기에는 의미가 있기 때문에 이런 복잡하고 어지러운 코드는 유지 보수하기가 어렵습니다. 다행히 이런 난장판을 많이 볼 일은 없을 것입니다.

  • 라비올리 코드: 파이썬에는 스파게티 코드보다도 라비올리 코드를 더 자주 볼 수 있습니다. 구조화도 되어있지 않으면서 수 백개의 비슷한 로직이나 작은 클래스, 또는 오브젝트로 이루어져 있는 코드를 라비올리 코드라고 합니다. 식탁을 써야하는지, 탁자를 써야하는지, 그도 아니면 새 책상이 필요한건지 도무지 기억할 수 없는 순간, 당신은 라비올리 코드 속에서 헤엄치고 있을 것입니다.

모듈

파이썬 모듈은 사용 가능한 주요 추상 레이어 중 하나이자, 추상 레이어의 가장 자연스러운 모습입니다. 추상 레이어는 코드를 기능 파트와 데이터 저장 파트로 나눌 수 있도록 해줍니다.

예를 들어 프로젝트의 레이어 중 하나는 사용자 인터페이스를 담당하고, 다른 하나는 저수준의 데이터 처리를 담당할 수도 있다. 이 두 레이어를 분리시키는 가장 자연스러운 방법은 인터페이스 기능과 저수준의 데이터 처리를 담당하는 기능을 각각의 파일에 넣는 것입니다. 이런 경우에 인터페이스 파일은 데이터 처리 파일을 불러와야 합니다. 이는 importfrom ... import 구문으로 가능합니다.

import 구문을 사용하는 즉시 해당 모듈을 불러올 수 있습니다. 이는 내장 모듈인`os`, sys, 프로젝트에 포함시킨 모듈과 따로 설치한 서드파티 모듈도 마찬가지입니다.

이 책의 스타일 안내서처럼 하려면, 모듈의 이름은 소문자로 짧게 짓고, 점(.)이나 물음표(?) 같은 특수 문자의 사용을 지양해야 합니다. 그러니까 my.spam.py 같은 이름으로 만들면 안됩니다! 파이썬이 모듈을 찾는 걸 방해할겁니다.

my.spam.py 의 경우, 파이썬은 my 라는 이름의 폴더에서 spam.py 이라는 이름의 파일을 찾습니다. 이렇게 하면 안됩니다. 아래 글을 참조하세요. 파이썬 문서에서 점(.) 표시가 어떻게 사용되는지에 대한 예시

my_spam.py 처럼 모듈 이름을 짓는 것도 가능합니다. 하지만 우리의 친구 밑줄(_)은 모듈 이름으로는 자주 쓰이지 않습니다.

몇 가지 이름 짓기 규칙 외에는 파이썬 모듈을 만들기 위해 달리 특별히 필요한 것은 없습니다. 하지만 모듈이라는 개념을 잘 사용하고 문제를 발생시키지 않기 위해서는, 모듈을 불러오는 방법을 이해할 필요가 있습니다.

구체적으로 살펴봅시다. import modu 구문이 있으면 이는 호출자로서 같은 디렉토리 안에서 적절한 파일, 즉 modu.py 라는 파일을 찾습니다. 만약 해당 파일이 발견되지 않는다면 파이썬 인터프리터는 modu.py 파일을 “path” 에서 재귀적으로 찾습니다. 그래도 발견되지 않으면 ImportError exception을 띄웁니다.

일단 modu.py 이 발견되면, 파이썬 인터프리터는 독립적으로 그 모듈을 실행합니다. modu.py 모듈 안의 모든 최상위 구문이 실행됩니다. 불러온 다른 모듈이 있다면 그것도 함께 실행됩니다. 함수와 클래스 정의는 모듈의 디렉토리에 저장됩니다.

그런 다음에야 모듈의 변수와 함수, 그리고 클래스를 사용할 수 있게 됩니다. 여기서 호출자는 모듈의 네임스페이스(namespace)를 통해 이것들을 사용합니다. 모듈의 네임스페이스(namespace)는 아주 유용하고 강력하게 사용되는 파이썬 프로그래밍의 핵심 개념입니다.

많은 언어에서 include file 지시문은 전처리장치(preprocessor)가 해당 파일의 모든 코드를 가져와 호출자에 ‘복사’해 넣는 명령어로 사용됩니다. 하지만 파이썬에서는 다릅니다. 모듈에 포함된 코드는 네임스페이스(namespace)에서 독립적으로 실행됩니다. 이는 일반적으로 모듈에 포함된 코드가 오작동을 하는 경우가 없다는 뜻입니다.

모듈을 불러오는 특별한 구문인 from modu import * 를 써서 파이썬이 어떻게 작동하는지 확인해봅시다. 일반적으로 이러한 구문은 잘못된 습관입니다. Using import * ** 구문은 코드를 읽기 어렵게 만들고, 코드의 독립성 여부를 판단하기 어렵게 합니다**.

from modu import func 구문은 모듈에서 딱 내가 원하는 함수만 불러와 전역 네임스페이스(global namespace)에 넣어둘 수 있는 좋은 방법입니다. 뿐만 아니라 이 구문은 전역 네임스페이스(global namespace)에서 무엇을 불러올지를 명확히 보여주기 때문에 import * 구문보다 덜 해롭습다. 단순히 import * 구문을 사용하는 것은 단지 타이핑을 덜 한다는 이점 뿐입니다.

Very bad

[...]
from modu import *
[...]
x = sqrt(4)  # Is sqrt part of modu? A builtin? Defined above?

Better

from modu import sqrt
[...]
x = sqrt(4)  # sqrt may be part of modu, if not redefined in between

Best

import modu
[...]
x = modu.sqrt(4)  # sqrt is visibly part of modu's namespace

코드 스타일 섹션에서 언급한 것처럼, 가독성은 파이썬의 주요 특징 중 하나입니다. 가독성이란 쓸데없이 긴 본문과 잡동사니들을 피한다는 뜻입니다. 그러므로 어느 수준까지는 간결성을 얻고자 하는 노력이 필요합니다. 그러나 너무 간결한 나머지 모호해서는 안됩니다. modu.func 처럼 클래스와 함수가 어디에서 왔는지 즉각 알려주는 구문은 코드의 가독성과 이해용이성을 크게 높여줍니다. 단 하나의 파일만 있는 아주 간단한 프로젝트가 아닌 한 그렇습니다.

패키지

파이썬은 아주 간단한 패키지 만들기 시스템을 제공합니다. 이 시스템은 단순히 파이썬 모듈 구조를 디렉토리로 확장한 것입니다.

__init__.py 가 있는 모든 디렉토리는 파이썬 패키지로 인식됩니다. 패키지의 여러 다른 모듈들은 일반적인 모듈과 비슷한 방법으로 불러와집니다. 그러나 패키지는 일반 모듈과는 달리 __init__.py 가 있다는 점이 특별합니다.이 파일은 패키지 전체의 모든 정의를 모아두는 용도로 쓰입니다.

pack/ 디렉토리의 modu.py 파일은 import pack.modu 구문으로 불러와집니다. 이 구문은 pack 에서 __init__.py 파일을 찾습니다. 그리고 해당 패키지의 모든 상위 구문을 실행합니다. 이 작업 후에 modu.py 파일에 정의된 모든 변수나 함수, 클래스를 pack.modu 네임스페이스(namespace)에서 쓸 수 있습니다.

흔히 보는 문제는 __init__.py 파일에 너무 많은 코드를 붙이는 경우입니다. 프로젝트가 복잡해질수록 디렉토리 구조 깊숙히 서브-패키지와 서브-서브-패키지가 있을 수 있습니다. 이런 경우에는 서브-서브-패키지 안에 있는 단 한 줄을 불러오려고 했더니 패키지 디렉토리를 가로지르며 만나게 되는 모든 __init__.py 파일을 실행하게 될 때가 있습니다.

따라서 패키지의 모듈과 서브-패키지에서 코드를 서로 공유할 필요가 없다면 __init__.py 은 빈 파일로 남겨두는게 일반적일 뿐만 아니라 좋은 습관입니다.

마지막으로 패키지 안쪽 깊숙히를 불러올 수 있는 편리한 구문이 있습니다. import very.deep.module as mod 입니다. 이 구문은 ``very.deep.module``같은 장황한 구문을 반복하는 대신에 mod 만으로 패키지를 사용할 수 있게 해줍니다.

객체지향 프로그래밍

파이썬은 종종 객체지향 프로그래밍 언어로 묘사됩니다. 이는 다소 오해의 여지가 있기에 명확히 할 필요가 있습니다.

파이썬에서 모든 것은 객체입니다. 그리고 객체처럼 다룰 수 있습니다. 이 말인즉 함수는 일급 객체라고 말하는 것과 같은 의미입니다. 클래스, 문자열, 심지어 타입도 파이썬에서는 객체입니다. 다른 객체처럼 파이썬의 객체도 타입이 있고 함수 인자값을 받을 수 있습니다. 메소드와 속성을 가질 수도 있습니다. 이러한 점을 고려하여 파이썬을 객체지향 언어라고 하는 것입니다.

하지만 자바와는 달리 파이썬은 객체지향 프로그래밍을 프로그래밍 패러다임으로 도입하고 있지 않습니다. 객체지향으로 만들지 않은 파이썬 프로젝트도 완벽하게 실행 가능합니다. 즉 클래스를 정의하지 않거나 거의 정의하지 않아도 사용할 수 있습니다. 클래스 상속이나 객체지향 프로그래밍의 다른 특징적인 방법을 사용하지 않아도 마찬가지입니다.

게다가 modules 섹션에서 볼 수 있듯이 파이썬이 모듈과 네임스페이스(namespace)를 다루는 방식은 개발자로 하여금 자연스럽게 캡슐화와 추상 레이어의 분리를 가능하게 해줍니다. 캡슐화와 추상 레이어의 분리는 둘 다 객체지향을 사용하는 가장 일반적인 이유입니다. 그러므로 파이썬 프로그래머는 비지니스 모델이 굳이 객체지향을 필요로 하지 않는다면 거기에 얽메이지 않아도 됩니다.

불필요한 객체지향을 피해야 할 이유는 많습니다. 어떤 상태와 기능은 한데 모아두는 편이 더 유용할 때가 있습니다. 이럴 때는 사용자 정의 클래스를 정의해서 쓰면 편리합니다. 문제는 함수형 프로그래밍의 문제점으로 지적되는 것처럼 “상태” 를 대입할 때 발생합니다.

어떤 아키텍쳐, 보통 웹 어플리케이션에서는 파이썬 프로세스에 복수의 인스턴스가 발생합니다. 동시에 발생하는 외부의 요청에 응답하기 위해서입니다. 이런 경우에는 구문의 실행이 멈춰져 예정된 오브젝트가 됩니다. 즉 어떤 고정된 정보를 계속 잡고 있는 것입니다. 이때문에 동시성의 문제나 경합 상황이 발생하기 쉽습니다.

이 문제 뿐만 아니라 다른 설명하지 않은 문제들이 발생하기도 합니다. 이 때문에 상태없는 함수가 더 나은 프로그래밍 패러다임으로 생각되기도 한다.

위 문제를 피하기 위한 또다른 방법은 가능한 한 모호한 문맥과 부작용을 최소화 한 함수와 프로시저를 사용하는 것입니다. 전역 변수를 아무렇게나 사용하거나, 함수 내부에 있는 퍼시스턴스 레이어의 항목을 남용하면 함수의 의미가 모호해집니다. 여기서 부작용이란 함수가 함수를 둘러싼 맥락을 바꿔버리는 변화를 의미합니다. 만약 함수가 퍼시스턴스 레이어에 있는 데이터나 전역 변수에 있는 데이터를 날려버리거나 저장해 버린다면, 이런 것을 부작용이라 부릅니다.

함수가 잘 분리되어 있고 부작용도 없는 순수 함수라면 다음과 같은 장점이 있습니다.

  • 순수 함수는 결정성이 있습니다. 즉, 같은 입력값이 있으면 언제나 같은 출력값을 냅니다.

  • 순수 함수는 수정하거나 최적화 작업이 필요할 때 쉽게 수정 및 변경할 수 있습니다.

  • 순수 함수는 단위 테스트를 하기 쉽습니다. 즉, 복잡한 테스트 셋업 작업과 데이터 삭제 작업에 손이 덜 갑니다.

  • 순수 함수는 다루기 쉽고, 기능을 더하기도 쉽고, 없애기도 쉽습니다.

한마디로, 많은 아키텍처에서 순수한 함수는 가장 효율적인 건축 자재입니다. 클래스나 오브젝트보다 낫습니다. 순수한함수는 어떤 컨텍스트나 부작용도 없기 때문입니다.

분명히 객체지향은 유용합니다. 그리고 꼭 필요한 경우가 많습니다. 예를 들면 데스크톱 어플리케이션이라든가, 조작 가능한 창, 버튼, 아바타, 탈것이 있는 게임같은 경우에 그렇습니다. 이런 것들은 컴퓨터의 메모리에 비교적 오랫동안 남아있기 때문입니다.

데코레이터

파이썬 언어에는 데코레이터라는 이름의 단순하지만 강력한 문법이 있습니다. 데코레이터는 함수나 메소드를 감싸는(데코레이트 하는) 함수나 클래스입니다. ‘데코레이트 된’ 함수나 메소드는 원래의 ‘데코레이트 되지 않은’ 함수나 메소드를 대체합니다. 함수는 파이썬에서 일급 객체이기 때문입니다. 이 작업은 ‘수작업’으로도 가능하지만 @decorator 문법을 쓰면 깔끔하게 쓸 수 있습니다. 그래서 많이들 씁니다.

def foo():
    # do something

def decorator(func):
    # manipulate func
    return func

foo = decorator(foo)  # Manually decorate

@decorator
def bar():
    # Do something
# bar() is decorated

이 방법은 외부의 관련없는 로직이 함수나 메소드의 핵심 로직을 오염시키는 사태를 피할 수 있기 때문에 유용합니다. 데코레이션을 써서 다루는 편이 좋은 기능으로 예를 들면 `메모이제이션<https://en.wikipedia.org/wiki/Memoization#Overview>`__ 과 캐싱을 들 수 있습니다. 무거운 함수의 결과값을 테이블에 저장한 다음, 이미 처리가 끝난 그 결과값을 재처리 없이 바로 쓸 수 있도록 하는 것입니다. 물론 이런 것은 함수 자체의 로직과는 별개입니다.

컨텍스트 관리자

컨텍스트 관리자는 동작에 추가적인 컨텍스트 정보를 추가하는 파이썬 오브젝트입니다. 이 추가 정보는 with 구문으로 컨텍스트를 새로 열면서 함수를 실행하는 형태로 만들어집니다. 마찬가지로 with 블럭 안의 모든 코드를 종료시킬 때도 함수를 실행합니다. 컨텍스트 관리자를 사용하는 가장 유명한 예시는 이렇습니다. 파일을 열고:

with open('file.txt') as f:
    contents = f.read()

이런 식의 패턴에 익숙한 사람은 이런 방식으로 open 을 실행하면 fclose 메소드가 언젠가는 반드시 실행됨을 잘 알 것입니다.

이런 기능을 직접 구현하는 2가지 쉬운 방법이 있습니다: 클래스를 사용하거나, 생성자를 사용하는 것입니다. 먼저 클래스를 사용하는 방법으로 위의 기능을 직접 구현해 봅시다.

class CustomOpen(object):
    def __init__(self, filename):
      self.file = open(filename)

    def __enter__(self):
        return self.file

    def __exit__(self, ctx_type, ctx_value, ctx_traceback):
        self.file.close()

with CustomOpen('file') as f:
    contents = f.read()

위 예시는 with 에서 사용될 2개의 추가 메소드가 있는 일반적인 파이썬 오브젝트입니다. CustomOpen 은 먼저 인스턴스화되고, 그 다음에 __enter__ 메소드가 호출됩니다. 그리고 __enter__ 함수가 리턴하는 as f 구문 안의 모든 결과값들이 f 에 할당됩니다. with 블럭 안의 구문들이 모두 수행된 후에는 __exit__ 메소드가 호출됩니다.

그러면 이번에는 파이썬의 contextlib 를 써서 생성자로 위의 기능을 구현해봅시다:

from contextlib import contextmanager

@contextmanager
def custom_open(filename):
    f = open(filename)
    try:
        yield f
    finally:
        f.close()

with custom_open('file') as f:
    contents = f.read()

이렇게 해도 위의 클래스 예시와 똑같이 동작합니다. 물론 이 방법이 더 간결합니다. custom_open 함수는 yield 구문에 닿을 때까지 실행됩니다. 그 다음 with 구문에게 제어권을 넘깁니다. as f 안에서 어떤 yieldf 에 할당되든 상관없습니다. finally 명령은 with 안에서 예외현상이 발생하든 말든 반드시 close() 를 실행합니다.

둘 중 어느 방법을 사용하든 결과는 마찬가지이기 때문에, 우리는 파이썬 선(禪)에 따라 어떤 때 어느 것을 쓸지 선택해야 합니다. 클래스를 쓰는 방법은 캡슐화해야하는 대량의 로직이 있을 때 좋습니다. 생성자를 쓰는 방법은 간단한 동작을 다룰 때 좋은 방법입니다.

동적 타이핑

파이썬은 동적 타이핑이 되는 언어라고 불립니다. 이 말은 변수가 고정된 타입을 가지고 있지 않다는 뜻입니다. 사실 파이썬의 변수는 다른 수많은 언어, 특히 정적 타입 언어의 변수와는 아주 다릅니다. 파이썬의 변수는 어떤 값이 쓰여지면 ‘태그’나 ‘이름’이 그 객체를 가리키는 컴퓨터 메모리의 한 조각이 아닙니다. 그렇기 때문에 변수 ‘a’가 값 1을 가진 다음, 값 ‘a string’을 가지고, 또 함수가 될 수도 있는 것입니다.

동적 타이핑은 종종 파이썬의 약점으로 여겨지기도 합니다. 실제로 동적 타이핑은 코드를 복잡하게 만들고 디버깅하기 어렵게 만듭니다. ‘a’라는 이름이 너무나 많은 것이 될 수 있기 때문에 개발자나 유지보수 담당자는 이 이름이 어떻게 쓰이는지, 문제와 무관한 객체인지를 확인하기 위해 코드 전체를 뒤져야 합니다.

이러한 문제를 피하기 위한 가이드라인을 소개합니다.

  • 다른 것에 같은 변수명을 붙이지 마라.

나쁜 예

a = 1
a = 'a string'
def a():
    pass  # Do something

좋은 예

count = 1
msg = 'a string'
def func():
    pass  # Do something

메소드나 함수를 짧게 쓰는 방법은 같은 이름을 상관없는 것들에게 붙이는 위험을 줄일 수 있습니다.

관련된 것이라고 해도 다른 타입을 가진다면 다른 이름을 붙이는 편이 좋습니다.

나쁜 예

items = 'a b c d'  # This is a string...
items = items.split(' ')  # ...becoming a list
items = set(items)  # ...and then a set

한 번 쓴 이름을 재활용 하는 것은 비효율적입니다. 대입문은 반드시 새로운 객체를 만들어야 합니다. 하지만 너무 복잡해지고, ‘if’ 분기와 반복절이 들어가서 코드가 여러 줄로 쪼개지면 변수가 어떤 타입인지 확인하기 어려워집니다.

함수형 프로그래밍의 코딩 습관처럼 재할당이 불가능한 변수의 사용을 추천합니다. 자바에서는 final 키워드로 가능합니다. 파이썬은 final 키워드가 없고, 이러한 방법은 파이썬의 철학에 반하는 일입니다. 하지만 변수를 한 번 이상 재할당하지 않는 것은 좋은 습관이며, 가변 타입과 불변 타입을 이해하는데 도움이 될 것입니다.

가변 타입과 불변 타입

파이썬에는 내장 타입과 사용자 정의 타입이 있습니다.

가변 타입은 내부에서 자체적으로 조작이 가능합니다. 일반적인 가변 타입에는 리스트와 딕셔너리가 있습니다. 모든 리스트에는 list.append()list.pop() 처럼 자체적으로 조작 가능한 메소드가 있습니다. 딕셔너리도 마찬가지입니다.

불변 타입은 자체적으로 내용을 바꾸는 메소드를 제공하지 않습니다. 예를 들면 integer 6으로 설정된 변수 x에는 “increment” 가 없습니다. 만약 x에 1을 더하고 싶다면 다른 integer 변수를 만들어서 이름을 부여해야 합니다.

my_list = [1, 2, 3]
my_list[0] = 4
print my_list  # [4, 2, 3] <- The same list as changed

x = 6
x = x + 1  # The new x is another object

이러한 차이 때문에 가변 타입은 “안정적” 이지 못해서 딕셔너리 키로 쓸 수 없습니다.

원래부터 가변적인 것에는 가변 타입을 적절히 사용하고, 원래부터 변하지 않는 것에는 불변 타입을 적절히 사용하는 방법이 코드의 목적을 명확히 하는데 도움이 됩니다.

예를 들면, 튜플은 리스트와 비슷하지만 변경이 불가능합니다. 튜를은 (1, 2) 로 만들 수 있습니다. 튜플은 내부적으로 변경이 불가능하기 때문에 딕셔너리의 키로 사용할 수 있습니다.

초보자가 깜짝 놀랄만한 파이썬의 특이한 점은 문자열이 변경 불가능하다는 점입니다. 이는 문자열의 일부를 다시 만들고 싶을 때, 애시당초 리스트로 만들어서 그 일부를 변경하는 편이 효율적이라는 뜻입니다. 리스트는 변경 가능하고, 전체 문자열이 필요하면 (‘join’)을 사용하여 한데 이어 붙일 수 있기 때문입니다. 하지만 한가지 알아둬야 할 점은 반복문에서 append() 메소드를 호출해 리스트를 만드는 것보다 리스트 컴프리헨션(list comprehension)을 쓰는 편이 더 좋고 빠르다는 것입니다.

나쁜 예

# create a concatenated string from 0 to 19 (e.g. "012..1819")
nums = ""
for n in range(20):
  nums += str(n)   # slow and inefficient
print nums

좋은 예

# create a concatenated string from 0 to 19 (e.g. "012..1819")
nums = []
for n in range(20):
  nums.append(str(n))
print "".join(nums)  # much more efficient

Best

# create a concatenated string from 0 to 19 (e.g. "012..1819")
nums = [str(n) for n in range(20)]
print "".join(nums)

문자열에 대해 마지막으로 말할 것은 join() 을 쓰는게 항상 좋지는 않다는 것입니다. 문자열이 몇 개 있을지 미리 정해져 있는 상태에서 새로운 문자열을 만들 때는 더하기 연산자를 쓰는 편이 실제로 더 빠릅니다. 하지만 문자열이 몇 개 있을지 정해져 있지 않거나, 이미 만들어진 문자열에 추가로 문자열을 더할 때는 join() 을 쓰는 편을 선호하게 될 것입니다.

foo = 'foo'
bar = 'bar'

foobar = foo + bar  # This is good
foo += 'ooo'  # This is bad, instead you should do:
foo = ''.join([foo, 'ooo'])

주석

str.join()+ 외에도 포맷 연산자 % 를 써서 미리 정해진 숫자의 문자열에 붙이는 방법을 쓸 수도 있습니다. 하지만 PEP 3101 에 따르면 % 연산자는 사용하지 말 것을 권장하고 있으며 str.format() 를 대신 쓸 것을 권장하고 있습니다.

foo = 'foo'
bar = 'bar'

foobar = '%s%s' % (foo, bar) # It is OK
foobar = '{0}{1}'.format(foo, bar) # It is better
foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # It is best

Vendorizing Dependencies

Runners