❗️ 파이썬 클린코드를 읽고 정리한 글입니다.
아래에 정리된 파이썬의 기능을 잘 활용하면 파이썬스러운 코드를 작성할 수 있다.
인덱스와 슬라이스를 적절히 잘 사용하면 그 코드를 파이써닉하다
라고 한다.
아래의 예제는 모두 리스트로 작성되었지만 문자열, 튜플, 리스트 모두 가능하다.
arr = [1,2,3,4]
# bad
last_item = arr[len(arr)-1] # 4
# good
last_item = arr[-1] # 4
arr1 = [1,2,3,4]
# bad
arr2 = [1,2,3,4]
# good
arr3 = arr1[::]
arr = [1,2,3,4,5,6,7]
# bad
arr1 = []
for i in range(0,len(arr),3):
arr1.append(arr[i])
# good
arr1 = arr[::3]
앞서 보았던 인덱스, 슬라이스 기능은 __getitem__
이라는 매직 메서드 덕분에 동작한다. myobject[key]
형태를 사용할 때 호출되는 메서드인데, 대괄호 안의 key
값을 파라미터로 전달한다. 즉, 내가 만든 클래스에 인덱스, 슬라이스 기능을 넣고 싶으면 __getitem__
과 __len__
메서드를 모두 구현하면 된다.
class Items:
def __init__(self, *values):
self._values = list(values)
def __len__(self):
return len(self._values)
def __getitem__(self, idx):
return self._values.__getitem__(idx)
test = Items(1,2,3,4)
print(len(test)) # 4
print(test[0]) # 1
어떤 코드의 시작과 끝에 특정 동작을 하도록 할 때 사용되는 기능이다. 예를 들면, 파일을 열면(시작) 닫아야하고(끝), 데이터베이스를 백업하기 위해 멈추면(시작) 실행시켜야한다(끝).
파일을 열고 닫을 때 컨텍스트 관리자 사용 유무에 따라 차이는 아래와 같다.
# bad
f = open(filename)
"""
파일 관련된 동작 코드
ex- 읽기/쓰기/삭제
"""
f.close()
# good
with open(filename) as f:
"""
파일 관련된 동작 코드
ex- 읽기/쓰기/삭제
"""
# bad
를 보면 파일을 여닫는 것을 직접 작성해야한다. 그래서 파일을 열기만 하고 닫는 동작의 코드를 작성하는 것을 잊을 수 있다. 이러한 위험성 때문에 # good
과 같이 with
문을 사용한다.
with
문은 컨텍스트 관리자로 진입하게 하는 데, 컨텍스트 관리자는 __enter__
, __exit__
메소드를 가진다.
__enter__
with
문이 실행되면 자동으로 이 메서드가 호출된다.__exit__
__enter__
의 마지막 코드의 실행이 끝나면 자동으로 이 메서드를 호출한다.__enter__
에서 예외나 오류가 발생해도 __exit__
는 항상 실행된다.데이터베이스를 백업을 하려면 데이터베이스를 멈춘 뒤에 해야한다. 백업이 실패 또는 성공을 해도 항상 데이터베이스는 실행을 시켜야한다. 그렇기때문에 컨텍스트 관리자로 구현을 하는 것이 파이썬스러운 코드이다.
def stop_database():
"""
데이터베이스 멈추는 코드
"""
def start_database():
"""
데이터베이스 실행하는 코드
"""
class DBHandler:
def __enter__(self):
stop_database()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
start_database()
def db_backup():
"""
데이터베이스 백업하는 코드
"""
def main():
with DBHandler():
db_backup()
앞서 살펴본 구현보다 contextlib
이라는 표준 모듈을 사용하면 좀 더 간결한 구현이 가능하다.
@contextlib.contextmanager
@contextlib.contextmanager
데코레이터를 적용하면 해당 함수의 코드를 컨텍스트 관리자로 변환한다. 여기서 함수는 제너레이터 형태여야한다.import contextlib
@contextlib.contextmanger
def db_handler():
stop_database() # __enter__
yield # db_backup() 실행
start_database() # __enter__
with db_handler():
db_backup()
contextlib.ContextDecorator
with
문 없이 그저 함수를 호출하기만 하면 자동으로 컨텍스트 관리자 안에서 함수가 실행된다. 다만, 컨텍스트 관리자 내부에서 사용하고자 하는 객체를 얻을 수 없다.class dbhandler_decorator(contextlib.ContextDecorator):
def __enter__(self):
stop_database()
def __exit__(self, exc_type, exc_val, exc_tb):
start_database()
@dbhandler_decorator
def offline_backup():
"""
데이터베이스 백업하는 코드
"""
프로퍼티란 객체의 어떤 속성에 대한 접근을 제어하려는 경우 사용된다. 다른 언어에서는 흔히 public
, private
, protected
라는 프로퍼티를 가지는데, 파이썬은 모든 객체의 프로퍼티는 public
이다.
개발을 하다보면 외부의 사용자(호출자)가 내부 객체의 속성을 호출하지 못하도록 해야할 때가 있다. 이를 강제하는 방법은 없지만, "이 속성은 호출 하지 않았으면 좋겠어요"라는 의미를 주는 몇 가지 규칙이 있다.
파이썬에서 객체 앞에 _
이 있으면 호출자는 해당 속성을 호출하면 안 된다. 물론, 호출해도 오류를 반환하거나 호출이 되지않는 등의 문제는 없이 정상적으로 호출이 된다. 하지만 객체가 외부로 공개하는 용도가 아니면 모든 멤버에는 접두사로 하나의 밑줄을 사용하는 것이 좋다.
# bad
class Connector:
def __init__(self, sourse):
self.sourse = sourse
self.timeout = 60
conn = Connector("mysql://localhost")
print(conn.source) # "mysql://localhost"
conn.timeout = 30
print(conn.timeout) # 30
# good
class Connector:
def __init__(self, sourse):
self.sourse = sourse
self._timeout = 60
conn = Connector("mysql://localhost")
print(conn.source) # "mysql://localhost"
conn._timeout = 30
print(conn._timeout) # 30
파이썬에서의 이중 밑줄(__)
파이썬에서의 이중 밑줄은 네이밍 맹글링이라고 부르는데 이는 프로퍼티와는 전혀 별개의 기능이다. 이중 밑줄이private
와 같은 효과라고 생각하는 것은 잘못된 생각이다.
파이썬에서는 _
은 "이 속성이 private로 사용될 것입니다."라는 뜻이라고 했다. 하지만 이러한 private 속성을 외부 사용자가 사용할 수 있게 해줘야 할 때가 있다. 예를 들면 사용자에게 어떠한 입력 값을 내부의 어떠한 속성에 담는데 이 속성은 내부 함수가 돌아갈 때 이 값을 보호하기 위해서 private로 만들었다고 하자. 이 경우에는 private 속성에 사용자가 값을 입력하기위해 접근해야한다. 이때 사용하는 것이 프로퍼티로 자바에서 게터(getter)와 세터(setter)의 역할과 동일하다. 게터는 현재의 private 속성 값을 가져오는 역할, 세터는 private 속성 값을 변경하는 역할을 한다.
# bad
class Person:
def __init__(self):
self._age = 0
def get_age(self): # getter
return self._age
def set_age(self, value): # setter
self._age = value
james = Person()
james.set_age(20)
print(james.get_age()) # 20
# good
class Person:
def __init__(self):
self._age = 0
@property
def age(self): # getter
return self._age
@age.setter
def age(self, value): # setter
self._age = value
james = Person()
james.age = 20 # 인스턴스.속성 형식으로 접근하여 값 저장
print(james.age) # 20
import re
EMAIL_FORMAT = re.compile(r"[^@]+@[^@]+[^@]+")
def is_valid_email(self, email):
return re.match(self.EMAIL_FORMAT, email) is not None
class User:
def __init__(self, username):
self.username = username
self._email = None
@property
def email(self):
return self._email
@email.setter
def email(self, new_email):
if not is_valid_email(new_email):
print("유효한 이메일이 아닙니다.")
return
self._email = new_email
사용자가 이메일을 추가하려고 User.email=사용자의 이메일
를 입력하면 @email.setter
가 실행되어 유효성 검사를 진행한다. 만약 사용자가 이메일 값을 확인하고 싶어 print(User.email)
를 실행하면 @property
가 실행되어 현재 값을 반환해준다.
deleter
게터와 세터 외에도 디리터도 존재하는데 자세한 내용은 공식 문서에 담겨있다.
이름에서 예상할 수 있듯이 디리터는 값을 삭제하는 것이다.
반복 가능한 객체를 이터러블 객체라고 한다. 여기서 반복 가능하다는 의미는 for
문을 사용해 루프를 돌며 반복적으로 값을 가져올 수 있는 것을 의미한다. 이터러블 객체가 값을 하나씩 들고 올 수 있는 것은 이터레이터 덕분이다. 이터러블 객체는 __iter__
라는 메서드를 가져야하고, 이터레이터는 __next__
라는 메서드를 가져야한다.
파이썬에서는 리스트, 튜블, 세트, 사전이 이터러블 객체로 for
문을 사용 가능한데, 개발자가 만든 객체를 for
문에서 사용할 수 있는 이터러블 객체로 만들고 싶다면 매직 메서드를 구현하면 된다.
파이썬은 객체를 반복하려고 하면 가장 먼저 iter()
함수를 호출한다. 이 함수가 처음으로 하는 것은 객체에 __iter__
라는 메서드가 있는지 확인하는 것이다. 메서드가 존재하면 해당 메서드를 실행한다.
from datetime import timedelta, date
class DateRangeIterable:
"""
자체 이터레이터 메서드를 가지고 있는 이터러블 객체
"""
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
self._present_day = start_date
def __iter__(self):
return self
def __next__(self):
if self._present_day >= self.end_date:
raise StopIteration
today = self._present_day
self._present_day += timedelta(days=1)
return today
>>> for day in DateRangeIterable(date(2019, 1, 1), date(2019, 1, 5)):
print(day)
...
2019-01-01
2019-01-02
2019-01-03
2019-01-04
DateRangeIterable
이 실행되니 __init__
함수가 실행되고, 반복하기 위해 iter()
함수를 실행시킨다. 그럼 __iter__
이 호출되는데 이 메서드는 self를 반환하여 자기 자신이 이터러블 객체라고 알린다. 따라서 next()
함수가 호출되고 __next__
메서드가 요소를 하나씩 반환한다. 더이상 요소가 없으면 StopIteration
예외를 발생시켜 파이썬에게 알려준다.
객체에 __iter__
함수가 없어도 반복할 수 있다. iter()
함수는 객체에 __iter__
함수가 없으면 __getitem__
을 찾고, 이 또한 없으면 그때서야 TypeError
를 발생시킨다. 즉, __getitem__
가 있으면 반복을 할 수 있다.
이를 시퀸스라고 하는데, 시퀸스는 __getitem__
, __len__
이 구현되어 있어야한다.
class DateRangeSequence:
"""
시퀀스 객체 구현.
"""
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
self._range = self._create_range()
def _create_range(self):
days = []
current_day = self.start_date
while current_day < self.end_date:
days.append(current_day)
current_day += timedelta(days=1)
return days
def __getitem__(self, day_no):
return self._range[day_no]
def __len__(self):
return len(self._range)
>>> for day in DateRangeSequence(date(2019, 1, 1), date(2019, 1, 5)):
print(day)
...
2019-01-01
2019-01-02
2019-01-03
2019-01-04
코드를 보면 알 수 있지만 __init__
함수가 바로 _create_range
를 호출하여 모든 요소를 생성한다. 그 후, iter()
함수가 __getitem__
을 실행하여 요소를 하나씩 가져온다.
즉, 시퀸스는 이미 모든 요소 값을 생성한 후 하나씩 가져오고, 앞서 봤던 이터러블은 요소 하나씩 생성 및 반환한다. 이러한 특징때문에 시퀸스는 메모리 공간을 많이 차지하지만 인덱싱에서는 O(1)
의 시간 복잡도를 가지고 이터러블은 메모리 공간을 적게 차지하지만 인덱싱에서는 모든 요소를 돌아하므로 O(n)
시간 복잡도를 가진다.
파이썬 문법에서 in
을 사용하면 호출되는 객체를 말한다. __contains__
메서드를 이용해 구현하는데 보통 불리언 값을 리턴한다. __contains__
메서드를 잘 사용하면 가독성이 높아지므로 파이써닉한 코드를 작성할 수 있게 된다.
element in container
# 파이썬은 위 코드를 다음과 같이 해석함
container.__contains__(element)
아래 예시를 보면 컨테이너 객체를 사용하면 왜 파이썬스러운 코드가 되는지 확인할 수 있다.
알고리즘 문제에서도 자주 출제되는 2차원 게임 지도에서 특정 위치에 표시를 하는 코드이다.
# bad --> 가독성이 떨어짐
def mark_coordinate(grid, coord):
if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height:
grid[coord] = MARKED
# good --> 가독성이 높아짐
class Boundaries:
def __init__(self, width, height):
self.width = width
self.height = height
def __contains__(self, coord):
x, y = coord
return 0 <= x < self.width and 0 <= y < self.height
class Grid:
def __init__(self, width, height):
self.width = width
self.height = height
self.limits = Boundaries(width, height)
def __contains__(self, coord):
return coord in self.limits
이렇게 하면 지도에게 특정 좌표가 포함되어 있는지만 물어보면 된다.
grid = Grid(x,y)
def mark_coordinate(grid, coord):
if coord in grid: # 좌표 coord가 지도에 포함되는지
grid[coord] = MARKED