[파이썬 튜토리얼] Sequence에 대한 산술 연산

PlanB·2024년 1월 13일
0

파이썬 튜토리얼

목록 보기
21/21

Level 1

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

조언

  • [1] + [2]의 결과가 무엇일지, [1] * 3의 결과가 무엇일지만 떠오른다면 넘어가도 된다. 서로 다른 타입의 Sequence 간 합이 불가능하다는 등의 내용은 직접 에러를 내보며 습득해도 늦지 않다.

연습문제

결과 예상하기

다음 코드의 실행 결과를 예상하자.

print([1, 2] + [1])
print((1, 2) + (1,))
print((1, 2) + (1,) * 3)
print((1, [2] * 2) + (1,) * 2)

자투리 지식

Sequence끼리 + 연산하는 것은 더하기보다 이어붙이기(concat)라고 말하는 것이 더 정확한 표현이다.

Level 2

Sequence * 0

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, Sequence * 1

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을 합치기

+ 연산에서는 피연산자 간에 타입이 맞지 않으면 에러가 발생한다. 하지만 부득이하게 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]

Level 3

Sequence + 빈 Sequence, Sequence * 1 - 메모리 관점에서

내용에 변화를 일으키지 않는 두 연산을 메모리 관점에서 살펴보자. 예를 들어 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를 대상으로 하는 복합 대입 연산

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

Constant Folding

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
profile
백엔드를 주로 다룹니다. 최고가 될 수 없는 주제로는 글을 쓰지 않습니다.

2개의 댓글

comment-user-thumbnail
2024년 3월 23일

안녕하세요~웹사이트 솔루션이대해 조언을 구하고싶은데 ㅠㅠ혹시 실례지만 카톡 thegood12 한통주실수있나요~ 광고그런거아니에용~ㅠㅠ정말공부하는데 부족해서 물어보고싶어서 ㅠㅠ그래용

답글 달기
comment-user-thumbnail
2024년 6월 17일

구글링하다 들어왔는데 많은 정보 얻고 갑니다. 이십대 마지막 도전으로 개발자 취준 시작하게 되었습니다. 혹시 선배님께선느 여기 어떻게 생각하는지 한번 봐주시면 감사하겠습니다! (https://bit.ly/4bXHxwO)

답글 달기