[TIL] 파이썬에 대한 이해

Kim Seungchan·2023년 4월 10일

파이썬 문법

인덴트 (Indent)

파이썬의 인덴트는 공백 4칸을 원칙으로 한다.

강제는 아니며 선택적으로 적용할 수 있다.

네이밍 컨벤션 (Naming Convention)

파이썬의 네이밍 컨벤션은 스네이크 케이스를 따른다.

  • 스네이크 케이스 (snakecase): 각 단어를 밑줄()로 구분하는 표기
  • 카멜 케이스 (camelCase): 단어를 대소문자로 구분하여 작명하는 방식, 첫 단어를 제외한 모든 단어의 첫 문자가 대문자로 시작
  • 파스칼 케이스 (PascalCase): 첫 시작 문자도 대문자로 하는 방식, 모든 단어의 첫 문자가 대문자로 시작

타입 힌트 (Type Hint)

파이썬 버전 3.5 부터 사용 가능하다.

가독성을 위한 명시적인 선언으로, 강제 규약이 아니다 보니,
여전히 동적으로 할당(동적 타이핑)될 수 있으므로 주의가 필요하다.

# 타입 선언
a: str = "abc"
b: int = 1

# 함수 정의
def fn(a: int) -> bool:
    ...

굳이 지정하지 않아도 문제는 없지만, 코드를 정리할 때만이라도 타입을 모두 지정하자.

mypy를 사용하면 타입 힌트에 오류가 없는지 자동으로 확인할 수 있다.

$ pip install mypy

$ mypy solution.py

리스트 컴프리헨션 (List Comprehension)

파이썬은 map, filter와 같은 함수형(Functional) 기능을 지원하며,
다음과 같은 람다 표현식(Lambda Expression)도 지원한다.

>>> list(map(lambda x: x + 10, [1, 2, 3]))
[11, 12, 13]

그러나 파이썬의 더 유용한 기능은 리스트 컴프리헨션이다.

리스트 컴프리헨션: 기존 리스트를 기반으로 새로운 리스트를 만들어내는 구문

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

반드시 리스트만 가능한 것은 아니다. 리스트 외에도 딕셔너리 등이 가능하다.

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

무리하게 복잡하게 작성할 경우 가독성을 떨어뜨릴 수 있으므로 적절히 사용하는게 중요하다.

대체로 표현식은 2개를 넘지 않아야 한다.

파이썬에서 사용되는 표현식 요소들의 의미

제너레이터 (Generator)

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

제너레이터가 없다면 메모리 어딘가에 데이터를 보관하고 있어야 한다.

제너레이터를 이용하면, 단순히 제너레이터만 생성해두고 필요할 때 언제든 데이터를 만들어낼 수 있다.

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

제너레이터가 여기까지 실행 중이던 값을 내보낸다(양보한다)는 의미로,
중간값을 리턴한 다음 함수는 종료되지 않고 계속해서 맨 끝에 도달할 때까지 실행된다.

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

>>> get_natural_number()
<generator object get_natural_number at 0x10d3139d0>

다음 값을 생성하려면 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

range

제너레이터의 방식을 활용하는 대표적인 함수

>>> range(5)
range(0, 5)

>>> type(range(5))
<class 'range'>

>>> list(range(5))
[0, 1, 2, 3, 4]

>>> 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(a) == len(b) # 1000000
True

# 리스트처럼 인덱스로 접근도 가능
>>> b[999]
999

# 그러나 a는 이미 생성된 값이 담겨 있고, b는 생성해야 한다는 조건만 존재
# 따라서 메모리 점유율이 훨씬 작다
>>> sys.getsizeof(a)
8697464
>>> sys.getsizeof(b)
48

enumerate

‘열거하다’라는 뜻으로, 여러 가지 자료형(list, set, tuple 등)을 인덱스를 포함한 enumerate 객체로 리턴한다.

>>> a = [1, 2, 1, 9]
>>> enumerate(a)
<enumerate object at 0x1010f83f0>

>>> list(enumerate(a))
[(0, 1), (1, 2), (2, 1), (3, 9)]

>>> for i, v in enumerate(a):
...     print(i, v)
0 1
1 2
2 1
3 9

// 나눗셈 연산자

정수형을 나눗셈할 때 동일한 정수형을 결과로 리턴하면서 내림(Floor Division) 연산자의 역할을 한다.

다시 말해 몫(Quotient)을 구하는 연산자다.

a // bint(a / b)와 동일하다.

>>> 5 / 2
2.5
>>> int(5 / 2)
2
>>> 5 // 2
2

# 나머지
>>> 5 % 2
1

# 몫과 나머지를 한 번에 구할 때
>>> divmod(5, 2)
(2, 1)

print

코딩 테스트 문제 풀이 과정에서 디버깅을 할 때 가장 자주 쓰는 명령이다.
(실무에서는 추천하지 않는 디버깅 방법, 디버거를 사용하거나 TDD 방식으로 접근)

print('A', 'B')
print('A', 'B', sep=', ')
print('A', end=', ')
print('B')

# 출력 결과
A B
A, B
A, B

`print(*objectssep=' 'end='\n'file=Noneflush=False)`

print 내장 함수

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

>>> a = ['A', 'B']
>>> print(' '.join(a))
A B
idx = 0
fruit = "Apple"

print('{0}: {1}'.format(idx + 1, fruit)
print('{}: {}'.format(idx + 1, fruit)
print(f'{idx + 1}: {fruit}')

# 출력 결과
1: Apple
1: Apple
1: Apple

가장 마지막 줄의 f-string(formated string literal) 방법이
변수를 뒤에 별도로 부여할 필요 없이 인라인으로 삽입할 수 있어 편리하게 사용할 수 있다.

무엇보다 다른 방식에 비해 간결하고 직관적이며 속도도 빠르다.

pass

널 연산(Null Operation)으로 아무것도 하지 않는 기능이다.

불필요한 오류를 방지할 수 있다.

목업(mockup) 인터페이스부터 구현한 다음에 추후 구현을 진행할 수 있게 한다.

locals

로컬 심볼 테이블 딕셔너리를 가져오는 메소드이다.

로컬 스코프에 정의된 모든 변수를 출력하기 때문에 편리하다.

다음과 같이 pprint로 출력하게 되면 보기 좋게 줄바꿈 처리를 해준다.

...
import pprint
pprint.pprint(locals())
...

코딩 스타일

PEP 8 – Style Guide for Python Code

Google Python Style Guide

변수명과 주석

변수명에 각각의 의미를 부여해 작명해라.

간단한 주석을 부여하는 편이 가독성이 높아 보인다.

주석은 한글로 달아도 무방하지만 영어로 작성하는 것에도 부담이 없어야한다.

실제 코딩 테스트 시에는 항상 코드에 상세한 주석을 달아두는 편이 좋다.

리스트 컴프리헨션

역할별로 줄을 구분하면 가독성이 높아진다.

str1s = [
    str1[i:i + 2].lower() for i in range(len(str1) - 1)
    if re.findall('[a-z]{2}', str1[i:i + 2].lower())
]
# 두 글자씩 가져와서 소문자로 함, 마지막 인덱스는 제외
# 해당 값이 알파벳만 2개 나타나는 경우에만 추가함

차라리 모두 풀어서 쓰는 것도 가독성을 위해서라면 나쁘지 않다.

str1s = []
for i in range(len(str1) - 1):
    if re.findall('[a-z]{2}', str1[i:i + 2].lower()):
        str1s.append(str1[i:i + 2].lower())

또한 표현식이 대체로 2개를 넘지 않아야 한다.

# 나쁜 예시
return [(x, y, z)
        for x in range(5)
        for y in range(5)
        if x != y
        for z in range(5)
        if y != z]

구글 파이썬 스타일 가이드

여러 지침 중 몇 가지만 살펴보자.

  • 함수의 기본 값으로 가변 객체(Mutable Object)를 사용하지 않아야 한다.
    대신 불변 객체(Immutable Object)를 사용한다. None을 명시적으로 할당하는 것도 좋은 방법이다.
# 나쁜 예시
def foo(a, b=[]):
    ...
def foo(a, b: Mapping = {}):
    ...

# 좋은 예시
def foo(a, b=None):
    if b is None:
        b = []
def foo(a, b: Optional[Sequence] = None):
    if b is None:
        b = []
  • True, False를 판별할 때는 암시적(Implicit)인 방법을 사용하는 편이 간결하고 가독성이 높다.
# 좋은 예시
if foo:
    pass

if not users:
    print('no users')

if foo == 0: # 정수를 처리할 때는 정수값을 직접 비교
    self.handle_zero()

if i % 10 == 0: # 마찬가지로 명시적으로(Explicitly) 값을 비교
    self.handle_multiple_of_ten()

# 나쁜 예시
if foo != []:
    pass

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()
  • 최대 줄 길이는 80자로 한다.

출처: 박상길. 파이썬 알고리즘 인터뷰 - 95가지 알고리즘 문제 풀이로 완성하는 코딩 테스트.

profile
기묘한 개발자 지망생

0개의 댓글