Python

파이썬 코딩 효율을 높여주는 7가지 문법들 (ft. Pythonic한 코드란?)

카루-R 2024. 9. 24. 02:17
반응형

환영합니다, Rolling Ress의 카루입니다.

파이썬에서는 'Pythonic'이라는 형용사가 있습니다. 말 그대로, 파이썬스러운 코드를 의미해요. 기존 파이썬 문법들과 크게 차이가 나지 않고, 간결하며 의미를 명확하게 이해하기 쉽다면 그런 코드는 모두 Pythonic하다고 볼 수 있겠습니다. 귀찮은 문법을 간결하게 표현할 수 있는 'Syntax sugar'와 어느 정도 일맥상통한다고 볼 수 있겠어요.

오늘은 Python의 코딩 효율을 높여주는 편리한 문법들을 살펴보겠습니다.


1. List comprehension

2의 배수를 리스트(배열)에 저장하는 코드를 작성해보겠습니다.

 

l = [] # 리스트를 선언하고
for x in range(1, 21): # 반복문을 돌며
  if x % 2 == 0: # 짝수라면
    l.append(x) # 추가한다

아마 다들 이런 코드를 생각하셨을 겁니다. 그러나, 이 코드는 복잡하고 귀찮습니다. 다음 코드를 보시죠.

l = [x for x in range(1, 21) if x % 2 == 0]

한 줄로 간결하게 for문을 축약했습니다. if 키워드는 생략이 가능하고, for 키워드만 있어도 됩니다. 미리 iterable한 객체를 만들어두고 이를 list에 담아둔다고 생각할 수도 있어요. 이름은 'list' comprehension이지만, set이나 dict 등에서도 사용이 가능합니다. 처음 보면 어색할 수도 있지만, 알고 난 뒤엔 애용하게 될 겁니다.

2. * (asterisk)

1부터 10까지 수를 리스트에 저장해봅시다.

l = [x for x in range(1, 11)]

오, 바로 위에서 list comprehension을 배웠으니 이렇게 한 줄로 간결하게 작성할 수 있다고요? 아쉽지만, 이 코드도 아직 지저분합니다.

l = [*range(1, 11)]

'묶음' 앞에 '*'를 붙이면 묶음을 개별 요소로 풀어줍니다. 여기서 말하는 '묶음'은 다음과 같은 것들이 들어갈 수 있어요.

  • 리스트 => 개별 원소로 해체
  • 가변인수 => 개별 원소로 해체
  • 제너레이터 => 개별 원소로 해체 (range 등)
  • 딕셔너리 => key만 해체
d = {k: str(k) for k in range(1, 11)}
print(*d) # result: 1 2 3 4 5 6 7 8 9 10
 

여기서 주의할 점은, 딕셔너리에 *를 붙인 경우 value가 아니라 key값만 해체됩니다. 가변인수 중 키워드="값" 형태로 쓰는 키워드 매개변수(**kwargs)의 경우 *를 두 개 (**) 붙여 key, value를 모두 끄집어낼 수 있습니다.

def func(**kwargs):
  for k, v in kwargs.items():
    print(k, v)

d = {'a': 1, 'b': 2, 'c': 3}
func(**d)

3. 반복문에서 인덱스 사용하기

Python에서는 반복문을 사용할 때 for를 사용합니다. while은 무한반복문을 제외하고는 거의 안 쓰여요. 그렇지만, 이런 경우가 있죠.

l = [*"abcdefg"]
i = 0
while i < len(l):           # 또는 for i in range(len(l))
  print(i, l[i])

배열에서 요소를 하나씩 가져오는데, 인덱스가 필요할 때. 이럴 때 별도의 변수 i를 선언하여 인덱스를 출력하고, 배열의 [ ] 연산을 이용하여 값에 접근하는 경우가 있습니다. 이런 방식은 제발 사용하지 마시길 바랍니다. 이건 파이썬스러운(Pythonic) 코드가 아니에요.

l = [*"abcdefg"]

for i, e in enumerate(l)
  print(i, e)

파이썬에서는 문법적으로 두 개 이상의 값을 반환할 수 있습니다. 그 말은 함수의 결과를 두 개 이상의 변수에 바로 대입할 수 있다는 뜻입니다. enumerate를 이용하면 인덱스와 값을 한번에 받을 수 있습니다. 이 경우 i에는 인덱스 번호가(0, 1, 2, 3, ...) e에는 값이 (a, b, c, d, ...) 들어갑니다.

l1 = [1, 2, 3, 4, 5]
l2 = ['A', 'B', 'C', 'D', 'E']

for a, b in zip(l1, l2):
  print(a, b)

이와 비슷하게, 두 개의 리스트를 묶어(zip) 동시에 순회할 수 있는 기능도 존재합니다. 이런 경우도 인덱스 변수를 도입할 필요가 없다는 점에서 강점을 가지죠. 인덱스 변수가 필요하신가요? zip을 enumerate로 감싸시면 됩니다.

4. 병렬 할당과 분해

바로 위에서 enumerate() 함수를 배웠습니다. 여기서 좌변에 두 개 이상의 값을 사용했죠.

l = ['월', '화', '수']
mon, tue, wed = l

리스트, 튜플 등에서 값을 분해할 수도 있습니다. enumerate가 실제로 값을 두 개 반환하는지, 리스트를 반환하는지, 튜플을 반환하는지 궁금해하지 않아도 돼요. 어쨌든 두 개의 값을 반환하므로 두 개의 변수를 통해 값을 각각 받아올 수 있다는 사실만 기억하면 됩니다.

a, b = b, a

이것을 응용하면 별도의 함수 없이 두 변수의 값을 간단히 바꿀 수 있습니다. 이 경우 오른쪽의 b, a는 자동으로 튜플이 됩니다. 즉, 우측은 (b, a)인 튜플이 되고 좌측은 그 튜플이 분해되어 값이 바뀌는 겁니다.

5. 자원 해제는 with으로

f = open("test.txt", 'r')
may_raise_error(f)
f.close()

이 코드의 문제점은 무엇인가요? open()으로 파일을 열었지만, may_raise_error() 함수가 에러를 raise 하는 경우 파일이 닫히지 않아 문제가 생길 수 있습니다. 이를 해결하기 위해 다른 언어에서는 try ~ finally를 쓰거나, 소멸자를 사용하죠.

f = open("test.txt", 'r')
try:
  may_raise_error(f)
finally:
  f.close()

이 코드가 잘못되었다는 건 아닙니다. 다만 'Pythonic'하지 못한 코드입니다

with open("test.txt", 'r') as f:
  may_raise_error(f)

단 두 줄 만에 작성이 끝납니다. with 구문을 벗어날 때, 선언한 변수(여기선 f)를 자동으로 닫아주기 때문에 우리가 직접 관리할 필요가 없어요. 예외 처리는 어디서 하냐고요? with 구문의 밖에서 하면 됩니다. 일관적인 예외처리를 할 수 있기 때문에 가독성이 좋아지는 건 덤입니다.

6. 무한집합을 정의하는 방법

컴퓨터는 일반적으로 튜링 완전(turing-complete)하지만, 기억장치의 유한성으로 인해 한계가 있습니다. (느슨한 튜링 완전) 이에 따라 함수형 프로그래밍 언어가 아닌 이상 무한집합을 정의하고 저장하는 건 사실 불가능에 가깝죠.

그렇지만, 농도(cardinality)가 ℵ_0(알레프 널)인 가산무한집합의 경우 수열의 귀납적 정의를 통해 무한집합을 정의할 수 있습니다. 아주 고등수학부터 대학수학까지 골고루 개념이 나옵니다. 쒯. 계산을 미리 해서 저장장치(리스트, 튜플, ...)에 집어넣는 게 아니라, 그때그때 필요한 만큼만 계산하는 방식입니다. 파이썬에서는 yield를 통해 이러한 지연 평가(lazy evaluation)를 지원합니다.

def Natural(n):
  for i in range(1, n+1):
    yield i

for i in Natural(1000):
  print(i)

이렇게 자연수의 무한 집합을 만들어 1000개를 꺼내올 수도 있고요. 엄밀히 말하면 제너레이터와 yield를 사용해서 그때그때 연산하는 방식에 가깝지만, 우리는 개념적으로 '무한집합'을 정의할 수 있어요.

def Fibo(n):
    a, b = 0, 1
    for i in range(n):
        yield a
        a, b = b, a + b
  
for i in Fibo(10):
  print(i)

이를 응용하면 피보나치 수열도 저장 공간의 제약 없이 구현이 가능합니다. 자연수의 응용으로 만들 수 있는 집합은 모두 구현이 가능하다고 보시면 됩니다.

7. 참 직관적인 비교 연산자

if 10 < a and a < 20:
  print('A is bigger than 10 and less than 20')

10 초과 20 미만을 표기할 때, 프로그래밍에서는 일반적으로 이항 비교연산자와 논리연산자를 결합하여 위와 같은 방법으로 많이 사용합니다. 사실 C 계열 언어에서는 선택지가 없어요. 저렇게 써야만 합니다. 그런데 파이썬은 다르죠.

if 10 < a < 20:
  print('A is between 10 and 20')

위와 같은 구문도 정상적으로 동작합니다. 마찬가지로, 아래와 같은 비교도 가능합니다.

if a == b == c:
  print('a, b and c are all equal.')

이 외에 decoration이라는 아주 강력한 기능이 있으나, 분량이 늘어나 다음에 소개하도록 하겠습니다.

반응형