[EP. 1] 파이썬답게(Pythonic)

Sea Panda·2024년 1월 2일
1

effective python

목록 보기
1/1
post-thumbnail

💡 파이썬 코딩의 기술

Velog글도 오랜만에 작성하는 것 같습니다. 그동안 부트캠프가 끝나고 나름대로 짧게 인턴도 해보고 여러가지 일이 있었는데 그건 다른 글에서 정리해보겠습니다. 이번 시리즈에서는 다시 기초로 돌아가 [파이썬 코딩의 기술(Effective Python) 개정 2판]을 천천히 시간날 때마다 정리해보려 합니다.

이 책이 처음부분을 대충보니 파이썬을 처음 접하시는 분들이라면 이 책말고 다른 책으로 시작하시길 바랍니다.

✏️ Pythonic

여러 해 동안 파이썬 커뮤니티 사람들은 특정 스타일을 따르는 코드를 묘사하기 위해 "파이썬답다(Pythonic)"라는 형용사를 사용해왔다. 여기서 "Pythonic"함은 컴파일러가 엄격하게 통제하거나 사용하길 강제하는 스타일이 아니다.

파이썬 프로그래머는 명시적인 것을 좋아하고, 복잡한 것보다 단순한 것을 좋아하며, 가독성을 최대한 높이려고 노력한다. 다음은 Python의 기본 철학을 담고있는 "파이썬의 선(The Zen of python)"이라는 것이다.

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! 네임스페이스는 정말 대단한 아이디어다. -- 자주 사용하자!

✏️ Python의 Version

만일 컴퓨터에 다양한 버전의 표준 CPython이 설치되어 있다면, 어떤 버전의 파이썬을 실행할지 잘 판단하여야 한다. 기본적으로 명령줄에서 파이썬을 실행할 때 디폴트로 어떤 버전이 실행될지 명확하지 않기 때문이다.

현재 사용중인 파이썬의 정확한 버전을 알고 싶다면 --version플래그를 활용하면 된다.

python --version

다른 방법으로는 내장 모듈인 sys의 값을 검사하면 현재 실행 중인 파이썬의 버전을 알아낼 수 있다.

import sys
print(sys.version_info)
print(sys.version)

파이썬 2는 2020년 1월 1일부로 수명이 다했다.여기서 말하는 수명이 다했다는 말은 더 이상 버그 수정, 보안 패치, 새로운 기능의 역포팅(backporting)이 이뤄지지 않는다는 뜻이다. 만일 파이썬 2로 작성된 코드를 사용해야 한다면 2to3(파이썬 설치 시 함께 설치됨)나 six같은 도구의 도움을 받아 파이썬 3로 코드를 포팅하는 방안을 고려하는 것이 좋다.

💡 포팅과 역포팅
포팅(Porting): 소프트웨어를 원래 설계된 바와 다른 컴퓨터 환경(CPU, 운영체제, 서드파티 라이브러리)에서 동작할 수 있게 변환하는 것을 의미한다.

역포팅(Backporting): 소프트웨어 시스템 또는 소프트웨어 구성 요소의 최신 버전에서 일부를 가져와 동일한 소프트웨어의 이전 버전으로 이식(Porting)하는 것을 의미한다. 버그패치나 새로운 버전의 기능을 이전 버전에서 사용할 수 있도록 하는 것이다.


✏️ PEP 8 스타일 가이드

PEP는 Python Enhancement Proposal의 약자로 파이썬 개선 제안이라고 한다. 여기서 PEP 8은 파이썬 고드를 어떤 형식으로 작성할지 알려주는 스타일 가이드이다. 하지만 이 가이드는 말 그대로 가이드일 뿐이기 때문에 문법만 맞다면 어떤 방식으로든 원하는 코드를 작성해도 코드는 돌아간다.

하지만 일관된 스타일을 사용하면 코드에 더 친숙하게 접근하고, 코드를 더 쉽게 읽을 수 있다. 그리고 다른 파이썬 프로그래머들과 협업 시에도 더 쉽게 협력할 수 있다. 그리고 가이드를 따르면 나중에 코드를 수정하기 쉬울 뿐 아니라 흔히 저지르기 쉬운 실수도 피할 수 있다. 이제 각각의 가이드에 대해 한 번 살펴보자.

공백

파이썬에서 공백(Whitespace)은 중요한 의미가 있다. 그렇기 때문에 공백과 관련한 다음 가이드라인을 따르는 것이 좋다.

  • Tap(탭) 대신 스페이스(Space)를 사용하여 들여쓰기 하라.
  • 문법적으로 중요한 들여쓰기에는 4칸 스페이르르 사용하라.
  • 라인 길이는 79개 문자 이하여야 한다.(한글 한 글자는 시각적으로 영문 두 글자에 해당한다고 계산)
  • 긴 식을 다음 줄에 이어서 쓸 경우에는 일반적인 들여쓰기보다 4 스페이스를 더 들여써야 한다.
# correct
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)
---------------------------------------
# wrong
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)
  • 파일 안에서 각 함수와 클래스 사이에는 빈 줄을 두 줄 넣어라.
  • 클래스 안에서 메서드와 메서드 사이에는 빈 줄을 한 줄 넣어라.
  • 딕셔너리(Dictionary)에서 키와 콜론(;) 사이에는 공백을 넣지 않고, 한 줄 안에 키와 값을 같이 넣는 경우에는 콜론 다음에 스페이스를 하나 넣는다.
  • 변수 대입에서 전후에는 스페이스를 하나씩만 넣는다.
  • 타입 표기를 덧붙이는 경우에는 변수 이름과 콜론 사이에 공백을 넣지 않도록 주의하고, 콜론과 타입 정보 사이에는 스페이스를 하나 넣어라.
# Correct:
def munge(sep: AnyStr = None): ...
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...
-----------------------------------------
# Wrong:
def munge(input: AnyStr=None): ...
def munge(input: AnyStr, limit = 1000): ...

명명규약

명명규약을 제대로 사용하면 코드를 읽을 때 각 이름이 어떤 유형에 속하는지 쉽게 구분할 수 있다. 이름에 관련한 가이드라인은 다음과 같다.

  • 함수, 변수, 애트리뷰트(Attribute)는 lowercase_underscore처럼 소문자와 밑줄을 사용한다.-> Snake Case(뱀표기법)
  • 비공개(Private)(한 클래스 안에서만 쓰이고 다른 곳에서는 쓰면 안 되는 경우) 인스턴스 애트리뷰트는 일반적인 애트리뷰트 이름 규칙을 따르되, __ leading_underscore처럼 밑줄 두 개로 시작한다.

    위의 방법을 "맹글링(mangling)"이라고 한다. 파이썬에서는 실제로 비공개로 만들수는 없다. 다만 애트리뷰트 앞에 더블언더스코어를 붙히면 "_classname__variablename"과 같은 이름으로 변환되어 애트리뷰트 이름 자체로 접근할 수 없게되는 것이다.

  • 클래스(예외도 포함한다)는 CapitalizedWord처럼 여러 단어를 이어 붙이되, 각 단어의 첫 글자를 대문자로 만든다.
  • 모듈 수준의 상수ALL_CAPS처럼 모든 글자를 대문자로 하고 단어와 단어 사이를 밑줄로 연결한 형태를 사용한다.
  • 클래스에 들어 있는 인스턴스 메서드는 호출 대상 객체를 가리키는 첫 번째 인자의 이름으로 반드시 self를 사용해야 한다.

    사실 반드시 self일 필요는 없다. 다만 클래스에 들어있는 인스턴스 메서드는 항상 자기 자신의 객체를 첫 번째 인자로 입력받기 때문에 관습적으로 self를 사용하는 것이다.

  • 클래스 메서드는 클래스를 가리키는 첫 번째 인자의 이름으로 반드시 cls를 사용해야 한다.

    위와 마찬가지다. 클래스 메서드는 클래스 객체를 첫 번째 인자로 입력받기 때문에 관습적으로 cls를 사용한다.

식과 문

"파이썬의 선"에서는 "문제를 해결할 명백한 방법이 하나 있으며, 가급적이면 유일해야 한다."라고 언급한다. PEP 8은 이런 가르침을 따라 식과 문장을 작성하는 스타일 규칙을 다음과 같이 정했다.

  • 긍정적인 식을 부정하지 말고(if not a is b) 부정을 내부에 넣어라(if a is not b).
  • 빈 컨테이너(Container)나 시퀀스(Sequence)([]나 '' 등)를 검사할 때는 길이를 0과 비교(if len(something) == 0)하지 말라. 빈 컨테이너나 시퀀스 값이 암묵적으로 False로 취급된다는 사실을 활용해 if not 컨테이너라는 조건문을 써라.
  • 마찬가지로 비어 있지 않은 컨테이너나 시퀀스([1]이나 'hi' 등)를 검사할 때도 길이가 0보다 큰지 비교하지 말라. 대신 if 컨테이너가 비어 있지 않은 경우 암묵적으로 True로 평가된다는 사실을 활용하라.
  • 한 줄짜리 if문이나 한 줄짜리 for, while루프, 한 줄짜리 except 복합문을 사용하지 말라. 명확성을 위해 각 부분을 여러 줄에 나눠 배치하라.
  • 식을 한 줄 안에 다 쓸 수 없는 경우, 식을 괄호로 둘러싸고 줄바꿈과 들여쓰기를 추가해서 읽기 쉽게 만들라.
  • 여러 줄에 걸쳐 식을 쓸 때는 줄이 계속된다는 표시를 하는 \문자보다는 괄호를 사용하라.

임포트

  • import 문(from x import y)을 항상 파일 맨 앞에 위치시켜라.
  • 모듈을 임포트할 때는 절대적인 이름(absolute name)을 사용하고, 현 모듈의 경로에 상대적인 이름은 사용하지 말라. 예를 들어 bar 패키지로부터 foo모듈을 임포트한다면 from bar import foo라고 해야 하며, 단지 import foo라고 하면 안 된다.
  • 반드시 상대적인 경로로 임포트해야 하는 경우에는 from . import foo처럼 명시적인 구문을 사용해라.
  • 임포트를 적을 때는 표준 라이브러리 모듈, 서드 파티 모듈, 직접 만든 모듈 순서로 섹션을 나눠라. 각 섹션에서는 알파벳 순서로 모듈을 임포트 하라.

✏️ bytes와 str의 차이를 알아두라

파이썬에는 문자열 데이터의 시퀀스를 표현하는 두 가지 타입이 있다. 바로 bytesstr이다. 아래 코드같이 bytes타입의 인스턴스에는 부호 없는 8바이트 데이터가 그대로 들어간다.

a = b'h\x65llo'
print(list(a))
print(a)

>>>
[104, 101, 108, 108, 111]
b'hello'

str인스턴스에는 사람이 사용하는 언어의 문자를 표현하는 유니코드 코드 포인트(code point)가 들어 있다.

❗️코드 포인트: 문자열을 대표하는 정수값으로 문자열에 할당된 코드값 그 자체를 의미한다.

a = 'a\u0300 propos'
print(list(a))
print(a)

>>>
['a', '`', ' ', 'p', 'r', 'o', 'p', 'o', 's']
à propos

여기서 중요한 사실은 str 인스턴스에는 직접 대응하는 이진 인코딩이 없고 bytes에는 직접 대응하는 텍스트 인코딩이 없다는 점이다. 유니코드 데이터를 이진 데이터로 변환하기 위해서는 strencode메서드를 호출해야 하고, 이진 데이터를 유니코드 데이터로 변환하려면 bytesdecode메서드를 호출해야 한다. 두 메서드를 호출할 시 인코딩 방식을 명시적으로 지정할 수도 있고, 시스템 디폴트 인코딩을 받아들일 수도 있다. 일반적으로는 UTF-8이 시스템 디폴트 인코딩 방식이다.

일반적으로 프로그램을 작성할 때 인코딩하거나 디코딩하는 부분을 인터페이스(프로그램의 주요로직, 핵심부분)의 가정 먼 경계 지점에 위치시키길 권장한다. 이런 방식을 유니코드 샌드위치라고 부른다. 조금 쉽게 설명하자면 코드에서 이진 데이터를 유니코드 데이터로 변환하는 작업을 가능한 최대한 빨리 수행하고, 가능한 마지막에 유니코드 데이터를 이진 데이터로 변환하라는 것이다. python에서는 이런 작업을 open모듈이 수월하게 해준다.

여기서 이진 8비트 값과 유니코드 문자열을 파이썬에서 다룰 때 꼭 기억해야 할 두가지 문제점이 있다.

1. 똑같이 작동하는 것처럼 보일뿐 호환되지 않는다.

bytesstr이 똑같이 작동하는 것 같지만 각각의 인스턴스는 서로 호환되지 않는다. 그렇기 때문에 전달 중인 문자 시퀀스가 어떤 타입인지를 항상 잘 알고 있어야 한다.

print(b'one' + b'two')
print('one' + 'two')

>>>
b'onetwo'
onetwo
b'one' + 'two'

>>>
Traceback ...
TypeError: can't concat str to bytes

+연산자를 사용하면 bytesbytes에 더하거나 strstr에 더할 수 있지만 서로 다른 인스턴스끼리는 더할 수 없다. 비교연산자(< 등)도 마찬가지다.

그리고 같은 문자열이라고 한다할지라도 bytesstr인스턴스가 같은지 비교하면 항상 False가 나온다.

문자열 포메팅에 사용되는 %연산자는 각 타입의 형식화 문자열에 대해 작동한다. 하지만 파이썬이 어떤 이진 텍스트 인코딩을 사용할지 알 수 없으므로 str인스턴스를 bytes형식화 문자열에 넘길 수는 없다.

print(b'red %s' % b'blue')
print('red %s' % 'blue')

>>>
b'red blue'
red blue
print(b'red %s' % 'blue')

>>>
Traceback ...
TypeError: %b requires a bytes-like object, or an object that
-> implements __bytes__, not 'str'

str 형식화 문자열에 bytes인스턴스를 넘길 수는 있지만, 이 경우 예상과 다르게 작동한다.

print('red %s' % b'blue')

>>>
red b'blue'

위 코드는 실제로 bytes인스턴스의 __repr__메서드를 호출한 결과로 %s를 대신한다. 따라서 b'blue'가 출력에도 그대로 남는다.

__str__, __repr__ 두 메서드 모두 객체를 문자열로 반환하는 공통점이 있다. 하지만 상세하게 들어가면 차이가 존재한다.
__repr__: 문자열로 객체를 다시 생성하여 출력 -> 개발자를 위함
__str__:서로 다른 데이터 타입을 가진 데이터끼리 상호작용하기 위해 문자열로 바꿔줌 -> 사용자를 위함

2. 파일 핸들과 관련된 연산들이 디폴트로 유니코드 문자열을 요구한다.

내장 함수인 open을 호출해 얻은 파일 핸들과 관련한 연산들이 디폴트로 유니코드 문자열을 요구하고 이진 바이트 문자열을 요구하지 않는다는 것이다. 예를 들어 이진 데이터를 파일에 기록하려 할 때, 다음과 같이 간단해 보이는 코드도 오류가 발생한다.

with open('data.bin', 'w') as f:
    f.write(b'\xf1\xf2\xf3\xf4\xf5')

>>>
Traceback ...
TypeError: write() argument must be str, not bytes

이는 파일을 열 때 이진 쓰기 모드(wb)가 아닌 텍스트 쓰기 모드(w)로 열었기 때문이다. 따라서 이진 데이터를 쓰기 위해서는 wb모드로 열어야 한다.

이는 쓰기뿐만 아니라 파일을 읽으려고 할 때 역시 마찬가지이다. 핸들이 텍스트 모드에 있으면 시스템의 디폴트 텍스트 인코딩을 bytes.encode(쓰기의 경우)와 str.decode(읽기의 경우)에 적용해서 이진 데이터를 해석한다. 대부분의 시스템에서 디폴트 인코딩은 UTF-8인데, UTF-8인코딩은 이진 데이터를 받아들일 수 없기 때문에 오류가 발생하는 것이다.

만일 디폴트 인코딩이 의심스러운 경우에는 명시적으로 openencoding파라미터를 전달해야 한다.


0개의 댓글