파이썬 문법

🖤devxyoon·2020년 9월 22일
0

1. 인덴트



파이썬의 인덴트(들여쓰기)는 공식 가이드인 PEP8에 따라 공백 4칸을 원칙으로 한다.
foo = long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

이 코드에서처럼 첫 번째 줄에 파라미터가 없다면, 공백 4칸 인덴트를 한 번 더 추가하여 다른 행과 구분되게 한다.

foo = long_function_name(
    var_one, var_two,
    var_three, var_four)

이 코드에서처러 여러 줄로 나눠쓸 경우 다음 행과 구분되도록 인덴트를 추가한다.

2. 네이밍 컨벤션



파이썬의 변수명 네이밍 컨벤션은 자바와 달리 각 단어를 밑줄(_)로 구분하여 표기하는 스네이크 케이스를 따른다. 이는 함수명도 마찬가지다.

파이썬으로 코딩 시에는 스네이크 표기법 코딩을 기본으로 하되 혹시 면접관이 이런 질문을 하게 된다면 파이썬의 PEP8 및 철학에 따라 스네이크 코딩을 지향한다고 얘기할 수 있어야 한다.

# 카멜 케이스
camelCase: int = 1

# 스네이크 케이스
snake_case: int = 1 

3. 타입 힌트



파이썬은 동적 타이핑 언어임에도, 타입을 지정할 수 있는 타입 힌트가 PEP484 문서에 추가됐다. CPython의 typing.py에는 선언할 수 있는 타입이 잘 명시되어 있으며, 다음과 같은 형태로 타입을 선언할 수 있다.
a: str = "1"
b: int = 1

예를 들어 기존에 타입 힌트를 사용하지 않는 파이썬 함수는 다음과 같이 함수를 정의해 사용해왔다.

def fn(a):

빠르게 정의해서 사용할 수 있다는 장점이 있지만 fn() 함수의 파라미터 a에는 숫자를 넘겨야 하는지, 문자를 넘겨야 하는지 전혀 알 수 없으며 이 함수의 리턴값이 무엇인지도 알 수 없다. a에 숫자를 넘겨야 하는데 잘못해서 문자를 넘기게 되면 버그가 발생하게 될 것이기 때문이다.

def fn(a: int) -> bool:

이처럼 타입 힌트를 사용하게 되면 이제 fn()함수의 파라미터 a가 정수형임을 분명하게 알 수 있으며 리턴 값으로 True 또는 Falase를 리턴할 것이라는 점도 확실하게 알 수 있다.

이와 같이 명시적으로 선언하게 되면 가독성이 좋아지며 버그 발생 확률을 줄일 수 있다.

4. 리스트 컴프리헨션



파이썬은 map, filter와 같은 함수형 기능을 지원하며 다음과 같은 람다 표현식도 지원한다.
>>> list(map(lambda x: x + 10, [1, 2, 3]))

[11, 12, 13]

리스트 컴프리헨션이란 기존 리스트를 기반으로 새로운 리스트를 만들어내는 구문이다.
다음은 홀수인 경우 2를 곱해 출력하라는 리스트 컴프리헨션이다.

>>> [n + 2 for n in range(1, 10 + 1) if n % 2 == 1]

[2, 6, 10, 14, 18]

리스트 컴프리헨션이라고 반딋 리스트만 가능한 것은 아니다. 버전 2.7 이후에는 다음과 같이 리스트 외에도 딕셔너리 등이 가능하도록 추가됐다.

a = {key: value for key, value in original.items()}

이처럼 한 줄로 간결하게 작성할 수 있는 리스트 컴프리헨션은 가독성이 좋은 편이지만 이 또한 무리하고 복잡하게 작성할 경우 가독성을 떨어뜨릴 수 있으므로 적절히 사용하는 것이 중요하다. 대체로 표현식은 2개를 넘지 않아야 한다.

5. 제너레이터



제너레이터는 루프와 반복 동작을 제어할 수 있는 루틴 형태를 말한다.

예를 들어 임의의 조건으로 숫자 1억 개를 만들어내 계산하는 프로그램을 작성한다고 가정해보자. 이 경우 제너레이터가 없다면 메모리 어딘가에 만들어낸 숫자 1억 개를 보관하고 있어야 한다. 그러나 제너레이터를 이용하면, 단순히 제너레이터만 생성해두고 필요할 때 언제든 숫자를 만들어낼 수 있다. 만약에 1억 개 중 100개 정도만 쓰인다면 차이는 더욱 클 것이다.

이때 yield 구문을 사용하면 제너레이터를 리턴할 수 있다.

기존의 함수는 return 구문을 맞닥뜨리면 값을 리턴하고 모든 함수의 동작을 종료한다. 그러나 yield는 제너레이터가 여기까지 실행 중이던 값을 내보낸다는 의미로, 중간값을 리턴한 다음 함수는 종료되지 않고 계속해서 맨 끝에 도달할 때까지 실행된다.

다음 코드의 경우처럼 while True 구문은 종료 조건이 없으므로 계속해서 값을 내보낼 수 있다.

def get_natural_number():
   n = 0
   while True:
      n += 1
      yield n

이 경우 함수의 리턴 값은 다음과 같이 제너레이터가 된다.

만약 다음 값을 생성하려면 next()로 추출하면 된다. 예를 들어 100개의 값을 생성하ㅗㄱ 싶다면 다음과 같이 100번 동안 next()를 수행하면 된다.

g = get_natural_number()
for _ in range(0, 100):
    print(next(g))

아울러 제너레이터는 다음과 같이 여러 타입의 값을 하나의 함수로 생성하는 것도 가능하다.

 def generator():
     yield 1
     yield 'string'
     yield True
     
g = generator()

>>> next(g)
1

>>> next(g)
'string'

>>> next(g)
True

6. range



제너레이터의 방식을 활용하는 대표적인 함수로 range()가 있다. 주로 for 문에서 쓰이는 range() 함수의 쓰임은 다음과 같다.
>>> 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

이 코드에서 range()는 range 클래스를 리턴하며, for 문에서 사용할 경우 내부적으로는 제너레이터의 next()를 호출하듯 매번 다음 숫자를 생성해내게 된다.

만약 생성할 숫자가 100만 개쯤 된다면 어떻게 될까? 메모리에서 적지 않은 공간을 차지할 것이고 생성 시간도 오래 걸릴 것이다. 그러나 제너레이터를 리턴하듯 range 클래스만 리턴하면 그렇지 않다. 생성 조건만 정해두고 나중에 필요할 때 생성해서 꺼내 쓸 수 있다.

a = [n for n in range(1000000)]
b = range(1000000)

실제로 len()으로 길이 비교를 해보면 둘 다 동일한 100만 개가 출력되며, 비교 연산자에서도 True를 리턴한다.

그러나 a에는 이미 생성된 값이 담겨 있고, b는 생성해야 한다는 조건만 존재한다.

이제 둘 사이의 메모리 점유율을 비교해보면 range클래스를 리턴하는 방식의 장점이 쉽게 와닿을 것이다.

>>> sys.getsizeof(a)

8697464

>>> sys.getsizeof(b)

48

range클래스는 인덱스로 접근 시에는 바로 생성사도록 구현되어 있기 때문에 다음과 같이 리스트와 거의 동일한 느낌으로 불편 없이 사용할 수 있다.

>>> b[999]

999

7. 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)]

이처럼 list()로 결과를 추출할 수 있는데, 인덱스를 자동으로 부여해주기 때문에 매우 편리하게 활용할 수 있다. 그렇다면 a = ['a1', 'b2', 'c3']가 있을 때 이 리스트의 인덱스와 값을 함께 출력하려면 어떻게 해야할까?

for l, v in enumerate(a):
    print(l, v)

8. // 나눗셈 연산자



// 연산자는 정수형을 나눗셈할 때 동일한 정수형을 결과로 리턴하면서 내림 연산자의 역할을 한다. 다시 말해 몫을 구하는 연산자다.
>>> 5 / 3

1.66666666667

>>> type(5 / 3)

<class 'float'>

# // 나눗셈 연산자는 int(a / b)와 동일하다

>>> 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)

9. print



가장 쉽게 값을 출력하는 방법은 콤마(,)로 구분하는 것이다. 이 경우 한 칸 공백이 디폴트로 설정되어 있으며, 그대로 출력하면 띄어쓰기로 값을 구분해준다.
>>> print('A1', 'B2')

A1 B2

다음과 같이 sep 파라미터로 구분자를 콤마(,)로 지정해줄 수도 있다.

>>> print('A1', 'B2', sep=',')

A1,B2

print() 함수는 항상 줄바꿈을 하기 때문에 긴 루프의 값을 반복적으로 출력하면 디버깅하기가 어려운데 이 경우 다음과 같이 end 파라미터를 공백으로 처리하여 줄바꿈을 하지 않도록 제한할 수 있다.

print('aa', end=' ')
print('bb')

aa bb

리스트를 출력할 때는 join()으로 묶어서 처리한다.

>>> 'a' = ['A', 'B']
>>> print(' '.join(a))

A B

다음과 같이 idx와 fruit이 정의되어 있다고 할 때,

>>> idx = 1
>>> fruit = "Apple"

idx 값에 1을 더해서 fruit와 함께 출력하는 방법은 어떤 방법이 있을까?

>>> print(f'{idx + 1}: {fruit}')

2: Apple

※ 아쉬운 점은 f-string은 파이썬 3.6+에서만 지원한다는 점이다. 그 이하 버전에서는 동작하지 않으니 이 점에 유의해야 한다.

10. pass



파이썬에서 pass는 Null Operation으로 아무것도 하지 않는 기능이다. pass는 먼저 목업 인터페이스부터 구현한 다음에 추후 구현을 진행할 수 있게 한다.


11. locals



locals()는 로컬 심볼 테이블 딕셔너리를 가져오는 메소드로 업데이트 또한 가능하다. 여기서 딕셔너리를 가져오는 부분에 집중해 살펴보자면, 우선 로컬에 선언된 모든 변수를 조회할 수 있는 강력한 명령이므로 디버깅에 많은 도움을 준다. 다음과 같이 출력할 수 있다.
import pprint

pprint.pprint(locals())

pprint로 출력하게 되면 보기 좋게 줄바꿈 처리를 해주기 때문에 가독성이 높다.
클래스 메소드 내부의 모든 로컬 변수를 출력해주기 때문에 디버깅에 많은 도움이 된다.

profile
서버는 죽지 않아요👼

0개의 댓글