프로그래밍에서 관용구(idiom)는 특정 작업을 수행하기 위해 코드를 작성하는 특별한 방법이다. 매번 동일한 구조를 반복하고 따르는 것이 일반적이다. 관용구는 코드이므로 언어에 따라 다르다. 모든 언어는 해당 언어로 작업을 처리하는 고유한 관용구를 가지고 있다. 이 관용구를 따른 코드를 관용적이라 부르고 특히 파이썬에서는 파이썬스럽다(Pythonic)고 한다.
권장사항을 따르고 파이썬스러운 코드를 작성하는 데는 여러 이유가 있지만 첫째로 관용적인 방식으로 코드를 작성하면 일반적으로 더 나은 성능을 내기 때문이다. 또한 코드도 더 작고 이해하기도 쉽다. 둘째로 이전 장에서 소개했듯 이 전체 개발팀이 동일한 패턴과 구조에 익숙해지면 실수를 줄이고 문제의 본질에 보다 집중할 수 있기 때문이다.
2장의 목표
- 인덱스와 슬라이스를 이해하고 인덱싱 가능한 객체를 올바른 방식으로 구현하기
- 시퀀스와 이터러블 구현하기
- 컨텍스트 관리자를 만드는 모범 사례 연구 그리고 어떻게 효율적으로 작성할 수 있는지
- 매직 메서드를 사용해 보다 관용적인 코드 구현
- 파이썬에서 부작용을 유발하는 흔한 실수 피하기
다른 언어와 마찬가지로 파이썬의 일부 데이터 구조나 타입은 자신이 가지고 있는 요소에 인덱스를 통해 접근하는 것을 지원한다. 그러나 파이썬은 다른 언어와 색다른 방법으로 접근하는 것을 지원한다.
예를 들어 C에서 배열의 마지막 요소에 접근하려면 배열의 길이에서 1을 뺀 위치에 있는 요소를 가져온다. 이렇게도 가능하지만, 파이썬에서는 음수 인덱스를 사용하여 끝에서부터 접근이 가능하다.
my_numbers = (4,5,3,9)
my_numbers[-1]
my_numbers[-3]

하나의 요소를 얻는 것 외에도 다음 명령과 같이 slice를 사용하여 특정 구간의 요소를 구할 수도 있다.

slice의 시작 인덱스는 포함하고 끝 인덱스는 제외하여 선택한 구간의 값을 가져온다는 것을 유의
시작, 끝 또는 간격 파라미터 중 하나를 제외할 수 있으며 이 경우 다음에 보이는 것처럼 시퀀스의 처음 또는 끝에서부터 동작한다.


위 모든 예제는 실제로는 slice함수에 파라미터를 전달하는 것과 같다. slice 함수는 파이썬 내장 객체이므로 직접 호출이 가능하다.
튜플, 문자열, 리스트의 특정 요소를 가져오려고 한다면 for 루프를 돌며 수작업으로 요소를 선택하지 말고 이와 같은 방법을 사용하는 것이 좋다.
위 기능들은 __getitem__ 이라는 매직 메서드 덕분에 동작한다. 이것은 myobject[key]와 같은 형태를 사용할 때 호출되는 메서드로 key에 해당하는 대괄호 안의 값을 파라미터로 전달한다.
매직 메서드는 파이썬에서 특수한 동작을 수행하기 위해 예약한 메서드로 이중 밑줄로 둘러싸여 있다.
특히 시퀀스는 __getitem__과 __len__을 모두 구현한 객체이므로 반복이 가능하다.
리스트 튜플과 문자열은 표준 라이브러리에 있는 시퀀스 객체의 예이다.
업무 도메인에서 사용하는 사용자 정의 클래스에 __getitem__을 구현하려는 경우 파이썬스러운 접근 방식을 따르기 위해서 몇 가지를 고려해야 한다.
클래스가 표준 라이브러리 객체를 감싸는 래퍼(wrapper)인 경우 기본 객체에 가능한 많은 동작을 위임할 수 있다. 즉 클래스가 리스트의 래퍼인 경우 리스트의 동일한 메서드를 호출하여 호환성을 유지할 수 있다.
예제를 통해서 객체가 어떻게 리스트를 래핑하는지 보여준다. 필요한 메서드가 있는 경우 그저 list 객체에 있는 동일한 메서드에 위임하면 된다.
from collections.abc import Sequence
class Items:
def __init__(self, *values):
self._values = list(values)
def __len__(self):
return len(self._values)
def __getitem__(self, item):
return self._values.__getitem__(item)
클래스가 시퀀스임을 선언하기 위해
collections.abc모듈의Sequence인터페이스를 구현해야 한다.
작성한 클래스가 컨테이너나 딕셔너리 같은 표준 데이터 타입처럼 동작하게 하려면 이 모듈을 구현하는 것이 좋다.
이러한 인터페이스를 상속받으면 해당 클래스가 어떤 클래스인지 바로 알 수가 있으며, 필요한 요건들을 강제로 구현하게 되기 때문이다.
이 예제에서는 컴포지션을 사용한다. 왜냐하면 내부적으로 list 클래스를 상속받지 않고 직접 작성한 구현체를 가지고 있기 때문이다.
그러나 만약 래퍼도 아니고 내장 객체를 사용하지도 않은 경우는 자신만의 시퀀스를 구현할 수 있다. 이때는 다음과 같은 사항에 유의해야 한다.
1. 범위로 인덱싱하는 결과는 해당 클래스와 같은 타입의 인스턴스여야 한다.
2. 일관성을 유지해야한다.
컨텍스트 관리자는 파이썬이 제공하는 유용한 기능이다. 이것이 특별히 유용한 이유는 패턴에 잘 대응되기 때문이다. 사전 조건과 사후 조건이 있는 일부 코드를 실행해야 하는 상황이 있다. 즉 어떤 중요한 작업 전후에 실행을 하려는 것이다. 컨텍스트 관리자는 이러한 상황에서 사용할 수 있는 가장 훌륭한 도구이다.
일반적으로 리소스 관리와 관련하여 컨텍스트 관리자를 자주 볼 수 있다. 예를 들어 일단 파일을 열면 파일 디스크립터 누수를 막기 위해 작업이 끝나면 적절히 닫히길 기대한다. 또는 서비스나 소켓에 대한 연결을 열었을 때도 적절하게 닫거나 임시 파일을 제거하는 등의 작업을 해야 한다. 그러나 예외가 발생하거나 오류를 처리해야하는 경우에는 쉽게 하기가 어려운데, 이를 해결하는 가장 일반적인 방법은 finally 블록에 정리 코드를 넣는 것이다.
fd = open(filename)
try:
process_file(fd)
finally:
fd.close()
그렇지만, 똑같은 기능을 매우 우아하고 파이썬스러운 방법으로 구현할 수도 있다.
with open(filename) as fd:
process_file(fd)
with문은 컨텍스트 관리자로 진입하게 한다. 이 경우 open함수는 컨텍스트 관리자 프로토콜을 구현한다. 즉 예외가 발생한 경우에도 블록이 완료되면 파일이 자동으로 닫힌다.
컨텍스트 관리자는 __enter__와 __exit__ 두 개의 매직 메서드로 구성된다. 첫 번째 줄에서 with문은 __enter__메서드를 호출하고 이 메서드가 무엇을 반환하든 as 이후에 지정된 변수에 할당된다.
이 라인이 실행되면 파이썬 코드가 실행될 수 있는 새로운 컨텍스트로 진입한다. 해당 블록에 대한 마지막 문장이 끝나면 컨텍스트가 종료되며 이는 파이썬이 처음 호출한 원래 컨텍스트 관리자 객체의
__exit__메서드를 호출함을 의미한다. 이러면 컨텍스트 관리자 블록 내에 예외 또는 오류가 있는 경우에도__exit__메서드가 여전히 호출되기 때문에 정리 조건을 안전하게 실행하는데 편하다.
컨텍스트 관리자가 파일이나 커넥션 등의 리소스 관리에서 매우 자주 사용되기는 하지만 오직 이런 분야에서만 사용 가능한 것은 아니다. 블록의 전후에 필요로 하는 특정 로직을 제공하기 위해 자체 컨텍스트 관리자를 구현할 수도 있다.
예를 들어 스크립트를 사용해 데이터베이스 백업을 하려는 경우를 생각해보자, 주의 사항은 백업은 오프라인 상태에서 해야 한다는 점이다. 즉 데이터베이스가 실행되고 있지 않은 동안에만 백업을 할 수 있으며 이를 위해 서비스를 중지해야 한다. 백업이 끝나면 백업 프로세스가 성공적으로 진행되었는지에 관계없이 프로세스를 다시 시작해야 한다.
첫 번째 방법은 서비스를 중지하고 - 백업을 하고 - 예외 및 특이사항을 처리하고 - 서비스를 다시 시작 하는 거대한 단일 함수를 만드는 것이다. 이를 코드로 살펴보면 다음과 같다.
def stop_database():
run("systemctl stop postgresql.service")
def start_database():
run("systemctl start postgresql.service")
class DBHandler:
def __enter__(self):
stop_database()
return self
def __exit__(self, exc_type, ex_value, ex_traceback):
start_database()
def db_backup():
run("pg_dump database")
def main():
with DBHandler():
db_backup()
이 예제에서는 DBHandler를 사용한 블록 내부에서 컨텍스트 관리자의 결과를 사용하지 않았다. 적어도 이 경우에 __enter__ 의 반환 값은 쓸모가 없다. 컨텍스트 관리자를 디자인할 때 블록이 시작된 후에 무엇이 필요한지 고려해야 한다. 일반적으로 필수는 아니지만 __enter__에서 무언가를 반환하는 것이 좋은 습관이다.
main() 함수에서는 유지보수 작업과 상관 없이 백업을 실행한다. 또한 백업에 오류가 있어도 여전히 __exit__를 호출한다.
__exit__ 메서드의 서명을 주목할 필요가 있다. 블록에서 발생한 예외를 파라미터로 받는다. 블록에 예외가 없으면 모두 None이다.
__exit__의 반환 값을 잘 생각해야 한다. 특별한 작업을 할 필요가 없다면 아무것도 반환하지 않아도 된다. 만약 __exit__가 True를 반환하면 잠재적으로 발생한 예외를 호출자에게 전파하지 않고 멈춘다는 것을 뜻한다. 그러나 이러한 건 오류를 조용히 무시하는 습관이므로 좋지 않다.
실수로
__exit__에서 True를 반환하지 않도록 주의해야한다.
앞의 예제와 같은 방법으로 컨텍스트 관리자를 구현할 수 있다. __enter__와__exit__ 매직 메서드만 구현하면 해당 객체는 컨텍스트 관리자 프로토콜을 지원할 수 있다.
이렇게 구현하는 방법이 일반적인 방법이지만 유일한 방법은 아니다.
이번에는 표준 라이브러리 특히 contextlib 모듈을 사용하여 보다 쉽게 구현하는 방법을 살펴보자
contextlib 모듈은 컨텍스트 관리자를 구현하거나 더 간결한 코드를 작성하는 데 도움이 되는 많은 도우미 함수와 객체를 제공한다.
__enter__와__exit__ 매직 메서드로 분리한다. import contextlib
@contextlib.contextmanager
def db_handler():
try:
stop_database()
yield
finally:
start_database()
with db_handler():
db_backup()
먼저 제너레이터 함수를 정의하고(yield 문 사용) contextmanager 데코레이터를 적용했다. 제너레이터 함수에 데코레이터를 적용하면 yield문 앞의 모든 것은 __enter__ 메서드의 일부처럼 취급된다는 것이다. __enter__ 메서드의 반환 값과 같은 역할을 하는 것으로 as x:와 같은 형태로 변수에 할당할 수도 있다.
with db_handler() as x:
위 예제에서는 yield 문에서 아무것도 반환하지 않았다. 이것은 암묵적으로 None을 반환하는 것과 같다.
이 지점에서 제너레이터 함수가 중단되고 컨텍스트 관리자로 진입하여 데이터베이스의 백업코드가 실행된다. 이 작업이 완료되면 다음 작업이 이어서 실행되므로 yield문 다음에 오는 것들은 __exit__로직으로 볼 수 있다.
이렇게 컨텍스트 매니저를 작성하면 기존 함수를 리팩토링하기 쉬운 장점이 있다.
컨텍스트 관리자를 구현할 수 있는 더 많은 방법이 있으며 이것 역시 표준 라이브러리인 contextlib 패키지에 있다.
믹스인 클래스는 다른 클래스에서 필요한 기능만 섞어서 사용할 수 있도록 메서드만을 제공하는 유틸리티 형태의 클래스이다.
반면에 컨텍스트 관리자 자체 로직은 앞서 언급한 매직 메서드에 필요한 로직을 구현해야한다.
class dbhandler_decorator(contextlib.ContextDecorator):
def __enter__(self):
stop_database()
return self
def __exit__(self, ext_type, ex_value, ex_traceback):
start_database()
@dbhandler_decorator()
def offline_backup():
run("pg_dump database")
이전 예제와 다른 점은 무엇일까? with문이 없다는 것이다. 그저 함수를 호출하기만 하면 offline_backup 함수가 컨텍스트 관리자 안에서 자동으로 실행된다. 이것이 원본 함수를 래핑하는 데코레이터가 하는 일이다.
이 접근법의 유일한 단점은 완전히 독립적인 것이라는 것이다. (좋은 특성이기도 하다)
컨텍스트 관리자는 파이썬을 차별화하는 상당히 독특한 기능이다. 따라서 가급적 컨텍스트 관리자를 사용하는 것이 이상적인 방법이다.