본 문서는 파이썬 코딩의 기술(원제: Effective Python: 59 Specific Ways to Write Better Python, 브렛 슬라킨 저, 오현석 역, 길벗 2020. 10. 30)을 공부 목적으로 정리한 글입니다. 필요한 부분만 발췌한 개인 정리용 글이기 때문에 보다 상세한 내용을 알고 싶으시다면 책을 직접 읽어보시길 권해드립니다. (네이버 도서 링크)
파이썬 코딩의 기술
아마존 파이썬 프로그래밍 분야 베스트셀러, 〈Effective Python〉 전면 개정 증보판! 파이썬의 매력과 강점을 이용해 강력하고 우수한 성능의 코드를 작성하는 90가지 방법!파이썬다운 방식으로 프
book.naver.com
Better Way 1. 사용 중인 파이썬의 버전을 알아두라
Better Way 2. PEP 8 스타일 가이드를 따르라
- PEP(Python Enhancement Proposal) #8: 스타일 가이드 (링크)
- 빈 컨테이너나 시퀀스가 비어있음을 확인할 때 길이를 이용하지 않아도 된다. (빈 컨테이너나 시퀀스는 암묵적으로 False로 취급된다.)
# 나쁜 예
if len(something) == 0:
...
# 좋은 예
if something:
...
- 임포트를 적을 때는 표준 라이브러리 모듈, 서드 파티 모듈, 직접 만든 모듈 순으로 섹션을 나눈다. 각 세션 내에서는 알파벳 순서로 모듈을 임포트 한다.
Better Way 3. bytes와 str의 차이를 알아두라
Better Way 4. C 스타일 형식 문자열을 STR.FORMAT과 쓰기보다는 f-문자열을 통한 인터폴레이션을 사용하라
- help("FORMATTING")을 통해 모든 형식 지정자에 대한 정보를 확인할 수 있다.
Better Way 5. 복잡한 식을 쓰는 대신 도우미 함수를 작성하라
- 예: URL의 Query string을 Parsing하는 상황
from urllib.parse import parse_qs
values = parse_qs("빨강=5&파랑=0&초록=", keep_blank_values=True)
print(repr(values)) # >>> {'빨강': ['5'], '파랑': ['0'], '초록': ['']}
print("빨강:", values.get("빨강")) # >>> 빨강: ['5']
print("초록:", values.get("초록")) # >>> 초록: ['']
print("투명도:", values.get("투명도")) # >>> 투명도: None
- 파라미터가 없거나 비어 있을 경우 0 대신 디폴트 값이 대입하고 싶음: Better Way 2에서 살펴본 대로 빈 리스트나 0이 모두 False로 평가된다는 사실을 이용해 다음과 같이 if 식을 이용
red = values.get("빨강", [""])[0] or 0
green = values.get("초록", [""])[0] or 0
opacity = values.get("투명도", [""])[0] or 0
print(f"빨강: {red!r}") # >>> 빨강: '5'
print(f"초록: {green!r}") # >>> 초록: 0
print(f"투명도: {opacity!r}") # >>> 투명도: 0
- 만약 Parsing된 값을 정수로 변환해서 바로 수식에 활용하기 위해서는 각 식을 int 내장 함수로 다시 한 번 감싸야하기 때문에 더 복잡한 형태가 됨. 이 경우 코드의 길이에 비해 가독성이 떨어지기 때문에 처음 코드를 읽는 사람이 이 식이 어떤 일을 하는지 파악하기에 추가적인 노력이 필요함. 대신 다음과 같이 if/else 조건식을 이용해서 풀면 코드를 간결하게 유지하면서 가독성을 높일 수 있음. (코드를 줄여 쓰는 것보다 가독성을 좋게 하는 것이 더 가치 있다. / 복잡한 식을 표현할 수 있는 파이썬의 함축적인 문법이 지저분한 코드를 만들어내지 않도록 하라.)
red_str = values.get("빨강", [""])
red = int(red_str[0]) if red_str[0] else 0
- 이를 if/else 문을 이용해 여러 줄로 나눠쓰면 더 좋다!
green_str = values.get("초록", [""])
if green_str[0]:
green = int(green_str[0])
else:
green = 0
- 위 예제처럼 단지 두세번 반복하더라도 로직을 반복 적용할 때는 꼭 도우미 함수를 작성하자 (DRY: Don't Repeat Youreself 원칙) + 식이 복잡해지면 식을 더 작은 조각으로 나눠서 로직을 도우미 함수로 옮길지 고려해야 한다.
def get_first_int(values, key, default=0):
found = values.get(key, [""])
if found[0]:
return int(found[0])
return default
red = get_first_int(values, "빨강")
green = get_first_int(values, "초록")
opacity = get_first_int(values, "투명도")
Better Way 6. 인덱스를 사용하는 대신 대입을 사용해 데이터를 언패킹하라
- 불변(Immutable) 순서쌍을 만드는 tuple 값이 있다고 가정하자.
pair = ("약과", "호박엿")
pair[0] = "타레과" # TypeError!
print(pair[0], "&", pair[1]) # >>> 약과 & 호박엿
- 데이터의 구조를 아는 상태에서 언패킹(Unpacking) 구문을 이용하면 인덱스를 사용하는 대신 변수에 값을 할당하여 각 값에 접근할 수 있다.
first, second = item # Unpacking
print(first, "&", second) # >>> 약과 & 호박엿
- 위 예제를 통해 확인할 수 있듯이 인덱스를 사용해 값에 접근하는 것 보다 언패킹을 이용한 접근은 시각적인 잡음이 적다. 즉, 언패킹을 현명하게 사용하면 더 명확하고 파이썬다운 코드를 만들 수 있다.
# Bubble Sort: Use temporary variable
def bubble_sort(a):
for _ in range(len(a)):
for i in range(1, len(a)):
if a[i] < a[i-1]:
temp = a[i]
a[i] = a[i-1]
a[i-1] = temp
# Bubble Sort: Use unpacking
def bubble_sort(a):
for _ in range(len(a)):
for i in range(1, len(a)):
if a[i] < a[i-1]:
a[i-1], a[i] = a[i], a[i-1]
snacks = [("베이컨", 350), ("도넛", 240), ("머핀", 190)]
# Bad Example
for i in range(len(snacks)):
item = snacks[i]
name = item[0]
calories = item[1]
print(f"#{i+1}: {name}은 {calories} 칼로리입니다.")
# Good Example
for rank, (name, calories) in enumerate(snacks, 1):
print(f"#{rank}: {name}은 {calories} 칼로리입니다.")
Better Way 7. range보다는 enumerate를 사용하라
- enumerate는 이터레이터를 지연 계산 제너레이터(Lazy generator)로 감싼다.
# enumerate가 반환한 이터레이터의 동작 방식
flavors = ["바닐라", "초콜릿", "피칸", "딸기")
it = enumerator(flavors)
print(next(it)) # >>> (0, '바닐라')
print(next(it)) # >>> (1, '초콜릿')
Better Way 8. 여러 이터레이터에 대해 나란히 루프를 수행하려면 zip을 사용하라
- zip 내장 함수는 튜플을 지연 계산하는 제네레이터를 만들어주어 여러 이터레이터를 나란히 이터레이션할 수 있도록 도와준다.
- zip으로 묶는 이터레이터의 길이가 다른 경우 가장 짧은 이터레이터의 길이까지만 튜플을 반환하고 더 긴 이터레이터들의 나머지 원소는 무시한다. 가장 긴 이터레이터를 기준으로 하고 싶은 경우 itertools 모듈의 zip_longest를 대신 사용할 수 있다.
Better Way 9. for나 while 루프 뒤에 else 블록을 사용하지 말라
- Loop 문 뒤에 오는 else 블록은 Loop 도중 break를 만나지 않은 경우 실행되지만, 동작이 직관적이지 않기 때문에 사용하지 말라 (다른 방법이 많다!)
# 서로소를 확인하는 예
a = 4
b = 9
# Bad example
for i in range(2, min(a, b) + 1):
print("검사 중", i)
if a % i == 0 and b % i == 0:
print("서로소 아님")
break
else:
print("서로소")
# Good example
def coprime(a, b):
for i in range(2, min(a, b) + 1):
if a % i == 0 and b % i == 0:
return False
return True
Better Way 10. 대입식을 사용해 반복을 피하라
- 대입문(assignment statement)은 a = b라고 쓰며 'a equal b'라고 읽고, 대입식(assignment expression)은 a := b라 쓰며 'a walrus b'라 읽는다. 대입식은 코드 중복 문제를 해결하고자 도입되었으며, 대입문이 사용될 수 없는 위치에서 변수에 값을 할당할 수 있다.
- 아래 예에서 count 변수는 if 문의 첫 번째 블록 안에서만 사용되지만, 대입문을 이용하여 정의하면 실제보다 변수의 중요도가 높아보인다. 이러한 문제를 대입식을 이용하여 완화할 수 있다.
fresh_fruit = {
"사과": 10,
"바나나": 8,
...
}
# Lemonade Example
def make_lemonade(count):
...
def out_of_stock():
...
## Bad example
count = fresh_fruit.get("레몬", 0)
if count:
make_lemonade(count)
else:
out_of_stock()
## Good example
if count := fresh_fruit.get("레몬", 0):
make_lemonade(count)
else:
out_of_stock()
# Cider Example
def make_cider(count):
...
## Bad example
count = fresh_fruit.get("사과", 0)
if count >= 4:
make_cider(count)
else:
out_of_stock()
## Good example
if (count := fresh_fruit.get("사과", 0)) > 4:
make_cider(count)
else:
out_of_stock()
- 조건에 따라 현재 위치를 둘러싸는 영역에 있는 변수에 값을 대입하고 그 변수를 바로 함수 호출에 사용하는 경우에도 대입식을 이용하면 가독성 좋은 코드를 작성할 수 있다.
def slice_bananas(count):
....
class OutOfBananas(Exception):
pass
def make_smoothies(count):
....
# Bad example
pieces = 0
count = fresh_fruit.get("바나나", 0)
if count >= 2:
pieces = slice_bananas(count)
try:
smoothies = make_smoothies(pieces)
except OutOfBananas:
out_of_stock()
# Good example
pieces = 0
if (count := fresh_fruit.get("바나나", 0)) >= 2:
pieces = slice_bananas(count)
try:
smoothies = make_smoothies(pieces)
except OutOfBananas:
out_of_stock()
- 또한, 대입식을 이용하여 파이썬에 존재하지 않는 switch/case 문 혹은 do/while 루프와 비슷한 코드도 작성할 수 있다.
# switch/case
## Bad example
count = fresh_fruit.get("바나나", 0)
if count >= 2:
pieces = slice_bananas(count)
to_enjoy = make_smoothies(pieces)
else:
count = fresh_fruit.get("사과", 0)
if count >= 4:
to_enjoy = make_cider(count)
else:
count = fresh_fruit.get("레몬", 0)
if count:
to_enjoy = make_lemonade(count)
else:
to_enjoy = None
## Good example
if (count := fresh_fruit.get("바나나", 0)) >= 2:
pieces = slice_bananas(count)
to_enjoy = make_smoothies(pieces)
elif (count := fresh_fruit.get("사과", 0)) >= 4:
to_enjoy = make_cider(count)
elif count := fresh_fruit.get("레몬", 0):
to_enjoy = make_lemonade(count)
else:
to_enjoy = None
# do/while
def pick_fruit():
....
def make_juice(fruit, count):
....
## Bad example
bottles = []
fresh_fruit = pick_fruit()
while fresh_fruit:
for fruit, count in fresh_fruit.items():
batch = make_juice(fruit, count)
bottles.extend(batch)
fresh_fruit = pick_fruit()
## Code reusability using loop-and-a-half
bottles = []
while True:
fresh_fruit = pick_fruit()
if not fresh_fruit:
break
for fruit, count in fresh_fruit.items():
batch = make_juicec(fruit, count)
bottles.extend(batch)
## Good example
bottles = []
while fresh_fruit := pick_fruit():
for fruit, count in fresh_fruit.itmes():
batch = make_juicec(fruit, count)
bottles.extend(batch)
'Notes' 카테고리의 다른 글
[Python] Effective Python, 2nd | Ch2. 리스트와 딕셔너리 - (1) 리스트 (0) | 2022.06.07 |
---|