데이터 공부/Python

파이썬 클린 코드 (2) - 파이썬스러운 코드 (1/2)

한소희DE 2023. 1. 26. 21:47
요즘 파이썬 클린코드(마리아노 아나야 지음) 라는 책을 읽고 있다.
한 챕터씩, 책을 읽으며 내가 이해한 대로 요약 및 정리를 해보도록 하겠다.

 

 

목차

1. '파이썬' 스러운 코드란?

2. 파이썬스러운 코드 1 - 인덱스와 슬라이스

2-1. 슬라이스(slice)의 동작 원리

2-2. 자체 시퀀스를 생성하는 방법

3. 파이썬스러운 코드 2 - 컨텍스트 관리자

3-1. 파이썬 스러운 방법으로 컨텍스트 관리자 구현 방법 (1) with 

3-2. 파이썬 스러운 방법으로 컨텍스트 관리자 구현 방법 (2) __enter__, __exit__ 매직 메소드 구현

3-3. 파이썬 스러운 방법으로 컨텍스트 관리자 구현 방법 (3) decorator 활용

 


 

 

 

 

1.' 파이썬'스러운 코드란?

 

모든 언어는 해당 언어로 작업을 처리하는 고유한 관용구가 있는데, 파이썬에서는 이 관용구를 따른 관용적인 코드를 파이썬스럽다고 말한다. 파이썬스러운 코드로 구현을 하면,

1. 성능도 더 좋고 2. 코드도 작게, 효율적으로 구현이 가능해진다. 또한 3. 동일한 패턴과 구조에 익숙해지면 실수를 줄이고 문제의 본질에 집중할 수 있기 때문에 파이썬 스러운 코드로 구현하는 것이 좋다고 한다.

 

따라서 파이썬스러운 코드를 작성하는 방법 몇 가지를 이 책에서 소개하고 있다.

 


 

 2. 파이썬 스러운 코드 1 - 인덱스와 슬라이스

시퀀스에서 특정 요소를 가져오려고 한다면, For Loop보다는 인덱스와 슬라이스를 이용하자.

 

파이썬의 시퀀스 자료형은 크게 세가지가 있다.

  • 문자열(string): 문자(character)들의 시퀀스
  • 리스트(list): [1 ,2, 3, 4, 5]
  • 튜플(tuple): (1, 2, 3, 4, 5)

이러한 시퀀스에서 특정 요소를 가져오려고 한다면, For Loop보다는 인덱스와 슬라이스를 이용하자.

 

 

[인덱스를 이용한 경우]

 

my_numbers = (1, 1, 2, 3, 5, 8, 13, 21)
print(my_numbers[1]) # output: 1

 

 

[슬라이스를 이용한 경우]

 

my_numbers = (1, 1, 2, 3, 5, 8, 13, 21)
print(my_numbers[1:2]) # output: (1,)

 

 

 

 

2-1. 슬라이스(slice)의 동작 원리

사실 위 예제에서처럼 my_numbers[1:2] 라고 전달하는 것은, 사실은 slice 라는 파이썬 내장 함수에 파라미터를 전달하는 것과 같다.

즉, my_numbers[1:2] 의 동작원리 my_numbers[slice(1, 2)]의 동작원리가 같다는 의미다.

 

my_numbers = (1, 1, 2, 3, 5, 8, 13, 21)

# 아래 두 output은 같은 동작 원리를 가짐
print(my_numbers[1:2]) # output: (1,)
print(my_numbers[slice(1, 2)]) # output: (1,)

 

 

이것은 어떻게 가능한 것일까? 바로 __getitem__ 이라는 매직 메소드 덕분에 가능하다.

매직 메소드란? 파이썬에서 특수한 동작을 수행하기 위해 예약한 메소드를 말한다.

이때, __getitem__ 이란, key에 해당하는 대괄호 안의 값을 파라미터로써 전달하여, slice의 동작을 가능케 하는 매직 메소드다. 

 

이런 매직 메소드를 이용해서, 자체 시퀀스를 생성할 수 있는데, 글로는 어려우니 아래 예제를 함께 살펴보자.

 

 

 

 

2-2. 자체 시퀀스 생성하는 방법

 

시퀀스는, __getitem____len__ 이라는 매직 메소드를 구현한 객체다.

이러한 점을 이용해서, class__getitem____len__ 이라는 매직 메소드를 구현해두면, class 도 시퀀스처럼 활용할 수 있다.

 

 

만약 아래와 같은 class 2개를 생성했다고 가정하자.

 

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)
        
        
 class Items_not_magic_method:
    def __init__(self, *values):
        self._values = list(values)

 

Items 는 시퀀스가 지닌 매직 메소드를 지니고 있고, Items_not_magic_method 는 매직 메소드를 지니고 있지 않다.

이때, 이 각 class를 대상으로, 시퀀스처럼 class 의 len, index slice 를 하고자 하면 어떤 결과를 얻을 수 있는지 살펴보자.

 

items = Items(1, 1, 2, 3, 5, 8, 13, 21)
print(items)  # output: <__main__.Items object at 0x102da4af0>
print(len(items))  # output: 8
print(items[1:2])  # output: [1]


items_not_magic_method = Items_not_magic_method(1, 1, 2, 3, 5, 8, 13, 21)
print(items_not_magic_method)  # output: <__main__.Items_not_magic_method object at 0x10531c9d0>
print(len(items_not_magic_method))  # output: TypeError: object of type 'Items_not_magic_method' has no len()
print(items_not_magic_method[1:2])  # output: TypeError: 'Items_not_magic_method' object is not subscriptable

 

Items class, Items_not_magic_method class 를 이용하여 items, items_not_magic_method 라는 두 객체(object)를 만들었다.

 

이때 items 는 마치 시퀀스 자료형처럼, 인입한 데이터에 대한 len()과 slice 를 할 수 있다.

하지만, items_not_magic_method 에서는 인입한 데이터에 대한 len()과 slice 값을 도출할 수 없다.

 

이처럼, 필요한 경우 class에 자체 시퀀스를 생성하는 방법을 사용할 수 있다는 점을 염두에 두어야 한다.

 

 

 

단, 이처럼 자체 시퀀스 생성 시 두 가지 권고사항이 있다.

 

 

1. 기존 클래스의 시퀀스 타입 = 범위로 인덱싱해서 갖는 결과의 시퀀스 타입 이어야 한다.

이유는 혼란 방지를 위함이다.

즉, 리스트 클래스에서 인덱싱해서 출력하는 결과의 타입도 리스트여야 한다는 의미다.

 

위 Items class 예제에서는, __init__을 살펴보면

 

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)

 

self._values = list(values) 로, list로 타입을 지정해두었으니까, print(items[1:2]) 의 아웃풋이 [1] 인 것이다.

만약 self._values = tuple(values) 로 수정한다면 print(items[1:2]) 의 아웃풋은 (1, )이 될 것이다.

 

 

2. slice 의 마지막 범위는 아웃풋에서 제외된다.

이유는 일관성 유지를 위함이다.

기존 시퀀스의 동작처럼, slice 의 마지막 범위는 아웃풋에서 제외한다.

이말은 즉, print(items[1:2]) 의 output이 [1,2]가 출력되는 것이 아닌, [1] 이 출력되도록 슬라이스의 마지막 범위는 아웃풋에서 제외해야 한다.

 

 


 

3. 파이썬스러운 코드 2 - 컨텍스트 관리자

 

컨텍스트 관리자란, 예외가 발생한 경우에도, 코드 블록의 마지막 문장이 끝나면 컨텍스트가 종료될 수 있도록 매니징하는 것을 의미한다.

대체로  __enter__과 __exit__ 두 개의 매직 메서드를 함수 내에 구성하여, 코드 정상 실행 유무와 상관 없이 컨텍스트를 안정적으로 마무리할 수 있도록 하는 데에 쓰인다.

 

예를 들면,
어떠한 서비스 회사에서 DB를 잠시 끄고 DB백업을 하고, DB를 다시 실행하는 로직이 있다고 가정하자. 이때 DB백업 작업에 대한 코드를 실행한다고 할때, 백업이 끝나면 백업 프로세스가 성공적으로 진행되었는지에 관계없이 DB는 반드시 다시 실행되어야 서비스에 문제를 야기하지 않을 것이다. 이럴 때 컨텍스트 관리자를 사용하면 유용하다.

혹은, 파일을 열었거나 소켓에 대한 연결을 열었을때도 적절하게 닫는 등의 할당된 모든 리소스를 해제하기 위한 작업을 하기 위해 컨텍스트 관리자를 쓴다.

 

 

 

3-1. 파이썬 스러운 방법으로 컨텍스트 관리자 구현 방법 (1) with 

with 의 open 함수는 컨텍스트 관리자 프로토콜을 구현한다. 즉, 예외가 발생해도 파일은 닫힌다.

 

# 파일 실행 시
with open(filename) as fd:
	process_file(fd)

 

 

 

3-2. 파이썬 스러운 방법으로 컨텍스트 관리자 구현 방법 (2) __enter__, __exit__ 매직 메소드 구현

책에 수록된 예시는 아래와 같다. 이런 식으로 구현하는 것이 가장 일반적이다.

예시의 로직은 위에서 잠시 언급한 예시대로, 'DB를 잠시 끄고 DB백업을 하고, DB를 다시 실행하는 로직' 이다.

 

def stop_database():
    run("systemtcl stop postgresql.service")

def start_database():
    run("systemtcl stop 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()

 

 __enter__ 에서 무언가를 반환하는 것은 필수는 아니나 좋은 습관이다.

 

__exit__ 에는 만약 예외가 발생하면 그 예외들을 파라미터로 받는다. 예외가 없으면 파라미터들은 None 값이다.
따라서 __exit__ 에서 True가 반환되도록 구성한다면, 발생한 예외가 삼켜질 수 있기 때문에 가급적 삼가야 한다.

아래 Case 1, Case 2를 살펴보자.

 

 

[Case 1. Error 가 발생하지 않는 경우]

 

class dbhandler_decorator(contextlib.ContextDecorator):
    def __enter__(self):
        stop_database()
        return self

    def __exit__(self, exc_type, ex_value, ex_traceback):
        print(exc_type, ex_value, ex_traceback) # 파라미터 값 출력
        start_database()


@dbhandler_decorator()
def offline_backup():
    print("pg_dump database")


# output
systemtcl stop postgresql.service
pg_dump database
None None None
systemtcl start postgresql.service

 

 

[Case 2. Error 가 발생하는 경우]

 

class dbhandler_decorator(contextlib.ContextDecorator):
    def __enter__(self):
        stop_database()
        return self

    def __exit__(self, exc_type, ex_value, ex_traceback):
        print(exc_type, ex_value, ex_traceback) # 파라미터 값 출력
        start_database()


@dbhandler_decorator()
def offline_backup():
    print(int("pg_dump database")) # string을 int로 변환할 수 없으므로 Error 반환


# output
systemtcl stop postgresql.service
<class 'ValueError'> invalid literal for int() with base 10: 'pg_dump database' <traceback object at 0x10315ebc0>
systemtcl start postgresql.service

 

 

 

3-3. 파이썬 스러운 방법으로 컨텍스트 관리자 구현 방법 (3) decorator 활용

혹은, @contextlib.ContextDecorator 를 사용하면 더 쉽고 간결하게 구현이 가능하다.

이처럼 데코레이터를 사용하면 로직을 한번만 정의해도 된다는 장점이 있다. 재사용성도 높다. 독립적이다.

다만, 독립적이기 때문에 이런 로직에서는 데코레이터 객체에 직접 접근은 어렵다는 특징도 있다.

 

import contextlib


class dbhandler_decorator(contextlib.ContextDecorator):
    def __enter__(self):
        stop_database()
        return self

    def __exit__(self, exc_type, ex_value, ex_traceback):
        start_database()


@dbhandler_decorator()
def offline_backup():
    print("pg_dump database")

 

 

만약 반드시 데코레이터 객체에 접근이 필요하다면 아래처럼 수정해도 되기는 한다.

 

def offline_backup():
    with dbhandler_decorator() as handler: ...

 

 

 

 

남은 2장은 다음 포스팅에 이어서 정리를 해보도록 하겠다.