Sequence에 해당하는 타입들은 모두 자신과 동일한 타입과 + 연산을, int 타입과 * 연산을 할 수 있다. + 연산은 두 Sequence를 이어붙인 결과를 만들고, * 연산은 주어진 값만큼 Sequence의 내용을 반복시켜 새로운 Sequence를 만들어내는 동작을 한다.
print([1, 2, 3] + [4, 5, 6])
print((1, 2) * 3)
결과
[1, 2, 3, 4, 5, 6]
(1, 2, 1, 2, 1, 2)
+ 연산의 경우, 피연산자 간 타입이 다르면 에러가 발생한다. 결과의 타입을 정할 수 없기 때문이다. 예를 들어 list + tuple에서, 파이썬은 list와 tuple 중 어느 것을 결과의 타입으로 사용할 지 단정지을 수 없다.
print([1, 2, 3] + (4, 5, 6))
결과
Traceback (most recent call last):
File "example.py", line 1, in <module>
print([1, 2, 3] + (4, 5, 6))
TypeError: can only concatenate list (not "tuple") to list
다음 코드의 실행 결과를 예상하자.
print([1, 2] + [1])
print((1, 2) + (1,))
print((1, 2) + (1,) * 3)
print((1, [2] * 2) + (1,) * 2)
Sequence끼리 + 연산하는 것은 더하기보다 이어붙이기(concat)라고 말하는 것이 더 정확한 표현이다.
Sequence에 0 이하의 값을 곱하면, 해당 Sequence의 길이가 0이 된다.
print([1, 2, 3] * 0)
print([1, 2, 3] * -5)
print((1, 2, 3) * 0)
print((1, 2, 3) * -5)
결과
[]
[]
()
()
Sequence에 빈 Sequence를 더하거나, 1을 곱하면 내용이 그대로 유지된다.
l = [1, 2, 3]
t = (1, 2, 3)
l = l * 1
t = t * 1
print(l)
print(t)
l = l + []
t = t + ()
print(l)
print(t)
결과
[1, 2, 3]
(1, 2, 3)
[1, 2, 3]
(1, 2, 3)
+ 연산에서는 피연산자 간에 타입이 맞지 않으면 에러가 발생한다. 하지만 부득이하게 list와 tuple을 서로 이어붙여야 하는 상황이 생길 수 있다. 이 경우 둘을 같은 타입으로 맞춰준 뒤 연하게 만들면 된다. 지금까지 배운 내용으로 할 수 있는 것은 아니고, 뒤 타입 변환에서 이야기할 내용을 가져왔다.
l = [1, 2, 3]
t = (1, 2, 3)
print(tuple(l) + t) # 둘을 모두 tuple로 만들어서 합치기
print(l + list(t)) # 둘을 모두 list로 만들어서 합치기
결과
(1, 2, 3, 1, 2, 3)
[1, 2, 3, 1, 2, 3]
내용에 변화를 일으키지 않는 두 연산을 메모리 관점에서 살펴보자. 예를 들어 list에 1을 곱하거나, 빈 list를 이어붙이는 연산은 적용 전과 후의 내용이 동일하다. 그러나 메모리 관점에서는 조금 특이한 점이 있다.
l = [1, 2, 3]
print(id(l))
l = l * 1
print(id(l))
l = l + []
print(id(l))
결과
1697527520768
1697527817984
1697527850624
l은 두 번의 연산(l * 1, l + []) 이후 내용에 변화가 없지만, 메모리 주소는 달라졌다. 내용이 동일한 copy가 만들어진다는 점에서 [:] Slicing과 비슷하다고 볼 수 있다.
tuple은 메모리 최적화에 의해, 연산 전후의 내용이 동일하다면 객체도 동일한 것을 그대로 사용한다.
t = (1, 2, 3)
print(id(t))
t = t * 1
print(id(t))
t = t + ()
print(id(t))
결과
2780269573568
2780269573568
2780269573568
list 타입의 변수 l에 요소를 추가한다고 하자. 복합 대입 연산자를 사용하는 것과, 산술 연산자 후 대입 연산을 하는 것에는 메모리 입장에서 차이가 있다.
l = [1, 2, 3]
print(id(l))
l += [4]
print(id(l))
l = l + [5]
print(id(l))
결과
1691093065088
1691093065088
1691093394816
복합 대입 연산자를 사용하는 경우, list가 점유하고 있는 메모리 주소는 유지한 채 내용만 수정되는 것을 볼 수 있다. l에 [4]를 추가한 결과가 그대로 다시 l에 대입되므로 객체를 따로 생성하지 않는 것이다.
반면, Immutable에서는 통하지 않는다.
t = (1, 2, 3)
print(id(t))
t += (4,)
print(id(t))
결과
1857486517696
1857486373568
s = 'abc'
print(id(s))
s += 'd'
print(id(s))
결과
2674937949104
2674938050416
tuple 리터럴끼리의 합이나, tuple 리터럴과 정수 리터럴 간의 곱이 발견되는 경우 constant folding된다. 다음은 이를 바이트코드로 확인한 예제다.
from dis import dis
dis(compile('(1, 2) + (3, 4)', '', 'eval'))
dis(compile('(1, 2) * 2', '', 'eval'))
결과
1 0 LOAD_CONST 0 ((1, 2, 3, 4))
2 RETURN_VALUE
1 0 LOAD_CONST 0 ((1, 2, 1, 2))
2 RETURN_VALUE
다음은 constant folding이 일어나지 않는 list의 예다.
from dis import dis
dis(compile('[1, 2] + [3, 4]', '', 'eval'))
dis(compile('[1, 2] * 2', '', 'eval'))
결과
1 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 BUILD_LIST 2
6 LOAD_CONST 2 (3)
8 LOAD_CONST 3 (4)
10 BUILD_LIST 2
12 BINARY_ADD
14 RETURN_VALUE
1 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 BUILD_LIST 2
6 LOAD_CONST 1 (2)
8 BINARY_MULTIPLY
10 RETURN_VALUE
Python 3.6 이하에서는 결과의 길이가 20을 초과할 경우, Python 3.7부터는 256을 초과할 경우 constant folding되지 않는다. 다음은 Python 3.7에서의 예다. (1,) * 256에 해당하는 바이트코드는 (1,)을 256번 반복하는 내용이 포함되므로 일부 생략했다.
from dis import dis
dis(compile('(1,) * 256', '', 'eval'))
dis(compile('(1,) * 257', '', 'eval'))
결과
1 0 LOAD_CONST 0 ((1, 1, 1, 1, ..., 1))
2 RETURN_VALUE
1 0 LOAD_CONST 0 ((1,))
2 LOAD_CONST 1 (257)
4 BINARY_MULTIPLY
6 RETURN_VALUE
구글링하다 들어왔는데 많은 정보 얻고 갑니다. 이십대 마지막 도전으로 개발자 취준 시작하게 되었습니다. 혹시 선배님께선느 여기 어떻게 생각하는지 한번 봐주시면 감사하겠습니다! (https://bit.ly/4bXHxwO)
안녕하세요~웹사이트 솔루션이대해 조언을 구하고싶은데 ㅠㅠ혹시 실례지만 카톡 thegood12 한통주실수있나요~ 광고그런거아니에용~ㅠㅠ정말공부하는데 부족해서 물어보고싶어서 ㅠㅠ그래용