PEP & TypeHint

이성준·2023년 6월 27일
0

PEP란?
Python Enhancement Proposals 로 파이썬 개발 제안서이다.
PEP는 새로운 기능을 제안하고 커뮤니티 의견을 수렴하여 파이썬의 디자인 결정을 문서화하는 파이썬의 주요 개발 프로세스를 일컫는다. 뒤에 번호를 부여하여 PEP 0 이런식으로 부여하는데, 그 중 특히 PEP 8은 파이썬의 코딩 스타일 가이드이다.


저자는 특히 코딩을 하는 방식에 관해서 얘기를 많이 했다. 나도 이르긴 하지만 현재 일을 하고 있는 친구에게 듣기로, 가독성이 협업에서 굉장히 중요하다고 했다. 이러한 코딩하는 스타일에 관한 내용이 바로 PEP 8에 담겨져 있고, 이 중 저자가 몇가지 강조한 점을 작성한 이후 추가로 알게된 내용이 있다면 그때그때 수정하겠다.

  • Python은 Camel 표기법이 아닌 Snake 표기법을 지향해야한다.
  • 영어로 주석을 달아서 제출하는 편이 좀더 프로페셔널 하다는 인상을 줄 수 있다.
  • 인덴트는 공백을 4칸으로 한다.
  • 함수의 기본값으로 가변객체를 사용하지 않아야한다.(구글 파이썬 스타일 가이드)
    대신 불변객체를 사용하여야 한다.
  • 최대 줄길이는 80자로 한다(구글 파이썬 스타일 가이드)


    파이썬의 철학 : 문제를 풀어낼 바람직하고도 유일하며 명확한 방법이 존재할 것이다.

이제 책을 읽으면서 놓칠법한 파이썬 문법들을 정리해보겠다. 평소에 딥러닝 모델을 돌리거나 파이토치를 뜯어보면 나오는 것들인데, 이번기회에 명확하게 집고 넘어가고자 한다.

타입 힌트

먼저 이 파트는 책과 블로그 왼쪽 블로그에서 깊게 참조했음을 알린다.

먼저 책에서는 타입힌트에 관한 얘기를 하는데, 우선 파이썬은 동적 타이핑 언어, 즉 내가 따로 자료형을 지정해주지 않아도 되고, 중간에 변수의 타입이 바꿔도 괜찮다. 이러한 특성때문에 다른 언어보다 배우기 쉽다는 장점이 있다.

만약 우리가 아래와 같이 작성을 했다고 해보자.

def fn(a):
	...

우리는 이 함수가 문자를 넘겨야 하는지 숫자를 넘겨야 하는지 전혀 알 수 없고, 함수의 리턴값 또한 알 수 없다. 이러한 함수가 하나 두개면 어떻게 파악할 수 있다고 쳐도 여러개의 함수가 얽히고 섥히게 된다면 가독성을 떨어트리고 버그의 원인이 될 수 있다.
위에 함수를 수정한다면 다음과 같이 수정할 수 있을 것이다.

def fn(a:int) -> bool:
	...

위와 같이 수정할 경우 함수에서 받는 파라미터 a가 정수형이고 리턴값이 bool형임을 확실하게 알 수 있다. 이렇게 명시적으로 선언하면 가독성이 좋아지고 버그 발생 확률을 줄일 수 있다.

물론 type hint는 강제성이 없다. 따라서 여전히 동적으로 할당 될 수 있고 이는 주의가 필요하다.

a: str = 1
print(type(a))

위와 같이 동적 타이핑 언어이기에 오류가 발생하지 않는데, 타입 힌트가 잘못 지정됐는지 확인하기 위해서는 mypy를 사용하여 알아차릴 수 있다. 아래는 위에 작성한 코드를 mypy를 이용해 타입 힌트를 검사한 결과이다.

"코딩 테스트는 일반적으로 짧은 알고리즘으로 끝나는 경우가 많기 때문에 굳이 타입을 지정하지 않아도 문제는 없지만 코드를 정리할 때 만이라도 타입을 지정하여 보기좋게 제출한다면, 코드 리뷰시 면접관에게 좋은 점수를 받을 것이다."
-[파이썬 알고리즘 인터뷰 79page]-

파이썬의 built-in Type인 int, str만으로는 우리가 원하는 모든 타입을 명확하게 표현할 수 없다. 이때 파이썬은 typing 모듈을 제공하는데 typing 모듈에서 제공하는 몇가지 타입들이 있다.

1. typing.Union
하나의 함수의 인자에 여러 타입이 사용될 수 있을때는 typing.Union을 사용하면 된다.

from typing import Union


def print_msg(msg: Union[str, int]) -> int:
    print(msg)
    return 1


print_msg('안녕 이건 오류가 아니야')
print_msg(1)

2. typing.Optional
Union[<타입>,None]은 Optional로 대체할 수 있다.
이때, 타입을 두개 즉, Optional[str,int]로 사용하면 오류가 발생한다.

from typing import Optional


def print_msg(msg: Optional[str]) -> int:
    print(msg)
    return 1


print_msg('안녕 이건 오류가 아니야')
print_msg(None)

여기서 None의 Type은 NoneType이다.

3. typing.List & typing.Tuple & typing.Dict

리스트와 튜블 딕셔너리안에 타입(dtype)을 나타내는 방법은 아래와 같다.

from typing import List, Tuple, Dict


names: List[str]
location: Tuple[int, int, int]
id_pw: Dict[str, int]

4. typing.TypedDict

딕셔너리의 경우 밸류의 타입이 하나로 고정돼 있는 것만은 아니기 때문에, TypedDict를 활용해서 더욱 다양한 데이터 타입의 딕셔너리를 사용할 수 있다.
이때 유의할 점은 객체를 선언할때 TypedDict 클래스를 상속받아야 한다.
또한 객체를 생성할때 초기 value값을 입력해 주어야 한다.(이때, TypedDict클래스는 "Right-hand side values are not supported in TypedDict" 이기 때문에 Decoration을 사용한 것처럼 바로 초기화를 할 수 없다.)

from typing import TypedDict


class StudentInfo(TypedDict):
    id: int
    password: int
    address: str


def print_info(students: StudentInfo):
    student_id = students['id']
    print(student_id)


student = StudentInfo(id=201900278, password=1, address="경기도 광명") #객체의 파라미터로 초기화
print_info(student)

또다른 방법으로 @dataclass 데코레이션(Decoration)을 사용할 수 있다.
이 데코레이션 문법을 타입 힌트와 함께 활용함으로써 class를 이용해서 구조체 형태로 선언할 수 있다.
(책 62page)


from dataclasses import dataclass


@dataclass
# StudentInfo = dataclass(StudentInfo)
# dataclass 가 StudentInfo를 상속받음
class StudentInfo:
	# 바로바로 초기화
    id: int = None
    password: int = None
    address: str = None


def print_info(students: StudentInfo):
    print(students.id)
    print(students.password)
    print(students.address)


student = StudentInfo()
student.id = 201900278
student.password = 1
student.address = "경기도 광명"

print_info(student)

나에게는 아래의 방식이 좀더 친숙하다.
다음에 Decoration관련해서 포스팅을 작성하겠다.
Decorator 관련 포스팅

5. typing.Callable
함수를 인자로 받는 경우에는 다음과 같이 Callable 타입을 활용하면 된다.
Callable[[input type, ... ],return type]

from typing import Callable


def first_function(func:Callable[[int, int], int]):
    summation = func(2,3)
    print("summation:", summation)


def second_function(a: int, b: int) -> int:
    return a+b


first_function(second_function)


만약, 여기서 함수의 인자의 타입을 신경쓰지 않고 리턴타입만 명시하고 싶다면 ...(Ellipsis)를 사용해서 다음과 같이 바꿀 수 있다.


from typing import Callable


def first_function(func:Callable[...,int]):
# 이때 여기서는 인자와 return type이 하나의 괄호안에 들어있다.
    summation = func('A','+')
    print("summation:", summation)
    return 1


def second_function(a: str, b: str) -> str:
    return a+b


first_function(second_function)

6. typing.TypeVar
Generic이란 여러 타입을 일반화 한것을 의미한다. 이는 자바에서도 중요한 개념으로써 사용된다.
이 Generic Type을 나타내기 위해서 typing.TypeVar을 사용하여 Generic 타입을 나타낼 수 있다.

from typing import Sequence, TypeVar, Iterable

T = TypeVar("T")  # T 대신 다른 문자/단어를 써도 되지만 일반적으로 T를 사용합니다.


def batch_iter(data: Sequence[T], size: int):
    for i in range(0, size):
        yield data[i:i + size]
# 이때, data의 크기를 벗어나도 끝까지 생성해준다

gen = batch_iter(["abc",3,"def"], 3)

print(next(gen))
print(next(gen))
print(next(gen))


list, tuple, range등과 같이 나열 가능한 자료형을 sequence type이라고 한다.

Generic Class의 사용이유
일단, 먼저 언급할 것이 파이썬의 경우 동적 타이핑 언어이기 때문에 Generic이 필요하지 않다. 하지만, Generic Class가 무엇인지 왜 필요한지를 이해할 필요가 있다. Generic은 위에서 말했듯이 일반화한 Type을 나타낸다. 그렇다면 Generic Class는 무엇일까? Generic Class란 Class안에 변수 Type을 Generic으로 설정해서 어떤 Type이 입력으로 들어오더라도 유연하게 대처하고자 만든 Class이다. 자세한 내용은 다음 영상을 참고하길 바란다.
Generic Clss란?

그렇다면, 어떤 Class가 Generic Class임을 파이썬에서는 어떤식으로 표현할까?

7. typing.Generic

다음 예시를 살펴보자

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('%s: %s', self.name, message)

파이썬 공식 문서
위의 __init__ 메소드의 value:T에서 우리는T의 타입이 정해짐을 알 수있다. 이렇게 정해진 type T가 set함수와 get 함수에도 영향을 미친다.
또한 위의 클래스가 Generic Class임은 인자로써 받은 Generic[T]가 암시한다.

profile
Time-Series

0개의 댓글