TL;DR - 빈 컨테이너 타입을 초기화 할 때는 괄호를 이용한 방법이 더 파이써닉 합니다.
Intro
파이썬에서는 다음과 같은 방법으로 빈 컨테이너 타입을 초기화 할 수 있습니다.
1.
괄호를 이용한 초기화
# List
empty_list = []
# Dictionary
empty_dict = {}
Python
복사
2.
Built-in 생성자를 이용한 초기화
# List
empty_list = list()
# Dictionary
empty_dict = dict()
# Set
empty_set = set()
Python
복사
개인적으로 두 가지 방법 중 빌트인 생성자를 이용한 방법을 더 선호했습니다.
생성자를 이용하면 명시적으로 타입이 표현되기 때문에 실수를 더 줄일 수 있기 때문입니다.
개발자마다 사용하는 폰트의 종류나 크기에 따라 대괄호, 중괄호, 소괄호 구분이 헷갈릴 수 있습니다.
코드를 적을 때 다른 개발자가 실수할 수 있는 부분을 없애자는 취지에서 명시적인 선언을 주로 사용했습니다.
또 Set과 Dict 모두 중괄호를 써서 선언하기 때문에 코드를 읽다보면 순간적으로 타입이 헷갈릴 수 있습니다.
아래 예시를 보면 확실히 생성자를 이용한 방법이 타입을 착각할 여지를 줄여줍니다.
# 1
this_is_dict = {1: 1}
this_is_set = {1}
# 2
this_is_dict = dict({1: 1})
this_is_set = set({1})
Python
복사
하지만 파이썬 생태계에도 type hinting이 보편화 되면서 위와 같은 문제가 잘 발생하지 않고 있습니다.
다시 위의 예시에 type hints를 추가해보겠습니다.
from typing import Dict, Set
# 1
this_is_dict: Dict[int, int] = {1: 1}
this_is_set: Set[int] = {1}
# 2
this_is_dict: Dict[int, int] = dict({1: 1})
this_is_set: Set[int] = set({1})
Python
복사
이제는 오히려 생성자를 이용한 방법이 redundant하게 보입니다.
그럼 어떤 방법이 더 파이써닉 할까요?
위의 두 가지 방식은 빈 컨테이너를 생성한다는 결과는 동일하나 내부 동작이 다릅니다.
두 가지 방법을 비교하고 차이점을 알아보겠습니다.
비교
속도
두 방법의 속도 차이는 다음과 같습니다.
실행 환경에 따라 다르겠지만 괄호를 이용한 방법이 언제나 더 빠릅니다.
import timeit
# 1
>>> timeit.timeit("[]", number=10_000)
0.0005502879999994548
>>> timeit.timeit("{}", number=10_000)
0.0005528019999871958
# 2
>>> timeit.timeit("list()", number=10_000)
0.0014496550000160369
>>> timeit.timeit("dict()", number=10_000)
0.0015732980000109364
>>> timeit.timeit("set()", number=10_000)
0.0015283129999943412
Python
복사
Memory
생성자를 이용하면 heap에 새로운 객체를 생성하지만, 괄호를 이용하면 메모리를 재사용합니다.
# 1
>>> for i in range(3):
... print(id([]))
...
4364799232
4364799232
4364799232
# 2
>>> for i in range(3):
... print(id(list()))
...
4364819648
4364819712
4364819456
Python
복사
Byte-codes
두 방법의 바이트 코드를 비교해보면 다르게 동작합니다.
import dis
# 1
>>> dis.dis(lambda: [])
1 0 BUILD_LIST 0
2 RETURN_VALUE
# 2
>>> dis.dis(lambda: list())
1 0 LOAD_GLOBAL 0 (list)
2 CALL_FUNCTION 0
4 RETURN_VALUE
Python
복사
Analysis
왜 위와 같은 차이가 나타날까요?
앞서 얘기했듯이 빈 컨테이너 타입을 생성한다는 결과 자체는 동일하지만 두 방법의 동작이 다릅니다.
괄호를 이용한 방식은 해당 데이터 타입의 객체를 빠르게 생성해주는 것에 중점을 두고 있습니다.
이에 반해 생성자를 이용한 방식은 객체 생성과 더불어 데이터의 타입 변환을 돕는 추가적인 기능을 지원합니다.
예시를 통해 더 자세하게 알아봅시다.
[ ] vs. list()
다음과 같은 Dictionary 타입의 데이터가 있습니다.
fruits = {'apple': 1, 'banana': 2}
Python
복사
이를 각각 [], list()에 전달하면 다음과 같이 동작합니다.
# 1
>>> [fruits]
[{'apple': 1, 'banana': 2}]
# 2
>>> list(fruits)
['apple', 'banana']
Python
복사
괄호를 이용하면 Dict 객체가 List의 element로 추가됩니다.
반면 생성자를 이용하면 Dict의 key만 추출되어 새로운 List를 만듭니다.
list()는 iterable 객체를 받아서 이를 이용해 새로운 List 객체를 생성합니다.
“list()는 Dict를 받았을 때 key를 추출하여 List를 만든다”는 추가적인 기능이 있는 것입니다.
Tuple을 전달하면 다음과 같이 동작합니다.
fruits = ('apple', 'banana')
# 1
>>> [fruits]
[('apple', 'banana')]
# 2
>>> list(fruits)
['apple', 'banana']
Python
복사
list()에 인자로 iterable한 객체를 전달하면 List로 변환됩니다.
{ } vs. set()
List와 유사하게 Set은 괄호 초기화 방법과 생성자 초기화 방법에 따라 다음과 같은 차이가 있습니다.
fruits = ('apple', 'banana')
# 1
>>> {fruits}
{('apple', 'banana')}
# 2
>>> set(fruits)
{'banana', 'apple'}
Python
복사
dict()
dict() 생성자를 이용하면 다음과 같은 기능을 이용할 수 있습니다.
dict()는 Tuple을 받았을 때 인자를 key:value로 하는 Dict를 생성합니다.
fruits = [('apple', 1), ('banana', 2)]
>>> dict(fruits)
{'apple': 1, 'banana': 2}
Python
복사
두 개의 element를 전달하지 않으면 에러가 발생합니다.
Conclusion
다시 빈 컨테이너를 초기화하는 이야기로 돌아와봅시다.
괄호를 이용한 초기화 방법과 생성자를 이용한 방법은 내부 동작이 다릅니다.
빈 컨테이너를 초기화 할 때, 생성자의 추가적인 기능은 필요하지 않습니다.
괄호를 이용한 방법이 성능적으로도 더 빠릅니다.
이전에는 개발자의 실수를 줄이기 위해 생성자를 사용하는 방법을 이용하기도 했지만, 이는 type hinting을 통해 극복 가능하게 되었습니다.
오히려 type hints와 생성자를 같이 쓰면 지저분 해보이기도 합니다.
따라서 빈 컨테이너를 초기화 할 때는 괄호를 이용한 방법이 더 파이써닉합니다.