목차
- [시작하기 전, 파이썬에 대한 기본적인 문법]
시작하기 전, 파이썬기본 문법
인덴트
: 일반적으로 쓰는 인덴트는
- 탭
- 4칸 공백
- 2칸 공백
이지만 PEP 8 이후 공백 4칸으로 통합한다
네이밍 컨벤션(파이썬의 변수명)
스네이크 케이스 Snake Case
- 자바와 달리 각 단어를 밑줄(_) 로 구분하여 표기
-
이는 함수명도 마찬가지.
직접 작성하는 코드는 소문자 변수명과 함수명을 기본으로 해야 함
-> 특히 파이썬은 자바 스타일로 코딩하는 것을 지양
(자바: 단어별로 대소 문자를 구별하여 표기- 카멜 케이스 Camel Case ) - 카멜 케이스 camelCase: int = 1
- 스네이크 케이스 snake_case: int = 1
타입 힌트
다음과 같은 형태로 타입을 선언가능
- a: str = “1”
- b: int = 1
[타입힌트 사용하는 함수]
def fn(a: int) -> bool:
이처럼 타입 힌트를 사용하게 되면,
- 이제 fn() 함수의 파라미터 a 가 정수형임을 알 수 있음
- 리턴 값으로 True 또는 False라는 것을 알 수 있음
(장점)
- 가독성 좋아짐
-
버그 발생률 낮아짐
VS
[기존에 타입 힌트를 사용하지 않는 파이썬 함수]
def fn(a):
...
(장점)
- 빠르게 정의해서 사용할 수 있음 (단점)
- fn() 함수의 파라미터 a 에는 숫자를 넘겨야 하는지, 문자를 넘겨야 하는지 전혀 알 수 없음
- 이 함수의 리턴값이 무엇인지 알 수 없음
- 나중에 프로젝트의 규모가 커지게 되면 가독성을 떨어뜨리게 되며 무엇보다 버그 유발의 주범이 됨.
- 코드를 정리할 때만이라도 타입을 모두 지정해서 보기좋게 제출한다면, 코드 리뷰 시 면접관 에게 좋은 점수를 받을 수 있을 것.
mypy
: 온라인 코딩 테스트 시에 이를 사용하면
-> 타입 힌트에 오류가 없는지 자동으로 확인가능
-> 이를 통해 수정 후 코드를 제출할 수 있음.
mypy 설치
$ pip install mypy
$ mypy solution.py
: 타입 힌트가 잘못 지정된 코드는 Incompatible return value type 오류 발생
-> 확인 후 직접 코드를 수정 가능.
리스트 컴프리헨션(List Comprehension)
파이썬은,
map , filter 와 같은 함수형 Functional 기능을 지원
1> 람다 표현식 Lambda Expression 도 지원.
>>> list(map(lambda x: x + 10, [1, 2, 3]))
[11, 12, 13]
2> 리스트 컴프리헨션
- 형식
[최종명령 범위 조건]
- 기존 리스트를 기반으로 새로운 리스트를 만들어내는 구문
- 하스켈 Haskell 같은 함수형 언어에서 기능을 차용해용
- ex
다음은 홀수인 경우 2를 곱해 출력하라는 리스트 컴프리헨션이다.>>> [n * 2 for n in range(1, 10 + 1) if n % 2 == 1] [2, 6, 10, 14, 18]
만약 리스트 컴프리헨션을 사용하지 않는다면
>>> a = [ ] >>> for n in range(1, 10 + 1): ... if n % 2 == 1: ... a.append(n * 2) ... >>> a [2, 6, 10, 14, 18]
- 리스트 컴프리헨션이라고 반드시 리스트만 가능한 것은 아님.
- 버전 2.7 이후에는 다음과 같이 리스트 외에도 딕셔너리 등이 가능하도록 추가됐다.
딕셔너리 버전
a = { }
for key, value in original.items():
a[key] = value
이와 같은 정의 코드는 다음과 같이 처리가능
a = {key: value for key, value in original.items()}
무리하게 복잡하게 작성할 경우 가독성을 떨어뜨릴 수 있으므로 적절히 사용하는 게 중요.
대체로 표현식은 2개를 넘지 않아야 함.
제너레이터 Generator
[정의] 루프의 반복 Iteration 동작을 제어할 수 있는 루틴 형태.
[ex]
임의의 조건 으로 숫자 1억 개를 만들어내 계산하는 프로그램을 작성한다고 가정
이 경우 제너레이터가 없다면 메모리 어딘가에 만들어낸 숫자 1억 개를 보관하고 있어야 함.
but
제너레이터를 이용하면, 단순히 제너레이터만 생성해두고 필요할 때 언제든 숫자를 만들어낼 수 있음.
-> 만약에 1억 개 중 100개 정도만 쓰인다면 차이는 더욱 클 것.
[yield]
- 제너레이터가 여기까지 실행 중이던 값을 내보낸다는(단어의 사전적 의미처럼 ‘양보하다’) 의미
- 이때 yield 구문을 사용하면 제너레이터 리턴 가능.
함수의 종류 | 차이점(설명) |
---|---|
기존 함수 | return 구문을 맞닥뜨리면 값을 리턴하고 모든 함수의 동작을 종료 |
모든 값을 메모리에 담고있음 | |
yield | 중간값을 리턴한 다음 함수는 종료되지 않고 계속해서 맨 끝에 도달할 때까지 실행됨. |
물론 다음 코드의 경우처럼 while True 구문은 종료 조건이 없으므로 계속해서 값을 내보낼 수 있음 |
>>> def get_natural_number():
... n = 0
... while True:
... n += 1
... yield n
...
이 경우 함수의 리턴 값은 다음과 같이 제너레이터가 된다.
>>> get_natural_number()
<generator object get_natural_number at 0x10d3139d0>
제너레이터 함수 추출
만약 다음 값을 생성하려면 next() 로 추출하면 됨.
예를 들어 100개의 값을 생성하고 싶다면 다음과 같이 100번 동안 next() 를 수행하면 됨.
>>> g = get_natural_number()
>>> for _ in range(0, 100):
... print(next(g))
...
1
2
3
...
98
99
100
제너레이터는 여러 타입의 값을 하나의 함수에서 생성하는 것도 가능
>>> 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 문에서 쓰임.
코드 설명
- range() 는 range 클래스를 리턴
- for 문에서 사용할 경우 내부적으로는 제너레이터의 next() 를 호출하듯 매번 다음 숫자를 생성해 냄.
>>> list(range(5))
[0, 1, 2, 3, 4]
>>> range(5)
range(0, 5)
>>> type(range(5))
<class 'range'>
>>> for i in range(5):
... print(i, end=' ')
...
0 1 2 3 4
다음은 숫자 100만 개를 생성하는 2가지 방법이다.
>>> a = [n for n in range(1000000)]
>>> b = range(1000000)
실제로 다음과 같이 len() 으로 길이 비교를 해보면 둘 다 동일한 100만 개가 출력되며,
비교 연산자에서도 True 를 리턴 함.
>>> len(a)
1000000
>>> len(b)
1000000
>>> len(a) == len(b)
True
차이점
- a : 이미 생성된 값이 담김,
- b : 생성해야 한다는 조건만 존재.
range 클래스를 리턴하는 b의 메모리 점유율이 훨씬 낮다.
>>> b
range(0, 1000000)
>>> type(b)
<class 'range'>
메모리 차이 확인
>>> sys.getsizeof(a)
8697464
>>> sys.getsizeof(b)
48
[b의 장점 결론]
- 똑같이 숫자 100만 개를 갖고 있으나 range 클래스를 이용하는 b 변수의 메모리 점유율이 훨씬 더 작음.
- 100만 개가 아니라 1억 개라도 b 변수의 메모리 점유율은 동일.
(생성 조건만 보관하고 있기 때문) - 미리 생성하지 않은 값은 인덱스에 접근이 안될 거라 생각할 수 있으나,
인덱스로 접근 시에는 바로 생성하도록 구현되어 있기 때문에 다음과 같이 리스트와 거의 동일한 느낌으로 불편 없이 사용 가능
>>> b[999]
999
enumerate
- enumerate()는 ‘열거하다’는 뜻의 함수로,
여러 가지 자료형( list , set , tuple 등)을 인덱스를 포함한 enumerate 객체로 리턴. - 인덱스를 자동으로 부여해주기 때문에 매우 편리하게 활용 가능
[사용 방법]
>>> a = [1,2,3,2,45,2,5]
>>> a
[1, 2, 3, 2, 45, 2, 5]
>>> enumerate(a)
<enumerate object at 0x1010f83f0>
>>> list(enumerate(a))
[(0, 1), (1, 2), (2, 3), (3, 2), (4, 45), (5, 2), (6, 5)]
[예제]
a = [‘a1’, ‘b2’, ‘c3’] 가 있을 때 이 리스트의 인덱스와 값을 함께 출력하려면?
(풀이 1)
for i in range(len(a)):
print(i, a[i])
단점) 불필요한 a[i] 조회 작업과 전체 길이를 조회하여 루프를 처리하는 형태가 깔끔X.
(풀이 2)
i = 0
for v in a:
print(i, v)
i += 1
단점) 값은 깔끔하게 처리했으나 이 경우 인덱스를 위한 변수를 별도로 관리하는 형태라 이 또한 깔끔X.
(풀이 3) 권장풀이
- a 가 ([‘A’, ‘B’, ‘C’])인 경우
- 시작 인덱스를 바꾸고 싶으면 enumerate(a, start=1):
- 튜플(tuple) 형식으로 변환
for entry in enumerate(a):
print(entry)
(0, 'A')
(1, 'B')
(2, 'C')
# for을 쓰는 이유
# 안쓰면 전체가 튜블로 한 번 더 묶이게 됨
a = ['A', 'B', 'C']
b = tuple(enumerate(a))
b
#((0, 'A'), (1, 'B'), (2, 'C'))
# for로 꺼내준다는 생각 이므로 이 코드도 가능
for i in tuple(enumerate(a)):
print(i)
#(0, 'A')
#(1, 'B')
#(2, 'C')
인덱스와 원소를 각각 다른 변수에 할당하고 싶다면 인자 풀기(unpacking)
for으로 인덱스랑 a를 같이 돌린다고 생각
for i, v in enumerate(a):
print(i, v)
#0 A
#1 B
#2 C
# 여기서도 시작 인덱스 바꾸기 가능
for i, v in enumerate(a, start =1):
print(i, v)
#1 A
#2 B
#3 C
// 나눗셈 연산자
/ 나눗셈 연산자
나눈 결과 실수형으로 출력
>>> 5 / 3
1.6666666666666667
>>> type(5 / 3)
<class 'float'>
// 나눗셈 연산자
몫 출력
나눈 결과를 int형으로 변환
>>> 5 // 3
1
>>> type(5 // 3)
<class 'int'>
>>> int(5 / 3)
1
>>> type(int(5 / 3))
<class 'int'>
# 참고로 나머지는 %
>>> 5 % 3
2
divmod() 몫과 나머지를 동시에 구하려면
>>> divmod(5, 3)
(1, 2)
- 실무에서는 이처럼 print() 를 활용하는 디버깅 방법은 추천하지 않음.
- 하지만 코딩 테스트 시에는 디버거를 사용하거나 TDD 방식 으로 접근하기도 어려움
-> 사실상 print() 가 디버깅을 위해 제공되는 유일한 기능
출력 방법
-
- 콤마( ,) 로 구분
- 이 경우 한 칸 공백이 디폴트로 설정되어 있으며,
그대로 출력하면 띄어쓰기로 값을 구분해준다.
>>> print('A1', 'B2') A1 B2
- sep 파라미터
- 구분자를 콤마( ,) 로 지정해줄 수도 있음
- 분리된 두 변수 사이를 무엇으로 채울지를 결정하는 파라미터로 기본값은 공백(스페이스) 하나
>>> print('A1', 'B2', sep=',') A1,B2
-
줄바꿈을 하지 않도록 제한
print('aa', end=' ') print('bb') ---------- aa bb
-
- join()
- 리스트 출력시 이로 묶어서 처리
>>> a = ['A', 'B'] >>> print(' '.join(a)) A B >>> a = ['A', 'B'] >>> print('-'.join(a)) A-B >>> a = ['A', 'B'] >>> print('+'.join(a)) A+B
-
- f-string(formated string literal) ((((추천하는 방식))))
- 변수를 뒤에 별도로 부여할 필요X
마치 템플릿을 사용하듯 인라인으로 삽입할 수 있어 편리하게 사용 가능
-> 기존의 % 를 사용하거나 .format 을 부여하는 방식에 비해 훨씬 간결하고 직관적이며 속도도 빠름
>>> print(f'{넣고자하는 정수 + 1}: { 넣고자하는 변수 이름}')
- 문자열 포매팅 더 자세히 알아보기
pass
- 널 연산 Null Operation 으로 아무것도 하지 않는 기능
- [쓰임]
- 코딩 시 코드의 전체 골격을 잡아 놓고 내부에서 처리할 내용은 차근차근 생각하며 만들겠다는 의도로 다음과 같이 코딩하는 경우가 있다.
# 아래 클래스는 실행X
class MyClass(object):
def method_a(self):
def method_b(self):
print("Method B")
c = MyClass()
# 다음과 같이 인덴트 오류가 발생
#def method_b(self):
#^
#IndentationError: expected an indented block
#-> method_a() 가 아무런 처리를 하지 않았기 때문
pass 는 이런 오류를 막는 역할을 함
(오류처리)
class MyClass(object):
def method_a(self):
# 여기에 pass 추가
pass
def method_b(self):
print("Method B")
c = MyClass()
locals
- 로컬 심볼 테이블 딕셔너리를 가져오는 메소드로 업데이트 또한 가능
- 여기서는 딕셔너리를 가져오는 부분에 집중해 살펴보자면, 우선 로컬에 선언된 모든 변수를 조회할 수 있는 강력한 명령
-> 디버깅에 많은 도움이 됨 - 특히 로컬 스코프에 제한해 정보를 조회할 수 있기 때문에 클래스의 특정 메소드 내부에서나 함수 내부의 로컬 정보를 조회해 잘못 선언한 부분이 없는지 확인하는 용도로 활용 가능. 변수명을 일일이 찾아낼 필요 없이 로컬 스코프에 정의된 모든 변수를 출력하기 때문에 편리.
[출력예시]
import pprint
pprint.pprint(locals())
*pprint* 로 출력하게 되면 보기 좋게 줄바꿈 처리를 해주기 때문에 가독성이 높다.
{ 'nums': [2, 7, 11, 15],
'pprint': <module 'pprint' from '/usr/lib/python3.8/pprint.py'>,
'self': <__main__.Solution object at 0x7f0994769d90>,
'target': 9}
클래스 메소드 내부의 모든 로컬 변수를 출력해 주기 때문에 디버깅에 많은 도움이 됨.