본문 바로가기

개발

내 코드를 우아하게 만드는 파이썬 꿀팁 😮

반응형

파이썬은 코드를 보다 쉽고 우아하게 작성할 수 있게 도와주는 여러 도구들을 내장하고 있습니다. 오늘은 이중 제가 자주 사용하는 몇 가지를 소개해드리겠습니다. 이러한 도구들을 잘 활용한다면 훨씬 가독성이 높고 우아한 코드를 작성할 수 있습니다. 글이 본의 아니게 꽤 길어졌는데, 코드 위주로 보시고 이해가 되지 않는 부분만 설명을 참고하시면 금방 읽으실 수 있을듯합니다. 😂

f-string

f-string은 파이썬 3.6부터 도입된 기능으로서, 문자열의 포맷팅을 쉽게 도와줍니다. 파이썬에 문자열을 만드는 따옴표 앞에 f를 붙여주기만 하면 됩니다. 이렇게 하면 해당 문자열에 {변수명}을 활용하여 바로 해당하는 변수를 문자열에서 사용할 수 있습니다. 예제를 확인해보겠습니다.

one = "Hello"
two = "World"
print(f"3...2...1.... {one}, {two}")

실행결과

3...2...1.... Hello, World

변수명 뒤에 :를 써주면 포맷팅을 해줄 수 있습니다.

test = "test"
print(f"3...2...1... {test:>10}")  # test변수에 10자리를 할당하고 우측 정렬

실행결과

3...2...1...       test

숫자같은 경우도 변수로 넣어줄 수 있습니다.

num = 42
print(f"I'm {num} years old")

실행결과

I'm 42 years old
PI = 3.141592653589793
print(f"PI is {PI}")

실행결과

PI is 3.141592653589793

소수점 둘째 자리까지만 나오도록 포맷팅 해보겠습니다. :뒤에 .2f를 써주면 소수점 자리를 둘째 자리까지 포맷팅 할 수 있습니다.

print(f"PI is {PI:.2f}")

실행결과

PI is 3.14

포맷팅에 대해서 좀 더 자세히 알고 싶으신 분들은 여기를 참고해주세요. 여기서는 f-string이 아니라 format 함수를 활용하고 있으나 포맷팅 방식은 거의 같습니다.

마법 같은 Tuple

파이썬에서 Tuple 자료형은 굉장히 마법 같은 친구입니다. 튜플은 리스트와 유사하지만, 한번 값을 할당하면 수정할 수가 없다는 특징을 가지고 있습니다. 아래와 같이 소괄호를 활용해서 튜플을 생성할 수 있습니다.

a = (1, 2, 3)
print(type(a))

실행결과

<class 'tuple'>

재밌는 점은 소괄호를 쓰지 않고 ,로 구분하는 것만으로도 튜플을 생성할 수 있습니다.

a = 2, 3, 4
print(type(a))

실행결과

<class 'tuple'>

튜플 분해하기

튜플은 분해(Unpakcing)가 가능합니다. 다음과 같이 튜플의 요소에 대응되는 변수를 만들어서 튜플을 할당하면, 각 변수에 대응되는 튜플의 요소가 할당됩니다.

a, b, c = ("hello", "world", 2020)
print(f"a: {a}, b: {b}, c: {c}")

실행결과

a: hello, b: world, c: 2020

이러한 특징 덕분에 파이썬에서는, temp변수를 만들지 않고도 변수들의 순서를 변경할 수 있습니다.

b, c, a = a, b, c
print(a, b, c)

실행결과

2020 hello world

튜플의 이러한 특징을 활용하면 가독성 높은 코드를 작성하는데 도움이 됩니다. 예를 들어, 튜플을 리턴하는 간단한 함수를 작성해보겠습니다. 아래 함수는 x와 y의 좌표값을 shift_x, shift_y만큼 이동한 후 변경된 좌표 값을 튜플로 반환하는 함수입니다.

def shift(x, y, shift_x=0, shift_y=0):
    x += shift_x
    y += shift_y
    return x, y

튜플은 분해가능하므로 아래와 같이 함수의 리턴 값을 x변수와 y변수에 분해해서 할당할 수 있습니다.

x, y = shift(5, 3, shift_x=10, shift_y=10)
print(x, y)

실행결과

15 13

만약에 분해하지 않고 변수를 할당했으면 아래와 같이 사용했어야 합니다. 물론, 아래 역시 간단한 코드이기 때문에 코드 가독성에 크게 문제가 있지 않습니다. 하지만, 코드가 복잡해지거나 리턴하는 튜플의 사이즈가 클 경우에는 숫자를 활용하여 튜플을 인덱싱 하면 변수를 추적하는데 에너지를 더 많이 소모하게 될 것입니다.

coordinate = shift(5, 3, shift_x=10, shift_y=10)
print(coordinate[0], coordinate[1])

실행결과

15 13

zip

세번째로 소개해드릴 도구는 파이썬의 내장 함수인 zip입니다. zip함수를 활용하면 같은 크기의 배열들을 한쌍으로 묶을 수 있습니다. 아래와 같이 idsnames라는 두개의 배열이 있습니다.

ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
names = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]

zip으로 이 두 배열을 묶어서 새로운 리스트를 만들어보겠습니다.

list(zip(ids, names))

실행결과

[(1, 'A'),
 (2, 'B'),
 (3, 'C'),
 (4, 'D'),
 (5, 'E'),
 (6, 'F'),
 (7, 'G'),
 (8, 'H'),
 (9, 'I'),
 (10, 'J')]

dict를 활용하면 딕셔너리를 만들수도 있습니다.

dict(zip(ids, names))

실행결과

{1: 'A',
 2: 'B',
 3: 'C',
 4: 'D',
 5: 'E',
 6: 'F',
 7: 'G',
 8: 'H',
 9: 'I',
 10: 'J'}

zip의 힘은 반복문에서 나타납니다. 만약, 크기가 같은 배열의 쌍을 반복문으로 돌리기 위해서는 아래와 같이 작성하면 됩니다.

for id_, name in zip(ids, names):
    print(id_, name)

실행결과

1 A
2 B
3 C
4 D
5 E
6 F
7 G
8 H
9 I
10 J

zip을 활용하지 않았다면 배열의 길이를 활용해서 인덱싱하는 방법을 사용했을 것입니다.

zip을 쓰지 않았더라면...

for idx in range(len(ids)):
    print(ids[idx], names[idx])

enumerate

enumerate를 활용하면 반복문에서 indexing을 쉽게할 수 있습니다. enumerate를 사용하지 않으면 보통 다음과 같은 방식으로 반복문을 인덱싱 합니다. 인덱싱을 위해 i라는 변수를 지정해줘야 하고, 계속 1을 더해줘야 합니다.

enumerate를 쓰지 않았더라면...

i = 0
for name in names:
    print(i, name)
    i+=1

enumerate를 사용하면 코드를 이렇게 바꿀 수 있습니다. i라는 변수를 정의하거나 1을 더하는 과정을 생략할 수 있습니다.

for i, name in enumerate(names):
    print(i, name)

실행결과

0 A
1 B
2 C
3 D
4 E
5 F
6 G
7 H
8 I
9 J

list comprehension

다음으로 소개해드릴 것은 list comprehension입니다. 워낙 많이 소개가 되어서 많은 분들이 아실 거라고 생각하지만, 간단하게 짚고 넘어가겠습니다.

아래와 같은 리스트의 각 요소를 5로 나눈 새로운 리스트를 생성하고 싶다고 합시다.

data = [80, 70, 88, 90, 30, 40, 55, 60, 80, 82, 35]

원래는 아래와 같은 방식으로 코드를 작성하게 될 것입니다.

new_data = []
for each in data:
    new_data.append(each/5)
print(new_data)

list comprehension은 위의 코드를 줄이면서 가독성을 높여줍니다. 성능도 약간이지만 더 좋다고 합니다.

new_data = [each/5 for each in data]
print(new_data)

실행결과

[16.0, 14.0, 17.6, 18.0, 6.0, 8.0, 11.0, 12.0, 16.0, 16.4, 7.0]

조건문이 있으면 for문 다음에 붙여주면 됩니다. 아래는 리스트의 요소가 70 이상인 요소들만 필터링해서 새로운 리스트를 만드는 예제입니다.

new_data = [each for each in data if each >= 70]
print(new_data)

실행결과

[80, 70, 88, 90, 80, 82]

주의: list comprehension을 복잡한 구문에서 사용하면 오히려 가독성을 낮출 수 있습니다. 최대 2중 for문에서만 list comprehension을 사용하도록 합시다.

Unpacking Operators (*, **)

이번에는 Unpakcing Operators에 대해 소개해드리겠습니다. 라이브러리의 문서를 보다 보면 간혹 *args, **kwargs라는 것을 볼 때가 있습니다. 여기서 사용되는 별표(* 또는 **)를 Unpacking Operators라고 합니다.

*는 리스트나 튜플 같은 Iterable 객체의 요소들을 분리할 때 사용됩니다. 즉, *(1, 2, 3, 4)1, 2, 3, 4 각각을 분리한 것과 같습니다. 예제로 살펴보겠습니다. a, b, c를 인자로 받아서 출력하는 say_something이라는 함수가 있습니다.

def say_something(a, b, c):
    print(a, b, c)

만약, 아래와 같이 튜플로 데이터를 정의해서 say_something 함수에 넣으면 어떻게 될까요?

data = ("Change", "the", "world")
say_something(data)

아래와 같은 에러가 발생합니다.

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-27-6789eede69f8> in <module>
----> 1 say_something(data)

TypeError: say_something() missing 2 required positional arguments: 'b' and 'c'

당연합니다. dataa의 인자로 들어갔고, bc가 정의되지 않았기 때문입니다. 저희는 data의 각 요소를 a, b, c에 매칭 시켜서 넣고 싶습니다. 이때, data앞에 unpacking operator (*)를 넣어주면 됩니다. 이렇게 하면 data의 각 요소가 분리되어 각각 a, b, c의 인자로 들어갑니다.

say_something(*data)

실행결과

Change the world

**는 딕셔너리의 요소를 분리할 때 사용됩니다. 즉, **{"a": 1, "b": 2, "c": 3}는 a=1, b=2, c=3 과 같습니다. 마찬가지로 예제로 살펴보겠습니다.

data = {
    "a": "Change",
    "b": "the",
    "c": "world"
}
say_something(**data)

say_something(**data)say_something(a="Change", b="the", c="world)와 같습니다. 따라서 아래와 같은 결과가 나오게 됩니다.

실행결과

Change the world

*args, **kwargs

함수의 인자로 정의되는 \*args\*\*kwargs 역시 다르지 않습니다. 함수를 정의할 때 인자로 *args**kwargs를 써주면 unpacking된 인자를 받는다는 의미입니다. 아래와 같은 함수를 정의해서 실행해봅시다.

def say_something(sender, *args, **kwargs):
    print(f"{sender} said args:", args)
    print(f"{sender} said kwargs:", kwargs)

say_something("SJ", "Change", "The", "World", hello="World", good="Morning")

실행결과

SJ said args: ('Change', 'The', 'World')
SJ said kwargs: {'hello': 'World', 'good': 'Morning'}

positional arguments로 넣어주었던 Change, The, Worldargs에서 튜플로 묶여서 출력되었고, keyword arguments로 넣어주었던 hello="World", good="Morning"kwargs에서 딕셔너리로 묶여서 출력되었습니다.

딕셔너리에서 디폴트 값 다루기

딕셔너리는 키와 값의 쌍으로 이루어진 데이터 타입입니다. 그런데 프로그래밍을 하다 보면 딕셔너리에 없는 키를 다룰 때가 종종 있습니다. 딕셔너리에 없는 키의 값을 꺼내려고 하면 KeyError가 발생하기 때문에, 아래와 같이 다룰 수 있습니다.

items = {"a": "apple", "b": "bear", "c": "cat"}

try:
    d = items["d"]
except KeyError:
    d = None

print(d)

실행결과

None

예외처리를 하지 않고 좀 더 우아하게 딕셔너리에 키가 없는 상황을 컨트롤해보겠습니다. 파이썬의 딕셔너리는 디폴트 값을 핸들링하기 위해 다음과 같은 함수들을 제공합니다.

get

get함수는 딕셔너리에 해당하는 키 값이 존재하면 해당 값을 리턴하고, 존재하지 않으면 디폴트 값을 리턴합니다. 디폴트 값으로 아무것도 써주지 않으면 디폴트 값은 None입니다. 이때, 원본 딕셔너리는 변경되지 않습니다.

print(items.get("d"))
print(items.get("d", "dark"))
print(items)

실행결과

None
{'a': 'apple', 'b': 'bear', 'c': 'cat'}

pop

pop함수는 딕셔너리에 해당하는 키값이 있으면 해당 값을 제거 후 리턴하고, 없으면 디폴트 값을 리턴합니다.

print(items.pop("d", "empty"))
print(items.pop("a"))
print(items)

실행결과

empty
apple
{'b': 'bear', 'c': 'cat'}

a 키가 딕셔너리에서 사라진 것을 확인할 수 있습니다.

setdefault

setdefault함수는 딕셔너리에 해당 키가 존재하면 해당 값을 리턴하고, 존재하지 않으면 해당 키와 디폴트 값을 딕셔너리에 추가한 뒤, 해당 값을 리턴합니다.

items = {"a": "apple", "b": "bear", "c": "cat"}
print(items.setdefault("a", "dog"))
print(items.setdefault("d", "dog"))
print(items)

실행결과

apple
dog
{'a': 'apple', 'b': 'bear', 'c': 'cat', 'd': 'dog'}

딕셔너리에 d: 'dog'가 추가된 것을 확인할 수 있습니다.

사용예제

counts = {}
items = ["a", "a", "b", "a", "z", "c", "c"]
for each in items:
    count = counts.setdefault(each, 0)
    counts[each] = count + 1
print(counts)

실행결과

{'a': 3, 'b': 1, 'z': 1, 'c': 2}

collections.defaultdict

파이썬에서 기본으로 제공하는 dict 말고 내장 모듈인 collectionsdefaultdict 자료를 활용해서 디폴트 값을 다루실 수도 있습니다. 해당 자료구조는 딕셔너리와 똑같으나 처음 선언할 때, 디폴트로 사용될 팩토리 함수를 인자로 넣어서 선언합니다.

from collections import defaultdict
list_dict = defaultdict(list)

위의 list_dict는 키가 없을 때 빈 리스트(empty list)를 생성하는 딕셔너리입니다.

list_dict["a"].append(5)
list_dict["a"].append(6)
print(list_dict["a"])

a라는 키가 사전에 없었지만 바로 5와 6을 append 할 수 있었습니다. 키가 없을 때 자동으로 빈 리스트를 만들었기 때문입니다. 만약 defaultdict를 사용하지 않았다면 아래와 같은 작업을 했을 것입니다.

list_dict = {}
try:
    list_dict["a"].append(4)
except KeyError:
    list_dict["a"] = []
    list_dict["a"].append(4)
print(list_dict)

실행결과

{'a': [4]}

우아한 반복문, itertools.product

파이썬에는 itertools라는 모듈이 있습니다. 해당 모듈을 사용하면 굉장히 우아한 반복문을 만들 수 있습니다. 예를 들어서 미국 기업들의 2015년부터 2019년까지의 매출액을 루프를 돌면서 받아오는 코드를 작성한다고 해봅시다.

corps = ["APPL", "TSLA", "VNDA", "KO", "MMM"]
years = [2015, 2016, 2017, 2018, 2019]

각각의 corp에 대해서 각각의 year를 매칭 해야 하니 이중 반복문을 써야 할 것입니다.

for corp in corps:
    for year in years:
        print(corp, year)

이때, itertools.product를 사용하면 위의 기능을 우아하게 구현할 수 있습니다.

import itertools
for corp, year in itertools.product(corps, years):
    print(corp, year)

실행결과

APPL 2015
APPL 2016
APPL 2017
...
KO 2019
MMM 2015
MMM 2016
MMM 2017
MMM 2018
MMM 2019

만약 반복문이 2중, 3중으로 가는 상황이 발생한다면 위의 itertoos.product함수를 고려해보면 좋을 듯합니다.

주의: itertools.product를 쓴다 하더라도 성능 향상은 기대할 수 없습니다. 코드의 가독성을 높이는 역할만 합니다. 따라서, 만약 반복문이 2중 3중으로 중첩되는 일이 발생한다면, 해당 알고리즘이 효율적인지를 먼저 검토해보시기를 권해드립니다.

itertools에서 제공하는 다양한 반복문을 살펴보고 싶으시다면 여기를 참고해주세요.


제가 준비한 내용은 여기까지입니다. 물론, 제가 소개한 내용을 모르셔도 코드를 작성하는데 문제는 없지만, 좀 더 파이썬스러운(Pythonic) 코드를 작성하는데 큰 도움을 줄 것입니다. 긴 글 읽어주셔서 감사합니다.

반응형