Introduction - What is PEP?
PEP는 Python Enhancement Proposal의 약자이다.
즉, 파이썬이 발전하는 과정 중에 논의된 내용들을 정리한 문서들을 의미한다.
Python을 사용하는 유저라면 필독이 권장되는 중요한 문서부터, 한 번쯤 읽어볼만한 문서까지 여러 종류가 있다.
그런 PEP 문서 중에서 몇 가지 흥미를 끄는 것을 읽고 정리를 해보려고 한다.
(사실 Style을 다루는 PEP 8, Docstring 컨벤션을 다룬, PEP 257이 메인 목표고 나머지는 잘 모르겠다.)
다른 PEP에 대해서는 PEP-0000을 참고하자.
A Foolish Consistency
파이썬을 비롯해 많은 언어에서 코드를 작성하는 것보다 읽는 것이 더 많을 것이라고 가정한다.
파이썬의 스타일 가이드는 파이썬 코드의 가독성(readability)과 일관성(consistency)을 유지하고, 향상하기 위한 방법론이다.
따라서, 이 스타일 가이드를 따르기 위해 기존 모듈이나 함수에서 따르던 스타일 가이드를 망치는 것은 PEP 문서에서도 권하지 않는다.
이 문서에서는 스타일 가이드를 따르지 않아도 되는 몇 가지 경우에 대해 소개해준다.
- 가이드 라인을 적용하는 경우, 코드가 더 읽기 어려워지는 경우
- 주변, 혹은 이전 코드와 일관성을 유지해야 하는 경우
- 스타일 가이드에서 권장하지 않는 기능을 지원하지 않는 이전 버전의 파이썬과의 호환성을 유지해야 하는 경우
대부분 굳이 스타일 가이드를 도입해서 일관성을 망가뜨려 가독성을 떨어뜨리는 경우이다. 꼭 이런 경우가 아니더라도, 팀 내부 스타일 가이드와 충돌하는 경우 등 다양한 상황이 있을 것이다.
Code Lay-out
Indentation
기본적으로 Indentation은 4 spaces를 쓴다.
파이썬은 def, if 등 여러 구문의 구분을 위해서 indentation을 쓰지만, 그 외에도 (), {}, [] 같은 괄호를 사용할 때, indentation을 사용한다.
이런 괄호 안의 줄들은 1) Python’s implicit line joining 2) hanging indent 를 사용해서 수직으로 정렬해야 한다.
Implicit line joining이 뭔데?
파이썬 코드 작성할 때, \ 없이도 연속된 줄이라는 것을 인식하는 것을 말한다.
# Error occur!
result = 10 + 20
+ 30 + 40
# Correct Code
result = 10 + 20 \
+ 30 + 40
반면, () {} [] 을 사용하면 \ 없이도, 코드가 연속된 것이라는 것을 알게 된다.
Hanging Indent
PEP 8 Style guide에서 권장하는 스타일 중 하나로, 여는 괄호 ( { [ 다음 두 번째 줄 부터 들여쓰기 및 변수 등을 넣는 스타일이다.)
Correct Use Case
# Python’s Implicit Line Joining foo = bar(var_one, var_two, var_three, var_four) # Hanging Indent def func_name( var_one, var_two, var_three, var_four): print(var_one)Wrong Use Case
# Python’s Implicit Line Joining # vertically align 되어야 한다. foo = bar(var_one, var_two, var_three, var_four) # Hanging Indent # variable과 function content 구분이 어려우므로 further indentation이 필요하다. def func_name( var_one, var_two, var_three, var_four): print(var_one)
조건문의 Indentation
if문 내부의 조건이 너무 길어 나누게 된다면, 자연스럽게 indentation이 들어가게 되는데, 이 경우 if 문 내부의 내용과 겹치게 된다.
if (condition1 and
condition2):
do_something()
따라서, 이를 구분하기 위해 여러 방법을 쓸 수 있다.
# 1. Add a comment
if (condition1 and
condition2):
# this function does blah blah
do_something()
# 2. Add some extra indentations
if (condition1 and
condition2):
do_something()
# 2.1. Add some extra indentations with hanging indent
if (
condition1 and condition2
condition3 and condition3
):
do_something()
최대 글자 수
한 라인 당 최대 79 characters를 권장하며, docstring이나 comment같은 경우는 72 char를 권장한다고 한다.
다만, 팀 내 합의가 있다면 99 char도 상관이 없다고 한다.
이렇게 제한하는 이유는 IDE나 여러 툴에서 여러 탭을 놓아서 비교하기 편하게 하기 위해서라고 한다.
코드 리뷰나 유지보수 툴들 내에서 사용편의성을 위해서 권장하는 내용이다.
(다만, 다른 책에서는 최근 모니터나 다른 툴들의 발전으로 인해 더 늘어나도 상관이 없다고 한다. 즉, 앞서 말했듯, 팀 내 합의나 사용자 편의성을 위해 꼭 지켜야 하는 룰은 아니라는 것이다.)
assert나 with문 등에서 \를 통해 라인을 나눠 위의 컨벤션을 잘 맞출 수 있다고 한다.
Line Break Before/After a Operator?
라인을 나눌 때, operator를 쓰고 나눌지, 아니면 나누고 쓸지에 대해 PEP8는 라인을 나누고 쓰라고 한다.
# Wrong Line Break
result = wage +
number +
loan_interest
# Correct Line Break
result = wage
+ number
+ loan_interest
Blank Lines
파일 내 class, method를 나누기 위한 빈 줄의 갯수까지 제안해준다.
- Class, Top-level function: 2 blank lines로 둘러싼다.
- member function, variable: single blank line으로 둘러싼다.
class Person:
def say_hello():
do_something()
def say_goodbye():
do_something()
# After 2 blank lines, define new class/top-level function
# TBH, It is better to define new class in new file
# unless it is deeply related to each other.
class Man:
def something():
pass
def nothing():
pass
Source File Encoding
별 다른 이유가 없다면 파이썬 내 인코딩은 UTF-8을 쓰도록 한다.
(별 다른 이유의 예시: Non-UTF-8 문자에 대한 테스트)
다시 말하면 그냥 UTF-8 을 쓰라는 소리다.
여기에 더해, Python Standard Library의 경우에는 Non-ASCII Identifier를 쓰지 말아야 하며, 무조건 영어를 써야 한다고 한다. Python Standard Library에 컨트리뷰션 할 때가 되면 기억하도록 하자.
Import Convention
Import에 대한 가이드도 있는데, 한 줄에 하나씩 임포트하며, 만약 같은 라이브러리에서 가져온다면 여러 개도 허용 된다고 한다.
# Correct Case
import os
import sys
from math import pow, sqrt
# Wrong Case
import os, sys
또, import하는 순서는 아래와 같다.
- Standard Library
- Third Party Library
- Local app, library specific imports
Absolute Import vs. Explicit Relative Import
대부분의 경우 Absolute Import를 장려하며, Standard Library에서는 무조건 Absolute Import를 사용해야 한다.
반면, 자신의 패키지가 복잡하게 구성되어, Absolute import를 쓰는 경우가 오히려 복잡해지는 경우, explicit relative import를 사용한다고 한다.
# Absolute Import
import os
import sys
from mypkg.family import Sibling
# Explicit Relative Import
from ./family import Silbing
단, from os import *과 같은 wild-card import는 권장하지 않는다.
그 이유는 어떤 것이 추가되는지 모르기 때문에 name space 내 충돌이나, automated tools 내에서의 충돌을 감지하기 어렵기 때문이다.
허용되는 경우도 있다. 조금 특별한 경우일 수 있겠지만, 내부 인터페이스를 public하게 노출하는 경우이다.
말 그대로, 문제가 되는 필요하지 않은 것들이 무차별적으로 import되는 것이다. 반면, 해당 파일 내의 인터페이스들을 모두 사용하거나, 로딩해야 하는 경우에는 허용된다는 것이다.
예시 몇 가지를 통해서 자세히 알아보자.
내부 인터페이스를 퍼블릭으로 노출해야 하는 경우
기본적으로 특정 모듈이 python 구현체로 존재하지만, 일부 함수에 대해 C로 최적화 구현된 모듈도 존재하는 경우
동일한 function에 대해 최적화된 결과를 내부적으로 결정해서 호출할 수 있다.
try: from optimized_c_version_module import * except: from pure_python_version_module import *plug-in 시스템에서 자동으로 로딩할 때
플러그인 시스템을 만들 때, 특정 폴더 내의 모듈을 자동적으로 로드해야 할 경우가 있다고 한다.
# plugins/__init__.py import glob import importlib # plugins 폴더 안의 모든 Python 파일을 찾음 modules = glob.glob("plugins/*.py") for module in modules: module_name = module[:-3].replace("/", ".") # "plugins.module_name" 형태로 변환 imported_module = importlib.import_module(module_name) globals().update(imported_module.__dict__) # Wildcard Import처럼 모든 요소를 가져옴위의 코드 대신
from plugins import *를 통해 간단하게 자동적으로 import할 수 있다. 대신
__all__가 정의되어 있어야 한다.__all__을 사용해 공개 API를 지정할 때# my_module.py __all__ = ["function_a", "function_b"] # Wildcard Import 시 이 리스트에 있는 것만 공개 def function_a(): return "Function A" def function_b(): return "Function B" def internal_function(): return "Internal Function (숨겨짐)"대형 패키지의 서브 모듈을 한 번에 관리/사용 할 때
my_package/ │── __init__.py │── module_a.py │── module_b.py# __init__.py (패키지 진입점) from .module_a import * from .module_b import * __all__ = ["func_a", "func_b"] # 네임스페이스를 제한# main.py from my_package import * # func_a와 func_b만 import됨 print(func_a()) # ✅ 정상 작동 print(func_b()) # ✅ 정상 작동
Module-level DUnder Names
DUnder는 _가 두 개 둘러 쌓여진 __all__, __init__등을 말한다.
이것들이 놓여지는 순서는
- Docstring of File
__Future__- normal DUnders
- import
이다.
"""This is the example module.
This module does stuff.
"""
from __future__ import barry_as_FLUFL
__all__ = ['a', 'b', 'c']
__version__ = '0.1'
__author__ = 'Cardinal Biggles'
import os
import sys
What is
__future__?하위 python version에서 상위 버전의 내용을 사용할 수 있도록 하는 모듈이다.
예를 들어, print의 경우, python 2에서
print "hello World"로 표현했다. (assert 문처럼)python 3에서는
print("hello World")로 표현하는데,python2에서 import를 통해 가능해진다.
# in python 2.0 from __future__ import print_function print("hello World") # --> possible즉, python의 구 버전에서 default로 세팅된 값들을 신버전에서 기본값으로 사용될 것들로 바꿔주는 역할이다.
이를 통해서, 아래의 효과를 꾀할 수 있다.
- 기존 기능이 새로운 기능과 충돌이 일어나는지 체크할 수 있다.
- 정식 출시 이전에 일부 기능을 미리 사용해볼 수 있다.
String Quotes
python에서 'hello'와 "hello"는 차이가 없다.
하지만, 하나를 정해서 사용하는 것을 권장하며, \ 를 사용하는 경우에만 다른 하나를 사용하는 것을 권장한다. 'she said \"hi\"'
다만, docstring에서는 "를 사용하는 것을 기본으로 한다.
Whitespace in Expressions and Statements
이 섹션에서는 코드 내에서 공백(=스페이스)를 어떻게 써야하는지에 대해 알려준다.
나는 대체로 스페이스에 후한 편이어서 남발하는데 좀 줄여야 할 것 같다.
() {} [] : ; ,
(), {}, []의 뒤에는 바로 문자가 나와야 한다.# Correct func(ham[1], {eggs: 2}) # Wrong func( ham[ 1 ], { eggs: 2 })() []를 function call, indexing, slicing으로 쓰는 경우, 이전 문자 바로 뒤에 붙어서 나와야 한다.# Correct spam(1) dct['key'] = value # Wrong spam (1) dct ['key'] = valuecomma(,) 뒤에는 바로 닫는 괄호가 나와야 한다.
# Correct foo = (0,) # Wrong foo = (0, ); : ,는 이전 문자 바로 뒤에 붙어서 나와야 한다.# Correct if x == 4: print(x, y); x, y = y, x # Wrong if x == 4 : print(x , y) ; x , y = y , x단,
:는 slice에서 binary operator와 같은 역할을 하기 때문에,:의 양쪽에서 동일한 만큼의 공백을 줘야 한다. (단, slice parameter가 생략되는 경우에는 공백을 사용하지 않는다.)# Correct: ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] ham[lower:upper], ham[lower:upper:], ham[lower::step] # Okay with no space ham[lower+offset : upper+offset] # either okay with same amounts of space in both side. ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] # Wrong: ham[lower + offset:upper + offset] ham[1: 9], ham[1 :9], ham[1:9 :3] ham[lower : : step] ham[ : upper]
Operators
변수 선언 시, alignment를 위해 2개 이상의 공백을 쓰지 않는다. 이는 C나 다른 언어와는 또 다른 것 같다.
# Correct x = 1 long_variable = 6 # Wrong x = 1 long_variable = 6line continuation을 위한
\뒤에 공백을 넣는 경우 compile error가 나온다.# Compile Error bcs of the space following \ a = 6 + \ 1Binary operator의 양쪽에 single space를 넣을 것
assignment(
=), augmented assignment(+=, -=), comparison(==, <>, in, …etc), Boolean(not…ect), even mathmatical operator(+, ..etc)에 대해 single space를 넣으면 좋다.단, 다른 priority를 가진 operator들이 섞이는 경우에는 lowest priority를 가지는 operator에게만 공백을 준다. 하지만 매우 복잡해지면 사용자의 재량에 맡긴다고 한다.
# Correct: i = i + 1 submitted += 1 x = x*2 - 1 hypot2 = x*x + y*y c = (a+b) * (a-b) # Wrong: i=i+1 submitted +=1 x = x * 2 - 1 hypot2 = x * x + y * y c = (a + b) * (a - b)Function annotation에 대해서도 양쪽에 single space를 넣어주자.
# Correct def func(s: str) -> int: ... # Wrong def func(s: str)->int:단,
=이 function의 keyword argument에 쓰이거나, default value notation에 쓰이는 경우, 공백을 쓰지 않는다.# Correct: def complex(real, imag=0.0): return magic(r=real, i=imag) # Wrong: def complex(real, imag = 0.0): return magic(r = real, i = imag)근데 또, argument annotation (argument typing hint)와 결합되는 경우에는 space를 쓴다고 한다. 알쏭달쏭한 PEP세상..
# Correct: # format of keyword: type = default_value or, keword=default_value def munge(sep: AnyStr = None): ... def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ... # Wrong: def munge(input: AnyStr=None): ... def munge(input: AnyStr, limit = 1000): ...if/for/while의 same line에 바로 내용 넣는 것을 지양할 것. (단 적은 내용은 상관없다.)
대신, 그러한 절들이 여러 개가 나오거나 복잡한 경우에는 하지 말 것# Wrong: if foo == 'blah': do_blah_thing() for x in lst: total += x while t < 10: t = delay() # Wrong: if foo == 'blah': one(); two(); three()Compound statement도 동일하다.
# Wrong one(); two(); three(); ... n()
When to Use Trailing Commas
주로 파이썬에서 comma(,)를 사용하는 것은 tuple을 만들 때, 혹은 여러 값을 동시에 사용할 때이다.
pair = (1,2) # tuple
x = (1) # int
first, second = 1, 2
하지만, 이 섹션에서 말하는 Trailing Comma는 list, tuple, dictionary의 끝에 나오는 ,를 말한다.
score_list = [
100,
96,
30, # <--- Here's the trailing comma
]
이 trailing comma를 사용하는 이유는 Git같은 versioin control system에서 유용하기 때문이다.
trailing comma가 없는 상태에서 새로운 데이터를 추가한다고 하자.
# list without trailing comma
score_list = [
100,
96,
30
]
# new data added
score_list = [
100,
96,
30,
70
]
이 경우, 30 라인이 삭제되고, 30, 라인과 70라인이 추가된다.
사실은 70데이터 하나만 추가된 것인데 말이다. 이러한 차이는 소스코드의 차이를 확인하는데 불편함을 준다.
따라서, 시스템 상으로 추가된 혹은 삭제된 데이터만 확인하기 위해서 trailing comma를 사용한다.
그렇다면 이 trailing comma는 어떻게 표현해야 좋을까?
# Correct:
FILES = [
'setup.cfg',
'tox.ini',
]
initialize(FILES,
error=True,
)
# Wrong:
FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)
'Python' 카테고리의 다른 글
| [PEP series] PEP-008 Style Guide (2) (0) | 2025.02.02 |
|---|