1. 파이썬답게 생각하기

이현우·2022년 6월 27일
0

BETTER WAY 1 사용중인 파이썬의 버전을 알아두라

파이썬 2는 2020년 1월 1일 수명이 다함
더 이상 버그 수정, 보안 패치, 새로운 기능의 역포팅이 이뤄지지 않는다.
따라서 파이썬 3를 사용해야 한다.

BETTER WAY 2 PEP 8 스타일 가이드를 따르라

  • 파이썬 개선 제안(Python Enhancement Proposal) #8 또는 PEP 8은 파이썬 코드를 어떤 형식으로 작성할 지 알려주는 스타일 가이드다.
  • 온라인 가이드 - https://www.python.org/dev/peps/pep-0008/

공백(탭, 스페이스, 새줄 등의 문자를 모두 합친 말)

  • 탭 대신 스페이스를 사용해 들여쓰기하라.
  • 문법적으로 중요한 들여쓰기에는 4칸 스페이스를 사용하라.
  • 라인 길이는 79개 문자 이하여야 한다.
  • 긴 식을 다음 줄에 이어서 쓸 경우에는 일반적인 들여쓰기보다 4 스페이스를 더 들여써야 한다.
  • 파일 안에서 각 함수와 클래스 사이에는 빈 줄을 두 줄 넣어라.
  • 클래스 안에서 메서드와 메서드 사이에는 빈 줄을 한 줄 넣어라.
  • 딕셔너리에서 키와 콜론 사이에는 공백을 넣지 않고, 한 줄 안에 키와 값을 같이 넣는 경우에는 콜론 다음에 스페이스를 하나 넣는다.
  • 변수 대입에서 = 전후에는 스페이스를 하나씩만 넣는다.
  • 타입 표기를 덧붙이는 경우에는 변수 이름과 콜론 사이에 공백을 넣지 않도록 주의하고, 콜론과 타입 정보 사이에는 스페이스를 하나 넣어라.

명명 규약

  • 함수, 변수, 애트리뷰트는 lowercase_underscore처럼 소문자와 밑줄을 사용한다. (snake case)
  • 보호해야 하는 인스턴스 애트리뷰트는 일반적인 애트리뷰트 이름 규칙을 따르되, _leading underscore처럼 밑줄로 시작한다.
  • 비공개(private)(한 클래스 안에서만 쓰이고 다른 곳에서는 쓰면 안 되는 경우) 인스턴스 애트리뷰트는 일반적인 애트리뷰트 이름 규칙을 따르되, __leading_underscore처럼 밑줄 두 개로 시작한다.
  • 클래스(예외도 포함한다)는 CapitalizedWord처럼 여러 단어를 이어 붙이되, 각 단어의 첫 글자를 대문자로 만든다. (PascalCase)
  • 모듈 수준의 상수는 ALL_CAPS처럼 모든 글자를 대문자로 하고 단어와 단어 사이를 밑줄로 연결한 형태를 사용한다.
  • 클래스에 들어 있는 인스턴스 메서드는 호출 대상 객체를 가리키는 첫 번째 인자의 이름으로 반드시 self를 사용해야 한다.
  • 클래스 메서드는 클래스를 가리키는 첫 번째 인자의 이름으로 반드시 cls를 사용해야 한다.

식과 문

  • 긍정적인 식을 부정하지 말고(if not a is b) 부정을 내부에 넣어라(if a is not b).
  • 빈 컨테이너나 시퀀스([]나 ''등)를 검사할 때는 길이를 0과 비교(if len(something) = 0)하지 말라. 빈 컨테이너나 시퀀스 값이 암묵적으로 False로 취급된다는 사실을 활용해 'if not 컨테이너'라는 조건문을 써라.
  • 마찬가지로 비어 있지 않은 컨테이너나 시퀀스([1]이나 'hi' 등)를 검사할 때도 길이가 0보다 큰지 비교하지 말라. 대신 if 컨테이너가 비어 있지 않은 경우 암묵적으로 True로 평가된다는 사실을 활용하라.
  • 한 줄짜리 if 문이나 한 줄짜리 for, while 루프, 한 줄짜리 except 복합문을 사용하지 말라. 명확성을 위해 각 부분을 여러 줄에 나눠 배치하라.
  • 식을 한 줄 안에 다 쓸 수 없는 경우, 식을 괄호로 둘러싸고 줄바꿈과 들여쓰기를 추가해서 읽기 쉽게 만들라.
  • 여러 줄에 걸쳐 식을 쓸 때는 줄이 계속된다는 표시를 하는 \문자보다는 괄호를 사용하라.

임포트

  • import 문(from x import y도 포함)을 항상 파일 맨 앞에 위치시켜라.
  • 모듈을 임포트할 때는 절대적인 이름(absolute name)을 사용하고, 현 모듈의 경로에 상대적인 이름은 사용하지 말라.
    • ex) bar 패키지로부터 foo 모듈을 임포트 한다면 from bar import foo라고 해야 하며, 단지 import foo라고 하면 안 된다.
  • 반드시 상대적인 경로로 임포트해야 하는 경우에는 from . import foo처럼 명시적인 구문을 사용하라.
  • 임포트를 적을 때는 표준 라이브러리 모듈, 서드 파티 모듈, 여러분이 만든 모듈 순서로 섹션을 나눠라. 각 섹션에서는 알파벳 순서로 모듈을 임포트 하라.

BETTER WAY 3 bytes와 str의 차이를 알아두라

파이썬에는 문자열 데이터의 시퀀스를 표현하는 두 가지 타입(bytes와 str)이 있다.

아래 코드처럼 bytes 타입의 인스턴스에는 부호가 없는 8바이트 데이터가 그대로 들어간다.(종종 아스키 인코딩을 사용해 내부 문자를 표시한다.)

a = b'h\x65llo'
print(list(a))	# [104, 101, 108, 108, 111]
print(a)		# b'hello'

str 인스턴스에는 사람이 사용하는 언어의 문자를 표현하는 유니코드 코드 포인트(code point)가 들어 있다.

a = 'a\u0300 propos'
print(list(a))	# ['a', '`', ' ', 'p', 'r', 'o', 'p', 'o', 's']
print(a)		# à propos

중요한 사실

  • str 인스턴스에는 직접 대응하는 이진 인코딩이 없고 bytes에는 직접 대응하는 텍스트 인코딩이 없다는 점
  • 유니코드 데이터를 이진 데이터로 변환하려면 str의 encode 메서드를 호출해야 하고, 이진 데이터를 유니코드 데이터로 변환하려면 bytes의 decode 메서드를 호출해야 한다.

유니코드 샌드위치를 사용! - 유니코드 데이터를 인코딩하거나 디코딩하는 부분을 인터세이흐의 가장 먼 경계지점에 위치시키는 방식

문자를 표현하는 타입이 둘로 나뉘어 있기 때문에 파이썬 코드에서는 다음과 같은 두 가지 상황이 자주 발생

  • UTF-8(또는 다른 인코딩 방식)로 인코딩된 8비트 시퀀스를 그대로 사용하고 싶다.
  • 특정 인코딩을 지정하지 않은 유니코드 문자열을 사용하고 싶다.

두 경우를 변환해주고 입력 값이 코드가 원하는 값과 일치하는지 확신하기 위해 종종 두 가지 도우미 함수가 필요하다.

첫 번째 함수는 bytes나 str 인스턴스를 받아서 항상 str을 반환

def to_str(bytes_or_str):
	if isinstance(bytes_or_str, bytes):
		value = bytes_or_str.decode('utf-8')
	else:
		value = bytes_or_str
	return value

print(repr(to_str(b'foo')))
print(repr(to_str('bar')))
print(repr(to_str(b'\xed\x95\x9c')))	# UTF-8에서 한글은 3바이트임

>>>
'foo'
'bar'
'한'

두 번째 함수는 bytes나 str 인스턴스를 받아서 항상 bytes를 반환한다.

def to_bytes(bytes_or_str):
	if isinstance(bytes_or_str, str):
		value = bytes_or_str.encode('utf-8')
	else:
		value = bytes_or_str
	return value

print(repr(to_bytes(b'foo')))
print(repr(to_bytes('bar')))
print(repr(to_bytes('한글')))

>>>
b'foo'
b'bar'
b'\xed\x95\x9c\xea\xb8\x80'

이진 8비트 값과 유니코드 문자열을 파이썬에서 다룰 때 꼭 기억해야 할 두 가지 문제점

  • bytes와 str이 똑같이 작동하는 것처럼 보이지만 각각의 인스턴스는 서로 호환되지 않기 때문에 전달 중인 문자 시퀀스가 어떤 타입 인지를 잘 알고 있어야 한다.
    • + 연산자를 사용하면 bytes를 bytes에 더하거나 str을 str에 더할 수 있다.
    • but str 인스턴스와 bytes인스턴스는 더할 수는 없다.
    • 이항 연산자를 사용하면 bytes를 bytes와 비교하거나 str을 str과 비교할 수 있다.
      • but str인스턴스와 bytes인스턴스를 비교할 수는 없다.
  • (내장 함수인 open을 호출해 얻은) 파일 핸들과 관련한 연산들이 디폴트로 유니코드 문자열을 요구하고 이진 바이트 문자열을 요구하지 않는다는 것
    • 'r' - 텍스트 읽기 모드 / 'rb' - 이진 읽기 모드
    • 'w' - 텍스트 쓰기 모드 / 'wb' - 이진 쓰기 모드
    • open 함수의 encoding 파라미터를 명시하는 방법으로도 가능

BETTER WAY 4 C 스타일 형식 문자열을 str.format과 쓰기보다는 f-문자열을 통한 인터폴레이션을 사용하라.

형식화(formatting)는 미리 정의된 문자열에 데이터 값을 끼워 넣어서 사람이 보기 좋은 문자열로 저장하는 과정이다.

  • 언어에 내장된 기능과 표준 라이브러리를 통해 네 가지 방식(C스타일 형식 문자열, 파이썬 % 딕셔너리 연산자, format, f-string)으로 형식화를 할 수 있다.
  • 파이썬 프로그래머가 사용할 수 있는 형식화 옵션 중에서 f-string이 최고이다.
    • 사용법 간단 / 기존의 형식화 문제점 해결
  • 개인적으로는 f-string은 3.6이상에서 지원하므로 하위 버전과의 호환을 위해서 format을 사용하는 중이다.

BETTER WAY 5 복잡한 식을 쓰는 대신 도우미 함수를 작성하라.

  • 파이썬 문법을 사용하면 아주 복잡하고 읽기 어려운 한 줄짜리 식을 쉽게 작성할 수 있다.
  • 식이 복잡해지기 시작하면 바로 식을 더 작은 조각으로 나눠서 로직을 도우미 함수로 옮겨야 한다.
  • 특히 같은 로직을 반복해 사용할 때는 도우미 함수를 꼭 사용하라.
  • 불(boolean) 연산자 or나 and를 식에 사용하는 것보다 if/else 식을 쓰는 편이 더 가독성이 좋다.
my_values = {'빨강': ['5'], '파랑': ['0'], '초록': ['']}

# 1
print('빨강:', my_values.get('빨강'))
print('초록:', my_values.get('초록'))
print('투명도:', my_values.get('투명도'))

빨강: ['5']
초록: ['']
투명도: None

# 2
red = my_values.get('빨강', [''])[0] or 0
green = my_values.get('초록', [''])[0] or 0
opacity = my_values.get('투명도', [''])[0] or 0
print(f'빨강: {red')
print(f'초록: {green}')
print(f'투명도: {opacity}')

빨강: '5'
초록: 0
투명도: 0

# 3
red = int(my_values.get('빨강', [''])[0] or 0)

# 4
red_str = my_values.get('빨강', [''])
red = int(red_str[0]) if red_str[0] else 0

# 5
green_str = my_values.get('초록', [''])
if green_str[0]:
	green = int(green_str[0])
else:
	green = 0

# 6
def get_first_int(values, key, default=0):
	found = values.get(key, [''])
	if found[0]:
		return int(found[0])
	return default

green = get_first_int(my_values, '초록')

BETTER WAY 6 인덱스를 사용하는 대신 대입을 사용해 데이터를 언패킹하라

  • 언패킹(unpacking)(풀기) 구문 → 언패킹 구문을 사용하면 한 문장 안에서 여러 값을 대입할 수 있다.
  • 오름차순 정렬 알고리즘에서 전형적인 인덱스 구문(그리고 임시 변수)을 사용해 list의 두 위치에 있는 원소를 서로 맞바꾼다.
  • But 언패킹 구문을 사용하면 한 줄로 두 인덱스가 가리키는 원소를 서로 맞바꿀 수 있다.
  • 파이썬 언패킹은 일반화돼 있으므로 모든 이터러블에 적용할 수 있다. 그리고 이터러블이 여러 계층으로 내포된 경우에도 언패킹을 적용할 수 있다.
  • 인덱스를 사용해 시퀀스 내부에 접근하는 대신 언패킹을 사용해 시각적인 잡음을 줄이고 코드를 더 명확하게 만들어야한다.
# 1
item = ('호박엿', '식혜')
first, second = item	# 언패킹
print(first, '&', second)
>>>
호박엿 & 식혜

# 2 - 언패킹 구문 미사용
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
names = ['프레즐', '당근', '쑥갓', '베이컨']
bubble_sort(names)
print(name)
>>>
['당근', '베이컨', '쑥갓', '프레즐']

# 3 - 언패킹 구문 사용
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]
names = ['프레즐', '당근', '쑥갓', '베이컨']
bubble_sort(names)
print(name)

# 4- for 루프 또는 그와 비슷한 다른 요소(컴프리헨션이나 제너레이터 식)의 대상인 리스트의 원소를 언패킹
snacks = [('베이컨', 350), ('도넛', 240), '머핀'190)
for i in range(len(snacks)):
	item = snacks[i]
	name = item[0]
	calories = item[1]
	print(f'#{i+1}: {name}{calories} 칼로리입니다.')
>>>
#1: 베이컨 은 350 칼로리입니다.
#2: 도넛 은 240 칼로리입니다.
#3: 머핀 은 190 칼로리입니다.

# 5
for rank, (name, calories) in enumerate(snacks, 1):
	print(f'#{rank}: {name}{calories} 칼로리입니다.')
>>>
#1: 베이컨 은 350 칼로리입니다.
#2: 도넛 은 240 칼로리입니다.
#3: 머핀 은 190 칼로리입니다.

BETTER WAY 7 range보다는 enumerate를 사용하라

  • enumerate를 사용하면 이터레이터에 대해 루프를 돌면서 이터레이터에서 가져오는 원소의 인덱스까지 얻는 코드를 간결하게 작성할 수 있다.
  • range에 대해 루프를 돌면서 시퀀스의 원소를 인덱스로 가져오기보다는 enumerate를 사용하라.
  • enumerate의 두 번째 파라미터로 어디부터 원소를 가져오기 시작할지 지정할 수 있다. (디폴트 값은 0이다)
# 1 - range 내장 함수
from random import randint

random_bits = 0
for i in range(32):
	if randint(0, 1):
		random_bits |= << i
print(bin(random_bits))

0b11101000100100000111000010000001

# 2 - 문자열로 이뤄진 list
flavor_list = ['바닐라', '초콜릿', '피칸', '딸기']
for flavor in flavor_list:
	print(f'{flavor} 맛있어요.')


바닐라 맛있어요.
초콜릿 맛있어요.
피칸 맛있어요.
딸기 맛있어요.

# 3 - 몇 번째 순서인지 알고 싶을 때
for i in range(len(flavor_list)):
	flavor = flavor_list[i]
	print(f'{i + 1}: {flavor}')

1: 바닐라
2: 초콜릿
3: 피칸
4: 딸기

# 4 - enumerate 사용
it = enumerate(flavor_list)
print(next(it))
print(next(it))

(0, '바닐라')
(1, '초콜릿')

# 5 - for 문에서 간결하게 언패킹하여 사용
for i, flavor in enumerate(flavor_list):
	print(f'{i + 1}: {flavor}')

1: 바닐라
2: 초콜릿
3: 피칸
4: 딸기

# 6 - 시작 지정
for i, flavor in enumerate(flavor_list, 1):
	print(f'{i}: {flavor}')

1: 바닐라
2: 초콜릿
3: 피칸
4: 딸기

BETTER WAY 8 여러 이터레이터에 대해 나란히 루프를 수행하려면 zip을 사용하라

  • zip은 둘 이상의 이터레이터를 지연 계산 제너레이터를 사용해 묶어준다.
  • zip 제너레이터는 각 이터레이터의 다음 값이 들어 있는 튜플을 반환한다.
  • 이 튜플을 for 문에서 바로 언패킹할 수 있다.
  • zip은 자신이 감싼 이터레이터 중 어느 하나가 끝날 때까지 튜플을 내놓는다. 따라서 출력은 가장 짧은 입력의 길이와 같다.
    • zip에 전달한 리스트의 길이가 같지 않을 것으로 예상한다면 itertools 내장 모듈에 들어 있는 zip_longest를 대신 사용하는 것을 고려하라.
# 1 - 리스트 컴프리헨션을 사용하면 소스 list에서 새로운 list를 파생시키기 쉽다
names = ['Cecila', '남궁민수', '현우']
counts = [len(n) for n in names]
print(counts)
>>>
[7, 4, 3]

# 2 - 만들어진 list의 각 원소는 소스 list에서 같은 인덱스 위치에 있는 원소와 관련이 있다. 두 리스트를 동시에 이터레이션할 경우 names 소스 리스트의 길이를 사용해 이터레이션 할 수 있다.
longest_name = None
max_count = 0
for i in range(len(names)):
	count = counts[i]
	if count > max_count:
		longest_name = names[i]
		max_count = count
print(longest_name)
>>>
Cecilia
# 문제는 이 루프가 시각적으로 잡음이 많다

# 3 - enumerate를 사용하면 약간 나아지지만, 이 코드도 여전히 이상적이지는 않다
for i, name in enumerate(names):
	count = counts[i]
	if count > max_count:
		longest_name = name
		max_count = count

# 4 - zip 내장 함수 사용
for name, count in zip(names, counts):
	if count > max_count:
		longest_name = name
		max_count = count

# 5 - 이터레이터의 길이가 다른 경우
names.append('Rosalind')
for name, count in zip(names, counts):
	print(name)
>>>
Cecila
남궁민수
현우

# 6 itertools의 zip_longest 사용
for name, count in itertools.zip_longest(names, counts):
	print(f'{name}: {count}')
>>>
Cecilia: 7
남궁민수: 4
현우: 2
Rosalind: None

BETTER WAY 9 for나 while 루프 뒤에 else 블록을 사용하지 말라

# 1 - 파이썬에서는 루프가 반복수행하는 내부 블록 바로 다음에 else 블록을 추가할 수 있다.
for i in range(3):
	print('Loop', i)
else:
	print('Else block')
>>>
Loop 0
Loop 1
Loop 2
Else block!
# 여기서 else는 '처리할 예외가 없는 경우에 이 블록을 실행하라'는 뜻

# 2 - 실제로 루프 안에서 break 문을 사용하면 else 블록이 실행되지 않는다
for i in range(3):
	print('Loop', i)
	if i == 1:
		break
else:
	print('Else block')
>>>
Loop 0
Loop 1

# 3 - 빈 시퀀스에 대한 루프를 실행하면 else 블록이 바로 실행됨
for x in []:
	print('이 줄은 실행되지 않음')
else:
	print('For Else block!')
>>>
For Else block!

# 4 - while 루프의 조건이 처음부터 False인 경우(루프가 한 번도 실행되지 못하는 경우)에도 else 블록이 바로 실행됨
while False:
	print('이 줄은 실행되지 않음')
else:
	print('While Else block!')
>>>
While Else block!

# 5 - 서로소를 찾는 동작
a = 4
b = 9
for i in range(2, min(a, b) + 1):
	print('검사 중', i)
	if a % i == 0 and b % i ==0:
		print('서로소 아님')
		break
else:
	print('서로소')
>>>
검사 중 2
검사 중 3
검사 중 4
서로소

# 6 - 차라리 도우미 함수를 작성하는 것이 좋음
# 6-1 원하는 조건을 찾자마자 빠르게 함수를 반환하는 방식
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

assert coprime(4, 9)
assert not coprime(3, 6)

# 6-2 원하는 대상을 찾았는지 나타내는 결과 변수를 도입
def coprime_alternate(a, b):
	is_coprime = True
	for i in range(2, min(a, b) + 1):
		if a % i == 0 and b % i == 0:
			is_coprime = False
			break
	return is_coprime

assert coprime_alternate(4, 9)
assert not coprime_alternate(3, 6)

BETTER WAY 10 대입식을 사용해 반복을 피하라

  • 대입식은 영어로 assignment expression이며 왈러스 연산자(walrus operator)라고도 부름
  • 3.8에 새롭게 도입된 구문
  • 일반 대입문은 a = b라고 쓰고 왈러스 연산자는 a := b 라 쓴다.
# 1 - 필요 이상으로 잡음이 많다
fresh_fruit = {
	'사과': 10,
	'바나나': 8,
	'레몬': 5
}

def make_lemonade(count):
	...

def out_of_stock():
	...

count = fresh_fruit.get('레몬', 0)
if count:
	 make_lemonade(count)
else:
	out_of_stock()

# 2 - 대입식을 사용하였을 때
if count := fresh_fruit.get('레몬', 0):
	make_lemonade(count)
else:
	out_of_stock()

# 3 - count 대입이 변수를 쓸데없이 너무 강조
def make_cider(count):
	...
count = fresh_fruit.get('사과', 0)
if count >= 4:
	make_cider(count)
else:
	out_of_stock()

# 4 - 왈러스 연산자 사용
if (count := fresh_fruit.get('사과', 0)) >= 4:
	make_cider(count)
else:
	out_of_stock()
profile
GitHub - https://github.com/jenu8628

0개의 댓글