PEP8

파이썬을 배우고 있거나 사용하는 개발자라면 대부분 PEP8 에 대해 들어 봤거나 한두 번씩은 읽어 봤을 것이다. PEP8은 파이썬 코드 스타일에 대한 가이드이다.

파이썬 개발자라면 대부분 이 PEP8을 준수하려고 노력할 것이다. 하지만 오랜 시간 파이썬으로 개발해 온 개발자라도 PEP8의 내용을 전부 기억하고, 지키는 것은 힘들 수 있다.

그래서 보통 코드 스타일을 자동으로 검사해주는 flake8 이나 pycodestyle 같은 도구들을 함께 사용한다. 하지만 이 도구들은 코드 스타일을 체크해주는 역할만 하기 때문에 코드를 고쳐야 하는 것은 개발자 스스로 수행해야 한다.

처음 파이썬을 배울 때는 이런 식으로 직접 검사하고, 교정하는 것이 학습에 도움이 될 수 있지만, 코드량이 많거나 다른 개발자들과 협업하는 프로젝트에서 실수로 수정되지 않은 스타일의 코드가 커밋될 위험이 있다.

아래에서는 파이썬 코드 스타일을 자동으로 검사하고, 교정해주는 포맷터(formatter)에 대해 알아볼 것이다.


Black

Black은 파이썬 소프트웨어 재단(PSF)에서 개발하고, PEP8을 기반으로 가독성이 더 좋은 코드 스타일로 고쳐 주는 자동 포맷터이다.
Black 말고도 비슷한 기능을 가진 코드 포맷터로는 autopep8과 구글에서 개발한 yapf도 있다. 하지만 Black은 다른 코드 포맷터보다 좀 더 적극적으로 코드 스타일을 교정하는 편이다.

Black 공식 Github에는 “타협하지 않는”, “단호한” 의미를 풍기는 Uncompromising Code Formatter라고 소개하고 있으며, 개발자가 코드 스타일을 고치기 위해 고민하고 결정하는 시간을 절약할 수 있고 코드 스타일로 생기는 사소한 갈등을 방지해서 더 중요한 문제 해결에 집중할 수 있게 해준다.


설치와 실행하기

파이썬 패키지 관리 도구(ex. PIP)로 pip install black 명령을 실행해서 설치할 수 있다. Black은 커맨드 라인 도구(CLI)이기 때문에 아래와 같이 black 명령어와 대상 파일 또는 디렉터리를 지정해서 코드 스타일 검사와 교정 대상을 지정할 수 있다.

black {소스코드 파일 경로 또는 디렉터리}

본인의 파이썬 개발 환경에서 사용하는 IDE나 Editor에 Black을 바로 실행할 수 있도록 설정 해두면 더 편하고 빠르게 코드 스타일을 보정할 수 있다.


Black의 코드 스타일 알아보기

(1) 라인을 다루는 규칙

문장 끝에 남아 있는 공백은 제거 해주고, 한 줄에 완전한 표현식이나 문장을 간단하게 만들려고 한다. 하지만 한 줄이 88자를 초과하게 되면 줄로 내린 뒤 들여쓰기로 처리한다.

# 원본
def sample_function(template, file_path, header, debug=None, db_sync=False, *args, **kwargs):
    pass
def sample_function(
    template, file_path, header, debug=None, db_sync=False, *args, **kwargs
):
    pass

만약 그래도 88자가 넘으면 콤마로 구분된 실행 파라미터들을 하나씩 분해해서 위와 동일한 규칙을 적용한다

def sample_function(
    template,
    file_path,
    title,
    contents,
    header,
    debug=None,
    db_sync=False,
    *args,
    **kwargs
):
    pass


(2) 라인 길이

위에서 라인을 다룰 때도 적용되었지만 Black은 기본적으로 라인당 88자를 규칙을 사용한다. 이 숫자는 PEP8의 80(또는 79)자에서 10%를 더한 숫자이다. 88자를 사용하는 이유는 80자 또는 79자를 고집하는 경우보다 더 짧은 파일(.py)을 만들어 낸다고 한다.

나도 예전부터 PEP8의 80자 제안은 잘 지키지 않는 편이었다. 한 줄에 완전한 문장이 다 들어올 때 가독성이 더 높은 것도 있고, 요즘 같이 고해상도 모니터에서 굳이 80자만 볼 필요가 있을까 싶었다. 하지만 Black의 88자 규칙도 나쁘지 않다고 생각하는 이유는 한 줄의 길이가 길면 Diff를 할 때 확실히 비효율적이긴 하기 때문이다.

만약 Flake8을 사용한다면 max-line-length 설정을 88자로 설정하거나 E501 에러를 제외시키고, flake8-bugbear를 함께 사용해서 B950을 추가하는 것을 추천한다.

아래 .flake8 설정을 참고하자.

[flake8]
max-line-length = 80
...
select = C,E,F,W,B,B950
ignore = E203, E501, W503


(3) 빈 줄(Empty Line)

PEP8에는 자주 빈 줄을 추가하는 것을 권장하지 않는다. 클래스 레벨 Docstring 다음에 처음 나오는 속성이나 메서드 사이에는 1개의 빈 줄을 추가한다.


(4) Trailing Comma

라인 끝마다 콤마를 추가하는 것이 기본이다. 하지만 한 줄에 딱 맞는 표현식인 경우에는 콤마를 붙이지 않는다. 이렇게 하면 88자로 제한된 길이를 초과하지 않을 가능성이 1% 증가 된다고 한다.

튜플(tuple)의 경우 단 1개의 요소만 갖고 있다면 마지막 콤마를 제거하지 않는다. 그 이유는 데이터 타입이 변경될 위험이 있기 때문이다.

bar = (1,)
foo = (1)

print(type(bar))  # <class 'tuple'>
print(type(foo))  # <class 'int'>


(5) 문자열

작은따옴표(‘)보다 큰 따옴표(“)를 사용한다. 큰 따옴표로 빈 문자열을 표현하면 글꼴이나 코드 하이라이팅에 관계없이 큰 따옴표 1개와 혼동되지 않는다.

기존에 작은따옴표를 컨벤션으로 정했는데 Black을 사용하려고 하면 어쩔 수 없이 큰 따옴표로 규칙으로 바꿔야 한다. 이 과정에서 의견이 분분할 수 있는데 이때는 다음과 같이 해보는 것을 제안하는 것도 좋을 것 같다.

키보드에서 작은따옴표를 입력하는 게 큰 따옴표를 입력하기 위해 Shift키를 함께 누르는 것보다 편하고 빠르기 때문에 코딩할 때는 작은따옴표를 사용하자. 그리고 IDE에 Black을 자동 실행하도록 설정하거나 Commit 전에 Black으로 자동 교정하는 것이다.


(6) 줄 바꿈과 이항 연산자

Black은 이항 연산자 전에 줄 바꿈을 추가한다. 개정된 PEP8에 따르면 이항 연산자 전에 줄 바꿈 하는 것이 가독성이 더 좋다고 한다.
하지만 Flake8을 사용한다면 W503 line break before binary operator 경고가 발생할 수 있는데 W503은 PEP8에 맞지 않기 때문에 flake8에 W503을 무시하도록 설정해야 한다.

# 원본
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)
# Black 실행 결과
income = (
    gross_wages
    + taxable_interest
    + (dividends - qualified_dividends)
    - ira_deduction
    - student_loan_interest
)


(7) 슬라이스

문자열이나 리스트를 슬라이스 할 때 사용하는 콜론(:)은 이항 연산자와 동일하게 양쪽에 동일한 양의 공백이 있어야 한다. 하지만 슬라이스에 매개변수가 없는 경우에는 공백이 생략된다.

bar = [1, 2, 3, 4, 5]

foo = bar[2 - 1 : -1]


(8) 괄호

파이썬 문법은 선택적으로 괄호를 사용할 수 있다. Black은 기본적으로 한 줄에 완전한 문장이 들어올 수 있거나 내부 표현식이 구분자로 더 쪼개질 수 없다면 괄호를 생략한다.


(9) 호출 체인

ORM 같은 API들은 호출 체인을 활용할 수 있다. 보통 이러한 코드 스타일을 Fluent Pattern이라고 한다. Black은 호출 체인에서 각 메서드 호출 앞에 붙는 점(.)을 기준으로 코드 스타일을 교정한다.

# 원본
manage = OrderManage.objects.select_related(
    'order_item_no', 'order_item_no__brand_no',
    'order_delivery_no', 'order_delivery_no__order_no', 'order_delivery_no__order_no__user_grade',
    'order_item_no__item_no', 'order_item_no__option_no', 'order_no'
).prefetch_related(
    Prefetch(
        'order_delivery_no__details',
        queryset=OrderDeliveryDetail.objects.select_related(
            'delivery_company_no'
        ).filter(is_deleted=const.FALSE, is_cancel=const.FALSE).all()
    )
).filter(is_deleted=False).order_by('-order_item_no__brand_no')
# Black 실행 결과
manage = (
    OrderManage.objects.select_related(
        "order_item_no",
        "order_item_no__brand_no",
        "order_delivery_no",
        "order_delivery_no__order_no",
        "order_delivery_no__order_no__user_grade",
        "order_item_no__item_no",
        "order_item_no__option_no",
        "order_no",
    )
    .prefetch_related(
        Prefetch(
            "order_delivery_no__details",
            queryset=OrderDeliveryDetail.objects.select_related("delivery_company_no")
            .filter(is_deleted=const.FALSE, is_cancel=const.FALSE)
            .all(),
        )
    )
    .filter(is_deleted=False)
    .order_by("-order_item_no__brand_no")
)


맺으며

코드 스타일 가이드는 어떤 프로그래밍 언어이던지 중요하다. 왜냐하면 대부분의 코드는 여러 개발자들에게 공유되고 전파되기 때문에 마치 같은 사람이 작성한 것 같이 일관성 있게 관리하는 것이 가독성과 생산성에 중요한 역할을 줄 수 있다.

자신이 속한 그룹이나 개인마다 선호하는 코드 스타일 가이드를 정하는 것도 좋지만, 가장 중요하게 생각하는 가치는 “가독성”이다. 그렇기 때문에 보편적으로 따르는 관행적인 코드 스타일 규칙을 채택하는 것이 고민과 갈등을 줄이는 방법이 될 수 있다.