0b
는 2진수, 0o
는 8진수 0x
는 16진수를 의미한다. bin(x)
, oct(x)
, hex(x)
를 사용하면 정수를 다른 진수값으로 표현하는 문자열로 변환할 수 있다.
부동소수점은 IEEE 754를 따르며 64bit값으로 저장된다.
파이썬의 객체는 어떤 연산자와도 함께 동작할 수 있다. 물론 어떠한 객체들끼리 연산하냐에 따라 연산의 결과가 다를 수 있다. 가령 정수끼리의 +
는 합이라면 문자열끼리의 +
는 concat
이다.
또한, go와는 다르게 서로 타입이 달라도 직관적으로 연산이 수행될 것 같으면 된다. 가령 정수와 분수는 서로 더할 수 있다.
동등 연산자인 ==
는 x와 y값이 같은 지 평가한다.
a = [1,2,3,4] # 기준
b = [1,2,3,4] # 같음
c = [1,2,4,3] # 다른 순서
d = [1,2,3,4,6] # 크기가 다름
print(a == b) # True
print(a == c) # False
print(a == d) # False
a = {"key1" : 2, "key2" : 4} # 기준
b = {"key1" : 2, "key2" : 4} # 같음
c = {"key1" : 2, "key3" : 4} # key값이 다름
d = {"key1" : 2, "key2" : 1} # key는 같으나 value가 다름
print( a == b ) # True
print( a == c ) # False
print( a == d ) # False
a = set([1, 2, 3]) # 기준
b = set([3, 1, 2]) # 동등
c = set([3, 1, 2, 4]) # 같은 요소로 이루어지지 않음
d = set([3, 1, 6]) # 같은 요소로 이루어지지 않음
print(a == b) # True
print(a == c) # False
print(a == d) # False
file과 부동 소수점 처럼 서로 호환성이 없는 타입의 객체들 끼리 비교할 때는 에러가 발생하지 않고 False
를 반환한다.
하지만 타입이 다르다해서 반드시 False
는 아니다. 다음과 같이 정수와 부동소수점을 비교해도 True
가 나올 수 있다.
print(2 == 2.0) # True
x is y
와 x is not y
와 같은 식별 연산자는 두 변수가 메모리에 있는 동일한 객체를 가리키고 있는 지 검사한다. 일반적으로 x == y
이지만 x is not y
인 경우도 있다. 다음의 예가 바로 그 예이다.
a = [1,2,3]
b = [1,2,3]
print(a == b) # True
print(a is b) # False
실제로 is
연산자를 사용해 객체를 비교하는 일은 드물다. 두 객체가 서로 같은 고유값(메모리에 있는 객체)를 가질 것이라는 것이 합당한 이유가 없다면 ==
를 많이 사용한다.
조건 표현식(conditional expression)은 if-else
조건문을 간단하게 만들어준다.
if a <= b:
minvalue = a
else:
minvalue = b
이 코드를 조건 표현식으로 사용하여 다음과 같이 줄여 쓸 수 있다.
minvalue = a if a <= b else b
a if a <= b else b
로 쓰고 여기서 if문의 조건에 따라 a
냐 b
냐가 정해진다. 정해지면 minvalue
에 대입이 된다.
반복(Iteration)은 파이썬 컨테이너(리스트, 튜플, 사전 등) 파일뿐만 아니라 제너레이터(generator)에서도 모두 지원되는 파이썬의 중요한 기능이다. 반복을 지원하는 객체 s는 아래의 연산을 적용할 수 있다.
for
loop이다. 이는 값을 하나씩 순회하는 방법이다. 다른 연산은 모두 이를 기반으로 하고 있다.in, not in
구문은 iterable한 객체 s가 객체 x를 포함하도 있는 지 여부를 in
연산자를 사용하여 검사할 수 있다. 문자열이라면 in
과 not in
연산자는 부분 문자열을 찾을 때 적용할 수 있다. 'hello' in 'hello world'
는 True
가 된다. in
연산자는 와일드카드 또는 어떤 형태의 패턴 매칭(pattern matching)도 지원하지 않는다.
iterable한 객체는 언패킹을 할 수 있는데 언패킹 할 때 몇 개의 원소가 있는 지 알 수 없으니 다음과 같이 별표 변수(starred variable) 또는 에스테리스크 변수(asterisk variable)을 포함하는 언패킹의 확장 형식을 사용할 수 있다.
items = [1,2,3,4,5]
a, b, *extra = items
print(a, b, extra) # 1 2 [3, 4, 5]
*extra, a, b = items
print(a, b, extra) # 4 5 [1, 2, 3]
a, *extra, b = items
print(a, b, extra) # 1 5 [2, 3, 4]
위 예제에서 *extra
는 나머지 항목들을 언패킹한다. 이 값은 항상 list이다. 물론 에스테리스크 변수를 두 번 이상 사용할 수는 없다. 왜냐하면 각 변수에 해당하는 원소들을 언패킹한 후 나머지를 에스테리스크 변수로 할당하는 것이기 때문이다.
에스테리스크 변수는 list에 쓰이면 확장을 하게해주는데 이를 스플래팅(splatting)이라고 한다.
items = [1,2,3,4,5]
a = [10, *items, 11]
b = [*items, 100, *items]
print(a) # [10, 1, 2, 3, 4, 5, 11]
print(b) # [1, 2, 3, 4, 5, 100, 1, 2, 3, 4, 5]
하지만 파일이나 제너레이터 같은 반복 가능한 객체의 대다수는 일회성만 반복을 지원한다. 즉 *로 반복 가능한 객체를 확장하면 내용은 소진되며 후속 반복에서는 더 이상 값을 생성하지 않는다.
시퀸스는 크기를 가지며 0부터 시작하는 정수 인덱스로 항목에 접근할 수 있는 반복 가능한 컨테이너이다. 문자열, 리스트, 튜플이 시퀸스에 포함된다. 시퀸스는 반복(iterable)과 관련된 모든 연산에서 다음이 추가된다.
참고로 s*n은 n개의 시퀸스를 복사 생성하는데, 생성된 복사본은 단지 참조형태로 요소(element)를 복사하는 얕은 복사이다.
a = [3,4,5]
b = [a] # 요소는 a가 된다.
c = 4 * b
print(c)
a[0] = -99
print(c)
b는 a의 list를 요소로 참조하고 있다. c는 4 * b
로 b의 요소를 4번 반복한 리스트로 만들어진다. 문제는 c에서 복사한 요소들은 b에서 참조하고 있는 요소와 같다.
c 요소 ---> b 요소 ---> a 메모리
그래서 a리스트의 요소를 바꾸면 b와 c가 영향을 받는 것이다.
이 문제를 해결하려면 a의 내용을 복사해 시퀸스를 직접 생성하는 방법이 있다.
a = [3, 4, 5]
c = [list(a) for _ in range(4)]
print(c) # [[3, 4, 5], [3, 4, 5], [3, 4, 5], [3, 4, 5]]
a[1] = -999
print(c) #[[3, 4, 5], [3, 4, 5], [3, 4, 5], [3, 4, 5]]
문자열과 튜플은 변경 불가능(immutable)한 객체이므로 한 차례 생성하면 수정할 수 없다.
리스트나 다른 변경 가능한 시퀸스의 내용은 아래의 연산을 이용할 수 있다.
참고로 dict의 keyt들 역시도 set과 마찬가지인 성격을 갖기 때문에 집합 연산이 가능하다. 즉, key-view, item-view를 가진 객체에서도 동작한다는 것이다.
a = {'x' : 1 , 'y': 2, 'z': 3}
b = {'w' : 2 , 'q': 32, 'z': 3}
print(a.keys() & b.keys()) # {'z'}
다음은 두 dict의 key 중 중복되는 key를 가려내는 방법이다.
맵핑 객체는 key-value 간의 연결이며 대표적으로 dict타입이 있다. 아래의 표를 적용할 수 있다.
연산 | 설명 |
---|---|
x = m[k] | 키를 이용한 인덱스 |
m[k] = x | 키를 이용한 할당 |
del m[k] | 키를 이용한 항목 삭제 |
k in m | 키의 존재 여부 검사 |
len(m) | 매핑 객체에 들어 있는 항목 개수 |
m.keys() | 키를 모두 반환 |
m.values() | 값을 모두 반환 |
m.items() | (key,value) 쌍으로 반환 |
맵핑 객체는 문자열, 숫자, 튜플 등과 같이 변경 불가능한 객체를 키값으로 사용할 수 있다. 튜플을 키로 사용할 경우, 다음과 같이 괄호를 생략하고 콤마로 구분된 값을 작성할 수 있다.
d = {}
d[1,2,3] = "foo"
d[1,0,3] = "bar"
k = {}
k[(1,2,3)] = "foo"
k[(1,0,3)] = "bar"
print(d[1,2,3]) # foo
print(k[1,2,3]) # foo
리스트 컴프리헨션의 일반적인 문법은 다음과 같다.
[expression for item1 in iterable1 if condition1
for item2 in iterable2 if condition2
...
for itemN in iterableN if conditionN]
가령, 리스트의 정수를 제곱하여 새로운 리스트를 만든다면 다음과 같이 만들어야 할 것이다.
nums = [1 ,2 ,3, 4, 5]
squares = []
for n in nums:
squares.append(n*n)
print(squares) # [1, 4, 9, 16, 25]
이를 간단하게 리스트 컴프리헨션으로 변경하면 다음과 같다.
nums = [1 ,2 ,3, 4, 5]
squares = [n * n for n in nums]
print(squares) # [1, 4, 9, 16, 25]
만약 추가적으로 여기서 n이 4이상인 값을 걸러내고 싶다면 다음과 같이하면 된다.
nums = [1 ,2 ,3, 4, 5]
squares = [n * n for n in nums if n < 4]
print(squares) # [1, 4, 9]
참고로 리스트 컴프리헨션 안에서 사용되는 변수는 모두 리스트 컴프리헨션의 내부 변수로만 사용된다. 같은 이름을 가진 다른 변수가 리스트 밖에 있어도 shadowing을 걱정할 필요가 없다.
리스트 뿐만 아니라 집합도 컴프리헨션 문법을 사용할 수 있다. 가령, 리스트에서 고유한 값들이 있는 지를 확인하고 싶다면, set으로 변경하도록 할 수 있다.
nums = [1 ,2 ,3, 4, 5, 2, 3, 1, 5, 6, 2, 0, 0, 1, 3]
num_set = {num for num in nums}
print(num_set) # {0, 1, 2, 3, 4, 5, 6}
set
에서 key:value
로 표시한 더 해주면 dict를 만들기 때문에 dict 컴프리헨션을 할 수 있다.
data = [
{"key": "name", "value": "gyu"},
{"key": "age", "value": 15},
{"key": "address", "value": "New York"},
]
data_dict = {datum['key']:datum['value'] for datum in data}
print(data_dict) # {'name': 'gyu', 'age': 15, 'address': 'New York'}
단, 집합과 dict를 생성할 때 마지막에 들어온 값이 기존 값을 덮어쓴다는 점에 유의하자.
컴프리헨션 문장 안에서는 어떠한 종류의 예외 처리도 포함할 수 없다. 이를 위해서 함수를 예외문으로 감싸는 방법이 있다.
def toint(x):
try:
return int(x)
except ValueError:
return None
values = ['1', '2', '-4', 'n/a', '-3', '5']
data1 = [ toint(x) for x in values ]
print(data1) # [1, 2, -4, None, -3, 5]
data2 = [ toint(x) for x in values if toint(x) is not None ]
print(data2) # [1, 2, -4, -3, 5]
다음과 같이 toint
라는 함수를 만들어서 컴프리헨션에서 사용하지 못하는 try-except
를 이용할 수 있다.
제너레이터 표현식(generator expression)은 리스트 컴프리헨션과 동일한 계산을 수행하지만, 결과를 반복적으로 생성하는 객체이다. 문법은 대괄호, 대신 괄호를 사용한다는 점만 제외하면 리스트 컴프리헨션을 사용할 때와 같다.
nums = [1,2,3,4]
squares = (x*x for x in nums)
리스트 컴프리헨션과 달리 제너레이터 표현식은 실제로 리스트를 생성하거나 괄호 안의 표현식을 즉시 평가하지 않는다. 대신에 반복을 통해 필요한 때마다 값을 생성하는 제너레이터 객체를 반환한다. 결과는 다음과 같다.
nums = [1,2,3,4]
squares = (x*x for x in nums)
print(next(squares)) # 1
print(next(squares)) # 4
print(next(squares)) # 9
print(next(squares)) # 16
print(next(squares)) # #Traceback (most recent call last): ~ StopIteration
제너레이터 표현식을 for-loop에 쓰면 한 번만 사용할 수 있다. 반복을 두 번 시도하면 아무것도 없을 수 없다.
nums = [1,2,3,4]
squares = (x*x for x in nums)
for n in squares:
print(n) # 1 4 9 16
리스트 컴프리헨션과 제너레이터 표현식에는 미세한 차이가 있다. 리스트 컴프리헨션에서 파이썬은 결과 데이터를 담은 리스트를 실제로 생성한다. 제너레이터 표현식에서는 요구에 따라 데이터를 어떻게 생성하는 지를 알고있을 뿐, 데이터를 바로 생성하진 않는다. 이는 메모리 사용 효율을 크게 높일 수 있다는 정점이 있다.
with open('data.txt') as f:
lines = (t.strip() for t in f) # 파일을 연다
comments = (t for t in lines if t[0] == '#') # 줄을 읽어 앞뒤 공백문자를 없앤다.
for c in comments: # 주석 모두
print(c)
다음의 코드를 보면 file
의 라인들을 읽어와 앞 뒤 공백을 지워주는 제너레이터 lines
를 만들어주었다. 만약 data.txt
파일의 크기가 100GB정도면 메모리에 모든 데이터를 올리기 힘들 것이다.
이를 해결해주는 방법이 바로 제너레이터이다. 제너레이터는 파일에서 line을 읽는 방법을 기억하지 실제로 메모리에 모든 정보를 올리지 않는다. comments
제너레이터 내부에서도 모든 lines
데이터를 가져오는 것이 아니라 lines
데이터를 사용해서 데이터를 생성한다는 것을 기억할 뿐이다.
for c in comments:
부분이 실행될 때, 이제 generator가 실행된다. 다만, 한번에 모든 데이터를 가져오는 것이 아니라, 한 라인씩 데이터를 가져와 읽을 뿐이다. 때문에 100GB가 넘는 용량의 text파일이라도 메모리는 한 라인만 가져와 읽기 때문에 문제가 없다.
즉, 리스트 컴프리헨션과 달리 제너레이터 표현식은 시퀸스처럼 동작하는 객체를 생성하지 않는다는 것이다. 즉, 인덱싱 할 수 없으며, append()
와 같은 보통의 리스트 연산은 동작하지 않는다. 하지만 list()
를 사용하여 제너레이터 표현식을 리스트로 변환할 수 있다.
num = [ 1, 2, 3, 4]
gen_num = (n*n for n in num)
print(list(gen_num)) # [1, 4, 9, 16]
다음과 같이 사용할 수 있다.
파이썬은 내장 함수 enumerate()
를 제공한다. enumerate()
는 (인덱스, 값)
형식으로 일련의 튜플을 반환하는 iterator를 생성한다.
data = ['ew', '123', 'ewq' , 'asd']
for i,v in enumerate(data):
print(f'index:{i} value:{v}'.format(i,v))
#index:0 value:ew
#index:1 value:123
#index:2 value:ewq
#index:3 value:asd
enumerate()
함수에 start
키워드 인수를 전달하여 시작값을 다르게 할 수 있다.
for i, x in enumerate(s, start=100):
문장
루프를 순회할 때 2개 이상의 시퀸스를 동시에 반복 수행할고 싶은 경우가 있다. 가령, 반복할 때마다 다른 시퀸스에서 항목을 하나씩 가져오는 코드를 다음과 같이 만들 수 있다.
s = [1,2,3,4,5]
t = [3,12,4]
i = 0
while i < len(s) and i < len(t):
x = s[i]
y = t[i]
i += 1
print(x, y) # 1 3 , 2 12 , 3 4
다음의 코드는 굉장히 verbose하다. zip()
함수를 사용하면 반복가능한 객체 s와 t의 요소를 각각 순서대로 뽑아준다. 단, 이 둘의 len
값이 서로 다르다면 짧은 것에 맞게 순회한다음 종료된다.
s = [1,2,3,4,5]
t = [3,12,4]
for x ,y in zip(s,t):
print(x, y) # 1 3 , 2 12 , 3 4
다만, zip()
의 반환값은 iterator이기 때문에 list처럼 사용할 수 없다. 이를 list로 변경하고 싶다면 list(zip(s,t))
로 묶어주어야 한다.
for-else
절이 가능한다. else
절은 루프가 아예 실행되지 않으면 즉각적으로 실행되고,그렇지않으면 마지막 반복을 수행한 후 실행된다. 루프가 break문으로 일찍 종료되면 실행되지 않는다.
사실 for-else
문을 굳이 사용할 필요는 없이 for
문 내부에 flag
를 넣어서 for
문 외부에 if
문을 두어 체크하도록 하는 것과 같으므로 자주 사용되지 않는다.
예외(exception)이 발생하면 에러 메시지를 표시하고 제어 흐름에서 벗어난다. raise
문으로 예외를 일으킬 수 있는데, raise Exception([value])
이다. 여기서 Exception
은 예외 타입을 의미한다. value
는 선택할 수 있는 값으로 예외에 대한 자세한 설명을 담는다.
raise RuntimeError('Unrecoverable Error')
이 error를 잡으려면 try-except
문을 사용하면된다.
예외가 발생하면 매칭되는 except
절을 실행한다. 일어날 가능성이 있는 모든 예외와 try문이 일치할 필요는 없다. 일치하는 except
절을 찾을 수 없으면 예외는 계속 전파되며, 실질적으로 예외를 제어할 수 있는 다른 try-except
블록에서 처리하게 된다. 프로그래밍 스타일상 코드는 실제로 복구할 수 있는 예외만 잡아야 한다. 복구 불가능한 예외는 전파되도록 두는 게 더 낫다.
만약 예외가 잡히지 않으면 프로그램의 최상위 수준까지 도달하여 인터프리터는 에러 메시지를 출력하면서 실행을 중단한다.
만약 raise
문만 단독으로 쓴다면, 이는 최근에 생성된 예외가 다시 발생한다는 것이다. raise
문은 이전에 발생한 예외가 처리되는 동안에만 동작한다.
try:
file = open('foo.txt', 'rt')
except FileNotFoundError:
print("Well, that didn't work.")
raise # 현재 예외가 발생한다.
각각의 except
절은 as var
와 같은 형식으로 지정자(modifier) as와 함께 사용할 수 있다. as
지정자는 예외가 발생하면 예외 타입 인스턴스가 있는 자리에 변수 이름을 지정해준다. isinstance()
를 사용해 예외 타입을 검사할 수 있다.
예외에는 몇 가지 표준 속성이 있는데, 이는 에러에 대한 응답으로 추가 작업이 필요한 코드에서 사용하면 유용하다.
e.args
: 에러를 설명하는 문자열로 보통 문자열로 된 에러 메시지를 전달한다. 선택 사항으로 파일 이름을 담은 2개~3개짜리 튜플을 전달한다.e.__cause__
: 예외를 처리하는 응답의 용도로 다른 예외를 의도적으로 일으켰을 때 이전 예외를 담고 있는 속성이다. e.__context__
: 다른 예외를 처리하는 동안에 예외가 발생했을 때 이전 예외를 담고 있는 속성이다.e.__traceback__
: 예외와 연관된 스택 역추적 객체이다.예외값을 유지하는 데 사용되는 변수는 해당 except
블록에서만 접근할 수 있다. 제어가 이 블록을 벗어나면 변수는 정의되지 않은 변수가 된다.
다중 예외 처리 블록은 다음 예와 같이 여러 개의 except
절을 사용해 작성한다.
def doSomething():
pass
try:
doSomething()
except TypeError as e:
# type error
pass
except ValueError as e:
# Value error
pass
하나의 예외 절이 다음과 같이 여러 예외 타입을 처리할 수 있다.
def doSomething():
pass
try:
doSomething()
except (TypeError,ValueError) as e:
pass
그냥 Exception
을 사용하면 프로그램 종료 관련 예외를 제외하고는 모두 잡을 수 있다.
def doSomething():
pass
try:
doSomething()
except Exception as e:
pass
예외를 모두 한 번에 잡으려 할 때는 사용자에게 에러에 관한 정확한 정보를 알려주도록 신경 써야한다.
try-except-else
문법을 지원한다. else
는 반드시 except
뒤에 나오며 try
절에서 예외가 발생하지 않은 경우에 else
가 실행된다.
try:
file = open('foo.txt', 'rt')
except:
print("Unable to open foo")
data = ''
else:
data = file.read()
file.close()
finally
문은 try-except
블록에서 일어난 일과 관계없이 꼭 실행해야 할 정리 작업을 정의한다. 즉, 에러가 나든 안나든 반드시 실행된다.
file = open('foo.txt', 'rt')
try:
pass
...
finally:
file.close()
# 무슨 일이 있던 간에 파일을 닫는다.
finally
절은 에러를 처리할 목적으로 사용하는 것이 아니라, 에러 발생 유무와 상관없이 꼭 실행해야 하는 코드를 작성하려고 사용한다.
파이썬에는 내장 예외(built-in exception)만 60개 이상이 있다. 표준 라이브러리까지 고려하면 수백가지로 불어난다. 게다가 코드의 어떤 부분에서 어떤 종류의 예외가 발생하는 지 미리 결정하기 힘든 경우도 있다. 예외는 함수 호출 서명(function's calling signature, 함수 시그니처)의 일부로 기록되지 않으며 코드에서 제대로 예외를 처리하는 지 확인하는 컴파일러도 없다. 그 결과 예외 처리가 무질서하게 느껴질 때가 있다.
예외는 상속을 통해 계층 구조로 되어있다. 특정 에러를 대상으로 하는 대신 좀 더 일반적인 오류 범주에 초점을 맞추는 것이 더 쉬울 수 있다. 가령 컨테이너에서 값을 가져올 때 발생할 수 있는 여러가지 오류를 살펴보자.
try:
item = items[index]
except IndexError: # items가 sequence라면 발생
...
except KeyError: # items가 mapping객체라면 발생
...
이 코드처럼 구체적으로 두 가지 예외를 처리하는 코드를 작성하는 대신, 다음과 같이 작성하는 것이 더 쉽다.
try:
item = items[index]
except LookupError:
...
LookupError
는 상위 수준의 예외 그룹을 대표하는 클래스이다. IndexError
와 KeyError
는 모두 LookupError
에서 상속되므로 except
절에서 두 예외 중 하나를 잡게 된다. 그러나 LookupError
는 조회와 관련 없는 예외를 포함할 만큼 광범위하지는 않다.
내장 예외의 일반 카테고리를 보여준다.
BaseException
클래스는 가능한 모든 예외에 대응되므로 예외 처리에 직접 사용하는 경우는 드물다. 이 클래스에는 SystemExit
, KeyboardInterrupt
, StopInteration
과 같은 제어 흐름에 영향을 주는 특수 예외를 포함하고 있다. 사용자는 이런 예외를 잡는 걸 바라지 않는다.
대신 프로그램과 관련된 일반 에러는 Exception
에서 모두 상속된다.
ArithmeticError
은 ZeroDivisionError
, FloatingPointError
, OverflowError
와 같이 모두 수학과 관계있는 에러를 위한 기본 클래스이다.
ImportError
는 모두 import와 관계있는 에러를 위한 기본 클래스이다.
LookupError
는 모두 컨테이너 조회와 관계있는 에러를 위한 기본 클래스이다.
OSError
는 모두 운영체제 및 환경에서 발생하는 에러를 위한 기본 클래스이다.
OSError
는 파일, 네트워크 연결, 권한, 파이프, 시간 초과 등과 관련된 광범위한 예외를 포함하고 있다.
ValueError
예외는 연산에 잘못된 입력값을 제공할 때 발생한다.
UnicodeError
는 유니코드 관련 인코딩 몇 디코딩 오류를 묶은 ValueError
의 부분 클래스이다.
아래는 Exception
에서 파생되지만 더 큰 예외 그룹의 일부가 아닌 몇몇 기본 내장 예외를 보여준다.
에러 처리 중 제어 흐름을 변경하기위해 사용되는 몇 가지 에러가 있다. 다음 아래의 예외는 BaseException
에서 직접 상속받는다.
SystemExit
예외는 프로그램을 의도적으로 종료할 때 사용한다. 인수로 종료 코드 또는 문자열 메시지를 제공할 수 있다. 문자열이 제공되면 sys.stderr
를 통해 그 내용이 출력되고, 프로그램은 종료 코드 1과 함께 종료된다.
import sys
if len(sys.argv) != 2:
raise SystemExit(f'Usage: {sys.argv[0]}')
KeyboardInterrupt
예외는 프로그램이 SIGINT
시그널 즉, ctrl +c
를 눌렀을 때 발생하는 시그널을 받으면 발생한다. 이 예외는 비동기 식이기 때문에 약간 특이하다. 즉, 프로그램 내에서 어떤 문장이든 어떤 식으로도 발생할 수 있다. 이 예외가 발생하면 파이썬은 기본 동작을 당순히 종료하는 것 뿐이다. signal
라이브러리를 이용하여 SIGINT
시그널을 제어할 수 있다.
StopIteration
예외는 반복 프로토콜의 일부이며 반복의 종료를 알린다.
새로운 예외 클래스를 만들기 위해서는 Exception
으로부터 상속받아 새로운 클래스를 정의하면 된다.
class NetworkError(Exception):
pass
새로 정의한 예외를 사용하기 위해서는 다음과 같이 raise
문을 사용하면 된다.
raise NetworkError('Cannot find host')
예외를 발생시킬 때 raise
문에 추가적인 값은 예외 클래스 생성자의 인수로 사용되낟. 이 인수는 대부분 어떤 에러 메시지를 담은 문자열이다. 하지만 사용자 예외는 다음과 같이 하나 이상의 예외값을 받도록 작성할 수 있다.
class DeviceError(Exception):
def __init__(self, errno, msg):
self.args = (errno, msg)
self.errno = errno
self.errmsg = msg
__init__()
을 사용해 custom exception을 생성할 때, 위처럼 인수를 담는 튜플을 self.args
속성에 저장하는 것이 중요하다. 이 속성은 예외 역추적 메시지를 출력할 때 사용되기 때문이다.
예외는 상속을 통해 계층적으로 조직할 수 있다. 가령, 앞에서 정의한 NetworkError
예외는 더 구체적인 다양한 에러를 위한 기본 클래스로 쓰일 수 있다.
class HostnameError(NetworkError):
pass
class TimeoutError(NetworkError):
pass
def error1():
raise HostnameError('Unknown host')
def error2():
raise TimeoutError('Timed out')
try:
error1()
except NetworkError as e:
if type(e) is HostnameError:
#이 이 에러에 대한 특별한 작업을 수행한다.
위 코드는 except NetworkError
절에서 NetworkError
에서 파생된 예외를 모두 잡는다. 발생한 예외의 구체적인 타입을 알아내려면 type()
으로 예외값의 타입을 검사하면 된다.
예외를 처리할 때 종종 다른 예외를 일으키고 싶을 수 있다. 이를 위해 연쇄 예외(chained exception)을 사용한다.
class ApplicationError(Exception):
pass
def do_something():
x = int('N/A') # ValueError발생
def spam():
try:
do_something()
except Exception as e:
raise ApplicationError("It failed") from e
연쇄 예외를 시키고 싶다면 from
으로 현재 except
로 받은 예외를 써주어야한다. 그렇지않으면 raise
한 예외만 반환되게 된다.
spam
함수 실행 시 다음의 에러 메시지를 받게된다.
Traceback (most recent call last):
File "main.py", line 9, in spam
do_something()
File "main.py", line 5, in do_something
x = int('N/A') # ValueError발생
ValueError: invalid literal for int() with base 10: 'N/A'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "main.py", line 13, in <module>
spam()
File "main.py", line 11, in spam
raise ApplicationError("It failed") from e
__main__.ApplicationError: It failed
에러 메시지를 잘보면 두 예외를 다 포함하는 메시지를 받게 된다. 만약 위의 코드에서 from None
이라고 쓰면 에러가 전파되지 않고, ApplicationError
만 발생한다.
ApplicationError
를 raise하게 되면 결과 예외의 __case__
속성에 다른 예외가 포함된다.
try:
spam()
except ApplicationError as e:
print('It failed, Reason:', e.__cause__)
실행해보면 It failed, Reason: invalid literal for int() with base 10: 'N/A'
메시지를 받게 된다.
except
블록에 나타나는 프로그래밍 실수도 연쇄 예외를 발생시키지만, 이것은 약간 다른 방식으로 동작한다. 가령, 다음과 같이 살짝 버그가 있는 코드를 만들어 보자
def spam():
try:
do_something()
except Exception as e:
print("It failed:", err) # err가 정의되어 있지 않음
다음과 같이 except
문안에 정의되지 않은 변수를 사용하여 에러를 발생시키면 다음과 같은 메시지가 나오게 된다.
Traceback (most recent call last):
File "main.py", line 9, in spam
do_something()
File "main.py", line 5, in do_something
x = int('N/A') # ValueError발생
ValueError: invalid literal for int() with base 10: 'N/A'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "main.py", line 14, in <module>
spam()
File "main.py", line 11, in spam
print("It failed:", err) # err가 정의되어 있지 않음
NameError: name 'err' is not defined
출력되는 예외 추적 메시지는 이전 결과와 다르다는 것을 알 수 있다.
다른 예외를 처리하는 동안 예기치 않은 예외가 발생하면 __context__
속성은 예외가 발생했을 때 처리하고 있던 예외 정보를 유지한다다.
try:
spam()
except Exception as e:
print("It fauled Reason:", e)
if e.__context__:
print('While handling:', e.__context__)
print('__cause__:', e.__cause__)
아래의 출력이 나온다.
It fauled Reason: name 'err' is not defined
While handling: invalid literal for int() with base 10: 'N/A'
__cause__: None
예기치 않은 예외는 exception 그 자체로 들어오지만, 예기치 않은 예외가 발생하기 이전에 담고 있던 예외 정보는 e.__context__
에 저장되는 것이다. 이때에는 e.__case__
의 정보가 없다.
연쇄 예외에서 예상되는 예외와 예기치 않은 예외 사이에는 중요한 차이가 있다. 첫번째 예에서는 예상되는 예외를 도려하여 코드를 작성하였다. 가령, 코드는 명시적으로 try-except
블록에 감싸져 있다.
try:
do_something()
except Exception as e:
raise ApplicationError("It failed") from e
두 번째 예에서 except
블록에 프로그래밍 실수가 있었다.
try:
do_something()
except Exception as e:
print("It failed:", err) # err가 정의되어 있지 않음
두 경우의 차이점은 미묘하지만 중요하다. 이는 연쇄 예외 정보가 __cause__
또는 __context__
속성에 각각 저장되는 이유이기도 하다. __cause__
속성은 실패 가능성이 예상되는 경우를 위해 예약되어 있다. __context__
속성은 두 경우 모두 설정되지만, 다른 예외를 처리하는 동안 발생하는 예기치 못한 예외에 대한 유일한 정보 소스가 된다.
예외는 오류가 발생한 위치에 대한 정보를 제공하는 스택 역추적(stack traceback)과 관련이 되어있다. traceback은 exception의 __traceback__
속성에 저장된다. 디버깅을 위해 traceback 메시지를 직접 생성할 수도 있다. 다음과 같이 traceback
모듈을 사용하여 역추적 메시지를 생성할 수 있다.
import traceback
class ApplicationError(Exception):
pass
def do_something():
x = int('N/A') # ValueError발생
def spam():
try:
do_something()
except Exception as e:
print("It failed:", err) # err가 정의되어 있지 않음
try:
spam()
except Exception as e:
tblines = traceback.format_exception(type(e), e, e.__traceback__)
tbmsg = ''.join(tblines)
print("It failed")
print(tbmsg)
traceback.format_exception
에 첫번째 안자는 exception
의 타입이고 두 번째는 exception
인스턴스, 그 다음 traceback
으로 출력할 msg를 넣으면 된다.
예외 처리(exception handling)은 대규모 프로그램에서 적절히 처리하기 어려운 작업 중 하나이다. 하지만 이를 쉽게 처리할 몇 가지 방법들이 있다.
첫번째는 코드의 특정 위치에서 처리할 수 없는 예외는 잡지 않는다. 다음의 함수를 보자
def read_data(filename):
with open(filename, 'rt') as file:
rows =[]
for line in file:
row = line.split()
rows.append(row[0], int(row[1]) , float(row[2]))
return rows
다음의 함수를 보면 open()
함수로 파일을 불러오는데, 오류가 발생할 수 있다. 이 오류를 함수 내에서 try-except
문으로 잡아야하는가? 그건 아니다. 호출자가 잘못된 파일 이름을 함수로 전달하면 이를 복구할 방법은 없다. 파일을 열 수도 없고, 읽을 수도 없으며 그 외 수행할 수 있는 다른 작접도 없다.
이런 경우에는 작업이 실패하도록 두고, 예외를 호출자에게 보고하는 게 좋다. read_data()
에서 에러 검사를 하지 않는다고, 예외가 어디에서도 처리되지 않는다는 것을 의미하지 않는다. 즉, read_data()
의 역할은 예외를 처리하는 것이 아니다. 어쩌면 사용자에게 파일 이름을 생각나게 하는 코드가 이 예외를 처리하는데 더 큰 도움이 될 것이다.
이는 go
나 c
와 같이 특수한 오류 코드나 wrapper로 감싼 결과 타입을 사용하는 언어의 오류 처리와는 상반된다. 이러한 언어에서는 작접 전체에서 에러에 대한 반환 코드를 항상 확인하도록 세심한 주의를 기울여야 하지만, 파이썬에서는 이렇게 하지 않도록 한다. 어떤 작업이 실패하고 이를 복구하기 위해 할 수 있는 일이 없으면, 그냥 실패하도록 두는 것이 좋다. 예외는 프로그램의 상위 레벨로 전파되며 일반적으로 이를 처리하는 것은 다른 코드의 책임이다.
한 편 다음예와 같이 잘못된 데이터로부터 함수를 복구해야할 수도 있다.
def read_data(filename):
with open(filename, 'rt') as file:
rows =[]
for line in file:
row = line.split()
try:
rows.append(row[0], int(row[1]) , float(row[2]))
except ValueError as e:
print('Bad row', row)
print('Reason:',e)
return rows
에러를 잡을 때는 가능한 except
절을 좁은 범위(narrow)로 만들자. 위 코드에서 except ValueError
대신 except Exception
을 사용해 오류를 모두 잡도록 작성할 수 있다. 그러나 그렇게되면 무시해도 되는 프로그래밍 에러도 잡게된다. 코드를 그렇게 작성하지 말도록 하자. 이는 디버깅을 어렵게 만든다.
마지막으로 명시적으로 예외를 일으키는 경우에는 다음과 같이 자기 자신만의 예외 타입을 만드는 것이 좋다.
class ApplicationError(Exception):
pass
class UnauthorizedUserError(ApplicationError):
pass
def spam():
...
raise UnauthorizedUserError('Go away')
...
대규모 코드 기반에서 작업할 때 어려운 문제 하나는 프로그램 실패에 대한 책임을 특정하는 것이다. 자신만의 예외를 만들면 의도적으로 일으킨 에러와 프로그래밍 실수를 더 잘 구별할 수 있다.
프로그램이 여기서 정의한 ApplicationError
와 충돌하는 경우 해당 오류가 왜 발생했는 지 즉시 알 수 있다. 왜냐하면 우리가 예외 코드를 작성했기 때문이다. 반면에 프로그램이 TypeError
또는 ValueError
와 같이 파이썬 내장 예외 중 하나와 충돌한다면 심각한 문제로 볼 수 있다.
예외가 발생한 경우, 파일이나 lock, connection 등의 시스템 자원을 적절히 관리하는 일은 쉽지않다. 가령, 예외 발생 때문에 lock같은 중요한 자원을 해제하는 기능이 실행되지 않을 수 있다.
with
문을 사용하면 컨텍스트 관리자 역할의 객체가 제어하는 런타임 context 안에서 일련의 문자을 실행할 수 있다.
with open('debuglog', 'wt') as file:
file.write('Debugging\n')
...
file.write('Don\n')
import threading
lock = threading.Lock()
with lock:
# critical section
...
# critical section done
첫번째 예에서 with
문은 제어 흐름이 블록을 벗어날 때 열린 파일을 자동으로 닫는다. 두 번째 예에서 with
문은 이어서 나오는 문장 블록에 제어가 진입할 때와 빠져나올 때 자동으로 lock을 획득하고 해제한다.
with obj
문은 제어 흐림이 이어서 나오는 블록에 진입하고 빠져나올 때 일어나는 일을 객체 obj
가 관리하게 된다.
with obj
문이 실행되면 새로운 context에 진입한다는 신호로 obj.__enter__()
가 호출된다.
제어 흐름이 context를 벗어날 때는 obj.__exit__(type, value, traceback)
가 호출된다. 발생한 예외가 없으면 __exit__()
의 3개 인수는 모두 None
으로 설정된다. 예외가 발생하면 이 3개의 인수는 제어 흐름을 context에서 벗어나게 한 예외 타입, 예외값, traceback 정보를 담느낟.
__exit__()
메서드가 True
를 반환하면 발생한 예외가 처리되었고, 더 이상 context 밖으로 전달되지 않는다는 것을 의미한다. False, None
을 반환하면 발생한 예외가 컨텍스트 밖으로 전달된다.
with obj
문은 추가로 as var
형식으로 지정자를 받아들인다. as
지정자가 있으면 obj.__enter__()
에서 반환된 값이 var
에 할당되는 것이다. 이 값은 일반적으로 obj
와 동일한데 그 이유는 같은 단계에서 객체를 구성하고 context 관리자로 사용할 수 있기 때문이다. 가령, 다음의 클래스를 보도록 하자.
class Manager:
def __init__(self, x):
self.x = x
def yow(self):
pass
def __enter__(self):
return self
def __exit__(self, ty, val, tb):
pass
이 클래스와 함께 인스턴스를 생성하고 생성한 인스턴스를 context 관리자로 사용할 수 있는 것이다.
with Manager(42) as m:
m.yow()
다음은 리스트 transaction에 대한 재밌는 코드이다.
class ListTransaction:
def __init__(self, thelist):
self.thelist = thelist
def __enter__(self):
self.workingcopy = list(self.thelist)
return self.workingcopy
def __exit__(self, type, value, tb):
if type is None:
self.thelist[:] = self.workingcopy
return False
이 클래스는 기존 리스트에 일련의 변경 작업을 수행한다. 하지만 변경된 내용은 예외가 발생하지 않은 경우에만 리스트에 적용된다. 예외가 발생하면 원래의 리스트는 변경되지 않는다. 다음은 이 클래스를 사용한 예이다.
items = [1,2,3]
with ListTransaction(items) as working:
working.append(4)
working.append(5)
print(items) # [1, 2, 3, 4, 5]
try:
with ListTransaction(items) as working:
working.append(6)
working.append(7)
raise RuntimeError("We're hosed")
except RuntimeError:
pass
print(items) # [1, 2, 3, 4, 5]
다음과 같이 예외가 발생하면 list에 발생한 모든 일들을 transcation하게 처리한다.
assert
문으로 프로그램에 디버깅 코드를 추가할 수 있다.
assert test [,msg]
여기서 test
는 True, False
로 평가되는 표현식이다. 만약 test
가 False
로 평가되면 assert
문에 지정한 메시지인 msg
와 함꼐 AssertionError
예외가 발생한다.
def write_data(file, data):
assert file, 'write_data: file not fined!'
프로그램이 올바르게 동작하기 위해, 반드시 수행되어야 할 코드에는 assert
문을 사용하지 않도록 한다. 왜냐하면 파이썬이 최적화 모드(interpreter에 -o
옵션을 지정)로 동작할 때는 assert
문이 실행되지 않기 때문이다. 특히 사용자 입력이나 중요 연산의 성공 여부를 확인하기 위해 assert
문을 쓰면 안된다.
대신 assert
문은 항상 참이어야 하는 불변성을 검사할 때 사용된다. 만약 불변성을 위반하면 이는 사용자의 오류가 아니라 프로그램의 버그를 의미하는 것이다.
가령, write_data()
가 최종 사용자가 사용하도록 의도된 함수라면 assert
문은 일반적인 if
문과 올바른 에러 처리 코드로 대체되어야 한다.
assert
문의 일반적인 용도는 테스트다. 가령, 함수에 대한 최소한의 테스트를 수행할 때 사용할 수 있다.
다시 말해, assert
문은 사용자의 입력을 확인하기 위함이 아니라, 내부 프로그램의 일관성을 확인하려는 것이다.
파이썬의 프로그램 실행의 기본 모델은 명령형 프로그램(imperative programming)이다. 즉 프로그램은 소스 파일에 나타나는 순서대로 하나씩 실행되는 문장들로 구성된다. 그리고 세 가지 기본 제어 흐름 구조 if, while, for
만 있다. 파이썬이 어떻게 프로그램을 실행하는 지 이해하는 데 있어 모호함 같은 것은 없다.
가장 복잡하고 잠재적으로 에러가 발생하기 쉬운 기능은 exception(예외)다. exception은 자원의 적절한 관리를 엉망으로 만들 수 있다. 이 문제는 context 관리자와 with
문을 사용하여 해결해야 한다.