소개
이전에도 예외 (exception) 가 소개되었지만, 이 섹션에서는 오류 검사 및 예외 처리에 대한 몇 가지 추가 세부 사항을 다룹니다.
프로그램의 실패 방식
Python 은 함수 인수의 유형이나 값에 대한 검사 또는 유효성 검사를 수행하지 않습니다. 함수는 함수 내의 문과 호환되는 모든 데이터에서 작동합니다.
def add(x, y):
return x + y
add(3, 4) ## 7
add('Hello', 'World') ## 'HelloWorld'
add('3', '4') ## '34'
함수에 오류가 있는 경우, 런타임에 예외 (exception) 로 나타납니다.
def add(x, y):
return x + y
>>> add(3, '4')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
'int' and 'str'
>>>
코드를 검증하기 위해, 테스팅 (testing) 에 대한 강조가 있습니다 (나중에 다룹니다).
예외 (Exceptions)
예외는 오류를 알리는 데 사용됩니다. 예외를 직접 발생시키려면 raise 문을 사용합니다.
if name not in authorized:
raise RuntimeError(f'{name} not authorized')
예외를 잡으려면 try-except를 사용합니다.
try:
authenticate(username)
except RuntimeError as e:
print(e)
예외 처리 (Exception Handling)
예외는 첫 번째 일치하는 except로 전파됩니다.
def grok():
...
raise RuntimeError('Whoa!') ## Exception raised here
def spam():
grok() ## Call that will raise exception
def bar():
try:
spam()
except RuntimeError as e: ## Exception caught here
...
def foo():
try:
bar()
except RuntimeError as e: ## Exception does NOT arrive here
...
foo()
예외를 처리하려면, except 블록 안에 문을 넣으십시오. 오류를 처리하기 위해 원하는 모든 문을 추가할 수 있습니다.
def grok(): ...
raise RuntimeError('Whoa!')
def bar():
try:
grok()
except RuntimeError as e: ## Exception caught here
statements ## Use this statements
statements
...
bar()
처리가 완료된 후, 실행은 try-except 뒤의 첫 번째 문으로 재개됩니다.
def grok(): ...
raise RuntimeError('Whoa!')
def bar():
try:
grok()
except RuntimeError as e: ## Exception caught here
statements
statements
...
statements ## Resumes execution here
statements ## And continues here
...
bar()
내장 예외 (Built-in Exceptions)
약 20 여 개의 내장 예외가 있습니다. 일반적으로 예외의 이름은 무엇이 잘못되었는지 나타냅니다 (예: ValueError는 잘못된 값을 제공했기 때문에 발생합니다). 이것은 완전한 목록이 아닙니다. 자세한 내용은 문서를 확인하십시오.
ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError
예외 값 (Exception Values)
예외에는 연관된 값이 있습니다. 이 값은 무엇이 잘못되었는지에 대한 더 구체적인 정보를 포함합니다.
raise RuntimeError('Invalid user name')
이 값은 except에 제공된 변수에 배치되는 예외 인스턴스의 일부입니다.
try:
...
except RuntimeError as e: ## `e` holds the exception raised
...
e는 예외 유형의 인스턴스입니다. 그러나 인쇄될 때는 종종 문자열처럼 보입니다.
except RuntimeError as e:
print('Failed : Reason', e)
여러 오류 잡기 (Catching Multiple Errors)
여러 개의 except 블록을 사용하여 다양한 종류의 예외를 잡을 수 있습니다.
try:
...
except LookupError as e:
...
except RuntimeError as e:
...
except IOError as e:
...
except KeyboardInterrupt as e:
...
또는, 이를 처리하는 문장이 동일한 경우, 그룹화할 수 있습니다.
try:
...
except (IOError,LookupError,RuntimeError) as e:
...
모든 오류 잡기 (Catching All Errors)
모든 예외를 잡으려면 다음과 같이 Exception을 사용하십시오.
try:
...
except Exception: ## DANGER. See below
print('An error occurred')
일반적으로, 이와 같은 코드를 작성하는 것은 좋지 않은 생각입니다. 왜 실패했는지 알 수 없기 때문입니다.
오류를 잡는 잘못된 방법 (Wrong Way to Catch Errors)
다음은 예외를 사용하는 잘못된 방법입니다.
try:
go_do_something()
except Exception:
print('Computer says no')
이것은 가능한 모든 오류를 잡으며, 전혀 예상하지 못한 이유 (예: 설치되지 않은 Python 모듈 등) 로 인해 코드가 실패할 때 디버깅을 불가능하게 만들 수 있습니다.
다소 나은 접근 방식 (Somewhat Better Approach)
모든 오류를 잡으려는 경우, 이것이 더 합리적인 접근 방식입니다.
try:
go_do_something()
except Exception as e:
print('Computer says no. Reason :', e)
이는 실패에 대한 구체적인 이유를 보고합니다. 가능한 모든 예외를 잡는 코드를 작성할 때는 오류를 보고/확인할 수 있는 메커니즘을 갖는 것이 거의 항상 좋은 생각입니다.
하지만 일반적으로, 합리적인 범위 내에서 오류를 좁게 잡는 것이 더 좋습니다. 실제로 처리할 수 있는 오류만 잡으십시오. 다른 오류는 지나가게 하십시오. 아마 다른 코드가 처리할 수 있을 것입니다.
예외 다시 발생시키기 (Reraising an Exception)
잡힌 오류를 전파하려면 raise를 사용하십시오.
try:
go_do_something()
except Exception as e:
print('Computer says no. Reason :', e)
raise
이를 통해 조치를 취하고 (예: 로깅) 오류를 호출자에게 전달할 수 있습니다.
예외 모범 사례 (Exception Best Practices)
예외를 잡지 마십시오. 빠르게, 그리고 크게 실패하십시오. 중요하면 다른 사람이 문제를 처리할 것입니다. 그 누군가인 경우에만 예외를 잡으십시오. 즉, 복구하고 정상적으로 계속 진행할 수 있는 오류만 잡으십시오.
finally 문
예외 발생 여부에 관계없이 실행되어야 하는 코드를 지정합니다.
lock = Lock()
...
lock.acquire()
try:
...
finally:
lock.release() ## this will ALWAYS be executed. With and without exception.
일반적으로 리소스 (특히 락, 파일 등) 를 안전하게 관리하는 데 사용됩니다.
with 문
최신 코드에서는 try-finally 문이 종종 with 문으로 대체됩니다.
lock = Lock()
with lock:
## lock acquired
...
## lock released
더 익숙한 예시:
with open(filename) as f:
## Use the file
...
## File closed
with 문은 리소스에 대한 사용 컨텍스트를 정의합니다. 실행이 해당 컨텍스트를 벗어나면 리소스가 해제됩니다. with 문은 이를 지원하도록 특별히 프로그래밍된 특정 객체에서만 작동합니다.
연습 문제 3.8: 예외 발생시키기
이전 섹션에서 작성한 parse_csv() 함수는 사용자가 지정한 열을 선택할 수 있도록 하지만, 이는 입력 데이터 파일에 열 헤더가 있는 경우에만 작동합니다.
select 및 has_headers=False 인수가 모두 전달되면 예외가 발생하도록 코드를 수정하십시오. 예를 들어:
>>> parse_csv('/home/labex/project/prices.csv', select=['name','price'], has_headers=False)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "fileparse.py", line 9, in parse_csv
raise RuntimeError("select argument requires column headers")
RuntimeError: select argument requires column headers
>>>
이 검사를 추가한 후, 함수에서 다른 종류의 유효성 검사를 수행해야 하는지 질문할 수 있습니다. 예를 들어, 파일 이름이 문자열인지, types 가 리스트인지 또는 이와 유사한 사항을 확인해야 할까요?
일반적으로, 이러한 검사를 건너뛰고 잘못된 입력에 대해 프로그램이 실패하도록 하는 것이 가장 좋습니다. 트레이스백 메시지는 문제의 원인을 지적하고 디버깅을 돕습니다.
위의 검사를 추가하는 주된 이유는 비논리적인 모드에서 코드를 실행하는 것을 방지하기 위함입니다 (예: 열 헤더가 필요한 기능을 사용하면서 동시에 헤더가 없다고 지정하는 경우).
이는 호출 코드 측의 프로그래밍 오류를 나타냅니다. "발생하지 않아야 하는" 경우를 확인하는 것은 종종 좋은 생각입니다.
연습 문제 3.9: 예외 잡기
작성한 parse_csv() 함수는 파일의 전체 내용을 처리하는 데 사용됩니다. 그러나 실제 환경에서는 입력 파일에 손상된, 누락된 또는 잘못된 데이터가 있을 수 있습니다. 다음 실험을 시도해 보십시오.
>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "fileparse.py", line 36, in parse_csv
row = [func(val) for func, val in zip(types, row)]
ValueError: invalid literal for int() with base 10: ''
>>>
parse_csv() 함수를 수정하여 레코드 생성 중에 생성된 모든 ValueError 예외를 잡고 변환할 수 없는 행에 대한 경고 메시지를 인쇄하십시오.
메시지에는 행 번호와 실패한 이유에 대한 정보가 포함되어야 합니다. 함수를 테스트하려면 위의 missing.csv 파일을 읽어보십시오. 예를 들어:
>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Row 4: Couldn't convert ['MSFT', '', '51.23']
Row 4: Reason invalid literal for int() with base 10: ''
Row 7: Couldn't convert ['IBM', '', '70.44']
Row 7: Reason invalid literal for int() with base 10: ''
>>>
>>> portfolio
[{'name': 'AA', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'shares': 150, 'price': 83.44}, {'name': 'GE', 'shares': 95, 'price': 40.37}, {'name': 'MSFT', 'shares': 50, 'price': 65.1}]
>>>
연습 문제 3.10: 오류 무시하기
parse_csv() 함수를 수정하여 사용자가 명시적으로 원하는 경우 구문 분석 오류 메시지를 무시할 수 있도록 합니다. 예를 들어:
>>> portfolio = parse_csv('missing.csv', types=[str,int,float], silence_errors=True)
>>> portfolio
[{'name': 'AA', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'shares': 150, 'price': 83.44}, {'name': 'GE', 'shares': 95, 'price': 40.37}, {'name': 'MSFT', 'shares': 50, 'price': 65.1}]
>>>
오류 처리는 대부분의 프로그램에서 제대로 처리하기 가장 어려운 것 중 하나입니다. 일반적으로 오류를 조용히 무시해서는 안 됩니다. 대신 문제를 보고 사용자가 원하는 경우 오류 메시지를 무시할 수 있는 옵션을 제공하는 것이 좋습니다.
요약
축하합니다! 오류 검사 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.