컴퓨터의 개념 및 실습
This project is maintained by jinsooya
Copying Objects and Iterating Operations
박 진 수 교수
Intelligent Data Semantics Lab
Seoul National University
객체 참조란?
주로 변수를 통해 객체 참조를 한다.
객체 참조의 값이 다르다는 것은 어떤 의미인가?
다음 예는 객체의 값이 같은 경우지만 객체 참조가 다른 것을 보여 준다.
x = 'stay hungry, stay foolish'
y = 'stay hungry, stay foolish'
print(x)
print(y)
먼저, 두 문자열의 값이 같은지 확인해보자.
# 둘의 값이 같은가?
x == y
이처럼 두 객체의 값이 같은지 확인하는 연산자로는 ==(같다)가 있다.
그렇다면 이 두 문자열이 서로 같은 객체를 참조하는지 확인하는 방법은 없을까?
있다.
항등 연산자를 사용하면 된다.
항등 연산자(identity operator)란?
사용하는 방법은 다음과 같다.
x is y
x 와 y 가 같은 객체를 참조하면(즉, 같은 메모리 공간에 있으면) ‘참(True)’ 을 반환하고, 아니면 ‘거짓(False)’을 반환한다.
# > x = 'stay hungry, stay foolish'
# > y = 'stay hungry, stay foolish'
# 둘은 같은 객체를 참조하고 있는가?
x is y
이번에는 새로운 변수 z 에 x 를 할당해보자.
# > x = 'stay hungry, stay foolish'
# z에 x를 할당한다.
z = x
print(z)
# > y = 'stay hungry, stay foolish'
# > z = 'stay hungry, stay foolish'
# z와 y의 값은 같은가?
z == y
# z와 y는 같은 객체를 참조하고 있는가?
z is y
# > x = 'stay hungry, stay foolish'
# > z = 'stay hungry, stay foolish'
# z와 x는 같은 객체를 참조하고 있는가?
z is x
객체를 복사하는 방법으로는 세가지가 있다.
객체를 복사하는 가장 간단한 방법은 할당 연산자 = 를 사용하는 것이다.
할당 연산자는 실제로 해당 객체 자체를 ‘복사’하는 것이 아니라, 해당 객체에 대한 객체 참조를 복사한다.
객체를 할당하면 객체 참조만 복사하기 때문에 사실상 두 변수는 같은 객체를 참조한다.
문제는 가변자료형이다.
가변자료형은 값을 변경할 수 있기 때문에, 한쪽의 값을 변경하면 다른 쪽도 변경한 값을 참조하게 된다.
x = [1, 2, 3] # 가변자료형 리스트를 x에 할당한다.
y = [1, 2, 3] # 같은 값을 가진 또 다른 리스트를 y에 할당한다.
print(x)
print(y)
x is y
# x를 z에 할당한다.
z = x
print(z)
# > x, y, z = [1, 2, 3]
# z에 'a'를 추가한다.
z.append('a')
print(z)
print(x)
print(y)
# z와 x는 같은 객체를 참조하는가?
z is x
# z와 y는 같은 객체를 참조하는가?
z is y
얕은 복사(shallow copy)란?
원본 객체와 별도로 개별 복사본을 생성하지만, 원본과 복사본 둘 다 같은 객체를 참조하고 있다.
얕은 복사를 하는 방법은 크게 네 가지로 나누어 볼 수 있다.
x = [1, 2, 3]
y = x # 할당(객체 참조)한다.
y is x # y는 x와 같은 객체를 참조하고 있다.
# > x = y = [1, 2, 3]
x[-1] = 'a' # x의 마지막 객체를 'a'으로 교체한다.
print(x)
print(y)
# > x = [1, 2, 'a']
z = x[:] # 분할 연산자[:]로 얕은 복사를 한다.
z is x # z는 x와 다른 객체를 참조하고 있다.
print(z)
print(x)
# > z = [1, 2, 'a']
z[0] = 'b' # z의 첫 번째 객체를 'b'로 교체한다.
print(z)
print(x)
print(y)
x = [1, 2, 3]
y = x # 할당(객체 참조)한다.
y is x # y는 x와 같은 객체를 참조하고 있다.
# > x = y = [1, 2, 3]
x[-1] = 'a' # x의 마지막 객체를 'a'으로 교체한다.
print(x)
print(y)
# > x = [1, 2, 'a']
z = x.copy() # copy() 메소드로 얕은 복사를 한다.
z is x # z는 x와 다른 객체를 참조하고 있다.
print(z)
print(x)
# > z = [1, 2, 'a']
z[0] = 'b' # z의 첫 번째 객체를 'b'로 교체한다.
print(z)
print(x)
print(y)
x = [1, 2, 3]
y = x # 할당(객체 참조)한다.
y is x # y는 x와 같은 객체를 참조하고 있다.
# > x = y = [1, 2, 3]
x[-1] = 'a' # x의 마지막 객체를 'a'으로 교체한다.
print(x)
print(y)
# > x = y = [1, 2, 3]
z = list(x) # 리스트 생성자로 얕은 복사를 한다.
z is x # z는 x와 다른 객체를 참조하고 있다.
print(z)
print(x)
# > z = [1, 2, 'a']
z[0] = 'b' # z의 첫 번째 객체를 'b'로 교체한다.
print(z)
print(x)
print(y)
x = [1, 2, 3]
y = x # 할당(객체 참조)한다.
y is x # y는 x와 같은 객체를 참조하고 있다.
# > x = y = [1, 2, 3]
x[-1] = 'a' # x의 마지막 객체를 'a'으로 교체한다.
print(x)
print(y)
# > x = [1, 2, 'a']
import copy # copy 모듈을 불러온다.
z = copy.copy(x) # copy 모듈의 copy() 함수로 얕은 복사를 한다.
z is x # z는 x와 다른 객체를 참조하고 있다.
print(z)
print(x)
# > z = [1, 2, 'a']
z[0] = 'b' # z의 첫 번째 객체를 'b'로 교체한다.
print(z)
print(x)
print(y)
문제점
얕은 복사의 경우
[참고]
중첩(nested) 복합자료형이란 복합자료형이 또 다른 복합자료형을 객체로 가지고 있는 자료 구조를 말한다.
x = [1, 2, ['x', 'y', 'z']]
# 얕은 복사를 한다.
y = x[:]
# 둘의 값이 같은지 확인한다.
x == y
# 둘이 같은 객체를 참조하는지 확인한다.
x is y
# x와 y의 첫 번째 객체 참조가 같은지 확인한다.
x[0] is y[0]
x[0]과 y[0]는 같은 객체를 참조하고 있다.
# > x = y = [1, 2, ['x', 'y', 'z']]
# x의 첫 번째 객체를 1에서 'a'으로 바꾼다.
x[0] = 'a'
print(x)
print(y)
x[0]과 y[0]는 분명히 같은 객체를 참조했는데 왜 둘의 값이 달라졌을까?
다시 한번 둘의 객체 참조를 확인해보자.
x[0] is y[0] # x와 y의 첫 번쨰 객체 참조가 같은지 확인한다. => 다르다.
다시 확인해보니, 이제 x[0]와 y[0]는 서로 다른 객체를 참조하고 있다.
그 이유는 무엇일까?
가변 복합자료형라도 참조하는 객체가 불변자료형이면, 얕은 복사를 했을 때 원본이나 복사본 값이 바뀌어도 서로 영향을 미치지 않고 사용할 수 있다.
비록 원본과 복사본 모두 같은 객체를 참조하지만,
즉, 새로운 객체를 생성한 후 이 객체의 주소를 참조하게 된다.
앞의 예에서
x[0]의 값인 1을 ‘a’로 바꿨는데y[0]의 값은 그대로이다.문제는 복합자료형 안에 가변자료형이 있는 중첩 복합자료형일 때다.
이 경우에 대해 알아보도록 하자.
# > x = ['a', 2, ['x', 'y', 'z']]
# > y = [1, 2, ['x', 'y', 'z']]
# x와 y의 마지막 객체 참조가 같은지 확인한다.
x[-1] is y[-1]
얕은 복사를 했기 때문에 x[-1]과 y[-1]는 같은 객체를 참조하고 있다.
# y의 마지막 객체를 'z'에서 'b'로 바꾼다.
y[-1][-1] = 'b'
print(y)
print(x)
이번에는 x[-1][-1]와 y[-1][-1]의 값이 둘 다 바뀌었다.
그 이유는 무엇일까?
x[-1][-1] is y[-1][-1] # x와 y 마지막 객체의 마지막 객체 참조가 같은지 확인한다.
앞의 예에서 알 수 있듯이 리스트 안에 또 다른 가변자료형(리스트, 딕셔너리, 세트)이 있다면, 얕은 복사를 할 때 리스트에 있는 객체의 객체 참조(메모리 위치)만 복사하기 때문에, 복사본 역시 원본 리스트와 같은 객체를 참조한다.
따라서 이 중 하나가 수정되면 나머지에서도 수정한 내용이 반영된다.
깊은 복사(deep copy)란?
깊은 복사는 얕은 복사처럼 원본의 객체 참조를 복사하는 것이 아니라 새로 만든 복사본의 객체 위치를 참조한다.
따라서 얕은 복사와는 다른 결과를 가져온다.
얕은 복사는 같은 객체를 참조하지만, 깊은 복사는 값은 같지만 원본과는 완전히 서로 다른 객체(복사본)를 참조한다.
x = [1, 2, ['x', 'y', 'z']]
# 깊은 복사를 한다.
import copy
y = copy.deepcopy(x)
# 둘의 값이 같은지 확인한다.
x == y
# 둘이 같은 객체를 참조하는지 확인한다.
x is y
# x와 y의 첫 번째 객체 참조가 같은지 확인한다.
x[0] is y[0]
x[0]와 y[0]는 같은 객체를 참조하고 있다.
깊은 복사를 했는데도 왜 같은 객체를 참조하고 있을까?
불변자료형은 값을 바꿀 수 없기 때문에 같은 객체를 참조해도 된다.
따라서, 비록 깊은 복사를 하더라도 같은 객체를 참조한다.
# x의 첫 번째 객체를 1에서 'a'으로 바꾼다.
x[0] = 'a'
print(x)
print(y)
다시 한번 둘의 객체 참조를 확인해보자.
# x와 y의 첫 번째 객체 참조가 같은지 확인한다.
x[0] is y[0]
다시 확인해보니, 이제 x[0]와 y[0]는 서로 다른 객체를 참조하고 있다.
# > x = ['a', 2, ['x', 'y', 'z']]
# > y = [1, 2, ['x', 'y', 'z']]
# x와 y의 마지막 객체 참조가 같은지 확인한다.
x[-1] is y[-1]
깊은 복사를 했기 때문에 x[-1]과 y[-1]는 서로 다른 객체를 참조하고 있다.
즉, 깊은 복사를 하면 불변자료형은 얕은 복사를 해서 객체 참조가 같지만, 가변자료형이면 별도의 복사본을 만들기 때문에 얕은 복사와는 달리 객체 참조가 다르다.
# y의 마지막 객체를 'z'에서 'b'로 바꾼다.
y[-1][-1] = 'b'
print(y)
print(x)
y[-1][-1]의 값을 ‘z’에서 ‘b’라 바꿔도 x[-1][-1]의 값은 영향을 받지 않는다.
그 이유는 다음과 같이 둘 다 서로 다른 객체를 참조하고 있기 때문이다.
# x와 y 마지막 객체의 마지막 객체 참조가 같은지 확인한다.
x[-1][-1] is y[-1][-1]
가변자료형을 얕은 복사한 후 하나의 값을 바꾸면 참조하는 나머지 값도 같이 바뀌는 문제가 있는데 왜 기본적으로 깊은 복사를 하지 않고 얕은 복사를 하는가?
얕은 복사를 하면 참조되는 객체를 서로 공유하기 때문에 별도로 객체 자체를 따로 복사하는 것 보다 속도도 빠르고 메모리 공간도 적게 사용하기 때문에 깊은 복사보다 더 효율적이다.
결합 연산자 +
# 리스트와 리스트 더한 결과를 반환한다.
[1, 2, 3] + ['a', 'b', 'c']
반복 결합 연산자 *
# 튜플을 5번 반복한 새로운 튜플을 반환한다.
(1, 2, 3) * 5
멤버십 연산자 in/not in
'a' in {'a', 'b', 'c'} # 집합 안에 'a'가 있는지 확인한다.
'x' not in {'a', 'b', 'c'} # 집합 안에 'x'가 없는지 확인한다.
질의 함수 : len(), all(), any()
연산 함수 : min(), max(), sum()
정렬 함수 : reversed(), sorted()
순환문과 함께 사용하는 유용한 함수 : range(), enumerater(), zip()
len(x)
len([1, 2, 3]) # 리스트의 길이(즉, 담고 있는 객체의 수)를 반환한다.
len('Python') # 문자열의 길이(즉, 문자의 개수)를 반환한다.
all(i)
x = [7, -5, 8, 3, 9, 0]
all(x)
0이 거짓이라 False를 반환한다.
any(i)
# > [7, -5, 8, 3, 9, 0]
any(x)
앞에서 불린을 설명할 때 거짓’의 값은 False, 0, 0.0, None, 빈 문자열(’‘), 빈 복합자료형([], (), {}, set())이라고 했는데 이를 any()나 all() 함수로 간단히 확인해볼 수 있다.
# 불린형에서 False로 처리되는 것들이다.
any([False, 0, 0.0, None, '', [], (), set(), {}])
all([-15, 1.34, [1], (1,), set('a'), {None: 'NaN'}, ''])
빈 문자열이 있다.
min()과 max()는 순회형의 값 중 최솟값과 최댓값을 반환하는 함수다.
min(i, key)
x = [7, -5, 8, 3, 9]
min(x) # x 중 최솟값을 찾아서 반환한다.
max(i, key)
# > x = [7, -5, 8, 3, 9]
max(x) # x 중 최댓값을 찾아서 반환한다.
min()과 max() 함수는 key 의 전달인자로 함수를 사용할 수 있다. 이때 함수 이름만 사용하며, 함수 뒤에 붙는 괄호는 생략한다.
다음 코드는 key 로 전달 하는 함수로 abs()를 사용한다.
# > x = [7, -5, 8, 3, 9]
min(x, key=abs) # x의 절대 값 중에 최솟값을 찾아서 반환한다.
min()과 `max() 함수에 숫자가 아닌 문자도 올 수 있다.
s = ['a', 'B', 'c', 'd', 'E']
max(s) # s 중 최댓값을 찾아서 반환한다.
왜 가장 큰 알파벳이 ‘E’가 아니라 ‘d’일까?
# > s = ['a', 'B', 'c', 'd', 'E']
min(s) # s 중 최솟값을 찾아서 반환한다.
왜 가장 작은 알파벳이 ‘a’가 아니라 ‘B’일까?
유니코드 문자의 순서를 기준으로 최솟값과 최댓값을 찾기 때문이다. 알파벳은 대문자가 먼저 오고, 그 다음에 소문자가 온다.
ord('d')
ord('B')
ord()는 해당 유니코드 문자의 정숫값을 반환하는 함수다.
대소문자를 구분하지 않고 최댓값을 구하려면 다음처럼 문자열 클래스의 lower() 메소드를 불러 모두 소문자 로 처리한 후 비교해서 최댓값 알파벳 문자를 찾는다.
# > s = ['a', 'B', 'c', 'd', 'E']
max(s, key=str.lower) # s 중 대소문자 구분하지 않고 최댓값을 찾아서 반환한다.
sum(i[, start])
x = [7, -5, 8, 3, 9]
sum(x) # x의 모든 값을 더한 값을 반환한다.
sum(x, 9) # 9와 x의 모든 값을 더한 값을 반환한다.
reversed(seq)
L = [7, -5, 8, 3, 9]
reversed(L)
type(reversed(L)) # reversed()가 반환한 객체의 자료형을 확인한다.
list(reversed(L)) # reversed()가 반환한 객체를 리스트로 형변환한다.
sorted(i, key=None, reverse=False)
sorted() 함수의 기본 정렬 방식은 오름차순이다.
L = [7, -5, 8, 3, 9]
sorted(L) # 오름차순으로 정렬한다.
sorted(L, reverse=True) # 내림차순으로 정렬한다.
sorted(L, key=abs) # 절댓값으로 해서 오름차순으로 정렬한다.
sorted(L, key=abs, reverse=True) # 절댓값으로 해서 내림차순으로 정렬한다.
다음 세 함수는 8장의 순환문에서 상세히 다룰 것이다.
range(start, stop, step)
enumerate(i, start=0)
(인덱스, 객체)의 튜플 쌍으로 순회할 수 있는 열거형(enumerate) 객체를 반환한다.zip(i1, …, iN)
THE END