프로젝트 구성하기

../_images/33907151224_0574e7dfc2_k_d.jpg

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

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

In this section, we take a closer look at Python's modules and import systems as they are the central elements to enforcing structure in your project. We then discuss various perspectives on how to build code which can be extended and tested reliably.

저장소의 구조

중요합니다.

Just as Code Style, API Design, and Automation are essential for a healthy development cycle. Repository structure is a crucial part of your project's architecture.

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

  • 프로젝트 이름
  • 프로젝트 설명
  • 파일들 한무더기

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

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

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

Of course, first impressions aren't everything. You and your colleagues will spend countless hours working with this repository, eventually becoming intimately familiar with every nook and cranny. The layout is important.

샘플 저장소

tl;dr: This is what Kenneth Reitz recommended in 2013.

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

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 를 확인하세요.

Of course, you are also free to publish code without a license, but this would prevent many people from potentially using or contributing to your code.

Setup.py

위치 ./setup.py
목적 패키지와 배포 관리

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

필요한 파일

위치 ./requirements.txt
목적 개발 의존성

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

If your project has no development dependencies, or if you prefer setting up a development environment via setup.py, this file may be unnecessary.

문서

위치 ./docs/
목적 패키지 참조 문서

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

테스트 도구

For advice on writing your tests, see 코드 테스트하기.

위치 ./test_sample.py or ./tests
목적 패키지 통합 테스트와 단위 테스트

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

./test_sample.py

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

tests/test_basic.py
tests/test_advanced.py

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

  • site-packages에 패키지를 설치하게 한다.
  • 패키지 경로를 잘 찾기 위해 간단한(하지만 명확한) 경로를 사용한다.

I highly recommend the latter. Requiring a developer to run setup.py develop to test an actively changing codebase also requires them to have an isolated environment setup for each instance of the codebase.

To give the individual tests import context, create a tests/context.py file:

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

import sample

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

from .context import sample

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

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

Makefile

위치 ./Makefile
목적 전반적인 관리 작업

If you look at most of my projects or any Pocoo project, you'll notice a Makefile lying around. Why? These projects aren't written in C... In short, make is an incredibly useful tool for defining generic tasks for your project.

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

코드 구성이 핵심

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

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

  • Multiple and messy circular dependencies: If the classes Table and Chair in furn.py need to import Carpenter from workers.py to answer a question such as table.isdoneby(), and if conversely the class Carpenter needs to import Table and Chair to answer the question carpenter.whatdo(), then you have a circular dependency. In this case you will have to resort to fragile hacks such as using import statements inside your methods or functions.
  • Hidden coupling: Each and every change in Table's implementation breaks 20 tests in unrelated test cases because it breaks Carpenter's code, which requires very careful surgery to adapt to the change. This means you have too many assumptions about Table in Carpenter's code or the reverse.
  • Heavy usage of global state or context: Instead of explicitly passing (height, width, type, wood) to each other, Table and Carpenter rely on global variables that can be modified and are modified on the fly by different agents. You need to scrutinize all access to these global variables in order to understand why a rectangular table became a square, and discover that remote template code is also modifying this context, messing with the table dimensions.
  • Spaghetti code: multiple pages of nested if clauses and for loops with a lot of copy-pasted procedural code and no proper segmentation are known as spaghetti code. Python's meaningful indentation (one of its most controversial features) makes it very hard to maintain this kind of code. The good news is that you might not see too much of it.
  • Ravioli code is more likely in Python: it consists of hundreds of similar little pieces of logic, often classes or objects, without proper structure. If you never can remember, if you have to use FurnitureTable, AssetTable or Table, or even TableNew for your task at hand, then you might be swimming in ravioli code.

모듈

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

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

As soon as you use import statements, you use modules. These can be either built-in modules such as os and sys, third-party modules you have installed in your environment, or your project's internal modules.

To keep in line with the style guide, keep module names short, lowercase, and be sure to avoid using special symbols like the dot (.) or question mark (?). A file name like my.spam.py is the one you should avoid! Naming this way will interfere with the way Python looks for modules.

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

If you like, you could name your module my_spam.py, but even our trusty friend the underscore, should not be seen that often in module names. However, using other characters (spaces or hyphens) in module names will prevent importing (- is the subtract operator). Try to keep module names short so there is no need to separate words. And, most of all, don't namespace with underscores; use submodules instead.

# OK
import library.plugin.foo
# not OK
import library.foo_plugin

Aside from some naming restrictions, nothing special is required for a Python file to be a module. But you need to understand the import mechanism in order to use this concept properly and avoid some issues.

Concretely, the import modu statement will look for the proper file, which is modu.py in the same directory as the caller, if it exists. If it is not found, the Python interpreter will search for modu.py in the "path" recursively and raise an ImportError exception when it is not found.

When modu.py is found, the Python interpreter will execute the module in an isolated scope. Any top-level statement in modu.py will be executed, including other imports if any. Function and class definitions are stored in the module's dictionary.

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

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

It is possible to simulate the more standard behavior by using a special syntax of the import statement: from modu import *. This is generally considered bad practice. Using import * makes the code harder to read and makes dependencies less compartmentalized.

Using from modu import func is a way to pinpoint the function you want to import and put it in the local namespace. While much less harmful than import * because it shows explicitly what is imported in the local namespace, its only advantage over a simpler import modu is that it will save a little typing.

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

As mentioned in the 코드 스타일 section, readability is one of the main features of Python. Readability means to avoid useless boilerplate text and clutter; therefore some efforts are spent trying to achieve a certain level of brevity. But terseness and obscurity are the limits where brevity should stop. Being able to tell immediately where a class or function comes from, as in the modu.func idiom, greatly improves code readability and understandability in all but the simplest single file projects.

패키지

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

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

A file modu.py in the directory pack/ is imported with the statement import pack.modu. This statement will look for __init__.py file in pack and execute all of its top-level statements. Then it will look for a file named pack/modu.py and execute all of its top-level statements. After these operations, any variable, function, or class defined in modu.py is available in the pack.modu namespace.

A commonly seen issue is adding too much code to __init__.py files. When the project complexity grows, there may be sub-packages and sub-sub-packages in a deep directory structure. In this case, importing a single item from a sub-sub-package will require executing all __init__.py files met while traversing the tree.

Leaving an __init__.py file empty is considered normal and even good practice, if the package's modules and sub-packages do not need to share any code.

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

객체지향 프로그래밍

Python is sometimes described as an object-oriented programming language. This can be somewhat misleading and requires further clarifications.

In Python, everything is an object, and can be handled as such. This is what is meant when we say, for example, that functions are first-class objects. Functions, classes, strings, and even types are objects in Python: like any object, they have a type, they can be passed as function arguments, and they may have methods and properties. In this understanding, Python can be considered as an object-oriented language.

However, unlike Java, Python does not impose object-oriented programming as the main programming paradigm. It is perfectly viable for a Python project to not be object-oriented, i.e. to use no or very few class definitions, class inheritance, or any other mechanisms that are specific to object-oriented programming languages.

Moreover, as seen in the modules section, the way Python handles modules and namespaces gives the developer a natural way to ensure the encapsulation and separation of abstraction layers, both being the most common reasons to use object-orientation. Therefore, Python programmers have more latitude as to not use object-orientation, when it is not required by the business model.

There are some reasons to avoid unnecessary object-orientation. Defining custom classes is useful when we want to glue some state and some functionality together. The problem, as pointed out by the discussions about functional programming, comes from the "state" part of the equation.

In some architectures, typically web applications, multiple instances of Python processes are spawned as a response to external requests that happen simultaneously. In this case, holding some state in instantiated objects, which means keeping some static information about the world, is prone to concurrency problems or race conditions. Sometimes, between the initialization of the state of an object (usually done with the __init__() method) and the actual use of the object state through one of its methods, the world may have changed, and the retained state may be outdated. For example, a request may load an item in memory and mark it as read by a user. If another request requires the deletion of this item at the same time, the deletion may actually occur after the first process loaded the item, and then we have to mark a deleted object as read.

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

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

Carefully isolating functions with context and side-effects from functions with logic (called pure functions) allows the following benefits:

  • 순수 함수는 결정성이 있습니다. 즉, 같은 입력값이 있으면 언제나 같은 출력값을 냅니다.
  • 순수 함수는 수정하거나 최적화 작업이 필요할 때 쉽게 수정 및 변경할 수 있습니다.
  • Pure functions are easier to test with unit tests: There is less need for complex context setup and data cleaning afterwards.
  • 순수 함수는 다루기 쉽고, 기능을 더하기도 쉽고, 없애기도 쉽습니다.

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

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

데코레이터

파이썬 언어에는 데코레이터라는 이름의 단순하지만 강력한 문법이 있습니다. 데코레이터는 함수나 메소드를 감싸는(데코레이트 하는) 함수나 클래스입니다. '데코레이트 된' 함수나 메소드는 원래의 '데코레이트 되지 않은' 함수나 메소드를 대체합니다. 함수는 파이썬에서 일급 객체이기 때문입니다. 이 작업은 '수작업'으로도 가능하지만 @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

This mechanism is useful for separating concerns and avoiding external unrelated logic 'polluting' the core logic of the function or method. A good example of a piece of functionality that is better handled with decoration is memoization or caching: you want to store the results of an expensive function in a table and use them directly instead of recomputing them when they have already been computed. This is clearly not part of the function logic.

컨텍스트 관리자

컨텍스트 관리자는 동작에 추가적인 컨텍스트 정보를 추가하는 파이썬 오브젝트입니다. 이 추가 정보는 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__ 메소드가 호출됩니다.

And now the generator approach using Python's own 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() 를 실행합니다.

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

동적 타이핑

Python is dynamically typed, which means that variables do not have a fixed type. In fact, in Python, variables are very different from what they are in many other languages, specifically statically-typed languages. Variables are not a segment of the computer's memory where some value is written, they are 'tags' or 'names' pointing to objects. It is therefore possible for the variable 'a' to be set to the value 1, then the value 'a string', to a function.

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

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

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

나쁜 예

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

좋은 예

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

Using short functions or methods helps to reduce the risk of using the same name for two unrelated things.

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

나쁜 예

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 has changed

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

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

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

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

One peculiarity of Python that can surprise beginners is that strings are immutable. This means that when constructing a string from its parts, appending each part to the string is inefficient because the entirety of the string is copied on each append. Instead, it is much more efficient to accumulate the parts in a list, which is mutable, and then glue (join) the parts together when the full string is needed. List comprehensions are usually the fastest and most idiomatic way to do this.

나쁜 예

# 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)

Better

# 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))

One final thing to mention about strings is that using join() is not always best. In the instances where you are creating a new string from a pre-determined number of strings, using the addition operator is actually faster. But in cases like above or in cases where you are adding to an existing string, using join() should be your preferred method.

foo = 'foo'
bar = 'bar'

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

주석

You can also use the % formatting operator to concatenate a pre-determined number of strings besides str.join() and +. However, PEP 3101 discourages the usage of the % operator in favor of the str.format() method.

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