Pythonic

Pythonic이란

프로그래밍을 하다보면, 관용구(idiom)라는 것을 다루게 된다.

관용구는 특정 작업을 수행하기 위해 코드를 작성하는 특별한 방법인데, 당연히 언어에 따라 다르다.

특히 Python을 하다보면 Pythonic이라는 단어를 자주하게 되는데, 파이썬의 고유한 메커니즘을 잘 따르는 코드를 파이썬스럽다 고 한다.

Pythonic한 코드를 작성했을 때 (=관용적인 방식으로 코드를 작성했을 때) 성능이 좋고, 코드도 짧고 이해하기도 쉽다.

사실 Pythonic 이라는 단어에 대해서 굉장히 여러 가지의 해석이 존재한다.

하지만 이들의 공통적인 본질을 따라가 보면, 최종적으로는 파이썬의 철학인 The Zen of Python 에 도달하게 된다.

Pythonic 함을 지키는 것이 바로 Zen of Python을 따르는 것이라고 보면 된다.

The Zen of Python

파이썬에는 이스터에그가 하나 숨겨져 있는데, 바로 this, 즉 자기 자신을 임포트 하는 것이다.

import this


The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

▣ Beautiful is better than ugly

될 수 있으면 보기 좋게 만들라는 뜻으로, 각종 네이밍이나 줄 띄움, 기능 모듈화 등 보기 편한 코드를 작성하기 위한 컨벤션들이 이를 실현하기 위한 도구에 해당된다.

◼ Sparse is better than dense 항목도 비슷한 의미를 가진다. 한 줄에 뭉쳐놓은 긴 코드를 작성하기보단, 여러 줄로 나누어 작성하라는 의미이다.

▣ Explicit is better than implicit

함축적인 표현 보다는 명확한 표현을 추구하라는 의미이다. 변수명이나 클래스 이름, 클래스 메소드 이름을 짓는 데에 적용될 수도 있고, object 클래스를 일부러 명시적으로 상속시키는 Python3 클래스 정의의 케이스도 있다. 아마 이 케이스에서 object의 암묵적인 상속이 unPythonic 하다고 주장한 사람들은 이 격언을 떠올린 것일지도 모른다.

프로그래밍은 단순한 기능의 구현 뿐만 아니라 다른 작업자와의 커뮤니케이션을 필요로 하는 작업이기 때문에 뭔가를 암묵적으로 숨겨 놓기보다는 항상 명시적으로 코드를 작성해 놓는 것이 좋다.

◼ Readability counts 와도 연관되는데, 되도록이면 명확하고 읽기 쉽게 이름을 짓고 코드를 작성하라는 격언이다.

▣ Simple is better than complex

최대한 간단한 구현 방법을 택하라는 의미이다. 어떤 언어가 그렇지 않냐 싶겠지만, 파이썬에서 특정한 기능을 구현하는 방식은 여러 가지가 존재한다. 하지만 파이썬이 기본적으로 제공하는 기능을 충분히 이용하지 않고 C언어 등에서 사용할 법한 문법으로 괴상하게 구현하는 방법과, 반대로 기능을 잘 살려 간단명료하게 구현하는 방법이 있다면 당연히 후자를 선택해야 한다.

▣ Complex is better than complicated

하지만 문제 상황 자체가 매우 어려워서 간단한 구현이 어려울 수도 있다. 이런 상황에서는 어거지로 간단한 구현을 하기 위해 여기저기 뒤얽힌 괴상한 코드를 만들어 내기보다는, 차라리 복잡한 문제를 위한 복잡한 구현을 택하라는 이야기이다.

◼ If the implementation is hard to explain, it’s a bad idea / ◼ If the implementation is easy to explain, it may be a good idea 와도 관련이 있는데, 단순히 제 기능을 하는 고성능의 구현을 택하기보단, 다른 작업자가 읽고 이해할 수 있는 구현을 선택하라는 의미이다.

▣ Flat is better than nested

코드를 서브모듈화, 서브클래스화 하는 것은 분명히 어느 수준까지는 더 나은 사용성과 편리함을 제공하지만, 코드를 계속 발전시키는 과정에서 단계를 너무 많이 만들면 오히려 사용성과 편리함은 감소할 수 있다. 따라서 정말 필요한 경우가 아니라면 한 모듈, 클래스 단계 내에서 수평하게 (flat) 코드를 작성해 놓는 것이 좋다는 의미이다.

▣ Special cases aren’t special enough to break the rules... Although practicality beats purity.

이 두 격언은 서로 상반되는 주장을 하고 있다. 한 쪽은 아무리 더 나은 구현을 위해서라도 미리 정해놓은 프로그래밍 규칙은 절대 위반할 수 없다는 입장이고, 다른 한 쪽은 실용성을 위해서라면 가끔씩은 허용이 필요하다는 입장이다. 이 상반되는 두 격언은 규칙 vs 실용성 이슈에 대한 명확한 정답은 없다는 것을 시사하며, 둘 사이에서 적절한 균형점을 찾아야 한다고 이야기 하고 있다.

▣ Errors should never pass silently... Unless explicitly silenced

각종 에러 상황에서 단순 에러 코드나 성공/실패 여부를 나타내는 bool 변수, 혹은 None 을 반환시키는 식으로 조용한 에러 처리를 하기 보다는 명확하게 에러를 raise 해 주는 것이 좋다고 말하고 있다. 프로그램이 내부에서 발생하는 에러 상황을 그대로 안고 길게 실행되어 나중에 에러 발생지점을 찾기 어렵게 만드는 것 보다, 프로그램 실행이 실패할 거라면 일찍, 에러 발생 지점과 최대한 가깝게 에러를 명시적으로 raise 하는 것이 좋다는 뜻이다.

◼ In the face of ambiguity, refuse the temptation to guess 는 에러가 발생하는 상황에서 에러에 대한 원인을 넘겨짚고 때려 맞추는 식으로 해결하기 보다는, 항상 논리적이고 신중한 접근을 통해 문제를 명확하게 해결하라는 격언이다. 넘겨짚는 식으로 해결된 (것 처럼 보이는 문제) 는 이후에 다른 문제를 발생시킬 가능성이 높다.

Python만의 문법과 idea가 적용된 pythonic한 코드의 예시에 대해서 살펴보자.

인덱스와 슬라이스

음수 인덱스를 통한 접근

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

>>> nums[-1]
21
>>> nums[-5]

슬라이싱

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

>>> nums[2:4]
(2, 3)
>>> nums[:-3]
(1, 1, 2, 3, 5)
>>> nums[::2]
(1, 2, 5, 13)

Context manager

컨텍스트 관리자(Context Manager)는 크게 두 가지 경우에 유용한 파이썬의 기능이다.

리소스 관리

일반적으로 파일이나 소켓 연결을 열었을 때 할당된 리소스를 모두 해제해주어야 하는데,

이 때 생각하지 못한 예외나 오류가 발생할 수도 있다.

이를 사전에 모두 처리하는 것은 어렵지만, 파이썬에서는 with문(PEP-343)을 사용해 파이썬스럽게 구현할 수 있다.

with open(filename) as fd:
    process_file(fd)

블록의 마지막이 실행되고 나면 컨텍스트가 종료되며, 오류가 있더라도 종료되므로 안전하게 실행할 수 있다.

컨텍스트 관리자 구현

컨텍스트 관리자는 일반적으로 __enter____exit__ 두 개의 매직 메서드만 구현하면 되지만, contextlib 모듈을 사용하여 더 쉽게 구현할 수도 있다.

__enter__ 메서드가 호출되면 새로운 컨텍스트로 진입하게 되며

컨텍스트의 마지막 문장이 끝나면 컨텍스트가 종료되며 처음 호출한 컨텍스트 관리자 객체의 __exit__ 메서드를 호출한다.

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

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

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

프로퍼티, 속성과 객체 메서드의 다른 타입들

파이썬에서의 밑줄

일반적으로 파이썬에서의 변수나 메소드 이름 앞의 밑줄은 private를 의미한다.

(단, private를 의미만 할 뿐 private하게 만들 수는 없다. 자세한 내용은 뒤에 설명)

그런데 밑줄 두 개는 전혀 다른 name mangling이라는 것을 실행한다.

name mangling이란 말그대로 이름을 만드는 것인데, _<class-name>__<attribute-name> 형태의 이름을 만든다.

이런 이름을 만드는 이유는 클래스가 여러 번 확장되더라도 충돌 없이 오버라이드를 하기 위한 것이다.

따라서 가끔 name mangling이 전혀 다른 이름을 만들어내어 본래의 이름으로는 해당 속성에 접근할 수 없게 되기 때문에

밑줄 두 개를 작성하는 것이 정말 private하게 만든다고 생각하는 사람이 있는데 이는 매우 잘못된 생각이다.

class Connector:
    def __init__(self, source):
        self.source = source
        self._timeout1 = 60
        self.__timeout2 = 120

    def connect(self):
        print(f"connecting with {self._timeout}s")

conn = Connector("postgresql://localhost")
>>> conn.connect()
connecting with 60s
>>> conn._timeout1
60
>>> conn.__timeout2
AttributeError: 'Connector' object has no attribute '__timeout2'
>>> conn._Connector__timeout2
120

프로퍼티

프로퍼티는 아래의 두 경우 처럼 객체의 어떤 속성에 대한 접근을 제어하기 위해 사용한다.

  • 객체에 값을 저장해야 하는 경우

  • 객체의 상태나 다른 속성의 값으로 어떤 계산을 하려고 하는 경우

타 언어에서 getter, setter를 만드는 것과 동일하다.

@property 데코레이터는 일반적인 getter와 역할이 같으며

@<property>.setter데코레이터는 setter와 역할이 같다.

class User:
    def __init__(self, username):
        self.username = username
        self._email = None

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, new_email):
        if not is_valid_email(new_email):
            raise ValueError("유효한 이메일이 아니므로 사용할 수 없음.")
        self._email = new_email

>>> user = User("clean_user")
>>> user.email = "clean_user"
유효한 이메일이 아니므로 사용할 수 없음.
>>> user.email = "clean_user@example.com"
>>> user.email
'clean_user@example.com'

프로퍼티를 사용하면 명령-쿼리 분리 원칙(command and query separation)을 따르기도 용이하다.

@property 데코레이터는 응답하기 위한 query이며

@<property>.setter데코레이터는 무언가를 하기 위한 command이다.

일반적인 프로그래밍 언어는 public, private, protected 세 가지 프로퍼티를 가지지만

파이썬은 모든 프로퍼티와 함수가 public하다. 따라서 호출자가 모든 객체의 속성을 호출할 수 있다.

밑줄(언더스코어)을 사용하여 다른 언어처럼 private를 의미할 수 있지만, 여전히 호출은 할 수 있다.

컨테이너 객체

__contains__() 메서드를 구현한 객체, 일반적으로 Boolean값을 반환하도록 구현

해당 키워드는 in 키워드가 발견될 때 호출됨.

# more Pythonic
class Boundaries:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def __contains__(self, coord):
        x, y = coord
        return 0 <= x < self.width and 0 <= y < self.height


class Grid:
    def__init__(self, width, height):
        self.width = width
        self.hegiht = height
        self.limit = Boundaries(width, height) # 의도를 직관적으로 설명하였음.

    def __contains__():
        return coord in self.limits

Last updated