파이썬 코딩의 기술 책 5장 내용인 클래스와 인터페이스에 대해 리뷰를 하겠다. collections.abc와 믹스인에 대해서는 다루지 않겠다. 

  1. 훅 , __call__ 
  2. 애트리뷰트
  3. @classmethod
  4. super
  5. 번외 - collections.abc

 


 

1. 훅

훅은 파이썬 내장 API를 호출할 때, 동작을 원하는 대로 바꿀 수 있는 함수이다. 예시를 보면 이해하기 쉬운데, 다음과 같이 sort에서 들어가는 len 함수가 훅이라고 할 수 있다.

tmpl = ['a','bb','ccc','abcd']
tmpl.sort(key = len) # len 이 훅!

 

또 다른 예시는 defaultdict에서 사용할 때다. defaultdict에서 딕셔너리가 변경될 때마다 print 하는 함수를 만들고, 이를 훅으로 사용할 수 있다. 

from collections import defaultdict
def log_print():
    print('revised')
    return

dic = defaultdict(log_print, {}) # log_print가 훅!
dic['a'] = 1 # -> revised
dic['b'] = 2 # -> revised

 

위 defaultdict에서의 훅으로 작은 클래스를 만들어 사용할 수도 있다. defaultdict에 값이 추가될 때 마다 count를 증가시키는 훅을 만든다고 가정하자.

class Count:
    def __init__(self):
        self.added = 0 #이 added를 애트리뷰트라고 함
        
    def missing(self):
        self.added += 1
        return
        
my_count = Count()
dic = defaultdict(my_count.missing, {})

 

이때 Count 클래스 내 __call__메서드를 사용하는 방법으로 바꿔줄 수 있다.  이 __call__은 객체를 함수처럼 호출할 수 있게 되기 때문에, 아래와 같이 코드를 생성할 수 있다. 

class Count2:
    def __init__(self):
        self.added = 0
        
    def __call__(self): #위 코드에서는 missing이었던 부분을 __call__로 치환
        self.added += 1
        return
        
my_count = Count2()
dic = defaultdict(my_count, {}) #callable 객체이기 때문에!
dic['a'] = 1
dic['b'] = 2
print(my_count.added) # -> 2 !

 

 

 

 

2. 애트리뷰트

훅 예제에서 만든 위 클래스에서는 self. 를 이용해 애트리뷰트를 만들었는데 이름의 유형을 간단 정리하면 아래와 같다.

self.added self.__added
공개 애트리뷰트 비공개 애트리뷰트
밑줄 두개, 비공개 필드
클래스 외부에서 접근 X

비공개 애트리뷰트는 자식 클래스에서도 (원래는) 접근을 못한다.

class Myclass:
    def __init__(self):
        self.__added = 1


class MyChildclass(Myclass):
    def get_added(self):
        return self.__added

 

그 이유는 파이썬 컴파일러가 비공개 애트리뷰트에 접근할 때 다음과 같은 접근 코드를 쓰기 때문이다. 자식 클래스 이름이 코드로 들어가기 때문에, 부모 클래스의 __added를 접근하지 못하는 것이다. 즉, __added의 접근 이름은 Myclass__added이란 것!

#하위 클래스에서 비공개 어트리뷰트 접근할 때

c = MyChildclasss()
c.get_added() #-> error! 접근 코드 : _MyChildclass__added

그렇기 때문에 접근 코드 이름을 하위 클래스인 _MyChildclass가 아닌 부모 클래스로 바꿔주면 된다.

assert c._Myclass__added == 1

 

 

 

3. @classmethod

cf.) 다형성 : 클래스가 자신에게 맞는 유일한 메서드 버전을 구현할 수 있다. 이를 활용하면 하위 클래스 객체를 만들거나 연결할 수 있는 제너릭 방법을 사용할 수 있다. 

 

class Myclass:
    @classmethod       # 클래스매서드 정의
    def generate_inputs(cls, config): 
    	# 파라미터인 cls, config는 하위클래스의 파라미터
        # 새로운 Myclass 인스턴스를 생성
        raise NotImplementedError
        
        
class Subclass(Myclass): #Myclass의 하위클래스
    #...
    def generate_inputs(cls, config):
        #...

위 예시코드에서 cls()는 제너릭 생성자로 작용한다. 간단하게 말하자면 self로 메서드를 호출하는 대신 cls을 사용하여 접근하는 것이다. 

그렇다면 어떤 경우에 굳이 왜 인스턴스 매서드(self로 접근할 수 있었던)가 아닌 클래스 메서드를 사용하는 걸까? 바로 다형성과 제너릭이다.

 

클래스 상속으로 다형성을 구현할 수는 있지만, 이 방법은 제너릭(타입에 상관없이 작동하는)하지 않다. 이를 위해 클래스 메서드 다형성을 사용해 클래스 전체에 적용한다.

 

 

 

 

4. super

cf.) __init__  : 이 메서드는 부모 클래스를 초기화하기 위해 자식클래스에서 쓰는 매서드이다. 참고로 파이썬에 있는 유일한 생성자 매서드다. 부모 클래스가 여러 개면 이 init의 작동 순서가 정해져 있지 않기 때문에 문제가 생길 수 있다. 

class Myclass:
    def __init__(self, value):
        self.value = value
        
        
class Myclass1:
    def __init__(self):
        self.value += 5
 

class Myclass2:
    def __init__(self):
        self.value *= 5


class Subclass(Myclass, Myclass1, Myclass2): #부모클래스 3개를 상속
    def __init__(self, value):
        Myclass.__init__(self, value)
        Myclass1.__init__(self)
        Myclass2.__init__(self)
        
        
sub = Subclass(value = 3) # 과연 self.value의 값이 어떻게 나올까?

 

 

super는 pytorch를 사용하는 사람들은 많이 봤을 법한 내장 함수로, 생성자인 __init__ 함수 아래에 사용하는 방법이다. super는 여러개 클래스를 상속받는 상황일 때 상위 클래스는 한 번만 호출하게끔 보장한다. 이에 따라 위 코드는 아래와 같이 다시 작성할 수 있다.

class Myclass:
    def __init__(self, value):
        self.value = value
        
        
class Myclass1(Myclass):
    def __init__(self, value):
        super().__init__(value)
        self.value += 5
        

class Myclass2(Myclass):
    def __init__(self, value):
        super().__init__(value)
        self.value *= 5
        

class SuperSubclass(Myclass1, Myclass2): #부모클래스 2개 + 최상위클래스 1개를 상속
    def __init__(self, value):
        super().__init__(value)
        
        
sub2 = SuperSubclass(value = 3) # 3 * 5 + 5 == 20

mro_str = '-> '.join(repr(cls) for cls in SuperSubclass.mro())
print(mro_str) # <class '__main__.SuperSubclass'>-> <class '__main__.Myclass1'>-> <class '__main__.Myclass2'>-> <class '__main__.Myclass'>-> <class 'object'>

참고로 super init 호출 순서는 MRO 정의를 따르는데, 이 MRO는 최상위 클래스부터 object 까지의 순서로 호출하지만, 작업은 호출된 순서의 역순으로 가게 된다. 즉, Myclass, Myclass2, Myclass1 순서로 실제 value가 계산되는 것이다. 

 

super 안 파라미터는 2개를 받을 수 있는데, 첫번째는 접근하고 싶은 MRO 뷰를 제공할 부모 타입(주로 내가 부를 클래스의 이름을 적는다), 다른 하나는 방금 지정한 MRO 뷰에 접근할 때 사용할 인스턴스(self)이다. 그러면 아래와 같이 SuperSubclass를 다시 정의할 수 있다. 

 

class SuperSubclass(Myclass1, Myclass2): #부모클래스 3개를 상속
    def __init__(self, value):
        super(SuperSubclass, self).__init__(value) 
        # SuperSubclass : MRO 뷰를 제공할 부모 타입
        # self : 위에 선언한 MRO 뷰 제공 타입에 접근할 인스턴스
sub2 = SuperSubclass(value = 3)

 

 

5. 번외

cf.) collection.abc : 커스텀 컨테이너 타입에 잘 메서드가 구현되어 있는지 확인하고, 실수한 부분을 알려주는 모듈이다. 아래와 같이 불러올 수 있다.

from collections.abc import Sequence
class Myclass(Sequence):
    ...

 


 

이번에는 클래스를 활용할 때 사용할 수 있는 여러 기능에 대해 알아보았다.  훅 함수의 제대로 된 정의, 공개 애트리뷰트와 비공개 애트리뷰트의 차이, 다형성을 위한 @classmethod로 제너릭을 부여하는 올바른 사용법과 ML 엔지니어라면 친숙할 super 에 대해 기술했다. 코드를 잘 짜는 ML 엔지이너가 되기, 힘내자! 공부한 모든 것을 실전에서 잘 활용할 수 있기를 바라며.

728x90

이번에는 책의 7장 내용인 '동시성(concurrency)과 병렬성(parallelism)'에 대해 먼저 정리하겠다. 주로 개념들을 잡고 가는 기록이 될 듯하다. 

  1. 동시성 , 병렬성
  2. subprocess
  3. thread
  4. coroutine

 

 


개념잡기 - 동시성 & 병렬성

동시성

동시성은 실제로 동시에 작업되는 것은 아니지만, 프로그램을 아주 빠르게 번갈아가며 실행되게 하는 것이다. 그렇기 때문에 작업속도가 빨라지는 일은 거의 없지만, 지연 시간이 있는 실행 경로가 많은 프로그램들을 동시성으로 작업하면 속도가 빨라질 수 있다.

 

병렬성

병렬성은 같은 시간에 다른 작업을 실제로 같은 시간에 처리하는 것이다. 실제로 병렬적으로 실행되기 때문에 속도가 빨라진다. CPU 코어가 여러 개일 때를 예시로 들 수 있다. 

 

cf.) 참고. 프로세스는 CPU에서 작업을 돌리는데, CPU 1개에서 여러 개의 작업을 돌리는 것은 보통 동시성의 관점에서 볼 수 있다(멀티프로세싱). 반면 CPU가 여러 개라면 실제로도 여러 개의 작업을 진짜 동시에 병렬로 돌릴 수 있다는 것!(==병렬성)

 

cf.) 프로세스와 스레드는 같지 않다. 스레드는 한 프로세스 안에 있는 친구다.

 

 

 

 

 

subprocess

파이썬에서는 자식 프로세스들을 관리할 수 있는데, subprocess 모듈 사용을 권장하고 있다. 이 때 파이썬의 자식 프로세스는 병렬 실행된다. 즉, 속도가 빨라지고 컴퓨터가 가진 모든 CPU를 사용할 수 있다. 아래 첫번째 코드와 같이 run 함수를 사용할 수 있다.

start_time = time.time()
for i in range(50):
    result = subprocess.run(['echo', 'hi!'])
    result.check_returncode()
print(f'소요된 시간 : {time.time()-start_time}') # 소요된 시간 : 0.19005894660949707

 

이보다 권장되는 건 Popen을 사용해서 작업하는 것이다. Popen으로 하위프로세스를 만들면 해당 프로세스를 검사(==polling)할 수 있기 때문이다. Popen은 아래와 같이 실행할 수 있는데, 두 번째 코드 덩어리는 .poll()을 활용한 상태 검사코드를 추가한 것이다.

## Popen 
start_time = time.time()
subp = []
for i in range(50):
    subp.append(subprocess.Popen(['echo', 'hi!']))

for sub in subp:
    sub.communicate()
print(f'소요된 시간 : {time.time()-start_time}') #소요된 시간 : 0.17681527137756348


## Popen + poll
## 왜인진 모르겠지만 시간이 더 줄었다
start_time = time.time()
subp = []
for i in range(50):
    subp.append(subprocess.Popen(['echo', 'hi!']))

for sub in subp:
    while sub.poll() is None: # 작업이 끝나면 poll()의 return값은 0이다.
        sub.communicate()
print(f'소요된 시간 : {time.time()-start_time}')# 소요된 시간 : 0.16349339485168457

 

 

 

 

 

 

thread

파이썬의 스레드를 알기 전 사전 지식을 탐험해보자. 파이썬은 보통 Cpython으로 구현체가 되어있는데, 이 Cpython의 프로그램 실행 순서는 다음과 같다.

 소스구문 해석 -> bytecode 변환 -> (스택 기반) 인터프리터로 실행

두 번째 순서인 바이트코드 변환을 위해서는 프로그램 실행되는 동안 인터프리터가 일관성을 유지해야 한다. 일관성을 유지하기 위해서 락을 걸어주는데, 이게 바로 GIL(Global Interpreter Lock)이다. 멀티스레드로 혹시라도 인터럽트가 발생하면 인터프리터의 상태가 바뀔 수 있기 때문에, 여기에 제한을 걸어주는 것이다. 

 

여기서 문제가 조금 생긴다. 스레드는 프로그램을 보다 빠르게 실행하고 싶은 목적으로 사용하는 경우가 있는데, GIL을 걸어버리면 속도를 높이는 병렬 처리는 조금 힘들어질 거다. 아래 코드는 파이썬에서 스레드를 사용하는 방법이다. 

#thread
from threading import Thread
class MyThread(Thread):
    def __init__(self):
        super().__init__()
        
    def run(self):
        print('1')

start_time = time.time()
threads = []
for i in range(50):
    th = MyThread()
    th.start()
    threads.append(th)

for th in threads:
    th.join()
print(f'소요된 시간 : {time.time()-start_time}')#소요된 시간 : 0.03010416030883789

# without thread
start_time = time.time()
for i in range(50):
    print(1)
print(f'소요된 시간 : {time.time()-start_time}')#소요된 시간 : 0.0019478797912597656

코드를 보면 알겠지만, 이번 예제에서는 thread를 사용했을 때 소요된 시간이 그렇지 않을 때보다 오래 걸렸다! 스레드 할당, 실행 조정 등에 대해 부가 비용이 드는 것은 차치하더라도, GIL로 인한 특성을 잘 보여주는 것이다.

 

이런 스레드는 블로킹 I/O에서 유용하게 쓰일 수 있다. 블로킹 I/O는 파이썬 프로그램이 시스템 콜을 사용해 파일 쓰고 읽기, 네트워크 상호작용하기, 통신하기 등등의 작업을 말한다. 이런 작업에서 직렬 실행을 하게 되면 시스템 콜 동안 다른 작업을 아예 할 수 없을 것이다. 이럴 때 thread를 이용하면 프로그램은 병렬로 실행되지 않는 대신, 시스템 콜은 병렬 실행이 가능하다! 

 

 

cf) python3.6부터는 byte(==8bit)가 아닌 16비트 명령어기 때문에 굳이 따지면 bytecode가 아닌 wordcode라고 한다. 그치만... 'word'code라고 하기엔 한국어는 3byte인걸...!

 

 

 

 

 

코루틴

I/O 동시성을 처리하기 위해 사용할 수 있는데, 파이썬 프로그램 내에서 동시에 실행되는 것처럼 보이게 하는 방법이다. 

  • async : 함수 앞에 await를 쓰면 코루틴 함수로 만들어준다.(generator의 yield 느낌)
  • await : 코루틴을 일시 중단시키는데, 해결 후에 async 함수에서 실행이 재개된다.

아래 코드는 코루틴 함수를 정의하는 방법이다. 두 번째 함수인 my_function2를 보면 for 문 안에서 코루틴 함수를 호출하고 있다. 해당 코루틴 함수인 my_function1은 즉시 호출되는 대신 await에서 사용할 수 있도록 인스턴스를 반환해준다.

두번째 함수의 asyncio.gather는 my_function1 코루틴을 동시에 실행하면서 해당 코루틴이 완료되면 my_function2 코루틴 실행 재개를 요청한다. 

import asyncio

#코루틴 함수 설정    
async def my_function1():
    ...
    await my_logic() # 코루틴 동작!
    
    
async def my_function2(a=4):
    ....
    task = []
    for _ in range(a):
        task.append(my_function1())
    
    await asyncio.gather(*task)

 

 

 

 

 

코루틴과 스레드 간단 차이점

  thread coroutine
동시성 가능 가능
비용 메모리 추가, 시작, 전환 비용
GIL, 동기화 필요
시작 비용 only

 

 

이번 글에서는 동시성, 병렬성의 간단한 사용법에 대해서 다루어봤다. 동기성 프로그래밍에는 익숙하지 않아서 책을 여러 번 읽어야 했었는데, 앞으로는 더욱 능숙하게 다룰 수 있게 되기를. 다음 포스트에서는 스레드와 코루틴 2탄을 적어보려 한다. 

 

 

728x90

지난 글에서는 책 '파이썬 코딩의 기술'을 리뷰하면서 함수에 대해 정리해 보았다. 이번에는 파이썬 컴프리헨션과 제너레이터의 여러 활용에 대해 알아보도록 하자. 파이썬 내장 라이브러리인 itertools에 대해서는 다루지 않을 예정이다. 

  1. 컴프리헨션
  2. 제너레이터 식 생성
  3. yield, next
  4. yield from
  5. send
  6. throw

 

1. 컴프리헨션

컴프리헨션이란

컴프리헨션은  다른 시퀀스나 iterable에서 새로운 데이터 구조(리스트, 딕셔너리 등)를 생성할 수 있는 구문으로, 컴프리헨션 내 왼쪽에서 오른쪽으로 실행된다. 아래 예시 표와 같이 가독성 측면에서 map, filter를 대신할 수 있다.

다만 컴프리헨션 예시에서 볼 수 있다시피, 컴프리헨션 내부에 하위 식이 많으면 가독성이 떨어지기 때문에 주의해야 한다. 

컴프리헨션 예시 [a*3 for a in tmps]
컴프리헨션 하위식 예시 [[0 for _ in range(3)] for _ in range(4)]
map사용 예시 map(lambda x: x*3, tmps)
map, filter 사용 예시 map(lambda x : x*2 , filter(lambda x : x % 2 ==0, tmps))

 

cf.) iterator(이터레이터)는 모두 iterable 한데, list, dic, tuple과 같이 반복 가능한 객체를 의미한다. 

 

컴프리헨션과 왈러스 연산자

컴프리헨션 내 수식이 길어지면 가독성이 떨어져 지양해야 한다고 위에서 언급했다. 하지만 불가피하게 식이 길어질 때는 어떻게 해야 할까? 왈러스 연산자(:=)를 사용하면 컴프리헨션 내에 대입식을 사용할 수 있다. 아래 예시 코드를 보자. 

#아래 코드의 결과는 같다.
def add_and_divide(a, b):
    return a%b
tmps = [2,3,4,5,6,7,8]
# comprehension only
{a : add_and_divide(a, 2) for a in tmps if add_and_divide(a, 2)} #-> {3: 1, 5: 1, 7: 1}

# comprehension + walrus
{a : cnt for a in tmps if (cnt:= add_and_divide(a, 2))} # -> {3: 1, 5: 1, 7: 1}

두 코드의 차이는 대입식을 활용해 반복을 피했다는 것이다. 

 

 

 

cf.) 지난 글에서 왈러스 연산자에 대해 다뤘었다. (https://hi-lu.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%BD%94%EB%94%A9%EC%9D%98-%EA%B8%B0%EC%88%A0-%EB%A6%AC%EB%B7%B0-self%EC%99%80-cls-bytes%EC%99%80-str-f-%EB%AC%B8%EC%9E%90%EC%97%B4)

 

파이썬 코딩의 기술 리뷰 - self와 cls, bytes와 str, f-문자열, 왈러스 연산자

오늘은 책 '파이썬 코딩의 기술'을 읽고 파이썬으로 코딩하는 데에 유용한 내용들을 정리해보고자 한다. 이 책의 챕터는 다음과 같이 구성되어 있다. 파이썬답게 생각하기 리스트와 딕셔너리 함

hi-lu.tistory.com

 

단점

입력 시퀀스가 길다면 컴프리헨션은 긴 리스트 인스턴스를 만들게 된다. 즉, 입력이 커지면 메모리를 많이 잡게 되고 프로그램이 꺼질 수 있다.

 

 

 

 

 

 

 

2. 제너레이터

제너레이터 식

리스트 컴프리헨션과 제너레이터를 일반화한 식으로, 해당 식을 실행할 때 출력 시퀀스 전체를 return하지는 않는다. 제너레이터 식을 호출할 때마다 식 내 원소를 뱉어내는 이터레이터를 생성한다. 

 

 

 

제너레이터 식 사용 방법

1. ()

사이에 컴프리헨션 생성 구문을 넣으면 제너레이터 식으로 사용할 수 있다. 아래 코드처럼, 제너레이터 식에서 원소를 가져오고 싶다면 next를 사용하면 된다. 

tmp = [1,2,3,4,5]
generator_tmp = (t for t in tmp)
print(generator_tmp) ## <generator object <genexpr> at 0x7ff298014ba0>

#generator 값 받아오기
print(next(generator_tmp)) ## 1

 

2. 함수

다음과 같이 함수와 yield로 제너레이터를 정의할 수 있다.

def generator_func(tmp):
    for t in tmp:
        yield t

print(generator_func(tmp)) # <generator object generator_func at 0x7ff2b80a5dd0>
next(generator_func(tmp)) # 1

 

 

제너레이터 기능

yield from

여러 제너레이터를 합성하고 싶을 때 사용할 수 있는 기능이다. 예를 들어 두 제너레이터 함수를 호출하는 새 제너레이터를 만들고 싶다고 하자. 그러면 아래와 같은 코드를 작성할 수 있을 것이다. 그러나 여기서 합성할 제너레이터가 많아진다면 for, yield문이 많아지고 가독성이 떨어지게 된다.

def generator_fcn1(tmp):
	for i in tmp:
    	yield i
        
        
def generator_fcn2(tmp):
    for i in range(len(tmp)):
        yield i


def generator_fcn3(tmp):
    for t in generator_fcn1(tmp):
        yield t
    for t in generator_fcn2(tmp):
        yield t​

 

그래서 나온 두 번째 방법은 yield from이다. 부모 제너레이터(여기서는 _fcn3를 의미함)에 제어를 맡기기 전에 제너레이터 fcn1, fcn2가 모든 값을 내보낸다. 코드는 아래와 같게 변경될 수 있다. fcn3와 fcn4는 같은 기능을 하고, 파이썬 인터프리터가 for문을 내포하므로 성능이 빨라진다.

def generator_fcn4(tmp):
    yield from generator_fcn1(tmp)
    yield from genereator_fcn2(tmp)​

send

제너레이터가 데이터를 출력하는 것과 데이터를 받아들이는 것을 같이 할 수는 없을까? (== 양방향 통신) send 메서드를 통해 아래와 같이 양방향 통신을 할 수는 있지만, 권장하지는 않는다.

def gen1():
    received = yield 1 
    return received  ## default None. 제너레이터를 이터레이션하면 yield는 None을 반환

## test 1 : send를 처음 호출할 때는 None만 가능
it = iter(gen1())
out = it.send('hi')
print(out) #-> TypeError: can't send non-None value to a just-started generator


## test2 : 이런식으로 send를 사용 가능!
it = iter(gen1())
out = it.send(None)
print(out) # 1
try :
    it.send('hi!')
except StopIteration:
    print('stop')
    ## 'hi!'
    ## 'stop'

코드 이해하는데도 직관적이지 않고, send로 보내는 값이 많아진다고 가정하면 더 복잡해진다. 그렇다고 yield from과 같이 쓰면 제너레이터 하나가 시작될 때마다 None이 등장하게 된다. 

 

 

throw

제너레이터 내에서 Execption을 던질 수 있지만, 이 또한 위 send와 같이 권장되는 사항은 아니다. 우선 throw는 다음과 같이 사용할 수 있다.

class ExError(Exception):
    pass
    
### 사용예시 1
def gen2():
    yield 1
    yield 2
    yield 3

it = gen2()
next(it)
it.throw(ExError('error만들기!')) # -> ExError: error만들기!​​

### 사용예시 2
def gen3():
    try:
        yield 1
        yield 2
        yield 3
    except ExError:
        yield 11
    
it = gen3()
print(next(it)) # -> 1
it.throw(ExError('Error 만들기!')) # -> 11

이 Excoption Error를 던짐으로써 제너레이터가 양방향 통신을 할 수는 있지만, 이 또한 코드가 복잡해지면 읽기 어려워진다. 차라리 아래와 같이 클래스로 이 트러블 컨테이너 객체를 생성하는 것이 낫다.

class Gen():
    def __init__(self, stop = 4):
        self.t = 0
        self.stop = stop
    def reset(self):
        self.t = 0
    def __iter__(self):
        while self.t <= self.stop:
            self.t+=1
            yield self.t
                
def check_error(tmp): # 위 MyError처럼 필요한 에러 함수 정의해서 넣기
    print('Error 만들기!')
    return False
    
## error가 있다면 yield 인자를 0으로 reset
gen = Gen()
for g in gen:
    if check_error(g):
        gen.reset()
    print(g)​

 

 

 


오늘은 컴프리헨션과 제너레이터에 대해 알아보았다. 파이썬으로 코딩을 하면 알게 모르게 컴프리헨션을 많이 써봤을 것이다. 혹시라도 입력 시퀀스의 메모리가 다  안 올라갈 것 같다면 제너레이터를 사용해 이터레이터를 만들어 사용하도록 하자. 

728x90

이번 3장에서는 새로운 기술을 알려준다기보단 어떻게 함수를 짤 때 좋은 코드를 작성할 수 있는지에 대한 팁이 많이 녹아져 있었다. 따라서 이번 편에서는 1. 함수 작성 팁과 2. 데코레이터에 대해 기술하겠다. 키워드 인자, 디폴트 인자 설정에 대해서는 정리하지 않을 예정이다. 


1. 함수 작성 팁

함수 return값에 4개 값 이상을 언패킹 하지 말기

너무 많은 값을 한꺼번에 언패킹하면 가독성이 떨어지고, 나중에 함수를 바꾸거나 재사용할 때 순서를 혼동하는 등 어려움이 발생한다.

#ex : 많은 값을 언패킹하지 말자!
def my_function(a, b):
    return a+b, a/b, a*b, a*2, a**2, b**2

add_result, * = my_function(a = 2, b = 3)​

return None 대신에 에러를 발생시키기

개인적으로 나도 코드를 작성할 때 특정한 경우들에 return None을 즐겨 쓰는 경향이 있다. None을 쓰는 것을 지양해야 하는 이유는 아래 예시를 보면 확실히 알 수 있다.

#Division 예시
def divide_fcn(a, b):
    try:
        return a/ b
    except ZeroDivisionError:
        return None

tmp1 = divide_fcn(0, 1) # return 0
tmp2 = divide_fcn(1, 0) # return None

#None인지 검사하는 코드
if not tmp1: 
    print('error!') 
if not tmp2:
    print('error!')
#### 두 if문 다 error를 print한다​

나는 보통 그래서 return값에 boolean을 추가하는 편이긴 하지만, 아래와 같이 Error를 발생시키는 방법을 연습하면 좋을 듯하다.

#ex
def division_fcn2(a, b):
    try:
        return a/b
    except ZeroDivisionError as e:
        raise ValueError('error!')​

클로저(closure) 사용하기

  1. 클로저는 자신이 정의된 영역 밖의 변수를 참조하는 함수이다.
  2. 함수는 파이썬에서 "일급 시민(first class citizen) 객체"이다. 즉 변수에 대입하거나, 다른 함수에 인자로 전달되거나, if와 같은 식에서 함수를 비교하거나 하는 등이 가능하다.  
  3. 파이썬은 시퀀스를 비교하는 구체적인 규칙이 있다. 예를 들어 튜플 내 원소를 비교할 때 0번째 원소가 같으면 그다음인 1번째 원소를 비교하는 것이다. 이를 통해 클로저로 정렬할 수 있다.

 

 

def sort_fcn(values, group):
    def helper(x): #클로저 기능 사용! 이 helper함수가 자신의 영역 밖에 있는 group을 참조할 수 있음
        if x in group:
            return (0, x)
        else:
            return (1, x)
    values.sort(key = helper)

sort_fcn(values, group)​

 

(매우 중요!) 여기서 짚고 넘어가야 하는 것은 파이썬 인터프리터가 변수를 참조할 때의 순서다. 

  1. 현재 함수의 영역
  2. 현재 함수를 둘러싼 영역 (ex. 위 예시로 보자면 helper의 기준에서 sort_fcn이 될 수 있음)
  3. 현재 코드가 들어 있는 모듈의 영역. == 전역 영역
  4. 내장 영역 (ex. len, str 등의 내장 함수가 있는 영역)

변수에 값을 대입할 때는 아래와 같은 방식으로 작동한다.

변수가 현재 영역에 있을 때 -> 변숫값을 새로운 값으로 바꿔줌
변수가 현재 영역에 없을 때 -> 새로운 변수 정의로 취급(1번과 같은 방식이 되게 하고 싶다면 nonlocal로 변수 선언을 해당 함수 내에 해줘야 함. 되도록 하지 말자.)
def sort_fcn(values, group):
    TF = True
    def helper(x):
        TF = False
        if x in group:
            return (0, x)
        else:
            return (1, x)
    values.sort(key = helper)
    return TF

tmp = sort_fcn(values, group) # tmp = True

곰곰히 생각해보면 위와 같이 작동해야 함수가 안전하게 동작한다.

변수 위치 인자 사용하기

*args를 사용해 가변적으로 인자를 받아준다. 함수로 들어가기 전에 튜플로 변환되는 특성이 있다.

#사용 예시
def my_function(a, *args):
...


a=0
my_list=[1,2,3]
my_function(a, my_list)​

가변 인자로 넣을 때 generator는 넣지 않도록 주의해야 한다. 튜플로 변환되기 때문에 메모리 에러가 날 수 있다.

 변수 위치 인자는 들어가는 인자들이 변경될 때 코드가 잘 작동하지 않을 수 있으므로 주의하자.

딕셔너리 형태로 값을 줄 수 있다. **kwargs를 사용하면 된다. (cf. 노파심에 쓰는 말. 무조건 변수명이 args, kwargs일 필요 없다!!)

 

def my_function(**kwargs):
    return a + b

my_dic = {'a':1 , 'b':2, 'c':'hello'}
my_function(my_dic) #return 3​

(그래도 가독성을 생각한다면) 키워드 인자를 사용하기

 

2. 함수 데코레이터

 

데코레이터는 자신이 감싸고 있는 함수가 호출되기 전, 후에 코드를 추가로 실행하는데, 보통 디버깅, 함수 등록 등에 사용한다. 아래 코드와 같이 데코레이터를 정의할 수 있다.

def trace(func): #func는 데코레이터가 받을 함수
    def wrapper(*args, **kwargs): #wrapper를 선언해주자
        result = func(*args, **kwargs)
        print(f'{func.__name__}({args!r}, {kwargs!r})' f'->{result!r}') 
        #함수 func 진행 후에 print가 실행되고 있다.
        #이해하기 편하게 책 본문 print문을 그대로 썼다.
        #참고 : f-문자열 내 {!r}은 repr()을 의미한다.
    	return result
    return wrapper

이제 데코레이터를 적용해보면 다음과 같이 함수를 작성할 수 있다. 참고로 @은 데코레이터 호출 후 반환 결과를, 원래 함수가 속하는 영역에 원래 함수 이름으로 등록하는 것과 같다. 즉, trace 내 wrapper함수가 my_function이라는 이름으로 등록된다. 예시에서는 데코레이터가 함수 실행 후를 장식하고 있다. 

#@ 뜻 : 데코레이터를 사용하겠다! 
@trace 
def my_function(a,b):
    '''간단한 덧셈함수를 써볼까?'''
    print(a+b)
    return a+b

my_function(a=1, b=2)
# 3
# my_function((1,2),{})->3

이때 my_function에 내장 함수 help를 호출한다면 어떻게 될까? 원래대로라면 '''간단한 덧셈 함수를 써볼까?'''가 반환돼야 할 것이다. 하지만 아래 이미지와 같이 데코레이터가 감싸고 있는 원래 함수 위치를 찾을 수 없어 독스트링이 정상적으로 반환되지 않는다.

그렇기 때문에 내장 데코레이터인 wraps를 추가해준다.

 

from functools import wraps
def trace(func): #func는 데코레이터가 받을 함수
    @wraps(func)
    def wrapper(*args, **kwargs): #wrapper를 선언해주자
        result = func(*args, **kwargs)
        print(f'{func.__name__}({args!r}, {kwargs!r})' f'->{result!r}') 
        #이해하기 편하게 책 본문 print문을 그대로 썼다.
        #참고 : f-문자열 내 {!r}은 repr()을 의미한다.
    	return result
    return wrapper

wraps는 데코레이터 내부에 들어가는 함수 내 중요 메타데이터를 복사해서 데코레이터가 적용된 함수에 적용해준다. 이제 help을 호출하니 독스트링이 잘 반환된다.


코드를 작성할 때 데코레이터에 익숙하지 않았는데, 책을 읽으면서 적용해봐야겠다 싶은 것들이 늘어서 좋다.

728x90

책 '파이썬 코딩의 기술'에서 2장 내용인 리스트와 딕셔너리에 대해 다뤄보도록 하겠다. 슬라이드, 슬라이싱에 대해선 정리하지 않을 예정이다. 다룰 항목은 다음과 같다.

  • 언패킹
  • key
  • dic 
    • get
    • setdefault
    • defaultdic

 


1. 언패킹

* (별표식)으로 값들을 다음과 같이 언패킹 할 수 있다. 이 *식은 리스트 인스턴스가 된다. 

def my_function():
    ...
    return a, b, c, d, e

#언패킹 no 사용
a, b, c, d, e = my_function

#언패킹 사용 예 1: a,b만 필요할 때
a, b, * = my_function

#언패킹 사용 예 2: 맨 앞뒤 return값만 필요할 때
a, *bdc, e = my_function

별표식이 항상 리스트를 만들기 때문에 너무 크면 메모리에 올라가지 않을 수 있음에 주의해야 한다.

 

 

2. key 인자

내가 정의한 객체에서 특정 정렬 조건을 적용하고 싶을 때 sort 내에서 key를 사용한다. 

list_ex = [ ... ] 
# 대소문자 구분 안 한 문자열 정렬 예시
list_ex.sort(key = lambda x: x.lower)

key 값으로 2개를 쓰고 싶다면 아래와 같이 튜플로 적용할 수 있다. (key1 순으로 정렬 후 key2순으로 정렬하고 싶을 때)

list_ex.sort(key = lambda x:(x.key1, x.key2))

기본 default는 오름차순 정렬이다. 내림차순 정렬을 원할 때는 아래와 같이 1) reverse를 사용하거나 2) 상수일 경우는 - 부호를 붙여주는 형태도 가능하다.

## 내림차순 정렬을 하고 싶을 때 
list_ex.sort(key = lambda x:(x.key1, x.key2), reverse = True)

## key로 받는 인자가 숫자일 때
list_ex.sort(key = lambda x: -x.key1)

## key1로 오름차순 후 key2로 내림차순을 하고 싶을 때
list_ex.sort(key= lmabda x:x.key2, reverse = True)
list_ex.sort(key = lambda x:x.key1)

 

 

3. 딕셔너리 주의점

책을 읽기 전까지 몰랐다. 파이썬 3.5 이전에는 딕셔너리 내 이터레이션 순서가 원소가 삽입된 순서와 일치하지 않았다는 사실을! 다행히 3.7 버전 이후부터는 딕셔너리 인스턴스 내 키를 삽입한 순서대로 돌려받는 게 default다. 

 

get을 이용하는 방법

딕셔너리 내부를 살펴볼 때 나는 보통 다음과 같이 in을 사용했다. 아래 코드에서 key가 없을 때 keyerror를 발생시켜야 하는데, 이 과정에서 코드가 깔끔하지 않게 된다.

my_dic = {}
...
#in
if key in my_dic:
	print(my_dic[key])


##in이 가독성이 안좋은 예시
try:
    print(my_dic[key])
except KeyError:
    my_dic[key]=1
    print('keyerror occurs')

get을 사용한다면 아래와 같이 코드가 깔끔해진다.

#dic get사용 + 왈러스 연산자 예시
if (tmp := my_dic.get(key) is None:
    my_dic[key] = 1​

 

setdefault

setdefault는 key를 딕셔너리에서 가져오게끔 하는데, key가 딕셔너리 안에 없을 경우 두 번째 인자를 디폴트 값으로 반환한다. 문제는 아래 코드의 2번째 예제처럼 setdefault의 return값이 전달되지 않았을 때 값이 직접 딕셔너리에 대입되는 것이다. 심지어 값을 get 해오는 건데 set이 붙는 이름도 이상하다!

#Defaultdict
tmp = my_dic.setdefault(key, 1)
#### my_dic에 key가 없다면 tmp = 1
#### my_dic에 key가 있다면 tmp = my_dic[key]

#2번째 에제
value = []
my_dic.setdefault(key, value)
print(my_dic) # {key : []}
value.append(1)
print(my_dic) # {key : [1]}

 

defaultdict

setdefault를 써야 할 경우 대부분은 defaultdict를 쓰면 해결이 된다. 심지어 새로운 dic를 만들고 싶을 때 클래스 내에 defaultdict를 사용하는 것이 더 좋다. setdefault는 key의 존재 유무에 상관없이 항상 default인스턴스를 만들기 때문이다.

from collections import defaultdict
my_dic = defaultdict(int) # default값이 int형인 딕셔너리 생성 
print(my_dic[key]) # --> {key : 0} #위에서 default를 int로 설정했기 때문에 0이 default다

## 딕셔너리 형태의 클래스를 만들 때 
class MyDic:
    def __init__(self):
        self.data =.defaultdict(set)
    def add(self, key, value):
        self.data[key].add(value)​

 

__missing__

dic의 특별 메서드로 __missing__을 구현할 수 있다. defaultdict 생성자에 함수가 들어갈 경우, 함수 인자는 받을 수 없다. key가 없을 때 open_picture라는 함수를 작동시키는 딕셔너리 클래스를 아래와 같이 구현할 수 있다.

#내장함수 __missing__이 없을 때
def open_picture(path):
    try:
        return open(path, 'a+b') # a+는 처음부터 읽는다는 건데, 파일이 없을땐 생성도 해준다
    except OSError:
        raise
my_dic = defaultdic(open_picture) # 에고 path를 전달하지 못하는데! --> error 발생!
handle = my_dic[path]
handle.seek(0)
...


#내장함수 __missing__이 있는 custom dict를 만든다면?
class MyDic(dict):
	def __missing__(self, key):
        value = open_picture(path)
        self[key] = value
        return value
my_dic = MyDic()
handle = my_dic[path]
handle.seek(0)
....​

이 방법이 setdefault보다 좋은 이유는 뭘까? setdefault는 key값이 있어도 계속 호출이 된다는 것이고, __missing__은 key가 없을 때만 실행된다는 것이 큰 차이다. 


 

 

728x90

오늘은 책 '파이썬 코딩의 기술'을 읽고 파이썬으로 코딩하는 데에 유용한 내용들을 정리해보고자 한다. 이 책의 챕터는 다음과 같이 구성되어 있다.

  1. 파이썬답게 생각하기
  2. 리스트와 딕셔너리
  3. 함수
  4. 컴프리헨션과 제너레이터
  5. 클래스와 인터페이스
  6. 메타클래스와 애트리뷰트
  7. 동시성과 병렬성
  8. 강건성과 성능
  9. 테스트와 디버깅
  10. 협업

이 중에서 첫 번째인 1. 파이썬답게 생각하기 중 일부를 기술하겠다.


1. 들어가면서

이번 챕터의 제목은 '파이썬답게 생각하기'다. '파이썬답게'란 무엇일까? 파이썬은 컴파일러의 엄격한 통제, 강요가 있는 스타일이 아니다. 파이썬은 1. 단순한 걸 좋아하고, 2. 가독성을 최대한 높이려 한다.  이제 두 키워드가 나왔다. '단순', 그리고 '가독성'. 

 

 

 

2. PEP 8 스타일 가이드 -명명 규약

PEP 8 (python enhancement proposal #8)은 파이썬 코드에 대한 스타일 가이드다. 이 중 명명 규약을 살펴보자.

  1. 함수, 변수, 에트리뷰트 -> 소문자, 밑줄 사용 (ex. function_name)
  2. 보호 필요한 인스턴스 에트리뷰트 -> 이름 앞에 밑줄 사용 (ex. _function_name)
  3. 비공개 인스턴스 에트리뷰트 -> 이름 앞에 밑줄 두 개 사용(ex. __function_name)
  4. 클래스 -> 첫글자 대문자 (ex. Myclass)
  5. self <- 클래스 내 인스턴스 메서드가 호출 대상 객체를 가리키는 인자의 이름으로, 첫 번째에 사용
    ##ex
    class Myclass:
    	...
    	def my_function(self, x, y):​
  6. cls <- 클래스 매서드가 클래스 가리키는 인자의 이름으로, 첫 번째에 사용
    ## ex.
    class Myclass:
    	...
        @classmethod
        def Myclass(cls, x, y):​

 

3. bytes 와 str

문자열을 표현할 때 bytes와 str을 사용한다.

bytes str
부호가 없는 8바이트 데이터 유니코드 코드 포인트 포함 (사람이 사용하는 언어의 문자를 표현)
직접 대응하는 텍스트 인코딩 X 직접 대응하는 이진 인코딩 X
.decode()를 사용해 str화 가능 .encode()를 사용해 bytes화 가능
ex.) b'hi' ex.) 'hi'
with open(file_name, 'wb')
with open(file_name, 'rb')
with open(file_name, 'w')
with open(file_name, 'r')

위 내용을 활용한 예시를 하나 들자면, bytes형태 파일을 str로 읽을 수 있다.

FILE_NAME='byte_file'
ENC_TYPE='UTF-8'
with open(FILE_NAME, 'rb') as f:
    tmp = f.read()
    result = tmp.decode(ENC_TYPE)

위 코드에서 ENC_TYPE은 인코딩 타입이다. 인코딩 타입에는 'Latin-1', 'Shift JIS', 'euc-kr', 'cp949', 'UTF-8'과 같은 텍스트 인코딩들이 있을 수 있다.

 

 

 

4. 문자열 출력 방법

문자열 형식화 방법들에 대한 장단점에 대해 알아보자. (단점은 생각도 안 하고 아무거나 쓰고 있었음)

%연산자- %튜플 or 딕셔너리 format f-문자열
print('내 %d원! %s가 준건데!' %(a, b)) '내 {}원! {}가 준건데!'.format(a,b) f'내 {a}원 {b}가 준건데!'
형식지정자 사용. C 스타일. 위치지정자 {} 사용 인터폴레이션을 통한 형식 문자열
) 길어지면 가독성 별로... ) 길어지면 가독성 별로...  
) a, b 순서 바뀌면....(타입에러)  {0}처럼 순서 지정 가능  
) a, b가 여러번 나오면...
(튜플에 같은값 계속 반복)
{0}처럼 순서 지정 가능  

결론 : 이왕이면 f-문자열을 쓰자!

 

cf.) 용어 정리 

  • formatting(형식화) : 미리 정의된 문자열에 데이터 값을 끼워넣어 문자열로 저장하는 과정

 

 

5. else 블록

else의 실행조건은 '루프가 정상적으로 완료되지 않으면 이 블록을 실행하라'가 아니다.

'이 블록 앞의 블록이 실행되지 않으면 이 블록을 실행하라'

이다. 그렇기 때문에 for, while 문 바로 뒤에 else가 온다면 무조건 실행된단 뜻이다. 그렇기 때문에 앞 블록이 break를 만났을 때는 실행되지 않는다. 추가적으로 while 조건이 False이거나 for문이 빈 시퀀스를 실행했을 때에도 else블록은 실행된다. 

### else 문 실행
for i in []:
	print(i)
    
else:
	print('else start!')
    
#### else 문 비실행
for i in range(2):
	print(i)
    if i==1:
    	break
else:
	print('else start!')

 

 

 

6. 왈러스 연산자( :=)

이 책을 읽으면서 가장 신기했던 내용이 왈러스 연산자(== 대입식)다. 대입식으로 대입문을 사용할 수 없는 위치에서 값을 대입할 수 있다.

### no walrus
cnt = my_fruit_count('사과')
if count:
    return 1
else:
    return 0
    
#### walrus
if cnt := my_fruit_count('사과'):
    return 1
else:
    return 0

  

위 예와 같이 왈러스 연산자를 쓰면 반복을 피할 수 있다. if문, while문, 비교 연산자 등에서 다양하게 사용할 수 있다. 

 

 


위 방법들로 파이썬 코드 가독성을 올릴 수 있고, 코드를 보다 단순하게 짤 수 있다. 

728x90

+ Recent posts