해당 시리즈는 [ 파이썬 알고리즘 인터뷰 ] 를 통해 학습한 내용을 정리한 글입니다.
✨ 2021. 01. 02 알고리즘 스터디 시작
with 유노재욱, 최강효준, 믹키성은, 영웅도연 👩🏻💻🧑🏻💻
1989년 12월, 🎄 크리스마스 프로젝트로 새로운 언어를 직접 만들어보기로 한 사람,,
바로 네덜란드의 컴퓨터과학자 '귀도 반 로섬' 씨,,, 🥺
그의 원칙은 다음과 같다.
그 후 30여년이 지난 지금, 파이썬은 문법이 매우 쉬워 입문자들이 프로그래밍을 처음 배울 때 가장 먼저 추천되는 언어로 불린다. 실행 가능한 수도코드라고 불리기도 한다.
(감삼다.. 감삼다... 🙂🙃🙂 )
파이썬을 코테를 치룰 언어로 선택했다면, 적어도 그 언어의 세부 구현까지 상세히 알고 있어야 한다.
만약 딕셔너리에서 입력 순서가 항상 유지된다고 어설프게 알고있다면 ( 버전 3.7 이상에서만 가능 )
잘못된 결과로 많은 시간을 허비하게 될 것이다. (삽질 .. ⛏)
공식 인터프리터인 CPython을 기준으로 하고, CPython의 내부 구현을 살펴보면서 원리를 정확히 이해하고 살펴보자.
파이썬의 대표적 특징인 인덴트는 공식 가이드인 PEP 8에 따라 공백 4칸을 원칙으로 한다.
PEP (;파이썬 개선 제안서)
파이썬 개발은 PEP 프로세스를 통해 진행된다.
PEP 프로세스는 새로운 기능을 제안하고, 커뮤니티의 의견을 수렴해 디자인 결정을 문서화하는 파이썬의 주요 개발 프로세스이다.
0번부터 시작해서 순서대로 번호르 부여하고, 3xx, 4xx 같은 형태로 나름의 규칙에 따라 번호를 부여한다.
1번부터 15번은 메타 PEP이다. PEP에 대한 PEP로, 대표적으로 파이썬의 코딩 스타일 가이드인 PEP 8은 여기에 해당된다.
이외에도 파이썬 버전 3과 관련된 제안들은 모두 3xxx으로 시작한다.
이 모든 방식을 직접 고민하기보다 파이참 커뮤니티 에디션과 같은 좋은 개발 도구를 활용하는 방법이 있다.
파이참을 이용하면 별도로 신경쓰지않아도 코딩 가이드를 자동으로 맞춰준다.
파이참에서 Reformat Code를 실행하면 자동으로 코드를 PEP 8 기준에 맞춰준다.
파이썬의 변수명 네이밍 컨벤션은 자바와 달리 각 단어를 밑줄(_)로 구분하여 표기하는 스네이크 케이스를 따른다.
또한 소문자 변수명과 함수명을 기본으로 한다.
파이썬과 달리 자바는 단어별로 대소문자를 구별하여 표기하는 카멜 케이스를 따른다.
이 책에서는 모든 변수명을 스네이크 케이스를 기본으로 작성했다고 한다.
우리도 파이썬을 코딩할 시에 스네이크 표기법을 기본으로 하되 면접관이 질문을 한다면
파이썬의 PEP 8 및 철학에 따라 스네이크 코딩을 지향한다고 말할 수 있어야 한다.
[ 카멜 케이스 ] 대소문자를 구분하여 섞어서 작명하는 방식 , 자바의 대표적인 표기 방식이다.
특이하게도 단어의 첫문자는 모두 대문자로 시작한다.
[ 파스칼 케이스 ] 첫 단어의 시작문자는 소문자로 표기하며, 첫 시작 문자도 대문자로 하는 방식
[ 스네이크 케이스 ] 각 단어를 _(;언더스코어)로 구분한다. 일반적으로 모두 소문자로 표기하지만 경우에 따라 시작 문자는 대문자로 표기하기도 한다.
파이썬은 대표적인 동적 타이핑 언어이지만, 타입을 지정할 수 있는 타입 힌트가 PEP 484 문서에 추가되었다.
CPython의 typing.py에는 선언할 수 있는 타입이 잘 명시되어 있고, 다음과 같은 형태로 타입을 선언한다.
a : str = "1"
b : int = 1
예를 들어 기존에 타입 힌트를 사용하지 않는 파이썬 함수는 다음과 같이 함수를 정의해왔다.
def fn(a): ...
빠르게 정의해서 사용할 수 있다는 장점이 있지만 fn() 함수의 파라미터 a에는 숫자를 넘겨야 하는지,
문자를 넘겨야 하는지 전혀 알수 없고, 이 함수의 리턴값이 무엇인지도 알 수 없다.
나중에 프로젝트의 규모가 커지면 가독성이 떨어지고, 버그 유발의 주범이 된다.
def fn(a : int) -> bool: ...
다음과 같이 타입 힌트를 사용한 코드를 보자.
이제 fn() 함수의 파라미터 a가 int, 정수형이라는 것과 리턴 값으로는 True, False를 리턴한다는 점을 알 수 있다. 이와 같이 명시적으로 선언하면 가독성도 좋아지고, 버그 발생 확률도 줄일 수 있다.
물론 실제로는 강제 규약이 아니다 보니, 여전히 동적으로 할당될 수 있다.
따라서 주의해서 할당을 하자!
코테에는 일반적으로 짧은 알고리즘으로 끝나는 경우가 많고, 타입은 지정하지 않아도 한눈에 보일 만큼 명확하기 때문에 굳이 지정하지 않아도 된다.
그러나 코드를 정리할 때만이라도 타입을 모두 지정해서 보기 좋게 제출한다면, 코드 리뷰 시 면접관에게 좋은 점수를 받을 수 있다.
온라인 코테일시에는 mypy를 사용하면 타입 힌트에 오류가 없는지 자동으로 확인할 수 있다!
$pip install mypy
$mypy solution.py
타입 힌트가 잘못 지정된 코드는 다음과 같인 Incompatible return value type오류가 발생하므로 확인 후 직접 코드를 수정할 수 있다.
파이썬은 map,filter와 같은 함수형 기능을 지원하고, 다음과 같은 람다 표현식도 지원한다.
리스트 컴프리헨션이란 기존 리스트를 기반으로 새로운 리스트를 만들어내는 구문이다.
다음은 홀수인 경우 2를 곱해 출력하라는 리스트 컴프리헨션이다.
>>> [n*2 for n in range(1,10 + 1) if n % 2 == 1 ]
[2, 6, 10, 14, 18]
리스트 컴프리헨션이라고 해서 반드시 리스트에만 가능한 것은 아니다.
딕셔너리 등에도 가능하다.
a = {}
for key,value in original.items():
a[key] = value
a = { key : value for key, value in original.items() }
리스트 컴프리헨션은 가독성이 좋은 편이지만 이 또한 무리하게 작성하면 가독성을 떨어뜨린다.
제너레이터는 루프의 반복 동작을 제어할 수 있는 루틴 형태이다.
예를 들어 임의의 조건으로 숫자 1억 개를 만들어내 계산해야 하는 프로그램을 작성한다고 가정하자.
이 경우 제너레이터가 없다면 메모리 어딘가에 만들어낸 숫자 1억 개를 보관하고 있어야한다.
제너레이터를 이용하면, 단순히 제너레이터만 생성해두고 필요할 때 언제든 숫자를 만들어낼 수 있다.
만약 1억개 중 100개 정도만 쓰인다면 차이는 더욱 클 것이다.
이때 yield 구문을 사용하면 제너레이터를 리턴할 수 있다.
기존의 함수는 return 구문을 맞닥뜨리면 값을 리턴하고 함수의 동작을 종료하지만,
yield는 제너레이터가 여기까지 실행 중이던 값을 내보낸다는 뜻으로
함수를 종료시키지 않고 계속해서 맨 끝에 도달할 때까지 실행된다.
def get_natural_number():
n=0
while True:
n+=1
yield n
이 함수의 리턴 값은 제너레이터가 된다.
g = get_natural_number()
for _ in range(0,100):
print(next(g))
1
2
3
...
99
100
만약 다음값을 생성하고 싶다면 next()로 추출하면 된다.
예를 들어 100개의 값을 생성하고 싶다면 다음과 같이 100번동안 next()를 수행하면 된다.
그리고 제너레이터는 여러 타입의 값을 하나의 함수에서 생성하는 것도 가능하다.
def generator():
yield 1
yield 'string'
yield True
>>>g=generator()
>>>g
generator object generator at 0x10a47c678
>>> next(g)
1
>>> next(g)
'string'
>>> next(g)
True
제너레이터의 방식을 활용하는 대표적인 함수로 range()가 있다.
range()는 range 클래스를 리턴하며, for 문에서 사용할 경우 내부적으로는 제너레이터의 next()를 호출하듯 매번 다음 숫자를 생성해내게 된다.
만약 생성할 숫자가 100만개쯤 된다면 메모리에 적지 않은 공간을 차지할 것이다.
그러나 제너레이터를 리턴하듯 range 클래스만 리턴하면 그렇지 않다.
생성 조건만 정해두고 나중에 필요할 때 생성해서 꺼내 쓸 수 있다.
다음은 숫자 100만개를 생성하는 2가지 방법이다.
>>> a = [ n for n in range(1000000)]
>>> b = range(1000000)
len(a) 과 len(b) 는 둘다 동일하고, 비교 연산자에서도 True를 리턴한다.
그러나 a에는 이미 생성된 값이 담겨있고, b는 생성해야 한다는 조건만 존재한다.
sys.getsizeof 를 사용해 메모리 점유율을 비교해보면 range 클래스를 리턴하는 방식의 장점이 쉽게 와닿는다.
똑같이 숫자 100만개를 갖고 있으나 range 클래스를 이용하는 b 변수의 메모리 점유율이 훨씬 작다.
enumerate() 는 열거하다는 뜻의 함수이다.
순서가 있는 자료형 (list,set,tuple 등)을 인덱스를 포함한 enumerate 객체로 리턴한다.
a = [1,2,3]
>>> a
[1, 2, 3]
>>>enumerate(a)
<enumerate object at 0x1010f83f0>
>>>list(enumerate(a))
[(0,1), (1,2), (2,3)]
이처럼 list()로 결과를 추출할 수 있는데, 인덱스를 자동으로 부여해주기 때문에 매우 편리하게 사용할 수 있다.
a = ['a1', 'b2', 'c3']
for i in range(len(a)):
print(i,a[i])
//값을 가져오기 위해 불필요한 a[i] 조회 작업과 루프를 처리하는 형태가 깔끔하지 않음
a = ['a1', 'b2', 'c3']
i = 0
for v in a:
print(i,v)
i += 1
//값은 깔끔하게 처리했지만 인덱스를 위한 별도의 변수를 사용해서 깔끔하지 않음
a = ['a1', 'b2', 'c3']
for i, v in enumerate(a):
print(i,v)
//인덱스와 값 모두 한번에 깔끔하게 처리 됨
5//3 == int(5/3)
// 연산자는 몫을 반환하는 연산자이다.
% 연산자는 나머지를 반환하는 연산자이다.
몫과 나머지를 동시에 구하려면 divmod() 함수를 사용하면 된다.
>>>divmod(5,3)
(1,2) //몫,나머지
코테에서 디버깅을 할 때 가장 자주 쓰이는 명령은 print()이다.
이를 좀 더 유용하게 활용할 수 있는 방법을 살펴보자
가장 쉽게 값을 구분하는 방법은 콤마(,)로 구분하는 것이다.
sep 파라미터로 구분자를 콤마로 지정해줄 수 있다.
>>> print('a','b')
a b
>>> print('a','b',sep=',')
a,b
print() 함수는 항상 줄바꿈을 한다. 이 경우 end 파라미터를 공백으로 처리하여 줄바꿈을 안하게 할 수 있다.
>>print('aa',end='')
>>print('bb')
aa bb
list를 출력할 때는 join()으로 묶어서 처리한다.
>>> a = ['a','b']
>>> print(' '.join(a))
a b
다음과 같이 idx, fruit이 정의되어 있을 때 idx에 1을 더해서 fruit과 함께 출력할 수 있는 방법이 있을까?
>>> idx = 1
>>> fruit = 'apple'
>>> print('{0}: {1}'.format(idx+1, fruit)) //인덱스는 생략 가능
2: apple
>>> print(f'{inx +1}: {fruit}
2: apple
f-string을 사용해서 출력하는 방법도 있다.
변수를 뒤에 별도로 부여할 필요 없이 마치 템플릿을 사용하듯 인라인으로 삽입할 수 있다.
무엇보다 %나 .format을 부여하는 방식에 비해 훨씬 간결하고 직관적이다.
아쉬운 점은 파이썬 3.6+ 에서만 지원한다.
코딩을 할 때 전체 골격을 잡고, 내부에서 처리할 내용은 차근차근 만드는 경우가 있다.
함수가 비어있으면 indent 오류가 발생하는데 이때 pass가 사용되다.
Class MyClass(object):
def method_a(self):
pass //에러 발생 안함!
def method_b(self):
print("Method B")
c = MyClass()
파이썬에서 pass는 널 연산으로 아무것도 하지 않는 기능이다.
pass는 먼저 목업 인터페이스부터 구현한 다음에 추후 구현을 진행할 수 있게 한다.
locals()는 로컬 심플 테이블 딕셔너리를 가져오는 메소드이고 업데이트 또한 가능하다.
로컬에 선언된 모든 변수를 조회할 수 있는 강력한 명령이므로 디버깅에 많은 도움이 된다.
(클래스 메소드 내부의 모든 로컬 변수를 출력해준다.)
특히 로컬 스코프에 제한해 정보를 조회할 수 있기 때문에 클래스의 특정 메소드 내부에서나 함수 내부의 로컬 정보를 조회해 잘못 선언한 부분이 없는지 확인하는 용도로 활용할 수 있다.
변수명을 일일이 찾아낼 필요 없이 로컬 스코프에 정의된 모든 변수를 출력하기 때문에 편리하다.
import pprint
pprint.pprint(locals())
pprint로 출력하게 되면 보기 좋게 출바꿈 처리를 해주기 때문에 가독성이 좋다.
채용을 위한 코딩 테스트에서는 제출한 코드의 품질을 평가할 수도 있다.
왜 코드를 이렇게 작성했나요?
물론 좋은 코드에 정답은 없지만 많은 사람이 선호하는 방식이 있다.
깨끗한 코드를 위한 다양한 지침이 많다.
( 파이썬의 PEP나 구글의 파이썬 스타일가이드 )
특히 파이참 같은 IDE를 사용하게 되면 PEP 8 기준으로 자동으로 경고를 띄워주므로 좋은 코드를 작성할 수 있다.
좋은 코드는 쳐다보기만 해도 향기가 난다고 한다. 🌸
나쁜 코드는 당연히 그 반대 ㅎ 💩
우선 코드 내에 변수명을 아무렇게나 지정하고 주석도 없이 작성한 코드를 보자
def numMatchSubseq(self, S:str, words: List[str]) -> int:
a = 0
for b in words:
c = 0
for i in range(len(b)):
d = S[c:].find(b[i])
if d<0 :
a -= 1
break
else:
c += d + 1
return a
언뜻 보면 이게 어떤 코드를 구현한 것인지 한 눈에 알아 챌 수 없다.
이 코드는 문자열에 매칭된 서브 시퀀스의 개수를 구하는 코드이다.
간단한 주석을 다는 것을 매우매우 추천한다.
def numMatchintSubseq(self, S:str, words: List[str]) -> int :
matched_count = 0
for word in words:
pos = 0
for i in range(len(word)):
# find matching position for each character.
found_pos = S[pos:].find(word[i])
if found_pos < 0 :
matched_count -= 1
break
else: # if found, take step position forward.
pos += found_pos + 1
return matched_count
변수명부터 의미를 담아서 부여했고, 간단한 주석을 붙여서 가독성을 높였다.
주석은 한글로 달아도 무방하지만 영어로 작성하는 것에도 부담이 없어야 한다.
적어도 최소한 자연스럽게 영어로 주석을 읽고 쓸 수 있도록 익숙해지자! ㅠ 😢
리스트 컴프리헨션은 대체로 표현식이 2개를 넘지 않도록 하자.
다음과 같이 여러 표현식을 여러 줄에 걸쳐 표현하면 가독성이 지나치게 떨어진다.
파이썬 공식 스타일 가이드인 PEP 8 에서 소개하지 않는 좋은 코드를 위한 지침들이 있다.
특히 가독성을 높이기 위한 지침들이 많다.
함수의 기본값으로 가변 객체를 사용하지 않아야 한다.
함수가 객체를 수정하면 ( 리스트에 아이템 추가 ) 기본값이 변경되기 때문이다.
따라서 다음과 같이 기본적으로 []나 {}를 사용하는 것은 지양해야 한다.
No : def foo(a,b=[]): ...
No : def foo(a,b : Mapping = {}) :
대신 다음과 같이 불변 객체를 사용한다. None을 명시적으로 할당하는 것도 좋은 방법이다.
Yes : def foo(a, b=None):
if b is None:
b = []
Yes : def foo(a, b : Optional[Sequence] = None) :
if b is None:
b = []
True, False 를 판별할 때는 암시적인 방법을 사용하는 편이 간결하고 가독성이 높다.
즉 굳이 False임을 if (foo ≠ [] ): 같은 형태로 판별할 필요가 없다.
if foo: 로 충분하다.
Yes :
if not users:
print('no users')
if foo == 0 :
self.handle_zero()
if i % 10 == 0 :
self.handle_multiple_of_ten()
No :
if len(users)==0:
print('no users')
if foo is not None and not foo :
self.handle_zero()
if not i % 10 :
self.handle_multiple_of_ten()
Zen of Python
- 아름다움이 추함보다 낫다.
- 명시적인 것이 암시적인 것보다 낫다.
- 단순함이 복잡함보다 낫다.
- 복잡함이 꼬인 것보다 낫다.
- 수평이 계층보다 낫다.
- 여유로운 것이 밀집된 것보다 낫다.
- 가독성이 중요하다.
- 특별한 경우라는 것은 규칙을 어겨야 할 정도로 특별한 것이 아니다.
- 비록 실용성이 순수성에 우선하지만 에러 앞에서는 절대 침묵하지 말라.
- 명시적으로 에러를 감추려는게 아니라면, 모호함을 앞에 두고 이를 유추하겠다는 유혹을 버려라.
💫 문제를 풀어낼 -바람직 하고도 유일하며- 명확한 방법이 존재할 것이다.