꾸준함이 중요한 Lion2me의 기술블로그

Python Tricks - 파이썬 코드를 정돈하기 위한 패턴

24 Jun 2023
DE

Python Tricks - 파이썬 코드를 정돈하기 위한 패턴

1. 데이터 구조에 관한 참고 할 내용

Python Tricks를 읽으며 데이터 구조 관점에서 파이썬에서 제공하는 데이터 구조 중 참고 할 만한 내용에 대해서 적고자 합니다.

1.1 ) Dictionary

dictionary 부분에서는 기본적으로 자주 사용하는 dict에 대해서는 자세하게 이야기하지 않겠습니다. 대부분의 경우 dict만으로도 문제를 해결 할 수 있고, 실제로 자주 사용하고 있는 편입니다. 그런데 이러한 dict를 다양한 형태로 구축하거나 활용하는 방법이 여럿 알 수 있어서 그 내용을 주로 적고자 합니다.

OrderedDict

Python3 부터는 Dictionary의 순서가 보장되고 있지만 실제 사양에서 보장하는 방안은 아닙니다.

대부분의 경우 쓸 필요는 없지만 명시적으로 알려주고 싶은 경우에는 OrderedDict 객체를 사용하면 됩니다.

defaultdict

이 클래스는 코딩테스트에서 자주 활용했었는데, 찾고자하는 키가 없을 때 default로 설정한 값으로 설정해주는 클래스입니다.

대부분의 경우 dict에서 요소를 찾을 때 get() 함수를 사용하면 예외등을 방지 할 수 있지만, List와 같은 객체도 기본값을 설정 할 수 있기 때문에 상당히 유용하게 사용됩니다.

ChainMap

여러 딕셔너리를 하나의 딕셔너리처럼 사용 할 수 있도록 하는 클래스입니다. 실제로 사용하면 상당히 유용 할 것이라 생각됩니다.

로그 구조화 색인 방식에서의 세그먼트를 최근 세그먼트부터 과거 세그먼트를 순차적으로 읽는 방법이 이와 비슷한 느낌이지 않을까? 라는 생각이 들 정도로, 지금까지 이러한 연산을 하기 위해서 Dictionary의 update를 사용해서 굳이 하나로 합치는 작업을 했었는데 이 클래스를 활용하면 그러한 작업을 단순화 할 수 있을 것 같습니다.

검색, 삭제, 삽입 등의 작업은 순차적으로 읽어나가면서 마주치는 하나의 키-값에 영향을 미친다고 합니다.

1.2 ) List

리스트 부분에서 적을 내용은 몇 가지 없는 것 같습니다.

List는 Linked가 아닌 가변 Array로 구성된다.

tuple은 List보다 메모리적으로 약간 이득이다.

Stack으로 사용 되고, Queue로 사용하려면 Deque 클래스를 사용하자.

1.3 ) namedtuple

namedtuple은 tuple에서 정확한 필드 명이 설정 된 형태라고 생각하면 됩니다.

기존의 tuple에서는 값의 인식을 인덱스 값으로 구분했기 때문에 우리는 T[0]과 같은 위치를 기반으로 데이터를 구분했었습니다. 하지만 nametuple은 그 값에 필드값을 설정하여, 데이터를 불러 올 때 인덱스가 아닌 고정 필드 값으로 불러 올 수 있도록 할 수 있습니다.

이 내용은 사실 별도의 데이터(모델) 클래스를 하나 만들면 해결되겠지만, 그 경우 복잡한 모델이 될 가능성이 있습니다. 데이토 모델로만의 사용은 nametuple은 충분한 효율을 가진다고 생각합니다.

추가적으로 typing.NameTuple 클래스는 여기에 typehint를 추가해서 상속 받아 모델 클래스를 만들 수 있도록 도와주는 클래스라고 합니다.

1.4 ) set

set은 중복을 허용하지 않습니다.

1.5 ) multiprocessing.Queue

멀티 프로세스 관점에서의 Queue가 따로 만들어져 있다는 사실을 이번에 알게 되어서 적어보려 합니다.

일반적으로 Python에서는 GIL의 적용으로 인해서 스레드를 늘리는 것 보다 멀티 프로세스로 병렬처리를 수행합니다. 이 경우 일반적인 Queue 클래스를 사용하기 어려운 부분이 있기 때문에 이 경우를 위해 별도로 만들어진 Queue라고 합니다.

1.6 ) heapq와 PriorityQueue

특정 조건에 따라 정렬 된 우선순위 큐를 구현하기 위해서 heapq와 PriorityQueue를 활용 할 수 있습니다. PriorityQueue 또한 heapq를 기반으로 구현되어 있기 때문에 두 로직은 유사하다고 생각 할 수 있습니다.

2. 반복과 이터레이터

Iterator(이터레이터)의 구성

파이썬은 Iterator는 정확히 말하면 __iter__ 메서드와 __next__ 메서드가 구현되어있는 클래스라고 말 할 수 있습니다.

__iter__

이 메서드는 __next__가 정의 된 객체를 리턴하는 메서드입니다. 실제로 반복하는 동작이 이루어지지는 않기 때문에 동일한 클래스로 구성 된 객체 일 가능성도 있고, 그렇지 않고 중간에서 Iterator를 선택 및 구성하는 클래스 일 수 있습니다.

__next__

실제로 값을 내보내는 함수입니다. 이 함수에서 return하는 값은 Iterator가 내보내는 결과를 의미합니다.

제너레이터

쉽게 Iterator를 구현하는 제너레이터

Iterator가 하는 동작이 상대적으로 간단한 경우에는 제너레이터를 통해 문제를 해결 할 수 있습니다.

return 대신 yield를 사용하여 값을 내보내는 메서드를 파이썬에서는 제너레이터 타입으로 객체화 해주고 있습니다. 이렇게 Iterator를 사용하면 문제는 상대적으로 간단해지고, 별도의 클래스에 __iter__ 메서드와 __next__를 구현 할 필요가 없습니다.

또한 Iterator는 기본적으로 순환하는 요소를 모두 리턴하고나면 StopIterator 예외를 발생시키는데, 제너레이터는 이 예외가 발생하기 전까지 정확히 동작시켜주기 때문에 안전한 편입니다.

제너레이터 표현식

제너레이터 표현식은 리스트 내포식과 유사합니다.

# 리스트 내포식
[ i for i in range(100) ]

# 제너레이터 표현식
( i for i in range(100) )

보기에는 대괄호( “[” ) 가 소괄호 ( “(“ )로 바뀐 정도라고 볼 수 있습니다. 이렇게 표현식을 정의하면 Iterator로 즉시 사용 할 수 있으며, 전체를 메모리에 올리는게 아닌 요소를 하나씩 사용 할 수 있습니다.

또한 리스트 내포식과 마찬가지로 if문을 활용하여 값을 걸러 낼 수 있습니다.

이터레이터 체인

Iterator를 엮어서 동시에 여러 동작을 포함시킬 수 있습니다. 이 부분은 책을 통해 처음 알게 되어서 예시를 포함해서 적어보겠습니다.

def gen():
    for i in range(10):
        yield i
        
def square(g):
    for i in g:
        yield i*i
        
def negated(g):
    for i in g:
        yield -i
        
t = negated(square(gen()))
for i in t:
    print(i)

#0
#-1
#-4
#-9
#-16
#-25
#-36
#-49
#-64
#-81

이렇게 동작하는 방식입니다. 제너레이터 관점에서 역할을 분리하여 메서드로 작성한다면 꽤나 유용한 방법이라고 생각됩니다만, 일반적인 백엔드 개발보다 데이터 엔지니어링이나 대규모 데이터를 차례로 Transform할 때 활용 될 것 같습니다.

딕셔너리 트릭

이 부분은 상당히 매력적인 내용이 가득합니다.

get의 사용

첫 번째 내용은 아무래도 get을 활용하여 값의 접근을 최소화하고 로직 안전성을 높이는 방법이였습니다.

일급함수 특징을 활용해서 switch case 문 구현

이 부분은 Python의 3.10버전에서 switch case를 지원하기 시작해서 유용한지 생각해 볼 내용이지만, 일급 함수의 특징을 잘 이야기 해주기 때문에 적어봅니다.

def dispatch_dict(operator, x, y):
    return {
    	'add': lambda: x+y,
    	'sub': lambda: x-y,
    	'mul': lambda: x*y,
    	'div': lambda: x/y,
    }.get(operator, lambda: None)()

위 코드는 operator의 값에 따라 각 람다 함수의 결과를 리턴합니다.

Dictionary의 독특한 특징

Dictionary에 대해서 새로운 사실을 하나 알게 되었는데, 그건 바로 __eq__를 통해 비교 결과가 동등하고 해시값이 같다면 키를 동일하게 취급한다는 점 입니다.

흥미로운 예시를 들어주었습니다.

t = {
	True : 'yes',
	1 : 'no',
	1.0 : 'maybe'
}
print(t[True])

# maybe

이러한 결과가 왜 등장하는지에 대해서 설명을 해주었는데 몇 가지로 요약 할 수 있습니다.

1. 파이썬의 Dict은 hash 테이블 기반으로 데이터를 저장합니다.

2. 파이썬의 Dict는 key값을 기반으로 hash를 지정합니다.

3. 파이썬에서 True, False는 1과 0으로 해석됩니다.

그래서 값을 읽어 올 때 별도로 저장되어 있지만 실제로는 마지막에 저장 된 1.0을 읽어오게 됩니다.

파이썬의 dir 함수

dir 함수를 사용하면 해당 클래스가 가진 속성을 한번에 볼 수 있습니다.

import datetime

dir(datetime)

#['MAXYEAR',
# 'MINYEAR',
# '__builtins__',
# '__cached__',
# '__doc__',
# '__file__',
# '__loader__',
# '__name__',
# '__package__',
# '__spec__',
# 'date',
# 'datetime',
# 'datetime_CAPI',
# 'sys',
# 'time',
# 'timedelta',
# 'timezone',
# 'tzinfo']