본문 바로가기

진리는어디에/Python

[Python] 파이썬 3의 Modern 함수들

본 포스트의 원문은 Bartek CiszkowskiModern Functions in Python 3이다. annotation을 이용해 함수 인자에 타입을 지정하고 mypy를 이용해 타입 검사를 하는 것에 대한 내용이지만 정작 mypy에 대한 설치나 사용 방법들에 대해서는 설명이 빠져 있다. mypy 관련 내용은 포스트 아래에 링크해 두었으니 참고 바란다.

파이썬은 빠르고 효과적으로 작업할 수 있는 언어로 지난 수십 년 동안 번창했습니다. 많은 현대 회사와 마찬가지로 우리 회사도 시스템 스택의 대부분에서 파이썬을 상당히 광범위하게 사용하지만 많은 경우 아직 파이썬 2.7을 계속 유지하고 있습니다.

가혹한 현실이지만 파이썬 2.7은 이미 한물 갔습니다. 솔직히 말해서 - 때가 되었습니다! - 우리는 Python 3.x에서 모든 새로운 코드를 작성했으며 기존 프로젝트를 업그레이드하기 위해 지속적으로 노력하고 있습니다.

G Adventures[각주:1]에서 Python 3이 성장함에 따라 우리는 2.7 기반에서 벗어나 파이썬 3의 이점을 활용하기 시작했습니다. 이것은 문화적 변화이며 우리가 결국 해결하게 될 접근 방식은 여기에서 논의되는 것과 균형을 이룰 것입니다. 이 기사에서는 함수 시그니쳐에 접근할 수 있는 다양한 방법, 그 의미 및 가장 베스트한 사용방법들에 대해 논의 합니다.

def cheeseshop(kind, weight, region=None):

위 코드에서 오늘날 모든 버전의 파이썬에서 사용 가능한 함수 정의를 볼 수 있습니다. 파이썬이 잘 만들어진 이유중 하나는 간단하고 깔끔하기 때문입니다. 여기에는 군더더기가 없습니다! 그러나 우리는 최근에 Go 프로그램언어를 많이 사용했고 엄격하게 타입을 제한하는 언어의 몇 가지 특혜들이 우리를 서서히 물들이고 있습니다. 파이썬3에서는 함수에 totally optional PEPs를 활용하고, annotation을 이용해 타입에 대한 주석 달 수 있습니다. 더 자세히 알아보기 전에 어떻게 생겼는지 살펴보겠습니다.

from decimal import Decimal

def cheeseshop(
    kind: str,
    weight: int,
    region=None: str,
) -> Decimal:

dlwp 꽤 흥미로워 졌습니다. 어노테이션(annotation)은 꽤 긴 논의거리가 될 수 있습니다. 그리고 보다 자세한 사항을 위해서는 PEP문서를 볼것을 권합니다. 아무튼 저는 여기에서 동적 타입 지정(dynamic typping)과 고정 타입 지정(static typing)에서 오는 이점과 어노테이션의 고수준을 유지하는 것에 포커스를 맞추도록 하겠습니다.

여기에서 내가 : 콜론 구분 기호를 사용하여 각 변수가 무엇인지 정의한 것을 볼 수 있습니다. 내 코드를 읽는 개발자에게 빌트인 타입을 사용하여 첫 번째 인수(kind)는 str(문자열)이라는 것을 알려주고 있습니다.

또한 함수 정의 끝에 -> Decimal은 함수가 반환하는 것을 정의합니다.

파이썬3은 typing 모듈을 통해 다양한 빌트인 타입을 제공합니다. 이 외에도 타입은 사용자가 정의한 모든 객체가 될 수 있습니다. 우리는 G Adventures에서 Django를 많이 사용하며 함수에 대해 기대하는 기본 모델로 변수에 주석을 추가하는 것이 유용하다는 것을 알았습니다

함수 어노테이션 속성에 대한 자세한 사항은 [여기]에 정리해 두었습니다.

다음은 Django 모델을 유형으로 사용하는 확장된 예입니다.

from decimal import Decimal
from django.db import models

class Cheese(models.Model):
    # model snipped for brevity
    name = models.CharField(...)

def cheeseshop(
    kind: Cheese,
    weight: int,
    region=None: str,
) -> Decimal:

이제 우리는 kind 인자가 Cheese의 인스턴스임을 알 수 있습니다! 우리는 원래 str을 생각했지만, 이후 복잡한 객체를 취하도록 함수를 업데이트했습니다. 우리는 리턴되는 Decimal 타입의 값을 계산하기위해 이 함수가 특정 세부정보를 가져올 것으로 예상합니다. 하지만 잠깐, 이 함수가 반환하는 소수(decimal)는 어떤 값을 가지고 있을까요? 흠, 이것을 위해 고유한 타입을 별도로 구현할 수도 있습니다!

from decimal import Decimal
from typing import NewType

Price = NewType('Price', Decimal)

def cheeseshop(
    kind: Cheese,
    weight: int,
    region: str=None,
) -> Price:

이 함수의 초기 구현을 되돌아보세요. 가독성에 대해 논쟁 할 수 있지만(annotation을 사용하는 것은 상당한 논쟁거리입니다), 여기서 우리가 가지고 있는 것은 컨텍스트입니다. 개발자는 작성하신 시간 보다 코드를 읽는 데 더 많은 시간을 소비합니다(반드시 소비해야 합니다). 여기에서 제공하는 정보는 객관적으로 더 자세히 설명되어 있습니다.

반응형

각설하고, 어노테이션에 대해한 것을 계속 살펴 보기전에, 일단, 함수 정의를 변경한 것을 먼저 살펴 보도록 하겠습니다.

def cheeseshop(
    kind: Cheese,
    weight: int,
    *,
    region: str=None,
) -> Price:

차이점이 보이시나요? 함수 중간에 별표 *를 추가했습니다. 이것이 의미하는 것은 args(positional argument)와 kwargs(keyword argument)의 분리입니다. 이것이 의미하는 바는 별표 * 뒤에 오는 인자들은 키워드 인자로 제공되어야 한다는 것입니다. 예를 들어 :

cheeseshop(le_bocke, 200, 'Quebec')

위 코드는 아래와 같은 예외를 발생 시킵니다.

File "cheese.py", line 24, in <module>
    cheeseshop(le_bocke, 200, 'Quebec')
TypeError: cheeseshop() takes 2 positional arguments but 3 were given

이것은 간단하지만 강력한 제약사항입니다. 키워드 인자는 함수의 정의를 다시 살펴 볼 필요 없이 함수 호출 중에 인자가 가지는 의미(context)를 제공하기 때문에 많은 상황에서 선호되는 형식입니다. 이 적용은 일관된 위치 기반 인자(positional argument) 및 키워드 기반 인자(keyword argument)를 보장하기 위해 현재 코드에 적용할 수 있는 가장 간단한 변경 사항 중 하나입니다.

Ok! 재밌긴 했지만, 그럼 계속해서, 기본 제공 타이핑 오브젝트를 어떻게 사용하는지, 어떻게 작동하는지를 살펴보도록 하겠습니다. 예시로 돌아가면:

from decimal import Decimal
from typing import NewType

Price = NewType('Price', Decimal)

def cheeseshop(
    kind: Cheese,
    weight: int,
    region: str=None,
) -> Price:

이 작업을 적용하는 단계로 넘어가 보겠습니다. mypy[각주:2]라는 도구를 사용하여 실제로 코드에서 정적 분석을 실행하고 잘못된 유형을 사용하고 있는지 알려줄 수 있습니다. 이것은 타입 힌트와 어노테이션이 실제로 빛을 발할 수 있는 곳입니다. 문제를 조기에 방지함으로써 동적 타입의 파이썬을 사용하는 동안 정적으로 타입이 지정된 언어의 이점을 얻을 수 있습니다.

mypy에게 예제 코드를 보여줍시다. 예제를 더 발전시키기 위해 내가 한 것은 우리의 Cheese 클래스를 non-Django 클래스로 만들고 단일 chees객체를 만들어 우리의 cheeseshop 함수에 인자로 넘긴것입니다. 전체가 어떻게 보이는지 봅시다.

from decimal import Decimal
from typing import NewType

class Cheese:
    def __init__(self, name, price):
        self.name = name
        self.price = price

Price = NewType('Price', Decimal)

def cheeseshop(
        kind: Cheese,
        weight: int,
        region: str=None,
) -> Price:
    print("From %s!", region)
    return weight * kind.price

baluchon = Cheese('Baluchon', 0.99)

print(cheeseshop(baluchon, 2.5)) # results in 2.475

이 코드와 결과를 보면. 코드가 꽤 깔끔해 보입니다. 실제로 이 코드는 잘 동작합니다. 그러나 그것에 대해 mypy를 실행하면 다음과 같은 결과를 얻습니다.

cheese.py:23: error: Argument 2 to "cheeseshop" has incompatible type "float"; expected "int"

아! cheeseshop에 대한 함수 호출에서 int를 기대했지만 float(2.5)를 전달했습니다.

이제 이 예에서, 매우 간단하게 만들어진 예에서는 그렇게 위험해 보이지 않지만 수수료, 요금 또는 현재 재정 상태와 관련된 모든 것을 계산하는 경우에는 어떻게 될까요? 예, 좋은 관찰력은 항상 옳습니다. 실행 전의 완벽한 체크는 몇 시간 동안 골치를 썩거나 바로 다음 목표를 향해 이동 할 수 있는 주요한 차이일 수 있습니다.

mypy에 대한 깊은 탐구는 오늘 여기에서 논의할 수 있는 것 이상이므로 mypy에 대한 내용은 매우 최소화되었습니다. 가장 좋은 첫 번째 방법은 mypy를 선택한 편집기에 연결하는 것입니다. Vim, Visual Studio Code, PyCharm 등은 모두 가능합니다.

mypy의 이점은 더 큰 응용 프로그램을 처리할 때 확실히, 더 명확하게 표시됩니다. 대부분의 어느정도 규모가 있는 프로젝트는 모듈, 외부 라이브러리, 여러 추상화 계층 및 사소한 실행 경로가 혼합되어 있습니다. 예, 우리 모두는 시스템의 나머지 부분과 격리된 깨끗한 코드를 작성하기 위해 노력하지만 불완전한 시스템은 항상 존재하며 우리가 달성하고자 하는 유토피아적 비전조차도 도전받을 수 있다는 것이 현실입니다.

파이썬은 작업을 수행하는 언어로서 믿을 수 없을 정도로 훌륭하게 성장했습니다. 다음 진화 단계는 우리 모두가 알고 사랑하는 언어의 속도와 유연성을 제공하면서 정적 분석의 이점을 수용하는 것입니다. 이러한 옵션을 제공함으로써 언어 제작자는 파이썬이 이전에 경험하지 못한 새로운 도구, 스타일 가이드 및 시스템 디자인의 새로운 영역에 대한 문을 열었습니다.

파이썬 3 기능에 대한 높은 수준의 학습들이 흥미진진하고 교육적이라는 것을 알게 되었기를 바랍니다. 추가로 타입 힌트함수 어노테이션에 대한 PEP를 읽어 볼것을 권장합니다. 이러한 주제에 대한 가장 강력한 예제(그리고 정기적으로 참조하는 것)이기 때문입니다.

부록 1. 같이 읽으면 좋은 글

  1. 아마도 원 글쓴이의 회사인듯 함 [본문으로]
  2. pip 로 설치 가능한 모듈. 원글에는 링크가 달려 있지 않아 설치와 사용을 정리한 사이트를 링크함 [본문으로]
유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!