기타 주제

지금까지 앞으로 이용하게 될 파이썬의 다양한 측면들을 대부분 살펴봤습니다. 이번 장에서는 파이썬 지식을 좀 더 풍성하게 만들어줄 측면들을 좀 더 다루겠습니다.

튜플 전달하기

함수에서 값을 두 개 전달하고 싶을 때가 있나요? 이럴 때 튜플을 이용하면 됩니다.

>>> def get_error_details():
...     return (2, 'details')
...
>>> errnum, errstr = get_error_details()
>>> errnum
2
>>> errstr
'details'

a, b = <표현식>을 사용하면 표현식의 결과를 값이 두 개인 튜플로 해석한다는 점에 유의합니다.

또한 이것은 파이썬에서 두 값을 서로 바꾸는 가장 빠른 방법이라는 뜻이기도 합니다.

>>> a = 5; b = 8
>>> a, b
(5, 8)
>>> a, b = b, a
>>> a, b
(8, 5)

특수 메서드

클래스에서 특별한 의미를 지닌 __init____del__ 같은 메서드가 있습니다.

특수 메서드(special method)는 파이썬에 내장된 타입의 특정 행위를 흉내 내는 데 사용됩니다. 예를 들어, 클래스에서 x[key] 같은 인덱싱 연산을 사용하고 싶다면(마치 클래스를 리스트나 튜플처럼 사용하듯이) __getitem__() 메서드만 구현하면 됩니다. 생각해 보면 이것은 파이썬이 list 클래스를 위해 해주는 일이기도 합니다!

다음 표에 몇 가지 유용한 특수 메서드를 정리했습니다. 모든 특수 메서드에 관해 알고 싶다면 매뉴얼을 참고합니다.

  • __init__(self, ...)

    • 이 메서드는 새로 생성된 객체를 사용하기 위해 객체가 반환되기 직전에 호출됩니다.
  • __del__(self)

    • 객체가 파괴(시점을 예상할 수 없으므로 이를 이용하는 것을 자제해야 합니다)되기 직전에 호출됩니다.
  • __str__(self)

    • print 함수를 사용하거나 str() 함수가 사용될 때 호출됩니다.
  • __lt__(self, other)

    • ~보다 작음 연산자(<)가 사용될 때 호출됩니다. 이와 비슷하게 모든 연산자(+, > 등)에 대한 특수 메서드가 있습니다.
  • __getitem__(self, key)

    • x[key] 인덱싱 연산이 사용될 때 호출됩니다.
  • __len__(self)

    • 파이썬에 내장된 len() 함수가 시퀀스 객체에 대해 사용될 때 호출됩니다.

단일 문장 블록

지금까지 살펴본 각 문장 블록은 들여쓰기 수준을 통해 나머지 부분과 구별됩니다. 그런데 여기에는 한 가지 알아둘 점이 있습니다. 문장 블록에 단 하나의 문장만 있다면 이를 조건문이나 반복문과 같은 줄에 지정할 수 있습니다. 다음 예제를 보면 쉽게 이해할 수 있습니다.

>>> flag = True
>>> if flag: print('Yes')
...
Yes

예제에서는 단일 문장을 곧바로 사용하고 블록을 따로 구분하지 않았다는 점에 유의합니다. 이렇게 하면 프로그램의 크기가 더 작아지긴 하지만 이 같은 축약된 방식은 오류 검사를 제외하곤 쓰지 않기를 적극 권장합니다. 주된 이유는 들여쓰기를 적절히 사용한다면 문장을 추가하기가 훨씬 쉬워질 것이기 때문입니다.

람다 문

lambda 문은 새 함수 객체를 생성하는 데 사용됩니다. 기본적으로 lambda는 매개변수 다음에 표현식을 하나 받습니다. 이때 람다는 함수의 본문이 되고, 이 표현식의 값은 새 함수에 의해 반환됩니다.

예제(more_lambda.py):

points = [{'x': 2, 'y': 3},
          {'x': 4, 'y': 1}]
points.sort(key=lambda i: i['y'])
print(points)

출력 결과:

$ python more_lambda.py
[{'y': 1, 'x': 4}, {'y': 3, 'x': 2}]

동작 원리

listsort 메서드는 리스트의 정렬 방식을 결정하는 key 매개변수를 받을 수 있다는 점에 유의합니다(보통 오름차순이나 내림차순만 알고 계실 겁니다). 예제에서는 특별한 방법으로 정렬하고 싶고, 이를 위해서는 함수를 작성할 필요가 있습니다. 이곳에서만 한번 사용할 함수를 정의하기 위해 별도의 def 블록을 작성하는 대신 여기서는 람다식을 사용해 새 함수를 생성합니다.

리스트 내포

리스트 내포(list comprehensions)는 기존 리스트에서 새로운 리스트를 구하는 데 사용됩니다. 숫자 리스트가 하나 있고 모든 숫자를 대상으로 숫자가 2보다 클 경우 여기에 2를 곱한 수로 구성된 리스트를 구하고 싶다고 해봅시다. 리스트 내포는 이 같은 상황에 안성맞춤입니다.

예제(more_list_comprehension.py):

listone = [2, 3, 4]
listtwo = [2*i for i in listone if i > 2]
print(listtwo)

출력 결과:

$ python more_list_comprehension.py
[6, 8]

동작 원리

예제에서는 어떤 조건(if i > 2)을 충족할 때 수행해야 할 작업(2*i)을 지정하는 방식으로 새로운 리스트를 구합니다. 원본 리스트는 변경되지 않은 채로 유지된다는 데 유의합니다.

리스트 내포를 이용할 때의 이점은 반복문을 사용해 리스트의 각 요소를 처리하고 이를 새로운 리스트에 저장할 때 반복적으로 작성해야 할 코드의 양을 줄일 수 있다는 것입니다.

함수에서 튜플 및 딕셔너리 받기

***를 사용해 함수에 대한 매개변수를 각각 튜플이나 딕셔너리로 받는 특별한 방법이 있습니다. 함수에 전달하는 인수의 개수가 가변적일 때 이 방법이 유용합니다.

>>> def powersum(power, *args):
...     '''각 인수를 거듭제곱한 합계를 반환'''
...     total = 0
...     for i in args:
...         total += pow(i, power)
...     return total
...
>>> powersum(2, 3, 4)
25
>>> powersum(2, 10)
100

args 변수에 * 접두사를 지정했기 때문에 함수에 전달되는 추가 인수는 모두 args에 튜플로 저장됩니다. ** 접두사를 지정했다면 추가 매개변수는 딕셔너리의 키/값 쌍으로 간주될 것입니다.

assert 문

assert 문은 무언가가 참이라는 것을 주장할 때 사용됩니다. 예를 들어, 현재 사용 중인 리스트에 요소가 최소한 하나는 있을 것이라고 확신하고, 이것이 참이 아닐 때는 오류를 일으키고 싶다면 assert 문이 이 같은 상황에 적합합니다. assert 문이 실패하면 AssertionError가 발생합니다. pop() 메서드는 리스트에서 마지막 항목을 제거한 후 반환합니다.

>>> mylist = ['item']
>>> assert len(mylist) >= 1
>>> mylist.pop()
'item'
>>> assert len(mylist) >= 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

assert 문은 신중하게 사용해야 합니다. 대부분의 경우 예외를 잡아서 문제를 처리하거나 오류 메시지를 사용자에게 보여주고 프로그램을 종료하는 편이 낫습니다.

데코레이터

데코레이터(decorator)는 래퍼 함수(wrapper function)를 적용하는 간편한 방법입니다. 데코레이터는 같은 코드가 계속 반복되는 기능을 "감싸는" 데 유용합니다. 예를 들어, 제가 쓸 용도로 retry라는 데코레이터를 만들었는데, 이것을 아무 함수에나 적용하고 프로그램을 실행하는 중에 오류가 발생하면 최대 5번까지 간격을 두고 해당 함수를 재시도하게 됩니다. 이 데코레이터는 원격 컴퓨터를 대상으로 네트워크 호출을 시도하는 상황에서 특히 유용합니다.

from time import sleep
from functools import wraps
import logging
logging.basicConfig()
log = logging.getLogger("retry")


def retry(f):
    @wraps(f)
    def wrapper_function(*args, **kwargs):
        MAX_ATTEMPTS = 5
        for attempt in range(1, MAX_ATTEMPTS + 1):
            try:
                return f(*args, **kwargs)
            except Exception:
                log.exception("Attempt %s/%s failed : %s",
                              attempt,
                              MAX_ATTEMPTS,
                              (args, kwargs))
                sleep(10 * attempt)
        log.critical("All %s attempts failed : %s",
                     MAX_ATTEMPTS,
                     (args, kwargs))
    return wrapper_function


counter = 0


@retry
def save_to_database(arg):
    print("Write to a database or make a network call or etc.")
    print("This will be automatically retried if exception is thrown.")
    global counter
    counter += 1
    # 처음으로 호출했을 때는 예외를 발생시키고
    # 두 번째 호출(즉, 재시도)부터는 문제 없이 동작합니다
    if counter < 2:
        raise ValueError(arg)


if __name__ == '__main__':
    save_to_database("Some bad value")

출력 결과:

$ python more_decorator.py
Write to a database or make a network call or etc.
This will be automatically retried if exception is thrown.
ERROR:retry:Attempt 1/5 failed : (('Some bad value',), {})
Traceback (most recent call last):
  File "more_decorator.py", line 14, in wrapper_function
    return f(*args, **kwargs)
  File "more_decorator.py", line 39, in save_to_database
    raise ValueError(arg)
ValueError: Some bad value
Write to a database or make a network call or etc.
This will be automatically retried if exception is thrown.

동작 원리

다음 내용을 참고하세요.

파이썬 2와 파이썬 3의 차이점

다음 내용을 참고하세요.

정리

이번 장에서 파이썬의 기능들을 몇 가지 더 알아봤지만 여전히 파이썬의 모든 기능들을 다루진 않았습니다. 하지만 현 시점에서는 실전에서 사용하게 될 대부분의 주제를 다뤘습니다. 앞으로 작성하게 될 프로그램이 무엇이든 여기서 다룬 내용으로도 시작하기에는 충분합니다.

다음 장에서는 파이썬을 좀 더 알아보는 방법을 살펴보겠습니다.