본문 바로가기

Programming/Python

[ProjectH4C] 코딩도장 Python Write-up (3)

예외 처리

예외(Exception)란 코드를 실행하다가 발생하는 에러를 뜻한다. 실행할 때마다 에러가 발생하는 것이 아니고, 특수한 경우에만 발생하는 에러가 있는데, 이러한 상황들을 예외라고 한다. 

def ten_div(x):
	return 10 / x

위의 함수는 10을 매개변수로 나눈 값을 리턴하는 함수이다. 평소에는 잘 동작하지만, 매개변수로 0을 넘겨주면 실행하는 도중에 에러가 발생한다. 

#tenDiv.py
def ten_div(x):
    return 10 / x

a = int(input())
print(ten_div(a))
$ python tenDiv.py
0	#0을 입력
Traceback (most recent call last):
  File "tenDiv.py", line 5, in <module>
    print(ten_div(a))
  File "tenDiv.py", line 2, in ten_div
    return 10 / x
ZeroDivisionError: division by zero

이 때 발생하는 에러는 ZeroDivisionError 이다. 이 뿐만 아니라 코드를 작성하고 실행하던 중에 자주 만날 수 있었던 AttributeError, NameError, TypeError 등을 모두 예외라고 한다.

 

예외가 발생하면 실행을 멈추고 어떠한 예외인지 출력해준 후에 프로그램이 종료되는데, 예외가 발생해도 실행을 멈추지 않고 계속 하게 해주는 방법을 예외 처리라고 한다.

 

try except

예외를 처리하기 위해선 실행할 코드를 try 안에 작성하고, except 안에는 예외가 발생했을 시 처리하는 코드를 작성하면 된다. 형태는 다음과 같다.

 

try:
	실행 코드
except:
	예외를 처리할 코드

10을 입력한 값으로 나눈 값을 출력하는 코드를 try ~ except 구문으로 작성하면 다음과 같다.

 

#tenDiv.py
try:
    x = int(input())
    y = 10 / x
    print(y)
except:
    print('예외 발생!')
$ python tenDiv.py
0	#0을 입력
예외 발생!	

예외 처리를 하게 되면 예외가 발생한 줄에서 바로 except로 넘어가며, 그 아래에 있는 코드들은 실행되지 않는다. 출력 결과를 보면 print(y)가 실행되지 않았음을 알 수 있다.

 

예외에 이름을 지정해서 특정 예외가 발생했을 시에만 예외 처리를 하도록 할 수도 있다.

y = [10, 20, 30]
 
try:
    index, x = map(int, input('인덱스와 나눌 숫자를 입력하세요: ').split())
    print(y[index] / x)
except ZeroDivisionError:    # 숫자를 0으로 나눠서 에러가 발생했을 때 실행됨
    print('숫자를 0으로 나눌 수 없습니다.')
except IndexError:           # 범위를 벗어난 인덱스에 접근하여 에러가 발생했을 때 실행됨
    print('잘못된 인덱스입니다.')

먼저 y 리스트를 선언한 후에, try 내에서 index와 x를 입력받고, y[index]를 x로 나누어주는 코드를 작성한다. 이 때 발생할 수 있는 예외로는 0으로 나누거나, y의 인덱스 참조가 잘못되는 경우 두 가지이다. 각각의 예외를 ZeroDivisionError, IndexError로 이름을 지정한 후에 처리해주는 코드를 아래에 각각 작성하였다. 실행 결과는 다음과 같다.

$ python except.py
인덱스와 나눌 숫자를 입력하세요: 1 0	#ZeroDivisionError
숫자를 0으로 나눌 수 없습니다.

$ python except.py
인덱스와 나눌 숫자를 입력하세요: 2 10	#정상 작동
3.0

$ python except.py
인덱스와 나눌 숫자를 입력하세요: 5 2	#IndexError
잘못된 인덱스입니다.

코드를 작성할 때 "except 예외이름" 뒤에 "as 변수" 를 작성하면, 해당 변수에 발생한 예외의 에러 메시지를 받아올 수 있다.

 

y = [10, 20, 30]
 
try:
    index, x = map(int, input('인덱스와 나눌 숫자를 입력하세요: ').split())
    print(y[index] / x)
except ZeroDivisionError as e:    # 숫자를 0으로 나눠서 에러가 발생했을 때 실행됨
    print('숫자를 0으로 나눌 수 없습니다.', e)
except IndexError as e:           # 범위를 벗어난 인덱스에 접근하여 에러가 발생했을 때 실행됨
    print('잘못된 인덱스입니다.', e)
$ python except.py
인덱스와 나눌 숫자를 입력하세요: 1 0
숫자를 0으로 나눌 수 없습니다. division by zero

$ python except.py
인덱스와 나눌 숫자를 입력하세요: 2 10
3.0

$ python except.py
인덱스와 나눌 숫자를 입력하세요: 5 2
잘못된 인덱스입니다. list index out of range

특정한 예외가 아닌 발생하는 모든 예외에 대해 처리해주고자 한다면 처리할 예외의 이름을 Exception으로 지정해주면 된다.

 

else

try 구문 안에서 코드를 실행하다가 예외가 발생하면 except 구문으로 넘어간다. 만약 try 안에서 예외가 발생하지 않았고, try가 끝난 후에 추가적으로 코드를 실행시키고자 한다면 else를 선언하고 그 안에 내용을 작성하면 된다. 형태는 다음과 같다.

 

try:
	실행할 코드
except:
	예외가 발생했을 때 실행
else:
	예외가 발생하지 않았을 때 실행

이처럼 else는 꼭 except 바로 뒤에 와야하며 except는 생략할 수 없다. 

 

finally

else는 예외가 발생하지 않았을 경우에 실행되지만, finally는 예외의 발생 여부와 상관없이 무조건 실행된다. finally를 포함한 코드의 형태는 다음과 같다.

try:
	실행할 코드
except:
	예외가 발생했을 때 처리하는 코드
else:
	예외가 발생하지 않았을 때 실행할 코드
finally:
	예외 발생 여부에 상관없이 실행할 코드

 

파이썬에서 지정한 예외들 외에 사용자가 직접 예외를 만들고 처리할 수 있다. 예외를 발생시킬 땐 raise 키워드를 사용한다.

 

try:
    x = int(input())
    if x%3!=0:
        raise Exception('3의 배수가 아님!')
    print(x)

except Exception as e:
    print('예외 발생!', e)
$ python raise.py
7
예외 발생! 3의 배수가 아님!

이터레이터

이터레이터는 값을 차례대로 꺼낼 수 있는 객체이다. 반복문에서 주로 사용하는 range를 예로 들면, range(100)은 사실 0 ~ 99의 숫자를 만들어 놓는 것이 아니라, 이들을 차례대로 꺼낼 수 있는 이터레이터를 만드는 것이다. range가 범위만큼의 값을 만들어 놓는 형식이라면 범위가 커지면 커질수록 메모리를 많이 사용하게 되고 성능이 저하되기 때문에, 파이썬에서는 이터레이터를 생성해놓고 해당 값이 필요할 때가 되면 값을 만들어내는 방식을 사용한다. 이렇게 데이터 생성을 뒤로 미루는 방식을 지연 평가라고 한다.

이터레이터는 반복 가능한 객체에서 사용 가능하다. 반복 가능한 객체란 문자열, 리스트, 딕셔너리, 세트 처럼 요소가 여러개 들어있고, 한 번에 한 개씩 꺼낼 수 있는 객체를 말한다. 아래의 코드는 1, 2, 3이 저장된 리스트를 이터레이터를 이용해 하나씩 꺼내는 예제 코드이다.

>>> it = [1, 2, 3].__iter__()
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

 __next__ 메소드를 이용해 차례대로 요소를 꺼내고, 더 이상 꺼낼 요소가 없으면 StopIteration 예외를 발생시킨다. 리스트 뿐만 아니라 딕셔너리, 세트, 문자열, range 에서도 모두 위와 같이 사용할 수 있다.

 

이터레이터를 직접 구현하기 위해 __iter__ , __next__ 메소드를 가지는 클래스를 하나 만든다.

#iterator.py
class Counter:
    def __init__(self, stop):
        self.current = 0
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.stop:
            r = self.current
            self.current += 1
            return r
        else:
            raise StopIteration

for i in Counter(3):
    print(i, end=' ')

Counter 이터레이터는 range의 동작을 구현한 이터레이터이다.

Counter 클래스가 선언되면 __init__ 메소드를 통해 current를 0으로, stop을 매개변수의 값으로 초기화 해준다.

__iter__ 메소드는 이터레이터를 반환하는 메소드인데, 해당 클래스 자체가 이터레이터이기 때문에 self를 리턴해주면 된다.

__next__ 메소드는 현재 숫자가 stop보다 작은 경우 current를 반환하고 1 증가시킨다. 만약 stop과 같아지게 되면 StopIteration 예외를 발생시킨다. 

 

이터레이터는 __next__ 메소드를 이용하지 않고 인덱스를 이용해서 접근할 수도 있다. 인덱스로 접근하기 위해 __getitem__ 메소드를 구현해야 한다.

 

class Counter:
    def __init__(self, stop):
        self.stop = stop
    
    def __getitem__(self, index):
        if index < self.stop:
            return index
        else:
            raise IndexError

print(Counter(3)[0], Counter(3)[1], Counter(3)[2])

for i in Counter(3):
    print(i, end= ' ')
$ python iterator.py 
0 1 2
0 1 2

위와 같이 __next__ , __iter__ 메소드가 없어도 __getitem__ 메소드를 이용해 이터레이터를 구현할 수 있다. 위의 코드에선 각 index에 저장된 값이 인덱스 번호랑 같기 때문에 index를 반환했지만, 만약 1, 2, 3 ... 을 반환하려면 return index+1 과 같이 구현할 수 있다.

 

iter, next 함수

파이썬에는 __next__ 메소드와 __iter__ 메소드를 호출해주는 내장 함수 next, iter가 존재한다. 이들의 인자는 반복 가능한 객체여야 한다. range의 iter를 호출해서 next를 사용하는 과정을 함수로 표현하면 다음과 같다.

>>> it = iter(range(3))	#it = range(3).__iter__()
>>> next(it)	#it.__next__()
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

 

제너레이터

제너레이터는 이터레이터를 생성해주는 함수이다. 이터레이터를 구현하기 위해서는 클래스 내부에 __iter__, __next__, __getitem__ 등을 구현해주어야 하지만 제너레이터는 yield 키워드만 사용하면 되기 때문에 이터레이터보다 손쉽게 구현할 수 있다.

다음은 제너레이터를 생성하고 사용하는 예제이다.

def number_generator():
    yield 0
    yield 1
    yield 2

for i in number_generator():
    print(i)
$ python Generator.py
0
1  

이터레이터와 다른 것이 아니냐는 의문이 들 수도 있지만, 제너레이터로 만든 객체가 사용할 수 있는 메소드 목록을 dir 함수로 출력해보면 이터레이터에서 사용하는 __iter__, __next__ 메소드가 들어있는 것을 확인할 수 있다. 실제로 제너레이터 객체의 __next__ 를 호출해주면 StopIteration 예외가 발생하는 것을 볼 수 있다.

>>> g = num_generator()
>>> g.__next__()
0
>>> g.__next__()
1
>>> g.__next__()
2
>>> g.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

이터레이터는 직접 return으로 값을 반환하고 예외를 발생시켰지만, 제너레이터는 yield에 지정한 값이 알아서 반환되고, 끝에 도달하면 자동으로 StopIteration 예외가 발생된다.

제너레이터의 동작 과정은 다음과 같다. 코드 실행 중 제너레이터 함수를 호출하게 되면, 맨 처음의 yield 값을 함수 바깥으로 전달(반환)하고서 코드의 실행을 함수 바깥으로 양보한다. 그러고나서 코드가 이어서 실행되다가 또 제너레이터 함수를 만나게 되면 그 다음의 yield 값을 반환하고 코드 실행을 함수 바깥으로 양보해준다. 그림으로 표현하면 다음과 같다.

 

 

제너레이터의 동작 과정. 출처 : 파이썬 코딩도장 40-2

 

제너레이터를 이용해 range의 동작을 구현한다면 코드는 다음과 같다.

def number_generator(count):
    n=0
    while n < count:
        yield n
        n += 1

for i in number_generator(3):
    print(i)

인자(count)를 받아서, n을 이용해 0부터 count - 1까지 yield를 이용해 생성한다. 이후에 for문에서는 i에 0부터 2까지 차례대로 저장된다. 이터레이터를 이용해 구현할 때보다 훨씬 간결해진 것을 볼 수 있다.

 

위의 내용들은 yield를 이용해 한번에 하나의 값을 바깥으로 전달하는 예시였는데, 하나의 yield로 여러 번 값을 전달하는 방법도 존재한다. 이 때에는 yield from 키워드를 이용해 값을 넘겨주며, yield from에는 반복 가능한 객체, 이터레이터, 제너레이터 객체가 와야 한다.

아래는 1, 2, 3을 저장한 리스트 x를 바깥으로 내보내는 예제이다.

 

def number_generator():
    x = [1, 2, 3]
    yield from x

for i in number_generator():
    print(i)
$ python Generator.py 
1
2
3

yield from x 를 이용하면 리스트에 들어있는 요소를 한 개씩 바깥으로 내보내게 된다. yield를 3번 사용해야 하는 것을 위와 같이 yield from을 이용해 한번에 해결할 수 있다.

코루틴

함수를 호출하면 해당 함수가 실행되고, 함수가 실행을 마치면 원래 실행중이던 코드로 다시 돌아간다. 돌아갈 때는 함수 내에서 사용하던 변수, 계산식은 모두 사라지게 된다. 아래는 calc 함수 내에서 덧셈 관련 함수인 add함수를 호출하는 코드이다.

 

#routine.py
def add(a, b):
    c = a + b
    print(c)
    print('add 함수')

def calc():
    add(1, 2)
    print('calc 함수')

calc()
$ python routine.py
3
add 함수
calc 함수

calc가 실행되면 add(1, 2)를 통해 add 함수를 호출하기 때문에 1+2인 3과 'add 함수'가 먼저 출력되고, add 함수가 끝나고 다시 calc 함수로 돌아와서 'calc 함수' 를 출력하게 된다. 이 때 calc 함수를 메인 루틴(Main Routine), add 함수를 서브 루틴(Sub Routine)이라고 한다. 메인 루틴에서 서브 루틴을 호출하면 메인 루틴은 대기 상태가 되고 서브 루틴이 실행되며, 서브 루틴이 실행을 종료하면 대기 중이던 메인 루틴이 이어서 실행하게 된다. 이 때 서브 루틴이 끝나면 서브 루틴의 내용은 모두 사라지게 된다.

하지만 메인, 서브 루틴처럼 종속되어있는 관계가 아닌 서로 협력하는 관계로서 코루틴(Coroutine)이라는 루틴이 존재한다. 메인 루틴과 코루틴은 서로 번갈아가며 코드를 실행한다. 메인 루틴에서 코루틴을 호출하면 코루틴이 실행되고 메인루틴은 대기 상태가 되고, 특정한 시점이 되면 코루틴이 다시 메인 루틴을 호출하고 코루틴은 대기 상태가 되는 식이다. 코루틴은 진입점(코드를 시작하는 시점)이 여러 개인 함수이다.

코루틴은 제너레이터의 특별한 형태로, yield를 이용해 바깥에서 값을 받아올 수 있다. 아래는 코루틴을 이용해 값을 받아오고, 받아온 값을 출력하는 예제이다.

 

def number_coroutine():
    while True:
        x = (yield)
        print(x)

co = number_coroutine()
next(co)

co.send(1)
co.send(2)
co.send(3)
$ python coroutine.py 
1
2
3

코루틴 함수를 작성할 땐 루틴이 종료되지 않고 계속 유지되어야 하기 때문에 꼭 무한루프로 만들어주어야 한다. 그리고 외부에서 받아온 값을 사용할 땐 (yield)와 같이 괄호로 묶어주어야 한다.

co 객체를 만들고 코루틴 내의 yield 부분까지 진행시키기 위해서 최초에 next(co)를 사용해준 뒤에 send 메소드를 통해 값을 전달해준다.

send(1)이 실행될 때 메인 루틴이 대기 상태가 되고 코루틴에서 1을 받아와서 출력한 다음 다시 코루틴이 대기 상태가 되고 메인 루틴으로 돌아와 send(2)를 실행하는 방식으로 동작한다.

코루틴은 값을 받아올 뿐만 아니라 바깥으로 전달하는 것도 가능하다. 이 때에는 (yield 변수)를 이용해서 외부에서 값을 받아옴과 동시에 변수의 값을 바깥으로 전달하게 된다. 

 

코루틴은 무한 루프로 동작하게 되는데, 중간에 강제로 종료하고 싶다면 close 메소드를 이용하면 된다. 아래는 코루틴에 20개의 숫자를 보낸 후에 코루틴을 종료하는 예제이다.

def number_coroutine():
    while True:
        x = (yield)
        print(x, end=' ')

co = number_coroutine()
next(co)

for i in range(20):
    co.send(i)

co.close()
$ python coroutine.py 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 

프로그램이 종료될 때 코루틴도 함께 종료되기 때문에 실제로 실행할 땐 close를 실행한 것과 실행하지 않은 것이 똑같아 보이지만, 만약 코루틴을 종료한 뒤 프로그램을 종료하지 않고 계속해서 실행해야 할 경우엔 위와 같이 close 메소드를 통해 코루틴을 종료할 시점을 직접 지정해줄 수 있다.

코루틴이 종료될 때는 GeneratorExit 예외가 발생한다. 만약 코루틴 함수 내에서 try except를 이용해 해당 예외를 처리하는 코드를 작성하면, 코루틴의 종료 시점이 언제인지 명확히 알 수 있다.

 

 

 

데코레이터

이전에 정적 메소드, 클래스메소드 등을 만들 때 메소드 위에 @staticmethod 등과 같이 이름을 붙였는데, 이들을 함수를 장식한다고 하여 데코레이터라고 한다. 어떠한 함수를 호출할 때마다 시작과 끝을 알리기 위에선 함수를 작성할 때마다 시작과 끝을 알리는 출력문을 작성해야 한다.

def hello():
    print('hello 함수 시작')
    print('hello')
    print('hello 함수 끝')

모든 함수에 적용시키고자 한다면 함수를 만들 때마다 시작과 끝에 출력문을 작성해주어야 한다. 이러한 경우 데코레이터를 사용하면 해결된다.

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():                           # 호출할 함수를 감싸는 함수
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환
 
def hello():
    print('hello')
 
def world():
    print('world')
    
trace_hello = trace(hello)
trace_hello()
trace_world = trace(world)
trace_world()
$ python decorator.py 
hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

 

시작과 끝을 알리고자 하는 함수를 trace의 인자로 넘겨주고, trace 함수 내의 wrapper 함수에서 시작을 출력하고 인자로 받은 함수를 실행한 후 끝을 출력하도록 작성하였다. 이 때 trace 함수를 데코레이터라 한다.

데코레이터를 사용해야 할 때마다 함수를 인자로 넘겨주고 사용하는 것도 함수가 많아지면 번거로워 질 수가 있다. 이 때 데코레이터를 사용할 함수를 정의할 때 바로 위에 @데코레이터 를 붙여주면, 위의 코드와 동일하게 사용할 수 있다. 이 때에는 hello(), world() 함수만 선언해도 데코레이터가 붙어서 출력된다.

 

def trace(func):                             
    def wrapper():                           
        print(func.__name__, '함수 시작')    
        func()                             
        print(func.__name__, '함수 끝')
    return wrapper                         
 
 
@trace
def hello():
    print('hello')
@trace
def world():
    print('world')
    
hello()
world()

함수의 매개변수와 반환 값을 데코레이터에서도 처리할 수 있다.

def trace(func):          # 호출할 함수를 매개변수로 받음
    def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정
        r = func(a, b)    # func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장
        print('{0}(a={1}, b={2}) -> {3}'.format(func.__name__, a, b, r))  # 매개변수와 반환값 출력
        return r          # func의 반환값을 반환
    return wrapper        # wrapper 함수 반환
 
@trace              # @데코레이터
def add(a, b):      # 매개변수는 두 개
    return a + b    # 매개변수 두 개를 더해서 반환
 
print(add(10, 20))
$ python decorator.py 
add(a=10, b=20) -> 30
30

이 때, wrapper 함수의 매개변수를 호출할 함수 add(a, b)의 매개변수와 똑같이 만들어주어야 한다. r에는 add(10, 20)의 결과가 저장되고 r을 반환함으로써 add(10, 20)의 결과가 반환된다.

 

클래스 내의 메소드에 데코레이터를 사용할 때에는 self 매개변수에 주의해야 한다. 인스턴스 메소드는 항상 첫 번째 매개변수를 self로 지정하는데, 이를 장식하는 데코레이터를 만들 때에도 wrapper의 첫 번째 매개변수를 self로, 인자로 받은 func의 첫 번째 매개변수도 self를 그대로 넣어주어야 한다.

 

데코레이터를 사용할 때 매개변수를 받는 데코레이터도 만들 수 있다. 이러한 데코레이터는 값을 지정할 때마다 동작을 바꿀 수 있다.

아래는 add 함수의 반환 값이 어떠한 수의 배수인지를 확인하는 데코레이터 예제이다.

 

def is_multiple(x):
    def real_decorator(func):
        def wrapper(a, b):
            r = func(a, b)
            if r % x == 0:
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, x))
            return r
        return wrapper
    return real_decorator

@is_multiple(3)
def add(a, b):
    return a + b

print(add(10, 20))
print(add(2, 5))
$ python decorator.py 
add의 반환값은 3의 배수입니다.
30
add의 반환값은 3의 배수가 아닙니다.
7

보통은 decorator 함수 내에 wrapper 함수가 존재하는 형태이지만, 매개변수를 받는 데코레이터를 만드려면 함수를 하나 더 만들어야 한다. 최초에 데코레이터가 사용할 매개변수를 받는 is_multiple 함수, 그리고 그 안에서 실제 데코레이터 역할을 해주는 real_decorator 함수, 그리고 그 안에 wrapper 함수를 만들어야 한다.

 

 

데코레이터는 함수가 아닌 클래스로도 구현이 가능하다. 클래스로 구현할 때는 먼저 __init__ 메소드를 만들어서 호출할 함수를 속성으로 저장한 다음, __init__ 메소드 내에 해당 데코레이터의 기능을 작성하면 된다. 아래의 예제는 함수의 시작과 끝을 알리는 데코레이터를 클래스로 구현한 것이다.

 

class Trace:
    def __init__(self, func):
        self.func = func

    def __call__(self):
        print(self.func.__name__, '함수 시작')
        self.func()
        print(self.func.__name__, '함수 끝')

@Trace
def hello():
    print('hello')

hello()
$ python decorator.py 
hello 함수 시작
hello
hello 함수 끝

 

 

클래스로 구현한 데코레이터도 매개변수와 반환값을 처리할 수 있다.

class Trace:
    def __init__(self, func):    # 호출할 함수를 인스턴스의 초깃값으로 받음
        self.func = func         # 호출할 함수를 속성 func에 저장
 
    def __call__(self, *args, **kwargs):    # 호출할 함수의 매개변수를 처리
        r = self.func(*args, **kwargs) # self.func에 매개변수를 넣어서 호출하고 반환값을 변수에 저장
        print('{0}(args={1}, kwargs={2}) -> {3}'.format(self.func.__name__, args, kwargs, r))
                                            # 매개변수와 반환값 출력
        return r                            # self.func의 반환값을 반환
 
@Trace    # @데코레이터
def add(a, b):
    return a + b
 
print(add(10, 20))
print(add(a=10, b=20))
$ python decorator.py 
add(args=(10, 20), kwargs={}) -> 30
30
add(args=(), kwargs={'a': 10, 'b': 20}) -> 30
30

 

클래스 데코레이션에서 매개변수와 반환값을 처리하려면 __call__ 메소드에 매개 변수를 지정하고, self.func에 매개변수를 넣어서 호출해주고 반환값을 반환하면 된다. 해당 코드에서는 __call__ 메소드의 인수를 위치 인수와 키워드 인수 모두 처리할 수 있는 가변 인수로 만들었다. 

 

매개변수를 가지는 클래스 또한 구현 가능하다. 이 때에는 __init__ 메소드에서 속성을 선언하고 매개변수로 받은 값을 저장해주면 된다.

class IsMultiple:
    def __init__(self, x):         # 데코레이터가 사용할 매개변수를 초깃값으로 받음
        self.x = x                 # 매개변수를 속성 x에 저장
 
    def __call__(self, func):      # 호출할 함수를 매개변수로 받음
        def wrapper(a, b):         # 호출할 함수의 매개변수와 똑같이 지정(가변 인수로 작성해도 됨)
            r = func(a, b)         # func를 호출하고 반환값을 변수에 저장
            if r % self.x == 0:    # func의 반환값이 self.x의 배수인지 확인
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, self.x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, self.x))
            return r               # func의 반환값을 반환
        return wrapper             # wrapper 함수 반환
 
@IsMultiple(3)    # 데코레이터(인수)
def add(a, b):
    return a + b
 
print(add(10, 20))
print(add(2, 5))
$ python decorator.py 
add의 반환값은 3의 배수입니다.
30
add의 반환값은 3의 배수가 아닙니다.
7

 

기존에는 __init__ 메소드에 호출할 함수를 매개변수로 받았지만, 매개변수가 있는 데코레이터라면 __init__ 메소드에 함수 대신 데코레이터가 사용할 매개변수를 받은 후에 초기화 시켜준다. 이후 __call__ 메소드에서 함수를 매개변수로 받고, 그 안에 wrapper 메소드를 정의하고 매개변수로 함수에서 사용하는 변수를 받아주면 된다. 함수의 결과를 반환하기 위해 r = func(a, b)를 선언해주고 이후에 r을 반환해주면 된다.

 

모듈과 패키지

파이썬에선 조금 더 복잡한 프로그램을 만들기 위해 각종 함수, 클래스 등을 담고 있는 외부의 파일들을 가져와서 코드를 작성할 수 있는데, 이 때 외부에서 가져오는 파일들을 모듈이라 하고, 이들을 묶어놓은 것을 패키지라 한다. 

모듈을 사용할 땐 import 키워드를 이용하고, 해당 모듈 안의 내용을 사용할 땐 모듈.함수(), 모듈.변수 처럼 사용하면 된다.

아래는 math 라는 수학 관련 함수를 모아둔 모듈을 가져와서 제곱근을 구하는 함수인 sqrt 함수를 사용하는 예제이다.

import math

a = math.sqrt(2.0)
b = math.sqrt(16.0)

print(a)
print(b)
$ python module.py
1.4142135623730951
4.0

a와 b에 각각 2, 16의 제곱근을 저장하고 이를 출력하는 코드이다. 이처럼 모듈 내의 함수는 모듈.함수()의 형식으로 사용해야 한다.

불러온 모듈의 이름이 긴 경우, 사용할 때마다 이를 전부 작성해주어야 하는데, 이를 보완하기 위해 모듈을 가져오면서 이름을 따로 지정해줄 수도 있다.

 

import math as m

a = m.sqrt(2.0)
b = m.sqrt(16.0)

print(a)
print(b)

위처럼 import 모듈 뒤에 as 이름 을 붙여주면 이름만 가지고도 모듈을 사용할 수 있다.

from을 이용하면 해당 모듈에서 원하는 함수, 클래스만 가져올 수 있다. 이 때는 함수를 사용할 때 앞에 모듈 이름을 붙이지 않아도 사용이 가능하다.

from math import sqrt

a = sqrt(2.0)
b = sqrt(16.0)

print(a)
print(b)

from으로 가져온 함수에 as 키워드를 이용해서 이름을 지정해줄 수도 있다. 이 때에도 해당 함수를 사용할 때 앞에 모듈 이름을 붙이지 않아도 된다.

from math import sqrt as s

a = s(2.0)
b = s(16.0)

print(a)
print(b)

 

 

패키지를 가져오는 방법도 동일하다. 파이썬의 표준 라이브러리에 있는 urllib 패키지 내에 있는 request 모듈을 가져오는 경우를 예로 들면 다음과 같다. 참고로 urllib는 url 작업을 위한 모듈을 모아둔 패키지 이며, url의 데이터를 읽어오는 역할을 하는 패키지가 request이다.

import urllib.request
response = urllib.request.urlopen('http://www.google.co.kr')
print(response.status)
$ python module.py 
200

as, from, from 모듈 as 이름 또한 패키지에서도 동일하게 사용 가능하다.

 

파이썬은 표준 라이브러리 외에도 패키지 인덱스를 통해 다양한 패키지들을 설치하고 사용할 수 있다. 이러한 패키지 인덱스들을 관리할 땐 pip 명령어를 이용한다. 만약 사용하는 파이썬이 3 이상의 버전이라면 pip3 명령어를 사용해야 한다. pip를 이용해 패키지를 받으려면 터미널에 다음 명령어를 입력하기만 하면 된다.

sudo pip3 install 패키지이름

 

 

'Programming > Python' 카테고리의 다른 글

[ProjectH4C] 코딩도장 Python Write-up (2)  (0) 2020.08.07
[ProjectH4C] 코딩도장 Python Write-up (1)  (0) 2020.08.06