- 오류가 발생해도 문제 없도록 코드에 방탄 처리
65. try/except/else/finally 각 블록을 잘 활용하라.
요약
try/finally
를 잘 활용하면, try
블록이 실행되는 동안 예외가 발생하던 아니던, 정리 코드를 실행할 수 있다.
else
블록을 사용하면, try
블록 안에 넣을 코드를 최소화하고, "try/except
블록"과 "성공적인 경우에 수행해야 할 코드"를 시각적으로 분리할 수 있다.
try
블록이 성공적으로 처리되고 finally
블록이 공통적인 정리 작업을 수행하기 전에 실행해야 하는 동작이 있는 경우 else
블록을 사용할 수 있다.
본문
- finally 블록
- 예외를 호출 stack의 위(함수 자신을 호출한 함수 쪽)으로 전달해야 하지만,
- 예외가 발생하더라도 정리 코드를 실행해야 한다면 try/finally를 사용하라.
- 파일 handle을 안전하게 닫기 위해 try/finally를 사용하는 경우가 종종 있다.
- 아래와 같이 구현하면 좋다.
def try_finally_example(filename):
print('* 파일 열기')
handle = open(filename, encoding='utf-8')
try:
print('* 데이터 읽기')
return handle.read()
finally:
print('* close() 호출')
handle.close()
filename = 'random_data.txt'
with open(filename, 'wb') as f:
f.write(b'\xf1\xf2\xf3\xf4\xf5')
data = try_finally_example(filename)
>>>
* 파일 열기
* 데이터 읽기
* close() 호출
Traceback ...
try_finally_example('does_not_exist.txt')
* 파일 열기
FileNotFoundError
- else 블록
코드에서 처리할 예외
와, 호출 스택을 거슬러 올라가며 전달할 예외
를 명확히 구분하기 위해 try/catch/else 를 사용하라.
- try 블록이 예외를 발생시키지 않으면 else 블록이 실행된다.
- else 블록을 사용하면, try 블록 안에 들어갈 코드를 최소화할 수 있다.
- try 블록에 들어가는 코드가 줄어들면, 발생할 여지가 있는 예외를 서로 구분할 수 있으므로 가독성이 좋아진다.
- 예제: 문자열에서 JSON 딕셔너리 데이터를 읽어온 후 -> 어떤 key에 해당하는 값을 반환
import json
def load_json_key(data, key):
try:
print('* JSON 데이터 읽기')
result_dict = json.loads(data)
except ValueError as e:
print('* ValueError 처리')
raise KeyError(key) from e
else:
print('* 키 검색')
return result_dict[key]
assert load_json_key('{"foo": "bar"}', 'foo') == 'bar'
>>>
* JSON 데이터 읽기
* 키 검색
load_json_key('{"foo": bad payload', 'foo')
>>>
* JSON 데이터 읽기
* ValueError 처리
Traceback...
load_json_key('{"foo": "bar"}', '존재하지 않음')
>>>
* JSON 데이터 읽기
* 키 검색
Traceback ...
KeyError: '존재하지 않음'
- 모든 오소를 한꺼번에 사용하기 (
try/except/else/finally
)
- 풀고자 하는 문제
- 수행할 작업에 대한 설명을 파일에서 읽어 처리
- 원본 파일 자체 변경
- try: 파일을 읽고 처리
- except: try 블록 안에서 발생할 것으로 예상되는 예외를 처리
- else: 원본 파일의 내용을 변경하고, 이 과정에서 오류가 생기면 호출한 쪽에 예외를 돌려준다.
- finally: 파일 핸들을 닫는다.
UNDEFINED = object()
def divide_json(path):
print('* 파일 열기')
handle = open(path, 'r+')
try:
print('* 데이터 읽기')
data = handle.read()
print('* JSON 데이터 읽기')
op = json.loads(data)
print('* 계산 수행')
value = (
op['numerator'] /
op['denominator'])
except ZeroDivisionError as e:
print('* ZeroDivisionError 처리')
return UNDEFINED
else:
print('* 계산 결과 쓰기')
op['result'] = value
result = json.dumps(op)
handle.seek(0)
handle.write(result)
return value
finally:
print('* close() 호출')
handle.close()
temp_path = 'random_data.json'
with open(temp_path, 'w') as f:
f.write('{"numerator": 1, "denominator": 10}')
assert divide_json(temp_path) == 0.1
with open(temp_path, 'w') as f:
f.write('{"numerator": 1, "denominator": 0}')
assert divide_json(temp_path) is UNDEFINED
with open(temp_path, 'w') as f:
f.write('{"numerator": 1 bad data')
divide_json(temp_path)
>>> 예외가 호출한 쪽에 전달된다.
66. 재사용 가능한 try/except 동작을 원한다면, contextlib와 with문을 사용하라.
요약
with
문을 사용하면, try/finally 블록을 통해 사용해야 하는 로직을 재활용하면서 + 시각적인 잡음도 줄일 수 있다.
contextlib
내장 모듈이 제공하는 contextmanager
데코레이터를 사용하면, 여러분이 만든 함수를 with 문에 사용할 수 있다.
context manager
가 yield 하는 값은 with 문의 as 부분에 전달된다.
- 이를 활용하면 특별한 context 내부에서 실행되는 코드 안에서 직접 그 context에 접근할 수 있다.
본문
- 파이썬의 with 문은
- 코드가 특별한 context 안에서 실행되는 경우를 말한다.
- e.g.
- 상호 배제 락(뮤텍스)을 with 문 안에서 사용하면, lock을 소유했을 때만 코드 블록이 실행된다는 것을 의미한다.
- Lock 클래스가 with 문을 적절히 활성해주므로, 아래의 try/finally 구조와 동등하다. (with 문이 더 좋은 코딩)
from threading import Lock
lock = Lock()
with lock:
lock.acquire()
try:
~~~
finally:
lock.release()
contextlib
- 내가 만든 객체나 함수를 ->
with
문에서 쉽게 쓸 수 있다.
with
문에서 쓸 수 있는 함수를 만들 수 있는 contextmanager
데코레이터를 제공한다.
- 이 데코레이터를 사용하는 방법이,
__enter__
와 __exit__
special method를 사용해 새로 클래스를 정의하는 방법보다 훨씬 쉽다.
- e.g.
- 디버깅 관련 로그를 더 많이 남기고 싶다.
- 다음 코드는 2단계의 심각성 수준에서 디버깅 로그를 남기는 함수를 정의한다.
- 프로그램의 default 로그 수준은 WARNING이다. 따라서 이 함수를 실행하면 error 메세지만 화면에 출력된다.
- 하지만 2번쨰 코드처럼,
contextmanager
을 정의하면 함수의 로그 수준을 일시적으로 높일 수 있다.
- with 블록 실행 전: 로그 심각성 수준 높임
- with 블록 실행 후: 심각성 수준을 이전 수준으로 회복
yield
식은 with 블록의 내용이 실행되는 부분을 지정한다.
- with 블록 안에서 발생한 예외는, 어떤 것이든 yield 식에 의해 다시 발생되기 때문에, 이 예외를 도우미 함수(이 경우는
debug_looging
) 안에서 잡아낼 수 있다.
import logging
def my_function():
logging.debug('디버깅 데이터')
logging.error('이 부분은 오류 로그')
logging.debug('추가 디버깅 데이터')
my_function()
>>>
ERROR.root: 이 부분은 오류 로그
from contextlib import contextmanager
@contextmanager
def debug_logging(level):
logger = logging.getLogger()
old_level = logger.getEffectiveLevel()
logger.setLevel(level)
try:
yield
finally:
logger.setLevel(old_level)
with debug_logging(logging.DEBUG):
print('* 내부:')
my_function()
print('* 외부:')
my_function()
with
와 대상 변수 함께 사용하기
- with 문에 전달된 context manager가 context 객체를 return 할 수도 있다.
- context manager 안에서 yield 값을 넘기면,
as
대상 변수에게 값을 제공할 수 있다.
- 이렇게 반환된 context 객체는 with 복합문의 일부로 지정된 local 변수에 대입된다.
- 이를 통해 with 블록 안에서 실행되는 코드가, 직접 context 객체와 상호작용할 수 있다.
- 아래의 코드는, 파일 핸들을 매번 수동으로 열고 닫는 것보다 더 파이썬다운 방식이다.
- 두 번째 코드는, Logger 인스턴스를 가져와서 로그 수준을 설정하고, yield로 대상을 전달하는 context manager을 만들 수 있다.
with open('my_output.txt', 'w') as handle:
handle.write('데이터입니다!')
@contextmanager
def log_level(level, name):
logger = logging.getLogger(name)
old_level = logger.getEffectiveLevel()
logger.setLevel(level)
try:
yield logger
finally:
logger.setLevel(old_level)
logging.basicConfig()
with log_level(logging.DEBUG, 'my-log') as logger:
logger.debug(f'대상: {logger.name}!')
logging.debug('이 메시지는 출력되지 않습니다')
>>>
DEBUG:my-log:대상: my-log!
logger = logging.getLogger('my-log')
logger.debug('디버그 메시지는 출력되지 않습니다')
logger.error('오류 메시지는 출력됩니다')
>>>
ERROR:my-log:오류 메시지는 출력됩니다.
with log_level(logging.DEBUG, 'other-log') as logger:
logger.debug(f'대상: {logger.name}!')
logging.debug('이 메시지는 출력되지 않습니다')
>>>
DEBUG:other-log:대상: other-log!
67. 지역 시간에는 time보다는 datetime을 사용하라.
요약
- 여러 다른 시간대를 변환할 때는, time 모듈을 쓰지 말라.
- 여러 다른 시간대를 신뢰할 수 있게 변환하고 싶으면,
datetime
과 pytz
모듈을 함께 사용하라.
- 항상 시간을 UTC로 표시하고, 최종적으로 표현하기 직전에 지역 시간으로 변환하라.
본문
- UTC는 timezoe과 독립적으로 시간을 나타낼 때 쓰는 표준이다.
- UTC와 지역 시간을 자주 상호 변환 하자.
time 모듈
- 현재 날짜와 시간을 표시하려면 아래와 같이 하라.
- UTC -> 지역시간
import time
local_tuple = time.localtime(time.time())
time_format = ‘%Y-%m-%d %H:%M:%S’
time_str = time.strftime(time_format, local_tuple)
print(time_str)
>>>
2020-08-27 19:13:04
- 여러 시간대를 다루거나, 여러 시간대 사이의 변환을 다룬다면
time
모듈을 쓰지말고, datetime
모듈을 사용하라.
datetime 모듈
from datetime import datetime, timezone
now = datetime(2020, 8, 27, 10, 13, 4)
now_utc = now.replace(tzinfo=timezone.utc)
now_local = now_utc.astimezone()
print(now_local)
>>>
2020-08-27 19:13:04+09:00
- 지역 시간 -> UTC로 된 유닉스 timestamp
time_str = ‘2020-08-27 19:13:04’
now = datetime.strptime(time_str, time_format)
time_tuple = now.timetuple()
utc_now = time.mktime(time_tuple)
print(utc_now)
>>>
1598523184.0
68. copyreg를 사용해, pickle을 더 신뢰성 있게 만들라.
내용
- 신뢰할 수 있는 프로그램 사이에 객체를 직렬화하고 역직렬화 할떄는, pickle 내장 모듈이 유용하다.
- 시간이 지남에 따라 클래스가 바뀔 수 있으므로(attribute의 추가나 삭제 등) 이전에 pickle 한 객체를 역직렬화하면 문제가 생길 수 있다.
- 직렬화한 객체의 하위 호환성을 보장하고자,
copyreg
내장 모듈과 pickle
을 함께 사용하라.
본문
- pickle 객체
- 직렬화: 파이썬 객체 -> byte stream
- 역직렬화: byte stream -> 파이썬 객체
- pickle
- pickle의 목적은 내가 제어하는 프로그램들이 이진 채널을 통해 서로 파이썬 객체를 넘기는 데 있다.
- 설계상 pickle 모듈의 직렬화 형식은 안전하지 않다.
- 직렬화한 데이터에는 원본 파이썬 객체를 복원하는 방법을 표현하는 데이터가 들어있는데, 이 데이터는 근본적으로 프로그램이라 할 수 있다.
- 이는 악이적인 pickle 데이터가, 자신을 역직렬화하는 파이썬 프로그램의 일부를 취약하게 만들 수도 있다는 뜻이다.
- json
- 반면 json 모듈은 설계상 안전하다. 직렬화한 json 데이터는 객체 계층 구조를 간단하게 묘사한 값이 들어있다.
- json 데이터를 역직렬화해도 파이썬 프로그램이 추가적인 위험에 노출되는 일은 없다.
- 서로를 신뢰할 수 없는 프로그램이 통신해야 할 경우에는 JSON 같은 형식을 사용해야 한다.
class GameState:
def __init__(self):
self.level = 0
self.lives = 4
state = GameState()
state.level += 1
state.lives -= 1
print(state.__dict__)
import pickle
state_path = 'game_state.bin'
with open(state_path, 'wb') as f:
pickle.dump(state, f)
with open(state_path, 'rb') as f:
state_after = pickle.load(f)
print(state_after.__dict__)
- 하지만 pickle은, Gamestate()가 변경되었을 때, 예전에 변경되지 않은 파일을 역직렬화해서 불러오면 문제가 생긴다.
copyreg
내장 모듈을 사용하면 이런 문제를 쉽게 해결할 수 있다.
- 파이썬 객체를 직렬화/역직렬화 할 때 사용할 함수를 등록할 수 있으므로 -> pickle의 동작을 제어할 수 있고, 이에 따라 pickle 동작의 신뢰성을 높일 수 있다.
default attribute 값
- 위 단점을 극복하기 위해선, 아래와 같이 하면 된다. (과거 GameState를 불러오면, 현재 변경된 GameState의 추가된 attribute가 default 값을 가질 수 있다.)
- 가장 간단한 경우, default argument가 있는 생성자를 사용하면
GameState
객체를 unpickle 했을 때도 항상 필요한 모든 attribute를 포함시킬 수 있다.
- copyreg.pickle(GameState, pickle_game_state)
- pickle_game_state
- unpickle_game_state
class GameState:
def __init__(self, level=0, lives=4, points=0):
self.level = level
self.lives = lives
self.points = points
def pickle_game_state(game_state):
kwargs = game_state.__dict__
return unpickle_game_state, (kwargs,)
def unpickle_game_state(kwargs):
return GameState(**kwargs)
import copyreg
copyreg.pickle(GameState, pickle_game_state)
state = GameState()
state.points += 1000
serialized = pickle.dumps(state)
state_after = pickle.loads(serialized)
print(state_after.__dict__)
클래스 버전 지정
- 하지만, 위 경우와 달리, 새로운 객체가 필드를 제거해벼려서, 예전 버전 객체와의 하위 호환성이 없어지는 경우도 발생한다. 이 경우, 위
디폴트 인자를 사용하는 접근 방법
을 사용할 수 없다.
- 대신,
pickle_game_state
와 unpickle_game_state
에 version을 도입하라.
class GameState:
def __init__(self, level=0, points=0, magic=5):
self.level = level
self.points = points
self.magic = magic
def pickle_game_state(game_state):
kwargs = game_state.__dict__
kwargs['version'] = 2
return unpickle_game_state, (kwargs,)
def unpickle_game_state(kwargs):
version = kwargs.pop('version', 1)
if version == 1:
del kwargs['lives']
return GameState(**kwargs)
copyreg.pickle(GameState, pickle_game_state)
print('이전:', state.__dict__)
state_after = pickle.loads(serialized)
print('이후:', state_after.__dict__)
안정적인 import 경로
- pickle을 할 때 마주칠 수 있는 다른 문제점으로, 클래스 이름이 바뀌어 코드가 깨지는 경우를 들 수 있다.
- 프로그램이 존재하는 생명 주기에서 클래스 이름을 변경하거나, 클래스를 다른 모듈로 옮기는 방식으로 코드를 refactoring 하는 경우가 있다.
- 이러한 경우에도 pickle 모듈을 잘 쓰고 싶다면?
copyreg
를 써라.
- copyreg를 쓰면, 객체를 언피클 할 때 사용할 함수에 대해, 안정적인 식별자를 지정할 수 있다.
- 이로 인해, 여러 다른 클래스에서 다른 이름으로 피클된 데이터를 역직렬화 할 때-> 서로 전환할 수 있다.
- copyreg를 쓰면, BetterGameState 대신 ,unpickle_game_state에 대한 임포트 경로가 인코딩 된다는 사실을 알 수 있다.
- 단점: unpickle_game_state 함수가 위치하는 모듈의 경로를 바꿀 수 없다는 작은 문제가 숨어 있다.
class BetterGameState:
def __init__(self, level=0, points=0, magic=5):
self.level = level
self.points = points
self.magic = magic
pickle.loads(serialized)
print(serialized)
copyreg.pickle(BetterGameState, pickle_game_state)
state = BetterGameState()
serialized = pickle.dumps(state)
print(serialized)
69. 정확도가 매우 중요한 경우에는 decimal을 사용하라.
내용
- 파이썬은 실질적으로 모든 유형의 숫자 값을 표현할 수 있는 내장 타입과 클래스를 제공한다.
- 돈과 관련된 계산 등과 같이 높은 정밀도가 필요하거나, 근삿값 계산을 원하는 대로 제어해야 할 떄는
Decimal
클래스가 이상적이다.
- 부동소수점 수로 계산한 근사값이 아니라, 정확한 답을 계산해야 한다면, Decimal 생성자에 float 인스턴스 대신 str 인스턴스를 넘겨라.
본문
rate = 1.45
seconds = 3*60 + 42
cost = rate * seconds / 60
print(cost)
>>>
5.364999999
from decimal import Decimal
rate = Decimal('1.45')
seconds = Decimal(3*60 + 42)
cost = rate * seconds / Decimal(60)
print(cost)
>>>
5.365
print(Decimal('1.45'))
>>> 1.45
print(Decimal(1.45))
>>> 1.44999
70. BaseException 의 종류에 대해 전부 소개