해당 포스팅은 PEP 8과 파이썬 클린 코드 책을 참고하며 작성했습니다.
파이썬을 공부하다가 "내 코드는 괜찮을까?"라는 생각이 머릿속을 스쳐지나갔습니다. "진짜 이대로 취업하면 팀에 도움이 될 수 있을까?"라는 고민과 아무리 살펴봐도 마음에 들지 않는 코드들 속에서 허우적 거리는 제 모습을 볼 수 있었습니다. 어떻게 "이 코드를 사람들이 읽을만한 코드로 바꿀 수 있을 것인가?"에 대해서 오늘 이야기 해보려고 합니다.
포스팅에서는 아래의 내용을 다루고 있습니다.
- 클린코드란 무엇인가
- PEP-8에 대하여
- 클린코드를 만들기 위한 라이브러리 소개(Pylint)
1. 클린 코드란 무엇일까?
클린 코드에 대한 유일하고 엄격한 정의는 존재하지 않습니다. 또한 클린 코드를 측정할 방법도 없을 것입니다. "아니! 클린 코드에 대한 정의도 없고 측정할 방법도 없으면 잘 돌아가는 코드면 괜찮은 거 아니야?"라고 생각할 수도 있을 것 같습니다. 저자는 클린 코드의 본질은 다른 엔지니어가 코드를 읽고 유지 관리할 수 있는지 여부에 달려 있다고 표현하고 있습니다.
예를 들어, 자전거를 타고 A라는 목적지에 가야된다고 해봅시다. 우리는 이미 목적지에 가본 경험이 있기 때문에 30분이 걸린다는 것을 알고 있습니다. 그러면 우리는 A라는 목적지에 가기 위해 30분 전에 출발하게 되겠죠. 근데 내가 아는 길이 공사중이라면, 다른 길로 돌아서 가야되고, 길이 포장이 안되어 있는 경우에는 자전거가 펑크가 날수도 있겠죠. 결과적으로 우리는 A라는 목적지에 시간 안에 도착하지 못할 수 있습니다. 위의 사례는 우리가 개발하는 것과 비슷하다고 볼 수 있습니다. 항상 도로를 닦아놔야 예상 시간 안에 도착할 수 있듯이, 코드가 유지보수가 가능하고 가독성이 높아야 정해진 시간 안에 일을 마무리될 수 있습니다.
만약에 도로를 닦는 작업을 제대로 하지 않았다면, 길 위에서 다양한 사고들로 인해 지체될 수도 있는 것처럼 새로운 요구가 추가될 때마다 리팩토링해야 하는 어려움이 존재합니다. 저자는 코드의 상태를 부채에 비유하며 아래와 같이 이야기합니다.
코드는 지금 바꾸는 것보다 미래에 변경하는 것이 어렵기 때문에 부채이다. 부채는 이자를 유발한다.
기술 부채가 발생했다는 것은 내일은 코드를 수정하기가 더 어렵고 비싸며 내일모레는 더더욱 비싸질 것이다.
그렇다면 부채를 쌓지 않기 위해 어떻게 해야할까요? 기본적으로 파이썬에서는 PEP-8이 기본적인 안내서가 될 것입니다. 하지만, 저자는 PEP-8 표준을 100% 준수하더라도 클린 코드의 요건을 충족하지 못할 수도 있다고 이야기 하며, 클린 코드는 유지보수성이나 소프트웨어 품질에 관한 것이라고 이야기하고 있습니다. 직장에서 개발 중이시라면, 직장 내에서 표준화된 구조를 사용하신다면, 그것을 코드에 반영하는 것이 중요할 것 같습니다. 그렇기 때문에 PEP-8을 무조건 따라하는 것이 좋지 않을 수도 있다는 것을 말씀드립니다. 하지만, 저는 아직 직장인이 아니기 때문에 파이썬의 표준 코딩 스타일 가이드인 PEP-8을 적용해보고자 합니다.
2. PEP-8
설명하기에 앞서서 PEP-8은 말그대로 코딩스타일 가이드입니다. 법처럼 정해진 것이 아니라는 것을 말씀드리고, PEP-8에서도 PEP-8과 해당 프로젝트 가이드가 충돌할 경우에는 당연히 프로젝트의 가이드를 따르라고 이야기합니다. 아래의 내용은 가이드의 전문을 담아내지는 못했습니다. 추가적으로 궁금하신 것이 있다면 여기를 클릭해주세요!
1) 들여쓰기
들여 쓰기에서는 4개의 공백(space)을 사용하십시오. 공백(space)은 선호하는 들여 쓰기 방법입니다. 탭(tab)은 탭(tab)으로 이미 들여 쓰기 된 코드와 일관성을 유지하기 위해서만 사용해야합니다. Python 3은 들여 쓰기를 위해 탭과 공백을 혼합하는 것을 허용하지 않습니다.
# correct
# 매개변수를 한줄에 나열할 수 없으면, 여는 괄호를 기준으로 정렬
foo = long_function_name(var_one, var_two,
var_three, var_four)
# 나머지와 인수를 구별하기 위해 4 개의 공백을 추가합니다.
def long_function_name(
var_one, var_two, var_three,
var_four):
print(var_one)
# 이어지는 코드는 들여쓰기를 추가해야합니다.
foo = long_function_name(
var_one, var_two,
var_three, var_four)
# wrong
# 한 줄에 인수가 다 포함이 안될 때, 첫 번째 줄의 인수는 넣지 않습니다.
foo = long_function_name(var_one, var_two,
var_three, var_four)
# 들여 쓰기를 구별 할 수 없기 때문에 더 들여 쓰기가 필요합니다.
def long_function_name(
var_one, var_two, var_three,
var_four):
print(var_one)
#optional
# 내어 쓰기는 4 칸 이외의 공백으로 할 수 있습니다.(예시는 2칸)
foo = long_function_name(
var_one, var_two,
var_three, var_four)
# if문 예시
# 추가적인 들여쓰기 없음
if (this_is_one_thing and
that_is_another_thing):
do_something()
# if구문과 본문을 구분하는 주석 추가
if (this_is_one_thing and
that_is_another_thing):
# Since both conditions are true, we can frobnicate.
do_something()
# 조건절이 길어지는 경우 들여쓰기도 가능
if (this_is_one_thing
and that_is_another_thing):
do_something()
# 들여쓰기 - 괄호
# 내용에 줄 맞추기 가능
my_list = [
1, 2, 3,
4, 5, 6,
]
result = some_function_that_takes_arguments(
'a', 'b', 'c',
'd', 'e', 'f',
)
# 맨 앞에 맞추는 것도 가능
my_list = [
1, 2, 3,
4, 5, 6,
]
result = some_function_that_takes_arguments(
'a', 'b', 'c',
'd', 'e', 'f',
)
2) 한줄의 최대 길이
한줄의 최대 길이는 기본적으로 최대 79자로 제한합니다. docstring이나 주석과 같이 구조적 제한이 적은 긴 텍스트 블럭의 경우 한 줄 최대 길이는 72로 제한합니다. 이렇게 제한하면, 필요한 편집기 창 여러 파일을 나란히 열 수 있습니다. 파이썬의 표준라이브러리는 79자 사용을 권장하지만, 일부 팀이 원하는 경우 docstring이나 주석은 72자로 제한한 상태에서 100자까지 늘리는 것도 가능합니다. 또한 백슬래시를 활용하는 것은 매우 적절합니다. 여러 줄을 사용하는 with 구문은 계속적으로 사용할 수 없는데, 이런 경우에는 백슬래시가 적용 가능합니다.
with open ( '/ path / to / some / file / you / want / to / read') as file_1, \
open ( '/ path / to / some / file / being / written', 'w') as file_2 :
file_2.write (file_1.read ())
3) 이항연산자(binary operator)는 앞에서 끊을까? 뒤에서 끊을까?
연산자의 경우 어떤 항목이 더해지고 빠지는 것을 한 눈에 알아볼 수 있게 하려면, 연산자를 포함하여 줄바꿈 해주는 것이 좋습니다.
# Wrong:
# operators sit far away from their operands
income = (gross_wages +
taxable_interest +
(dividends - qualified_dividends) -
ira_deduction -
student_loan_interest)
# Correct:
# easy to match operators with operands
income = (gross_wages
+ taxable_interest
+ (dividends - qualified_dividends)
- ira_deduction
- student_loan_interest)
4) 빈줄
가장 높은 위치에 있는 함수와 클래스의 정의 사이에는 2개의 줄 바꿈을 넣어서 사용하도록 합니다. 클래스 내부의 메서드 정의는 한 줄로 구분을 해줍니다. 또한 연관된 함수들의 그룹을 분리하는데 추가적인 빈줄을 사용할 수 있습니다.
5) 파일 인코딩
파일을 인코딩 할 때는 항상 UTF-8을 사용합니다. Python 2에서는 ASCII, Python 3에서는 UTF-8을 사용하는 파일에는 인코딩 선언이 없어야 합니다.
6) import
라이브러리를 불러올 때, import 문은 한 라인에 하나만 쓰고, 한 import 문에서는 하나의 모듈만 가져옵니다. import 문은 문서 맨위에 쓰되, 주석과 독스트링 다음에 씁니다. 그리고 표준 라이브러리 임포트, 제 3자 임포트, 로컬 라이브러리 임포트 순서로 그룹화해야 합니다. 그룹화할 때 그룹과 그룹 사이에는 빈 줄을 넣는 것이 좋습니다.
# Correct:
import os
import sys
from subprocess import Popen, PIPE
# Wrong:
import os, sys
7) 공백
괄호, 대괄호, 중괄호 사이 또는 쉼표 앞에 불필요한 공백을 사용하지 않는 것을 이야기 하고 있습니다. 저도 지금 예전 코드들을 보다보면, 불필요한 공백이 많이 있는 것을 볼 수 있었습니다.
# 중괄호, 대괄호, 괄호 안
# correct:
spam(ham[1], {eggs: 2})
# Wrong:
spam(ham[1 ], { eggs: 2})
# 뒤에 오는 콤마와 괄호 사이
# 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
# 슬라이싱에서의 공백
# 슬라이스에서 콜론은 이진연산자와 같이 적용되며, 양쪽에서 같은 크기를 가집니다.
# 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]
ham[lower+offset : upper+offset]
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 : : upper]
ham[ : upper]
# 함수 불러올 때, 변수 리스트 시작 앞 공백, 인덱싱을 알려주는 대괄호 앞 공백
# Correct:
spam(1)
dct['key'] = lst[index]
# Wrong:
spam (1)
dct ['key'] = lst [index]
# 다른 변수와 줄을 맞추기 위해 1을 초과하는 공백
# Correct:
x = 1
y = 2
long_variable = 3
# Wrong:
x = 1
y = 2
long_variable = 3
다른 권장사항으로 어디서든 후행공백을 피해야 합니다. 왜냐하면, 후행공백은 시각적으로 보이지 않아 혼란을 가져올 수 있습니다. 다양한 이진연산자(할당연산자, 비교연산자, 불린연산자, 증가할당연산자) 등은 항상 앞 뒤에 공백을 줘야 합니다. 연산자가 여러 개가 있는 경우에는 가장 우선순위가 낮은 연산자 주변에 공백을 추가하는 것을 고려하도록 합니다.
# 다양한 연산자에서의 사용법
# 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)
# 변수 할당할 때에는 붙여주도록 합니다.
# 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)
# 함수에 변수 주석과 default 값을 가진 경우
# Correct:
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): ...
# 합성구문
# Correct:
if foo == 'blah':
do_blah_thing()
do_one()
do_two()
do_three()
# Wrong:
if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()
if foo == 'blah': do_blah_thing()
else: do_non_blah_thing()
try: something()
finally: cleanup()
do_one(); do_two(); do_three(long, argument,
list, like, this)
if foo == 'blah': one(); two(); three()
8) 네이밍 컨벤션
코딩을 할 때, 가장 어려운 것이 네이밍인데 PEP-8에서는 네이밍 컨벤션은 leading underscores(앞쪽 언더바), tailing underscores(뒤쪽 언더바)를 파이썬에서 사용하도록 권장합니다. 클래스 이름과 예외는 카멜 케이스(CamelCase), 함수와 변수들은 underscore(언더바)로 나눠주는 스네이크 케이스로 작성합니다. 만약 함수 인수의 이름이 예약된 키워드와 충돌하는 경우에는 약어를 사용하는 것보다 tailing underscore를 1개 넣어주는 것을 권장합니다. 상수는 단어를 구분하는 밑줄과 함께 모두 대문자로 작성합니다.
이외에도 많은 이야기들이 있지만 크게 이정도만 설명하려고 합니다. 더 궁금하신 내용은 위에 달아드린 링크를 참조하시면 좋을 것 같습니다!
3. 찾아주는 라이브러리
우리는 항상 똑같은 실수를 반복합니다. 코딩도 동일한 것 같습니다. 나도 모르게 스페이스바를 치거나 이미 PEP8에 대해 알고 있지만 내 몸이 거부할 때가 있습니다. 요즘에는 좋은 툴이 있고, 라이브러리들이 있습니다. 이것들을 활용하면 내가 어디서 PEP-8이랑 다르게 만들었는지를 확인할 수 있습니다. 다양한 라이브러리들이 있겠지만 오늘은 파이참에서 pylint라는 라이브러리를 적용해보도록 하겠습니다.
1) 라이브러리 설치
파이참에서는 라이브러리를 설치하기 위해서는 File-settings에서 설치할 수 있습니다.
그림에서 p-study라는 project에 있는 패키지들을 확인할 수 있는 데 패키지들 밑쪽에 + 모양을 통해서 원하는 패키지를 찾으실 수 있습니다. +클릭 후 상단 돋보기 옆에 pylint라고 치면 해당 라이브러리를 확인할 수 있습니다. 아래부분의 install package를 클릭하셔서 package를 설치할 수 있습니다.
2) pylint를 코드에 적용해보자!
오늘 같이 적용해볼 코드는 "객체 지향 프로그래밍 이해하기2"에 있는 코드입니다.
class Student(object):
# 클래스 변수
tuition_per_raise = 1.0
def __init__(self, name, age, id, tuition):
self._name = name
self._age = age
self._id = id
self._tuition = tuition
# 인스턴스 메소드
def tuition_info(self):
return f'{self._name}\'s tuition fee : {self._tuition*Student.tuition_per_raise}'
# 클래스 메소드
@classmethod
def raise_price(cls, per): #cls : Student 클래스
if per <= 1:
print('Please Enter 1 Or More')
return
cls.tuition_per_raise = per
print('tuition increased.')
# 스태틱 메소드
@staticmethod
def under_age_26(inst):
if inst._age < 27:
return 'under 26'
return 'over 26'
# 인스턴스 생성
Kim = Student('Kim',26,201701234,100)
Lee = Student('Lee',28,201501223,200)
# 등록금 확인
print(Kim.tuition_info()) # Kim's tuition fee : 100.0
print(Lee.tuition_info()) # Lee's tuition fee : 200.0
# 가격 인상
Student.tuition_per_raise = 1.4
# 인상 후 가격 확인
print(Kim.tuition_info()) # Kim's tuition fee : 140.0
print(Lee.tuition_info()) # Lee's tuition fee : 280.0
# 가격 인상
Student.raise_price(1.6)
# 인상 후 가격 확인
print(Kim.tuition_info()) # Kim's tuition fee : 160.0
print(Lee.tuition_info()) # Lee's tuition fee : 320.0
# 스태틱 메소드 호출
print(Kim.under_age_26(Kim)) # under 26
print(Lee.under_age_26(Lee)) # over 26
설치가 완료되면, Terminal에서 "pylint 파일명.py"라는 명령어를 치면 확인할 수 있습니다. terminal 환경에서 아래와 같은 결과를 확인할 수 있습니다. 제가 적용한 코드는 10점 만점에서 5점도 안되네요.
위의 사진에서 파란색 부분을 클릭하시면, 우리가 수정할 수 있는 라인으로 올라갈 수 있습니다. 생각보다 공백에 대한 문제가 많이 있었네요. 공백에 대한 처리를 해줬더니 1.94점이나 오른 것을 확인할 수 있었습니다.
이런 방식을 활용해서 코드를 점검해 볼 수 있습니다. 무엇보다 중요한 것은 PEP-8은 무조건 따라야 하는 것은 아닙니다. 각 팀에서 의사소통이 더 원활한 방법을 활용해서 적용하는 것을 추천드립니다. 긴 글을 읽어주셔서 감사합니다.