제목은 테크닉으로 거창하지만 사실 파이썬의 문법을 정리한 글입니다. 파이썬 언어는 개발자 친화적인 독특한 특징을 가집니다. 이를 잘 이해한다면 자신만의 코딩 테크닉을 잘 구사할 수 있습니다. 파이썬 스타일과 주요 기능들을 먼저 보고, 다양한 프로그래밍 패러다임에서 이들의 모습을 살펴본 후 최적화 팁과 환경 구성으로 마무리하겠습니다.


목차


1. 파이썬 스타일

파이썬만의 코드 스타일, 파이썬스러움(pythonic)이 무엇인지 알아보자.
다음은 파이썬 개발의 철학이 담겨있는 문구들이다.

import this
# Beautiful is better than ugly.
# Explicit is better than implicit.
# Simple is better than complex.
# Complex is better than complicated.
# Flat is better than nested.
# Sparse is better than dense.
# Readability counts.
# Special cases aren't special enough to break the rules.
# Although practicality beats purity.
# Errors should never pass silently.
# Unless explicitly silenced.
# In the face of ambiguity, refuse the temptation to guess.
# There should be one-- and preferably only one --obvious way to do it.
# Although that way may not be obvious at first unless you're Dutch.
# Now is better than never.
# Although never is often better than *right* now.
# If the implementation is hard to explain, it's a bad idea.
# If the implementation is easy to explain, it may be a good idea.
# Namespaces are one honking great idea -- let's do more of those!

파이썬 개발자들은 “Simple is better than complex.”처럼 복잡함보다는 단순함을 선호하고, 가독성을 극대화하기 위해 명료성을 중시한다. 파이썬 커뮤니티에서 권장하는 스타일은 PEP 8에서 잘 설명하고 있다. 애플리케이션뿐만 아니라 이를 지탱하는 내부 구조에서도 파이썬스러움(pythonic)을 가진다. 그렇다면 파이썬스러움이란 구체적으로 무엇일까?

1.1. 파이썬 데이터 모델

파이썬 최고의 장점 중 하나는 일종의 프레임워크인 ‘파이썬 데이터 모델‘에 기반한 일관성에 있다. 파이썬 데이터 모델은 던더(더블언더바) 메소드(ex. __init__, __del__, …)와 API를 제공해서 객체를 정의하기만 해도 대부분의 표준 라이브러리를 자동으로 적용할 수 있다. 이러한 파이썬 데이터 모델 덕분에 사용자가 정의한 자료형도 내장 자료형과 똑같이 자연스럽게 동작할 수 있다. 즉 상속하지 않고도 덕 타이핑(duck typing) 메커니즘을 통해 자동으로 가능해진다. 우리는 단지 객체에 필요한 메서드를 구현하기만 하면 된다.

class MyList:
    def __init__(self, data_list):
        self.mylist = data_list
    def __len__(self):
        return len(self.mylist)
    def __getitem__(self, index):
        return self.mylist[index]

mylist = MyList([1,2,3,4,5])
print(len(mylist))
# 5
print(mylist[3]) 
# 4

예를 들면, 완전히 시퀀스형으로 동작하는 객체를 만들기 위해 우리는 특별한 클래스를 상속할 필요가 없다. abc.Sequence를 상속하지 않고도 그냥 시퀀스 프로토콜을 따르는 메서드(ex. __len__, __getitem__)5를 구현하기만 하면 된다. 시퀀스 프로토콜에 입각해 정의된 클래스는 일반 시퀀스처럼 잘 동작하고, 시퀀스 그 자체가 되어버린다. 물론 목적에 따라 프로토콜의 일부만 사용해도 된다.

1.2. 일급 객체 함수

파이썬의 핵심 개념 중 하나로 파이썬 함수는 일급 객체(first-class object)라는 점이다. 일급 객체 함수는 런타임에 생성될 수 있고, 직접 참조하고, 변수에 할당되고, 다른 함수의 인자로 전달되고, 함수의 결과로 반환될 수 있다. 즉 표현식과 if 문 등에서 비교할 수 있는 대상이 된다. 또 다른 측면으로는 다른 함수 내부에서 정의될 수 있는 클로저가 될 수 있다. 이로서 자식 함수는 부모 함수의 로컬 상태를 포착할 수 있다.

1.3. 그 이외 파이썬 스타일

파이썬은 자바처럼 변수에 자료형(type)을 지정할 필요없이 단순히 값만 관리한다. 모든 값을 객체로 만들어서 사용하므로 객체 내부에 객체를 생성한 클래스인 자료형을 항상 가진다. 예를 들어, 정수를 실수로 바꾸는 형 변환은 정수 객체를 실수 객체로 바꾸는 것이다. 변수에 값을 할당해서 변수를 정의하면 실제 변수에 객체가 할당(binding)된다. 객체 할당이 자료형을 결정하는 동적 자료형(dynamic typing) 방식을 가진다.

프로그램 코드 안에서 명시적으로 딕셔너리를 사용하고 있지 않더라도, 모든 파이썬 프로그램에서는 여러 딕셔너리가 동시에 활동하고 있다. 딕셔너리는 애플리케이션에서 널리 사용될 뿐만 아니라 파이썬 구현의 핵심 부분이기도 하다. 파이썬 딕셔너리 클래스는 상당히 최적화되어 있다. 파이썬의 고성능 딕셔너리 뒤에는 해시 테이블이라는 엔진이 있다.

파이썬에서 제공하는 클래스는 대게 디스크립터 기법을 사용해서 만들어졌다. 클래스 메소드, 정적 메소드, 프로퍼티, 함수 등도 디스크립터 기법을 사용해 이름으로 접근해서 처리할 수 있다. 디스크립터를 이해하면 다양한 도구에 접근할 수 있을 뿐만 아니라 파이썬 작동 방식과 파이썬의 멋진 설계도 깊이 이해할 수 있다. (디스크립터는 2.4절을 참고하자)

파이썬에서 프로토콜이 많은 부분에서 인터페이스 역할을 해주고 있다. 프로토콜6은 파이썬과 같은 동적 자료형을 제공하는 언어에서 다형성을 제공하는 비공식 인터페이스라 할 수 있다. 클래스는 여러 프로토콜을 구현해서 객체가 다양한 기능을 가질 수 있도록 해준다.

파이썬은 EAFP (It’s Easier to Ask Forgiveness than Permission) 스타일에 가깝다1. 이는 올바른 키나 속성이 있다고 가정하고, 그 가정이 잘못됐을 때 비로소 예외로 처리한다. 따라서 try/except 문을 많이 사용한다. 파이썬에서는 try/except를 예외 처리뿐만 아니라 일반적인 제어 흐름을 구현하기 위해서도 많이 사용한다.

파이썬은 다른 객체지향 언어(ex. 루비, 스몰토크, 자바8)와 같이 call by sharing 하는 매개변수 전달 방식만 지원한다.


2. 파이썬 주요 기능

파이썬에는 파이썬다운 유용한 문법들이 많다. 주요 기능을 살펴보자.

2.1. 리스트 컴프리핸션 (List Comprehension)

파이썬에서는 리터럴 표기법으로 원소를 나열해 시퀀스형(ex. list, dict, set) 객체를 만든다. 이는 원소가 많을 경우 일일이 나열하기 힘든 단점이 있다. 원소들이 특정 규칙으로 나열되는 시퀀스가 있다면 컴프리핸션 표현식으로 쉽게 나타낼 수 있다. 여기선 리스트 컴프리핸션 중심으로 소개한다.

digits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
sqaures = [x**2 for x in digits]
print(sqaures)
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

리스트 컴프리핸션은 한 리스트에서 다른 리스트를 간결한 문법으로 만들어낸다. lambda 함수를 필요로 하는 map과 filter 보다 리스트 컴프리핸션이 가독성이 좋다. 단, 표현식의 깊이를 두 단계 이상으로 사용하는 것은 파이썬 스타일에 맞지 않기 때문에 지양한다.

리스트 컴프리핸션을 사용하면 데이터 파이프라인을 깔끔하게 만들 수 있다. 이는 판다스의 apply 함수를 사용하는 것과 유사하다. 하지만, 이러한 벡터스러운 연산이 반복된다면 판다스, 넘파이 라이브러리를 사용하는 것을 추천한다.

def data_pipeline(data):
    data = [func_1(x) for x in data]
    data = [func_2(x) for x in data]
    ...
    data = [func_n(x) for x in data]
    data = [(lambda x:2*x)(x) for x in data]
    ...
    return data

리스트 컴프리핸션은 여느 리스트와 마찬가지로 리스트 전체를 통째로 메모리에 올린다. 리스트가 거대해서 메모리에 부담을 줄 수 있을 때는 다음 장에 소개되는 제너레이터를 고려하자.

2.2. 제너레이터 (Generator)

메모리에 다 올라가지않는 거대한 데이터는 데이터를 순회해서 원소 하나씩 필요할 때만 가져오면 좋다. 파이썬에서는 반복자(iterator)가 이런 일을 맡고 있다. 제너레이터는 반복자의 인터페이스를 완벽히 구현하고 있다2. 심플하게 함수는 yield 키워드로, 컴프리핸션 표현식은 괄호로 제너레이터를 만든다. 제너레이터 함수는 복잡한 논리를 표현할 수 있지만, 표현식은 간결함으로 가독성이 높다.

it = (len(x) for x in open('/your/home/corpus.txt'))
print(it)
# <generator object <genexpr> at 0x122490b>
print(next(it))
# {first line string}
print(next(it))
# {second line string}
...

제너레이터에서 주의할 점은 반환되는 반복자(iterator)에 상태가 있고, 재사용할 수 없다는 점이다. 다시 사용하려면 제너레이터 객체를 다시 만들어야 한다. 그리고 제너레이터 객체는 호출할 수 없다.

def read(data_path):
    with open(data_path, 'r') as rf:
        for line in rf:
            yield line

it = read('your/home/corpus.txt')
print(list(it)) 
# ["generator can't be reused.", "generator has state.", ... ]
print(list(it)) 
# []

여러 개의 제너레이터로부터 값을 생성할 때는 yield from 키워드를 사용하면 간결해진다.

def chain(*iterables):
    for i in iterables:
        yield from i

print(list(chain(it1, it2)))
# ['A', 'B', 'C', 0, 1, 2]

제너레이터 표현식은 다른 제너레이터 표현식과 함께 사용할 수 있다. 파이썬에서 이렇게 제너레이터들을 체인으로 연결하면 매우 빠르게 실행된다. 큰 입력 스트림에 동작하는 기능을 결합하는 방법으로 적절하다.

it = (len(x) for x in open('/your/home/corpus.txt'))
squares = ((x, x**2) for x in it)
print(next(sqaures))
# 144

if/elese 구문으로 각 케이스마다 다른 값을 리스트에 append하고 마지막에 리스트를 반환하는 함수가 있다고 하자. 이 경우는 각 if 문마다 제너레이터 yield 키워드를 사용하면 훨씬 더 코드가 깔끔해진다. 대신에 호출하는 쪽에서 반환되는 반복자에 상태가 있고 재사용할 수 없다는 사실을 알려야 한다. list 함수를 씌워주면 데이터를 다 뽑을 수 있다.

send 함수는 next 함수처럼 제너레이터가 다음 yield로 넘어가게 만들고, 제너레이터를 사용하는 호출자가 제너레이터에 데이터를 보낼 수 있게 해준다. 이런 방식으로 사용하면 제너레이터는 코루틴(coroutine)이 된다3. 코루틴 안에서 yield가 값을 생성하는 것은 의미있지만, 이는 사실 반복과 상관없다.

2.3. 데코레이터 (Decorator)

파이썬에서 데코레이터4는 호출 가능한 객체(ex. 함수, 메서드, 클래스)를 영구적으로 수정하지 않고도 동작을 확장 및 수정할 수 있게 해준다. 다시 말해, 기존 클래스나 함수의 동작에 일반적인 기능을 덧붙이고 싶을 때 데코레이터가 유용하게 쓰일 수 있다.

데코레이터는 단지 문법적인 편의성을 위한 syntactic sugar일 뿐이다. 데코레이터의 대표적인 예로는 로깅, 접근제어 및 인증, 실행시간 측정, 비율 제한, 캐싱 등이 있다. fairseq에서 register_model_architecture 데코레이터가 있다. 이는 함수에 이름을 부여해 argparse의 인자로 사용될 수 있도록 해준다.

데코레이터는 다른 함수를 인수로 전달해서 호출하는 일반적인 콜러블(callable)과 동일하지만, 런타임에 프로그램 동작을 변경하는 메타 프로그래밍에서 데코레이터가 상당히 유용하다.

다음은 데코레이터로 함수의 실행시간을 측정하는 예시이다.

from functools import wraps
from datetime import datetime

def calc_execution_time(func):
    @wraps(func)
    def inner(*args, **kwargs):
        start = datetime.now()
        result = func(*args, **kwargs)
        end = datetime.now()
        print(end - start)
        return result
    return inner

@calc_execution_time
def ner_algorithm(text):
    """ an_algorithm for name entity recognition """
    ...
    return tag_list

print(ner_algorithm.__doc__)
# ' an_algorithm for name entity recognition '
tag_list = ner_algorithm("python programming techniques")
# 0:00:12.001412

inner 클로저를 통해서 온전한 상태의 입력 함수인 func에 접근할 수 있고, 이 함수를 호출하기 전후에 추가 코드를 자유롭게 실행할 수 있다. 데코레이터를 사용하면 ner_algorithm 함수가 calc_execution_time 함수로 교체된다. 이의 단점은 원래 함수에 첨부된 메타 데이터(ex. __doc__)를 숨겨버리는 것이다. @wraps 데코레이터를 사용해서 원래 함수의 메타 데이터를 데코레이터 클로저로 복사할 수 있다.

데코레이터 함수는 파라미터도 받을 수 있다. 여기서는 전/후처리 단계로 출력하고 있어서 데코레이터의 기본 기능을 잘 표현한다.

def decorator_with_param(param):
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            print(param)
            print("-> preprocessing step")
            func_result = func(*args, **kwargs)
            print("-> postprocessing step")
            return func_result
        return inner
    return wrapper

@decorator_with_param("here are decorator args.")
def add(x, y):
    return x + y

result = add(10, 13)
# here are decorator args.
# -> preprocessing step
# -> postprocessing step

여러 개의 데코레이터도 하나의 함수에 적용할 수 있다. 3.1 장을 참고하자. 하지만, 함수 호출의 중첩도가 커지면 성능에 영향을 주기 때문에 주의가 필요하다.

다음은 파이썬에서 제공하는 대표적인 데코레이터 함수를 나타낸다. 여기선 내장 데코레이터 함수만 알아본다.

내장 데코레이터 함수표준 라이브러리 데코레이터 함수
– property
– classmethod
– staticmethod
– functools.wraps
– functools.lru_cache
– functools.singledispatch

사용자가 객체의 속성에 접근하기 위해서는 직접적인 접근이 아닌 메소드를 통한 우회 접근을 해야 한다. 자바는 이를 위해 각 속성마다 게터(getter)와 세터(setter) 함수를 구현해야 한다. 파이썬은 간단히 @property 데코레이터만 사용하면 된다.

@classmethod와 @staticmethod 데코레이터를 간단히 비교해보자. 이 둘의 공통점은 객체를 생성하지 않아도 곧바로 사용할 수 있다는 점이다.

class KSmethod:

    @classmethod
    def klassmeth(*args):
        return args #

    @staticmethod
    def statmeth(*args):
        return args #

print(KSmethod.klassmeth())
# (<class '__main__.KSmethod'>,)
print(KSmethod.klassmeth('spam'))
# (<class '__main__.KSmethod'>, 'spam')
print(KSmethod.statmeth())
# ()

@classmethod 데코레이터는 객체가 아닌 클래스에 연산을 수행하는 메서드를 정의한다. 그리고 메서드가 호출되는 방식을 변경해서 클래스 자체를 첫 번째 인수로 받게 한다. 반면에, @staticmethod는 메서드가 특별한 첫 번째 인수를 받지 않도록 메서드를 변경한다. 그냥 독립적인 평번한 함수일 뿐이다.

2.4. 디스크립터 (Descriptor)

파이썬 디스크립터는 디스크립터 프로토콜(descriptor protocol) 메서드를 구현한 객체이다. 이는 다른 객체의 속성으로 접근할 때 특별한 행동을 하는 객체를 생성한다. 디스크립터 프로토콜은 보통 __get__, __set__, __delete__ 메소드로 구성된다. 디스크립터는 크게 data 디스크립터와 non-date 디스크립터로 구분된다. 전자는 __get__ 메소드를, 후자는 __set__과 __delete__ 메소드를 사용한다.

그러면 왜 디스크립터를 사용할까? 클래스의 속성 접근에 대해 효율적으로 제어하기 위해서이다. 특정 속성을 값이나 타입을 제한할 수 있다(ex. 숫자만, 양수만, 특정 범위만, 문자열만 등). 또한, 객체가 생성된 뒤에도 접근 제어가 필요하다(디스크립터의 __set__ 메서드의 역할). 이러한 세밀한 접근 제어를 일반적으로 클래스를 정의할 때 명시한다면 코드가 불필요하게 커지고 가독성이 떨어지게 된다. 어떤 특징을 가지는 접근 제어가 필요하다고 하면 따로 디스크립터 클래스만 만들면 된다.

class DescriptorPER:

    def __init__(self):
        self.__per = 0

    def __get__(self, instance, owener):
        return self.__per

    def __set__(self, instance, value):
        if not isinstance(value, float):
            raise TypeError("PER can only an float")
        if value < 0.0:
            raise ValueError("PER can never be less than zero")
        self.__per = value

class FinancialStatements:

    per = DescriptorPER()

    def __init__(self, market_cap, per, pbr):
        self.market_cap = market_cap
        self.per = per
        self.pbr = pbr

    def __str__(self):
        return f"{self.market_cap} market_cap {self.per} per {self.pbr} pbr."

my_inc = FinancialStatements(123234322, 11.2, 3.3)
print(my_inc)
# 123234322 market_cap 11.2 per 3.3 pbr.

이는 속성에 아무대나 접근하지 못하게 하는 캡슐화와 같다. 파이썬은 디스크립터 클래스를 만들고, 속성에 스페셜 메소드로 접근해서 조회, 갱신, 삭제를 한다. 근본적인 목적은 속성 접근에 대해 차단하기 위함이지만, 넓게 생각하면 파이썬 디스크립터는 여러 클래스 사이에 공유될 수 있는 재사용 코드에 관한 것이다.

2.5. 내장 데이터 구조

파이썬에는 내장된 훌륭한 데이터 구조(자료구조)가 많다. 사용자는 따로 구현할 필요없이 가져다 쓰기만 하면 된다. 중요한 몇 가지만 살펴보자.

네임드튜플 (namedtuple)

네임드튜플을 이용하면 인덱스 대신에 이름으로 튜플 값에 접근할 수 있다. 네임드튜플은 튜플을 상속받기 때문에 튜플에 존재하는 다양한 최적화 이점을 가질 수 있다7. 이 때문에 몇 가지 고정된 속성만 가지는 간단한 객체를 만들 때는 클래스 대신에 네임드튜플을 사용하면 메모리 이득을 가질 수 있다.

from collections import namedtuple

UserInfo = namedtuple('UserInfo', 'name job interest age sex')
gritmind = UserInfo('yeongsu', 'developer', 'natural language processing', 32, 'man')

print(gritmind.job)
# developer
print(gritmind.age)
# 32
print(gritmind[2])
# natural language processing

네임드튜플의 첫 번째 매개변수는 타입명(typename)이라고 하여 네임드튜플로 생성되는 새 클래스의 이름이 된다. 일반적인 튜플의 단점은 정수 인덱스를 써서 데이터에 접근하는 것이다. 이는 가독성에 영향을 주고, 필드 순서를 혼동해서 버그를 유발할 수도 있다. 반면 네임드튜플은 데이터를 잘 구조화할 수 있고, 개발자의 의도를 명확히하여 가독성을 높일 수 있다.

덱 (deque)

일반적인 스택이나 큐의 경우 리스트를 반대쪽에서 삽입하거나 삭제하는 연산은 리스트 전체를 이동시켜야 하는 부담이 있다. 덱(deque)은 양쪽 어디서든 O(1) 시간 복잡도로 삽입과 삭제를 빠르게 할 수 있도록 설계된 thread-safe 양방향 큐이다. 최대 길이를 설정하여 제한된 원소만 유지할 수 있다.

from collections import deque

dq = deque(range(10), maxlen=10)

print(dq)
# deque([0, 1, 2, 3, 4, 5, 6, 7, 8 ,9], maxlen=10)
dq.rotate(3)
print(dq)
# deqeue([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
dq.appendleft(-1)
print(dq)
# deqeue([-1, 7, 8, 9, 0, 1, 2, 3, 4, 5], maxlen=10)

덱은 양쪽 끝에서 원소를 동일하게 삽입하거나 삭제할 수 있으므로 큐와 스택으로 모두 사용할 수 있다. 덱은 양쪽 끝에 삽입하거나 삭제하는 연산은 최적화되어 있지만, 중간 원소를 삭제하는 연산은 그리 빠르지 않다.

힙 큐 (heapq)

힙 큐는 일반 리스트에 뒷받침되는 이진 힙을 구현한다. 이 모듈은 파이썬에서 우선순위 큐를 구현하기 좋다. 힙 큐라고 해서 큐 클래스를 사용하진 않는다.

import heapq

q = []
heapq.heappush(q, (1, 'code'))
heapq.heappush(q, (2, 'study'))

while q:
    ni = heapq.heappop(q)
    print(ni)
# (1, 'code')
# (2, 'study')

정렬된 딕셔너리 (OrderedDict)

파이썬 딕셔너리는 원소들의 순서 정보가 없다. 리스트처럼 순서를 지정하고 싶으면 OrderedDict 클래스를 사용하면 된다. OrderedDict 클래스로 생성된 객체에 딕셔너리와 같이 값을 할당하면 내부적으로 자동으로 순서대로 추가된다.

from collections import OrderedDict

d = {'a': 1, 'b': 5}
od = OrderedDict(d)
od['c'] = 50

바이섹트 (bisect)

큰 리스트를 다룰 때 시간 복잡도의 부담이 있다. 정렬된 리스트는 n이 크더라도 O(log(n)) 시간에 값을 가져올 수 있다. 바이섹트(bisect) 함수는 리스트 정렬을 유지하면서 새로운 원소가 몇 번째에 들어가야 하는지 알려준다.

from bisect import bisect

def get_grade(score):
    breakpoints = [60, 70, 80, 90]
    grades = 'FDCBA'
    return grades[bisect(breakpoints, score)]

print([get_grade(score) for score in [33, 99, 77, 70, 89, 90, 100]])
# ['F', 'A', 'C', 'C', 'B', 'A', 'A']

bisect 함수로 이진 검색 알고리즘을 이용한 시퀀스 검색이 가능하고, insort 함수로 정렬된 시퀀스 안에 원소를 삽입할 수 있다. 바이섹트를 응용한 바이너리 트리인 blist와 레드-블랙 트리인 bintree 패키지도 사용해보자.


3. 프로그래밍 패러다임

다양한 프로그래밍 패러다임 관점에서 파이썬이 어떤 특징이 있는지 살펴보자. 각 패러다임에서 파이썬의 유용한 기능들이 어떻게 활용될까?

3.1. 객체지향 프로그래밍

3.1.1. 속성 / 메서드

파이썬에서는 모든 값을 객체로 만들어서 사용한다. 파이썬 객체는 객체의 설계도인 클래스라는 자료형을 항상 가진다. 이처럼 파이썬은 완전한 객체 지향적인 언어다. 파이썬에서 모든 함수는 객체이지만, 객체는 함수가 아니다. 클래스 내부에 __call__ 스페셜 메소드를 정의하면 객체도 함수처럼 호출 가능(callable)하게 만들 수 있다.

class Adder:
    def __init__(self, n):
        self.n = n
    def __call__(self, x)
        return self.n + x

adder = Adder(3)
print(adder(4))
# 7

파이썬 객체는 객체의 데이터를 말하는 속성(attribute)과 객체의 행위를 나타내는 메서드(method)를 가진다. 데이터 속성은 클래스 변수와 인스턴스 변수라는 두 가지 종류가 있다. 파이썬에서 객체의 속성은 객체의 이름공간에서 관리하지만, 클래스 속성과 인스턴스 메소드는 클래스 이름공간에서 관리한다.

@dataclass 데코레이터를 사용하면 타입 힌트만 작성하면 객체의 속성을 자동으로 생성할 수 있다.

from dataclasses import dataclass

@dataclass
class UserInfo:
    name: str
    age: int

객체는 repr 함수와 str 함수를 이용해서 문자열로 표현할 수 있다. repr 함수는 개발자가 객체를 보는 관점으로 표현하고, str 함수는 사용자가 객체를 보는 관점으로 표현한다.

파이썬 함수는 클래스 외부에 있을 수 있고 내부에 있을 수 있다. 클래스 외부에 있는 함수로는 int, print, input 등과 같은 내장 함수가 있다. 클래스 내부에 있는 함수는 메소드라고 불린다. 파이썬 메소드의 종류와 특징을 알아보자.

  • 인스턴스 메소드 (instance method)
    • 객체가 바인딩해서 만든 메소드이다. self로 정의해 객체가 사용할 수 있는 함수가 된다.
    • self 매개변수를 통해 동일한 객체에 정의된 속성 및 다른 메서드에 자유롭게 접근할 수 있다.
    • 다양한 기능을 제공하는 프로토콜인 스페셜 메소드(special method)가 있다.
      • 오버라이딩 (자료구조를 관리하는 collections 모듈 활용)
        • 상속하는 경우 : 예를 들어, collections.UserList/UserDict 상속 – 리스트/딕셔너리 연산자 오버라이딩 (ex. __getitem__, __setitem__, __missing__, … )
        • 상속하지 않는 경우 : 예를 들어, int 클래스 : 수학 연산자 오버라이딩 (ex. __add__, __floordiv__, __and__, … )
  • 클래스 메소드 (class method)
    • 클래스가 직접 호출해 사용할 수 있는 메소드이다.
    • cls 인자에만 접근할 수 있으므로, 객체 인스턴스 상태는 수정할 수 없다. 그러나 모든 인스턴스에 적용되는 클래스 상태를 수정할 수 있다.
    • 클래스 메서드를 사용하면 필요한 만큼 많은 대체 생성자를 추가할 수 있다.
  • 정적 메소드 (static method)
    • 함수를 클래스나 객체가 직접 사용할 수 있도록 정의해서 사용하는 것이다.
    • self나 cls 매개변수를 사용하지 않으므로, 객체 상태나 클래스 상태에 접근할 수 없다.
    • 주변의 모든 것과 독립적으로 존재하는 메서드이다. 따라서 테스트하기 쉽다.
    • 객체 지향 프로그래밍과 절차적 프로그래밍 스타일 사이를 연결할 수 있다.
  • 추상 메서드 (abstract method)
    • 선언되었지만 구현되지 않은 메소드이다.
    • 서브클래스로부터 반드시 구현되어야 한다.

3.1.2. 캡슐화 / 상속 / 다형성 / 합성

객체지향의 개념은 캡슐화 (encapsulation), 상속 (inheritance), 다형성 (polymorphism), 합성 (composition)으로 구성된다. 하나씩 간략하게 살펴보자.

캡슐화 (encapsulation)

객체를 사용하면 좋은 점은 모든 속성과 행위를 객체에 나타낼 필요가 없다는 점이다. 다른 객체와 상호 작용하는 데 필요한 부분만 나타내고 관련 없는 세부 사항은 감추어야 한다. 클래스를 설계할 때 객체들 사이의 기본적인 의사소통 수단을 정의하는 인터페이스(interface)를 고려하면 캡슐화를 할 수 있다. 이처럼 캡슐화에서 인터페이스가 중요한 역할을 한다.

파이썬은 클래스나 객체가 내부에 다 제공되기 때문에 일반적인 문법으로는 완벽한 캡슐화를 구성할 수 없다. 대신에 파이썬은 디스크립터 프로토콜 방식으로 캡슐화를 지원한다. 디스크립터는 __get__, __set__, __delete__와 같은 인터페이스(프로토콜)를 제공한다.

@property 데코레이터로도 캡슐화를 구현할 수 있지만, 재사용할 수 없어 코드가 장황해지기 쉽기 떄문에 디스크립터 사용을 추천한다. 참고로 맹글링 (mangling)9이라는 객체명 접근을 우회적으로 방지해주는 기능도 있다.

상속 (inheritance)

코드의 재사용과 구조적으로 설계를 위해서 클래스에 계층이 생기게 된다. 보통 일반적인 개념이 상위 클래스로 정의되고, 구체적인 개념이 하위 클래스로 정의된다. 상위 클래스의 속성과 메소드를 하위 클래스에서 사용하는 것을 상속이라 한다10.

상속은 is-a 관계를 나타낸다고 볼 수 있다. 자식 클래스가 부모 클래스부터 상속받으면 부모 클래스가 할 수 있는 모든 것을 할 수 있다. 일종의 확장체(extension)인 셈이다.

개념이나 현상이 복잡해질수록 여러 단계에 걸쳐서 추상화를 해야 하는데, 어느 수준까지 추상화를 해야 하는지 정하기 어렵다. 이에 자바, 닷넷, 스위프트와 같은 최신 객체지향 언어는 부모 클래스를 하나로 제한한다. 반면, 파이썬과 C++와 같은 일부 언어에서는 부모 클래스를 여러 개 둘 수 있다. 즉 다중 상속이 가능하다.

사용자 추상 클래스를 만들기 위해 메타 클래스 매개변수에 추상 메타 클래스를 지정한다.

from abc import *

class BaseClass(metaclass=ABCMeta):

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

    @classmethod
    @abstractmethod
    def klassmethod(cls):
        pass

    @staticmethod
    @abstractmethod
    def statmethod(arg):
        pass

    @property
    @abstractmethod
    def propmethod(self):
        pass

register 클래스 메소드를 사용하면 추상 클래스에 추가적인 클래스를 상속할 수 있다.

from abc import ABCMeta

class MyABC(metaclass=ABCMeta):
    """ contains integer and string """
    pass

MyABC.register(str)
MyABC.register(int)
print(issubclass(str, MyABC), issubclass(int, MyABC)) # 상속 관계 확인
# True True
print(isinstance("hello", MyABC), isinstance(777, MyABC)) # 생성 관계 확인
# True True

다형성 (polymorphism)

다형성이란 여러 클래스 또는 메소드를 사용해서 다양한 객체 관계를 가지는 구조를 말한다. 다형성은 클래스 상속에 관련된 오버라이딩(overriding)과 상속이 필요없는 스페셜 메소드를 사용하는 메소드 오버로딩(overloading)이 있다.

오버라이딩의 경우 부모 클래스의 어떤 메소드를 필요에 의해서 자식 클래스가 해당 메소드를 재정의해서 사용하는 경우를 말한다. 메소드 오버로딩의 경우 @multidispatch 데코레이터를 사용하면 간결하게 구현할 수 있다.

from multipledispatch import dispatch

@dispatch(int, int)
def join(x, y):
    print(" int + int ")
    return x + y

@dispatch(float, int)
def join(x, y):
    print(" float + int ")
    return x + y

@dispatch(int, int, int)
def join(x, y, z):
    print(" int + int + int ")
    return x + y + z

합성 (composition)

합성은 다른 클래스를 사용해 더 복잡한 클래스, 일종의 어셈블리(assembly)를 구축하는 작업이다. 상속과 다르게 부모/자식 관계가 없다. 상속은 is-a 관계를 가지지만, 합성은 has-a 관계를 가진다. 다시 말해, 합성은 서로 독립된 객체들이 하나로 합쳐지는데 이들의 관계가 포함 관계가 되는 것이다.

객체지향 프로그래밍을 위해 상속을 사용할지 아니면 합성을 사용할지 여부에 대한 고민이 필요하다. 상속과 합성은 각기 효과적인 클래스 설계 기술이다. 이들의 장단점을 이해하고 적절한 맥락에서 사용해야 한다.

3.2. 함수형 프로그래밍

파이썬은 일급 함수이므로 함수형 스타일로 프로그래밍할 수 있다. 함수형 프로그래밍 특징 중 하나가 고위(high-order) 함수이다. 고위 함수는 함수를 인수로 받거나, 함수를 결과로 반환하는 함수이다. 대표적인 예로 lambda, map, filter, apply 등이 있다. 이러한 고위 함수의 예들은 사실 리스트 컴프리핸션과 제너레이터 표현식의 등장으로 활용도가 다소 떨어졌다11. 이들이 가독성이 더 좋기 때문이다(ex. reduce 함수보다는 sub 함수를 선택).

귀도 반 로섬은 파이썬이 함수형 프로그래밍 언어를 지향하지 않았다고 했지만, operator와 functools와 같은 패키지들의 지원으로 파이썬도 함수형 코딩 스타일을 가질 수 있다.

from functools import reduce
from operator import mul

def factorial(n):
    #return reduce(lambda x, y: x*y, range(1, n+1))
    return reduce(mul, range(1, n+1))

print(factorial(5))
# 120

operator 모듈은 시퀀스에서 원소를 가져오는 lambda를 대체하는 itemgetter 함수와 객체의 속성을 읽는 lambda를 대체하는 attrgetter 함수를 제공한다.

from operator import itemgetter, attrgetter

print(sorted(user_tuples, key=itemgetter(1)))
# [('zahra', 31), ('halen', 39), ('adney', 42)]

print(sorted(user_objects, key=attrgetter('name')))
# [('adney', 42), ('halen', 39), ('zahra', 31)]

functools.partial 함수는 부분적으로 실행할 수 있도록 해주는 고위 함수이다. 어떤 함수가 있을 때 partial 을 적용하면 원래 함수의 일부 인수를 고정한 콜러블을 생성한다. 이 기법은 하나 이상의 인수를 받는 함수를 그보다 적은 인수를 받는 콜백 함수를 사용하는 API에 사용할 때 유용하다.

from operator mul
from functools import partial

triple = partial(mul, 3)
print([triple(x) for x in range(1, 10)])
# [3, 6, 9, 12, 15, 18, 21, 24, 27]

파이썬은 함수형 언어로 애초에 설계되지는 않았다. 단지 함수형 언어에 좋은 개념을 빌려왔다. 함수형 언어의 원조인 리스프(LISP)로부터 lambda, map, filter, reduce를 가져왔고, 하스켈(Haskell)에서 리스트 컴프리핸션 구문을 가져왔다.

3.3. 병렬 프로그래밍

병렬 프로그래밍에는 병행성(concurrency)와 병렬성(parallelism)이라는 개념이 있다. 병행성은 컴퓨터가 여러 일을 마치 동시에 하듯이 수행하는 것이고, 병렬성은 실제로 여러 작업을 동시에 실행하는 것을 말한다. 전자는 소프트웨어, 후자는 하드웨어 기준의 이야기이다.

파이썬은 전역 인터프리터 잠금(GIL; Global Interpreter Lock)으로 어쩔 수 없이 병행성만 가지는 상황이 생기기도 한다. GIL은 CPython이 바이트코드를 실행할 때 만드는 락이며 CPython이 선점형 멀티쓰레딩12의 영향을 받지 않도록 해준다14. 따라서 GIL은 한 번에 하나의 쓰레드만 실행하도록 한다. 참고로 파이썬의 표준 라이브러리는 CPython으로 구현된다.

왜 파이썬은 멀티쓰레디를 지원하는가? 그냥 동시에 여러 작업을 하는 것처럼 보이게 하는 프로그램처럼 순수히 병행성을 가지는 상황이 있기 때문이다. 또한, 시스템 호출13블로킹 입출력에서는 특수하게 GIL의 제약을 받더라도 쓰레드를 모두 병렬로 실행할 수 있다. 파이썬 쓰레드가 시스템 호출을 만들기 전에 GIL을 풀고 시스템 호출의 작업이 끝나는 대로 GIL를 다시 얻는다.

파이썬은 concurrent.futures 모듈의 ThreadPoolExecutor와 ProcessPoolExecutor 클래스를 통해 병렬을 쉽게 구현할 수 있도록 해준다. ProcessPoolExecutor는 하드웨어 자원 내에서 항상 병렬성을 보장해준다.

def download_one(id):
    ...
    return id

def download_all(id_list):
    workers = min(NUM_CPU, len(id_list))
    #with futures.ThreadPoolExecutor(workers) as executor:
    with futures.ProcessPoolExecutor(workers) as executor:
        res = executor.map(download_one, sorted(id_list))
    return len(list(res))

3.4. 메타 프로그래밍

파이썬에서는 상수, 변수, 함수, 클래스 등 모두 객체이기 때문에 매개변수로 사용할 수 있고, 수정할 수도 있다. 이러한 작업이 런타임에도 가능하다. 메타 프로그래밍이란 런타임에 프로그램 행위를 변경하는 작업을 말한다. 주로 데코레이터와 메타클래스를 활용한다. 메타클래스는 클래스 계층 구조 전체를 커스터마이징 할 수 있다. 반면에, 데코레이터는 클래스 하나에만 영향을 주며 서브클래스에는 영향을 주지 못하는 경우가 있다. 여기선 메타클래스 위주로 살펴본다.

메타클래스는 클래스를 생성하고, 클래스는 객체를 만든다. 파이썬 내의 모든 클래스는 메타클래스로 만들어진다. 메타클래스는 일종의 클래스 팩토리이다. 내장함수 type이 메타클래스이다. 클래스가 type을 상속받으면 메타 클래스가 된다. 내장함수 int와 최상위 클래스 object는 모두 메타 클래스로 생성된 것이다.

print(isinstance(int, type)) 
# True
print(isinstance(object, type)) 
# True

메타클래스를 이용하면 파이썬의 클래스 문을 가로채서 클래스가 정의될 때마다 특별한 동작을 제공한다. 싱글톤을 메타 클래스로 구현하는 예제를 보자. type을 상속받아 Singleton 클래스를 메타클래스로 만든다. 이를 MyClass에 적용해 오직 한 개의 인스턴스만 유지하도록 한다.

class Singleton(type):
    __instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls.__instances:
            cls.__instances[cls] = super().__call__(*args, **kwargs)
        return cls.__instances[cls]

class MyClass(metaclass=Singleton):
    pass

a = MyClass()
b = MyClass()
print(a is b)   
# True

파이썬에서 대표적인 메타클래스는 추상 클래스 abc의 메타클래스인 ABCMeta이다. 싱글톤과 같이 특별한 기질이 있는 경우를 제외하고는 메타클래스는 직접 정의하는 것은 지양한다15. 다음은 메타 프로그래밍 예시이다.

  • 클래스를 올바르게 정의했는지 검증할 때
  • 복잡한 클래스 계층을 만들 때, 스타일을 강제하거나 메서드를 오버라이드하도록 요구하거나 클래스 속성 사이에 철저한 관계를 두고 싶을 때
  • 동적으로 생성되는 속성을 가진 클래스를 만들 때. (이는 구조적 데이터(ex. json)를 처리하기 위해 사용 (데이터 랭글링 작업))
  • 런타임 때 코드를 생성할 때 (type 함수로 클래스를 동적으로 생성할 수 있음)

메타 프로그래밍을 효율적으로 하기 위해서는 임포트 타임과 런타임을 구분해야 한다. 임포트 타임에는 인터프리터는 .py 모듈에 들어 있는 소스 코드를 위에서부터 파싱하고 실행할 바이트코드를 생성한다.

3.5. 비동기 프로그래밍

비동기 프로그래밍은 병렬 프로그래밍과 흡사하게 여러 개의 작업을 동시에 처리하는 방식을 말하지만, 기다림이 필요한 네트워크나 파일 입출력을 좀 더 효율적으로 처리하는 데 최적화되어 있다.

비동기 프로그래밍은 하나의 쓰레드에서 오버헤드가 작은 비동기 함수들이 모두 수행되기 때문에 멀티쓰레딩과 멀티프로세싱보다 더 효율적이다. 멀티쓰레드와는 다르게 비동기는 하나의 쓰레드로 병행성을 만들기에 락(lock)을 관리할 필요가 없다. 비동기와 멀티쓰레드는 I/O-bound 작업에 , 멀티프로세싱은 CPU-bound 작업에 더 적합하다.

파이썬은 비동기 연산을 위해 asyncio 모듈과 async, await 키워드를 제공한다. 이들은 coroutine을 위해 설계됐고, 비동기 코드를 간략화하여 동기 코드와 흡사하게 읽기 쉽도록 했다.


4. 파이썬 운영 관리

4.1. 최적화 및 유용한 팁

다양한 최적화 기법은 필요성이 정당화되고 신중히 프로파일링해서 효과가 입증된 경우에만 사용되어야 한다.

최적화

  • 항목 수가 아주 많은 시퀀스는 set 형으로 구현하면 좋다. set 형은 항목이 들어 있는지 검사하는 과정이 최적화되어 있다.
  • 리스트 안에 숫자만 들어 있다면 배열(array.array)이 리스트보다 훨씬 더 효율적이다. C 배열만큼 가볍다.
  • 메모리뷰(memoryview) 내장 함수는 공유 메모리 시퀀스형으로 bytes를 복사하지 않고 배열을 다룰 수 있게 해준다. 즉, 버퍼 프로토콜을 사용한 제로 카피이다.
  • 리터널 집합 구문 (ex. {1, 2, 3})은 생성자 호출 방식(ex. set([1, 2, 3]) 보다 더 빠르고 가독성이 좋다. dis로 확인 가능하다.
  • 많은 양의 레코드를 처리하는 경우, 딕셔너리를 사용하는 것보다 튜플 또는 네임드튜플을 사용하면 메모리 사용량을 줄일 수 있다. 딕셔너리는 빠른 접근 속도를 위해 내부에 해시 테이블을 유지하므로 메모리 부담이 크다. 속도를 위해 공간을 포기한 셈이다.
  • 리스트가 매우 클 경우 제너레이터 표현식을 사용하는 것을 추천한다. 리스트 컴프리핸션은 리스트를 통째로 생성하기에 메모리에 부담이다.
  • 튜플은 CPython의 리스트보다 약간 적은 메모리를 차지하며 더 빨리 생성되지만(CPython의 tupleobject.c 와 listobject.c 참고), 사실 실제 성능 차이는 거의 없다.
  • 다이나믹 프로그래밍을 functools 의 lru_cache 데코레이터로 간단히 사용할 수 있다.
  • 속성이 몇 개 없는 수백만 개의 객체를 다룬다면, __slots__ 클래스 속성을 이용하면 메모리 사용량을 엄청나게 줄일 수 있다. 파이썬은 기본적으로 객체 속성을 __dict__ 이라는 딕셔너리형 속성에 저장한다. (그러나, 이러한 데이터를 자주 처리한다면 numpy나 pandas를 고려하자)

프로파일링

cProfile은 최적화를 위한 표준 라이브러리에 있는 도구이다. 다음과 같이 실행하면 각 함수가 얼마나 호출되었는지 실행 시간이 얼만지를 알려준다. 이를 통해 프로그램의 어느 부분에 실행 시간을 많이 소비하는지 파악할 수 있다.

$ python -m cProfile myscript.py

cProfile은 거시적 관점에서 성능을 봤다면, dis 내장함수는 미시적인 관점으로 특정 코드의 성능을 가늠할 수 있다. dis 함수는 매개 함수를 분해하여 파이썬 가상 시스템이 사용하는 바이트코드 명령들을 출력한다. 이를 성능 최적화에 활용할 수 있다.

import dis

def myfunction(x, y):
        return x ** y

dis.dis(myfunction)
#  3           0 LOAD_FAST                0 (x)
#              2 LOAD_FAST                1 (y)
#              4 BINARY_POWER
#              6 RETURN_VALUE

코드 라인별로 실행 시간을 측정해주는 line profiler 라는 라이브러리가 있다.

$ pip install line_profiler

분석을 수행할 함수에 @profile 데코레이팅을 한다. 그 후에 다음을 실행한다.

$ kernprof -l -v myscript.py

실행 시간 뿐만 아니라 memory_profiler를 이용해 메모리 사용량도 측정할 수 있다.

$ pip install memory_profiler
$ python -m memory_profiler myscript.py

유용한 팁

  • 내포된 리스트를 초기화할 때 동일한 리스트에 대한 참조를 조심하자. 예를 들어, [ [‘0’] * 3 ] * 5  가 아닌 [ [‘0’] * 3 for i in range(5) ] 처럼 매번 새로운 객체를 만들어야 한다.
  • del 명령은 이름을 제거하는 것이지, 객체를 제거하는 것이 아니다. 객체가 메모리에서 없어지도록 만드는 것은 참조의 존재 여부이기 때문에, 객체 참조 카운트가 0 이 되면 가비지 컬렉터가 해당 객체를 제거한다.
  • 모든 클래스에 적어도 __repr__ 메서드는 항상 추가하는 것이 좋다. 리턴값으로 모든 속성값을 명시하자. (기본 __str__ 구현은 단순히 __repr__을 호출하는 것이다)
  • 데이터를 디스크에 저장하거나 네트워크로 전송하기 때문에 데이터를 일렬로 빽빽하게 담아야 할 때는? struct.Struct 를 사용하자.
  • 언더바 두 개로 이름 맹글링으로 비공개 성격의 속성을 정의해 불필요한 접근을 막아보자. 이는 완벽한 보안 기능은 아니지만 실수로 접근하는 것을 막아준다.
  • 바이트로 변환하는 것을 인코딩, 그 반대는 디코딩이다. 기준은 컴퓨터에 있다.

4.2. 디버깅 & 예외처리

디버깅

  • 파이썬에서는 pdb 라는 대화식 디버거 모듈을 제공한다. import pdb; pdb.set_trace() 문으로 프로그램의 관심 지점에서 직접 파이썬 대화식 디버거를 시작할 수 있다.
  • 디버깅 출력용으로는 __repr__ 메소드를 사용하자. %r을 사용하여 1과 ‘1’의 차이를 주자. 파이썬 인터프리터는 __str__이 없는 경우 대책으로 __repr__ 메소드를 호출한다.
  • 람다는 함수에 이름이 없기에 스택 추적을 하기 어렵다. 따라서, 익명 함수는 디버깅과 에러 처리를 어렵게 하기에 유의해야 한다.
  • 단언문(assert)으로 내부 자체 검사(self-check)를 미리 정의된 조건으로 체크한다. 단언문은 어떤 조건을 테스트하는 디버깅의 핵심 보조 도구이다. 단언문으로 디버깅 작업이 상당히 빨라지고 유지보수하기 좋아진다. 단언문은 런타임 에러를 처리하기 위한 메커니즘이 아니라 디버깅을 돕는 일에 적합하다.

예외처리

  • try/except 다음에 else 블록을 넣으면 코드 양을 줄일 수 있고, try가 성공한 경우 실행할 코드를 시각적으로 구분해준다.
  • 예외처리로 의미가 모호한 None으로 출력하기 보다는 그냥 raise 함수를 사용해서 예외를 일으키자.
def divide(a, b):
    try:
        return a / b
    except ZeroDivisonError as e:
        raise ValueError('Invalid inputs') from e
  • 다양한 에러 타입을 상속받아 자신만의 에러 타입을 정의하자. 이는 코드 품질을 향상시키고 디버깅과 유지보수가 쉬워진다.
class NameTooShortError(ValueError):
    pass

def validate(name):
    if len(name) < 10:
        raise NameTooShortError(name)
  • 더 나아가서 자신의 예외들을 논리적으로 더 세분화된 계층 구조로 표현할 수도 있다. 그러나, 불필요한 복잡성이 생기기 쉬우므로 주의해야 한다.
  • 사용자 정의 클래스를 정의하면 1장에서 언급한 파이썬 스타일인 EAFP 코딩 스타일에 더 가까워질 수 있다.

4.3. 환경 구성

버전 및 경로

# 파이썬 인터프리터 실행파일 경로
$ python -c "import sys; print(sys.executable)"
/usr/bin/python
$ which python
/usr/bin/python
# 파이썬 버전
$ python -c "import sys; print(sys.version)"
'3.7.4 (default, Aug 13 2019, 20:35:49)
$ ls -al /usr/bin/python
lrwxrwxrwx 1 root root 24 Mar 27 05:02 /usr/bin/python -> /etc/alternatives/python3.7
# 파이썬 라이브러리 설치 경로
$ python -m site
sys.path = [
    '/usr',
    '/usr/anaconda3/lib/python37.zip',
    '/usr/anaconda3/lib/python3.7',
    '/usr/anaconda3/lib/python3.7/lib-dynload',
    '/usr/anaconda3/lib/python3.7/site-packages',
]
USER_BASE: '/home1/irteam/.local' (exists)
USER_SITE: '/home1/irteam/.local/lib/python3.7/site-packages' (doesn't exist)
ENABLE_USER_SITE: True
# 파이썬 라이브러리가 저장되는 site-packages 경로
$ python -c "import sys; print(sys.getsitepackages())"
# import 가 참조하는 모든 경로
$ python -c "import sys; print(sys.path)"
# sys.path 리스트에 새로운 경로 추가
import sys
sys.path.append('/new/path')

가상 환경

pip는 기본적으로 ‘전역’ 파이썬 환경에 설치한다. 문제는 모든 프로그램에서 단 하나의 버전의 패키지만 공유하게 된다. 동일한 패키지를 서로 다른 버전을 필요로 하는 여러 프로젝트가 있다면 문제가 된다. 이러면 버전 충돌이 있을 수 있고, 보안 위험도 발생할 수 있다. virtualenv로 가상 환경을 만들면 파이썬 환경을 격리시켜 의존성을 분리할 수 있다.

$ python3.7 -m venv ./venv
$ source ./venv/bin/activate
(venv) $ pip list
(venv) $ which pip
(venv) $ deactivate

PyPI 패키지 등록

PyPI (Python Package Index) 에 파이썬 패키지를 등록하면, pip 로 손쉽게 파이썬 패키지를 다운받을 수 있다.어떻게 PyPI 에 나의 파이썬 패키지를 등록하는지 알아보자.

(Step 1) 패키지 파일 구성

패키지를 등록할 수 있도록 다음과 같이 먼저 구성한다. 여기서는 pynori 를 예시로 들어본다.  (참고: https://github.com/gritmind/python-nori)

python-nori
    - pynori
        - __init__.py
        ...
        - dict
            - __init__.py
            ...
    - tests
        - __init__.py
    - .gitignore
    - LICENSE
    - MANIFEST.in
    - REAMEMD.md
    - requirements.txt
    - setup.cfg
    - setup.py
  • ‘__init__.py’ 을 각 폴더에 추가해서 폴더 구조에 맞게 모든 스크립트를 하나로 묶을 수 있도록 한다.
  • LICENSE 에 라이센스 정책 문서를 작성한다. (ex. Apache License)
  • MANIFEST.in 에 include 를 명시해서 패키지에 필요한 리소스성 파일의 경로를 추가한다.
  • setup.py : 패키지에 대한 정보를 작성한다.

(Step 2) 패키지 등록에 필요한 패키지 다운로드

$ python -m pip install --user --upgrade setuptools wheel
$ python -m pip install --user --upgrade twine
$ python setup.py sdist bdist_wheel

(Step 3) PyPI 등록

테스트용 test.pypi.org에 먼저 등록해보고 확인해본 다음 라이브에 등록하자.  

$ twine upload -r testpypi dist/*
$ twine upload dist/*


5. 각주

  1. EAFP 스타일과 반대로 LBYL (Look Before You Leap) 스타일이 있다. 이는 호출이나 조회를 하기 전에 전제 조건을 명시적으로 검사한다. 그래서 if 문을 많이 사용한다(ex. if key in mapping: return mapping[key]). C언어 스타일이기도 하다.
  2. 반복자(iterator)는 컬렉션에서 원소를 가져오기만 하지만 제너레이터는 원소를 생성할 수도 있다.
  3. 코루틴은 시뮬레이션, 게임, 비동기 입출력, 그 외 이벤트 주도 프로그래밍이나 협업적 멀티테스킹 등의 알고리즘을 자연스럽게 표현한다. – PEP 342.
  4. 데코레이터라는 명칭은 구문 트리를 파싱하고 애너테이션하는 컴파일러 분야에서의 용법과 관련이 더 깊다.
  5. __len__ 과 __getitem__ 메서드는 불변형이다. __setitem__ 메서드를 추가해서 가변형으로 만들 수 있다. 가변형이면 random.shuffle 함수를 사용할 수 있다. 이미 생성된 객체에 __setitem__ 메서드를 실행할 수도 있는데, 이를 멍키 패칭(monkey patching)이라고도 한다.
  6. 프로토콜은 어떤 역할을 완수하기 위한 메서드 집합을 말한다. 프로토콜은 상속과 무관하다. 인터페이스는 시스템에서 어떤 역할을 할 수 있게 해주는 객체의 공개 메서드의 일부로 설명할 수 있다.
  7. 네임드튜플은 튜플과 동일한 크기의 메모리만 사용하고, 리스트의 던더 메소드를 그대로 사용한다. 네임드튜플은 속성을 객체마다 존재하는 __dict__ 에 저장하지 않으므로 일반적인 객체보다 메모리를 적게사용한다.
  8. 자바에서 기본 자료형은 call by value 방식으로도 호출한다.
  9. 속성 이름 앞에 밑줄을 2개 붙이면, 앞에 ‘_{클래스명}’ 이 덧붙여져 변경된다.
  10. __base__ 메소드로 상위 클래스를 확인하고, __mro__ 메소드로 상속 계층을 확인할 수 있다.
  11. 그래도 곱할 때는 reduce를 사용한다.
  12. 선점형 멀티스레딩은 한 스레드가 다른 스레드를 인터럽트해서 프로그램 제어를 얻는 일을 말한다.
  13. 시스템 호출은 파이썬 프로그램에서 외부 환경과 대신 상호 작용하도록 OS에 요청하는 일이다.
  14. 파이썬 코드로는 GIL을 제어할 수 없지만 내장 함수나 C로 작성된 확장은 GIL을 해제할 수 있다. 하지만, 라이브러리 코드가 상당히 복잡해지므로 이런 방식을 사용하진 않는다.
  15. 메타클래스는 어렵지만, 흥미롭고 때로는 똑똑해 보이고 싶어 하는 프로그래머들이 남용하기도 한다. 배포용 코드에서 절대로 ABC나 메타 클래스를 직접 구현하지 말라(알렉스 마르텔리).

6. 참조

  1. 전문가를 위한 파이썬 (Fluent Python) – 루시아누 하말류 저/ 강권학 옮김
  2. 파이썬 코딩의 기술 (Effective Python) –  브렛 슬라킨 저/ 오현석 옮김
  3. 실전 파이썬 프로그래밍 (The Hacker’s Guide to Python) – 줄리안 단주 저/ 김영후 옮김
  4. 슬기로운 파이썬 트릭 (Python Tricks) – 댄 베이더 저/ 전석환 옮김
  5. 한권으로 개발자가 원하던 파이썬 심화 A to Z – 문용준,문성혁 저
  6. 객체지향 사고 프로세스 (The Object-Oriented Thought Process) – 맷 와이스펠드 저/ 박진수 옮김