Python의 미래, Python 3로 넘어가기

Python의 미래, Python 3로 넘어가기

사용자 삽입 이미지

저는 Python을 시작한지 약 3년이 되어가는 (현재는 Python이 주 언어인)개발자입니다. 저도 그랬고 많은 사람들도 그럴 것이라고 생각하기 때문에 이 글을 작성하기로 결정했습니다.

 

이 글의 또 다른 제목은 "Python을 처음 배울 때, 2로 시작할까요? 3으로 시작할까요?" 입니다.

 

왜?

많은 사람들이 Python 2에서 Python 3로 넘어가는 것을 꺼려하고 있습니다.

저마다 이유가 있겠지만 정말로 특별한 이유가 있지 않는 한 저는 주변인이 Python을 새로운 언어로 접하겠다고 한다면 무조건 Python 3을 권장하고 있습니다.

어차피 똑같은 Python인데 더 익숙한 2.x, 그리고 한국에 2.x에 대한 자료가 더 많으니까 2.x를 쓰면 안 될까요?

 

왜? Python 3로 넘어가야 할까요? 왜? 피하시나요?

5년, 6년이라는 시간은 컴퓨터 프로그래밍 세계에서는 아주 긴 시간입니다.

당장 3년 전까지만 했어도 대부분의 웹 사이트는 AngularJS, React 등 여러 프론트엔드 프레임워크와 거리가 꽤 멀었었고, 이벤트 기반의 넌블럭킹 IO 처리가 지금처럼 이곳저곳 엄청나게 많이 쓰이지는 않았습니다.

네 그렇습니다. 저 5년이라는 시간은 Python 2.7이 처음 세상에 나타난 때입니다. 정확히 2010년 7월 3일이죠.

 

흔히들 Python이 연구 목적으로 가장 많이 쓰인다고 합니다. 사실 저는 연구와 거리가 멀기 때문에 자세히는 모르는데 아직도 연구 목적으로 Python 2.x를 사용하는 곳이 꽤 많다고 합니다. 대부분 왜 그러냐고 물어보면 흔히들 "호환성"을 이야기하네요.

자, 그런데 현재 글을 쓰고 있는 시점에서 연구 목적으로 사용되는 대부분의 Python 라이브러리들은 Python 3을 완전히 지원하고 있습니다. NumPy, SciPy, matplotlib, Pandas, IPython, SymPy, 그리고 그 외 연구 목적으로 만들어진 수많은 Python 라이브러리들이 말이죠.

 

다시 생각해봅시다. 단도직입적으로 물어보겠습니다. 왜 Python 2.x를 사용하나요? 정말 특별한 이유가 있다면 그 이유는 무엇인가요?

 

서서히 다가가기

간단하게 시작해봅시다.

 

식 계산

Python을 아예 모르는 사람이라고 가정하고 아래와 같은 식(코드)을 봤을 때 어떤 결과를 예상하시나요?

>>> 3 / 2

Python 3.4의 결과는 다음과 같습니다:

>>> 3 / 2
1.5

예상한대로 나왔습니다. 3에서 2를 나누면 1.5죠. (Node.JS, Perl, Haskell, Lua, PHP 등의 언어 또한 이와 같이 동작합니다.)

이제 다음 Python 2.7의 결과를 확인해봅시다:

>>> 3 / 2
1

3에서 2를 나눴는데 어떻게 1이 나올까요?

Python 2.x에서는 int / int 의 결과는 항상 int이기 때문입니다. 위 식을 Python 2.x에서 원하는대로 돌아가게 하기 위해서는 다음과 같이 입력해야 합니다:

>>> 3 / 2.0
1.5
>>> 3.0 / 2
1.5

한 쪽의 자료형을 반드시 float으로 지정해주어야 한다는 소리입니다.

또는 from __future__ import division 을 파일 최상단에 삽입하여 해결할 수 있죠.

 

print를 이용한 출력

Python 2를 사용하셨다면 다음과 같은 print 구문이 익숙할 것입니다:

>>> print "Hello, Python!"

Hello, Python!

>>> print "Hello,", "Python!"

Hello, Python!

>>> print "Hello,", ; print "Python!" # 쉼표(trailing comma)는 줄바꿈을 막아줍니다.

Hello, Python!

>>> print # 그냥 print만 입력하면 줄바꿈만 됩니다. 꼭 인자를 넘기지 않아도 되는 루비같네요.

 

>>> print >> sys.stderr, "Oops!"

Oops!

Python 3부터는 print는 더이상 statement가 아닌 하나의 function입니다. 

따라서 다음과 같이 입력해야 합니다:

>>> print("Hello, Python!")

Hello, Python!

>>> print("Hello,", "Python!")

Hello, Python!

>>> print("Hello,", end=" "); print("Python!") # 키워드 인자 "end"로 넘겨줍니다.

Hello, Python!

>>> print() # 훨씬 일관성 있네요. 함수를 호출합니다.

 

>>> print("Oops!", file=sys.stderr) # 이상한 문법은 더 이상 없습니다.

Oops!

Python 3로 넘어오면서 separator(구분자)를 마음대로 바꿀 수도 있어졌습니다:

>>> print("Hello", " Python!", sep=",")
Hello, Python!

 

dict 다루기

(참고: 모든 예제에서는 이해를 돕기 위해 dict 순서가 넣은 순서대로 돌아가는 것처럼 작성했습니다. 실제로는 그렇지 않으나, 혹시나 원하시는 분들은 collections.OrderedDict를 확인하시기 바랍니다.)

Python 2.x에서는 다음과 같이 돌아갑니다:

>>> d = {'a': 1, 'b': 2, 'c': 3, 'd': 4, }

>>> keys = d.keys()

['a', 'b', 'c', 'd']

>>> del d['a']

>>> keys

['a', 'b', 'c', 'd']

>>> d.iteritems()

<dictionary-itemiterator object at 0x105e21ba8>

>>> d.items()

[('b', 2), ('c', 3), ('d', 4)]

하지만 Python 3.x에서는 다음과 같이 돌아갑니다:

>>> d = {'a': 1, 'b': 2, 'c': 3, 'd': 4, }

>>> keys = d.keys()

dict_keys(['a', 'b', 'c', 'd']) # 여기서 keys는 d 객체에 대한 view입니다.

>>> del d['a']

>>> keys

['b', 'c', 'd'] # 따라서 'a'가 삭제될 경우 view로 만들어진 keys 또한 그 결과가 반영됩니다.

>>> d.items()
dict_items([('b', 2), ('d', 4), ('c', 3)]) # .items() 는 Python 2.x에서의 .viewitems() 와 동일합니다.

>>> iter(d.items()) # 따라서 iterator-generator를 얻고 싶다면 이렇게 사용해야 합니다.

<dict_itemiterator object at 0x10b3d5e08>

 

Extended iterable unpack

Python 3에서는 다음과 같은 일이 가능합니다:

>>> first, *rest = range(5)

>>> first

0

>>> rest

[1, 2, 3, 4]

>>> a, *b, c = range(5)

>>> a

0

>>> b

[1, 2, 3]

>>> c

4

 

concurrent.Futures

threadpool을 만들 때나 processpool을 만들 때, 기존에는 귀찮게 사용해야 했던 반면에 Python 3에서는 concurrent.Futures를 이용해 아주 손쉽게 접근할 수 있습니다:

def big_calculation(num):

    return num ** 1000000

arguments = list(range(20))

 

# 이 작업은 무려 10초 이상 걸립니다.

list(map(big_calculation, arguments))

 

# 하지만 ProcessPool을 만들어 작업을 실행할 경우 모든 작업이 동시에 실행되기 때문에 훨씬 적은 시간이 소요됩니다.

from concurrent import futures

with futures.ProcessPoolExecutor() as executor:

    list(executor.map(big_calculation, arguments))

 

VirtualEnv의 내장

그동안 virutalenv를 사용하기 위해 pip를 통해 virtualenv 패키지를 설치하고 사용해야 했었습니다. 하지만 Python 3부터는 virtualenv가 내장 기능으로 들어가 pyvenv 명령어만 치면 사용할 수 있게 되었습니다.

 

더 자세한 예외 표시

아래와 같은 코드가 있습니다:

def oops():
    1 / 0

def ooops():
    try:
        oops()
    except ZeroDivisionError:
        raise RuntimeError("Oops!")

ooops()

 

Python 2.7에서는 다음과 같은 실행 결과가 나타납니다:

Traceback (most recent call last):
  File "te.py", line 15, in <module>
      main()
  File "te.py", line 12, in main
      ooops()
  File "te.py", line 9, in ooops
      raise RuntimeError("Oops!")
RuntimeError: Oops!

 

하지만 Python 3.x에서는 다음과 같은 실행 결과가 나타납니다:

Traceback (most recent call last):
  File "te.py", line 7, in ooops
      oops()
  File "te.py", line 3, in oops
      1 / 0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "te.py", line 15, in <module>
      main()
  File "te.py", line 12, in main
      ooops()
  File "te.py", line 9, in ooops
      raise RuntimeError("Oops!")
RuntimeError: Oops!

이처럼 Python 3.x에서는 특정 예외를 받아서 다시 한 번 예외를 발생시킬 때 안쪽에서 무슨 일이 일어났고 어떤 예외가 발생했는지까지 아주 자세히 나타냅니다. 반면 Python 2.7의 경우 실제 안쪽에서 어떤 부분에서 예외가 발생했는지 알 수 없기 때문에 직접 접근해서 오류를 해결하거나 바깥쪽 try - except 구문을 임시로 제거하여 확인하는 방법 밖에 없습니다.

 

비동기 처리를 위한 Project "Tulip"

이 글을 참고하시기 바랍니다.

Python 2.7에서는 gevent라는 라이브러리를 통해 사용할 수 있지만 Windows에서는 제대로 동작하지 않으며 성능이 몇 배까지 차이가 납니다.

성능 테스트는 여기서 확인해주세요.

무엇보다 빌트인 구현이라는게 가장 중요하겠죠. Python 3.5에서는 async/await 문법도 추가되어(C#의 그 문법) 다른 언어를 이미 사용했던 개발자라면 더욱 쉽게 접근할 수도 있게 되었습니다. 이를 통해 Python을 사용하던 개발자들이 C#, Node.JS와 같은 다른 언어로 접근하는 데에도 꽤 많은 도움이 되며, 반대로 Node.JS에서 콜백을 통한 비동기 처리, C# 에서 async/await를 통한 비동기 처리를 해봤던 개발자들 또한 Python으로 더욱 쉽게 접근할 수 있게 되었습니다.

 

super()

Python 2:

class A(object):
    def __init__(self):

        ...

class B(A):

    def __init__(self):

        super(B, self).__init__()

 

Python 3:

class A(object):

    def __init__(self):

        ...

 

class B(A):

    def __init__(self):

        super().__init__()

 

super() 의 인자로 넘어가는 값을 생략할 수 있습니다. 대신 이렇게 짜게 되면 Python 3 전용 코드가 되어버리니 호환 코드를 작성하려 하는 경우에는 주의하세요.

다시.

사용자 삽입 이미지

위에 나와있는 모든 변동사항은 Python 2에서는 하나도 반영된 것이 없으며 모두 Python 3의 변동사항입니다. 즉, 저 위에서 언급한 5년이라는 시간동안 극단적으로 말해 Python 2는 아무 발전도 없었습니다.

다시 한 번 묻겠습니다. 왜 더 이상 새로운 것이 하나도 없는 Python 2를 고집하시나요? 어느 누구는 Python 2의 성능이 더 빨라서라고 답할 수도 있겠지만 Python 3은 계속해서 발전하는 중이고 심지어 Python 3.5 체인지로그는 수많은 성능 향상도 포함하고 있습니다. 이어 PyPy도 Python 3을 지원하기 시작헀습니다.

 

더 이상 뒤쳐지지 않기 위해, 그리고 새로운 것을 만나기 위해 - 이제 그만 Python 2는 놓아주세요.

위에서 설명한 것 이외에도 여러분이 상상하는 것 이상으로 많은 기능이 Python 3에 존재합니다. 이 이점은 어떤 이에게는 정말로 여러 시간부담을 안고서 Python 3으로 넘어올 가치가 있는 이점일 수도 있습니다.

Python 3에서 실제 변경된 문법은 거의 없으며(추가된 문법이 있습니다.), 라이브러리가 호환되지 않는다면 똑같은 역할을 하는 라이브러리를 새로 짜서 Python 3에서 그 라이브러리를 성공시키면 됩니다(오히려 더 좋은 기회네요!), 또는 기존 라이브러리가 너무 좋다 - 라고 생각된다면 해당 라이브러리에 Python 3 호환 코드를 작성하여 PR을 보내 Contributors에 이름을 올릴 수도 있겠네요. (하지만 라이선스 문제나 기타 시간비용 문제가 발생할 수 있는 여지가 있는 경우에는 어쩔 수 없겠죠. 언제나 예외가 있을 수 있으며 이러한 경우는 충분히 이해합니다.)

 

아직도 Python 3에 대한 거부감이 있다면 아래 영상을 한 번 봐보세요.

 

어디서부터 출발할까?

Python을 새로 시작하는 사람이라면 Python 3 가이드를 보고, 책을 본다면 Python 3 기준으로 작성되어있는 책을 보고 시작하면 됩니다.

Python 2를 고집하던 개발자라면 py2to3, Python-Future와 같은 것을 보시면 됩니다. (Python-Future는 Python 2/3 호환코드를 작성하는데 많은 도움이 됩니다. 즉, 코드 호환에 대한 문제가 거의 사라집니다.)

그 외에 Python 2 or Python 3 등과 같은 여러 좋은 글도 공식적으로 올라가 있습니다.

 

Python 2.8에 대해 귀도(Guido van Rossum)는 Python language summit에서 "Python 2.8은 없을 것이다."라고 직접적으로 언급했었으며 Python 2.7에 대한 향후 업데이트 방향은 "유지보수" 및 "버그 수정"을 기준으로 잡기로 하였습니다. 즉, "기능의 추가나 변경"은 더 이상 이루어지지 않는다는 사실입니다. 또한 이러한 것들 모두 2020년에 완전히 지원이 끊길 예정입니다. 심지어 보안 패치도 말이죠.

 

아직 Python 3에 더 많은 기능(GIL의 제거, JIT 컴파일러의 추가 등)이 들어가야 한다고는 생각합니다. 하지만 이러한 것들이 없다고 해서 Python 2가 Python 3과 큰 차이가 없는 언어라고 하기는 어려우며 무엇보다 Python 3에 언젠가 저러한 것들이 추가될 수 있다는 점입니다.

 

옛 것은 버리세요. 이제 새로운 것을 접하세요.

같이보기:

 - Python 3, AsyncIO와 놀아보기

 - Python 3.5 미리보기: 무엇이 바뀌었고 무엇이 추가되었나?

 

Python 2 is legacy, Python 3 is the present and future of the language.