강건성과 성능 (1)

About_work·2023년 2월 25일
0

python 기초

목록 보기
22/65
  • 오류가 발생해도 문제 없도록 코드에 방탄 처리

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') # OSError 발생할 수 있음
    try:
        print('* 데이터 읽기')
        return handle.read()      # UnicodeDecodeError 발생할 수 있음
    finally:
        print('* close() 호출')
        handle.close()            # try 블록이 실행된 다음에는 항상 이 블록이 실행됨


filename = 'random_data.txt'

with open(filename, 'wb') as f:
    f.write(b'\xf1\xf2\xf3\xf4\xf5')  # 잘못된 utf-8 이진 문자열

# 오류가 나는 부분.
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)     # ValueError가 발생할 수 있음
    except ValueError as e:
        print('* ValueError 처리')
        raise KeyError(key) from e
    else:
        print('* 키 검색')
        return result_dict[key]            # KeyError가 발생할 수 있음

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)
    • 풀고자 하는 문제
        1. 수행할 작업에 대한 설명을 파일에서 읽어 처리
        1. 원본 파일 자체 변경
    • try: 파일을 읽고 처리
    • except: try 블록 안에서 발생할 것으로 예상되는 예외를 처리
    • else: 원본 파일의 내용을 변경하고, 이 과정에서 오류가 생기면 호출한 쪽에 예외를 돌려준다.
    • finally: 파일 핸들을 닫는다.
UNDEFINED = object()

def divide_json(path):
    print('* 파일 열기')
    handle = open(path, 'r+')             # OSError가 발생할 수 있음
    
    # 파일을 읽고 처리
    try:
        print('* 데이터 읽기')
        data = handle.read()              # UnicodeDecodeError가 발생할 수 있음
        print('* JSON 데이터 읽기')
        op = json.loads(data)             # ValueError가 발생할 수 있음
        print('* 계산 수행')
        value = (
            op['numerator'] /
            op['denominator'])            # ZeroDivisionError가 발생할 수 있음
            
    # try 블록 안에서 발생할 것으로 예상되는 예외를 처리
    except ZeroDivisionError as e:
        print('* ZeroDivisionError 처리')
        return UNDEFINED
        
    # 원본 파일의 내용을 변경하고, 이 과정에서 오류가 생기면 호출한 쪽에 예외를 돌려준다.
    else:
        print('* 계산 결과 쓰기')
        op['result'] = value
        result = json.dumps(op)
        handle.seek(0)                    # OSError가 발생할 수 있음
        handle.write(result)              # OSError가 발생할 수 있음
        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)

# 맨 처음 log_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 모듈을 쓰지 말라.
  • 여러 다른 시간대를 신뢰할 수 있게 변환하고 싶으면, datetimepytz 모듈을 함께 사용하라.
  • 항상 시간을 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 모듈

  • UTC -> 컴퓨터의 지역 시간인 KST
from datetime import datetime, timezone

now = datetime(2020, 8, 27, 10, 13, 4) # 시간대 설정이 안 된 시간을 만듦
now_utc = now.replace(tzinfo=timezone.utc) # 시간대 -> UTC로 강제지정
now_local = now_utc.astimezone() # UTC 시간 -> default 시간대로 변환
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

### serializaiton
state_path = 'game_state.bin'
with open(state_path, 'wb') as f:
    pickle.dump(state, f)

### deserialization
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_stateunpickle_game_state에 version을 도입하라.

class GameState:
    def __init__(self, level=0, points=0, magic=5):
        self.level = level
        self.points = points
        self.magic = magic

# 오류가 나는 부분. 오류를 보고 싶으면 커멘트를 해제할것
#pickle.loads(serialized)

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 의 종류에 대해 전부 소개

profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글