Python — 슬라이스와 가변 객체, 두 축이면 끝난다
Python은 매회 1문항이 기본이었지만 2026-1회에 사상 첫 3문항으로 뛰었다. 문항 수 대비 개념 범위가 가장 좁아 — 슬라이싱, 가변 객체(얕은 복사), dict/set — 정리 효율이 제일 좋은 언어다. 이 편의 코드는 전부 기출 복원이며 python3로 실행 검증했고, 헷갈리는 문항은 줄마다 변수 상태를 적는 추적 표로 풀이 과정을 다 펼쳐 놨다.
1. 슬라이싱 — 끝 인덱스 미포함, 음수 step은 끝에서 출발
s[시작:끝:step] — 세 자리 각각의 규칙만 알면 된다.
s[a:b] a부터 b '직전'까지 ← 끝 미포함이 오답 1순위
s[a:] a부터 끝까지
s[:b] 처음부터 b 직전까지
s[::2] 처음부터 2칸씩 (인덱스 0,2,4…)
s[1::2] 인덱스 1부터 2칸씩 (1,3,5…)
s[::-1] 전체 역순
s[::-2] '마지막'에서 2칸씩 역방향 ← 시작점이 끝이라는 게 함정
1.1 음수 step 추적 (2026-1 → 9A7A5A3A1A)
lst = list(range(10)) # [0,1,2,3,4,5,6,7,8,9]
for c in lst[::-2]:
print(c, end='A')
print()
[::-2]는 마지막 원소(9)에서 2칸씩 거꾸로:
| 방문 순서 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 원소 | 9 | 7 | 5 | 3 | 1 |
| 출력 | 9A | 7A | 5A | 3A | 1A |
→ 9A7A5A3A1A. end='A'는 마지막 1 뒤에도 붙는다(그래서 A가 5개). 8부터 시작한다고
보면 오답(8A6A4A2A0A), A를 4개로 세도 오답.
1.2 문자열 슬라이스 (2026-2 → _THIISING)
str = "_THIS_IS_KIM_SPEAKING"
# 0123456789...
a = str[:4] # 인덱스 0~3 → "_THI" (4는 미포함!)
b = str[6:8] # 6~7 → "IS"
c = str[18:] # 18~끝 → "ING"
ans = a + b + c # "_THIISING"
1.3 필터 결합 (2026-1 → veDamuH)
z = ''.join(c for c in y[::-1] if c not in 'ong')
# y = "HumanDev" → [::-1] "veDnamuH" → 'o','n','g' 제거(여기선 n 하나) → "veDamuH"
체크 3연타 — ① 끝 인덱스 미포함인가 ② 음수 step이면 시작이 마지막 원소인가 ③
end=/필터(if c not in)가 마지막 원소·모든 문자에도 적용되는가.
인덱싱 기출(2024-1 → Seynaau): i[1]은 두 번째 글자, i[-1]은 마지막 글자.
초기값(str = "S")을 빠뜨리지 않는 것까지가 문제다.
꼬리질문 대비 — "
s[5:2]는?"(시작>끝이고 step 양수면 빈 문자열''). "s[-3:]는?" (뒤에서 3글자). "step이 0이면?"(ValueError).
2. 가변 객체 — 별칭, 제자리 수정, 얕은 복사
Python에서 리스트·딕셔너리는 가변(mutable) 이고 참조로 전달된다. 이 두 가지가 함정의 근원이다.
2.1 in-place 수정은 원본을 바꾼다 (2024-3 → 3)
def func(lst):
for i in range(len(lst) // 2):
lst[i], lst[-i-1] = lst[-i-1], lst[i] # 양끝을 맞바꿈 = 제자리 뒤집기
lst = [1,2,3,4,5,6]
func(lst) # 반환값 없어도 lst는 [6,5,4,3,2,1]
print(sum(lst[::2]) - sum(lst[1::2])) # (6+4+2) - (5+3+1) = 12 - 9 = 3
리스트는 참조로 전달돼 함수가 return하지 않아도 원소 수정이 호출부에 남는다.
reverse(), sort(), append() 등 in-place 메서드는 원본을 바꾸고 반환값은 None이다.
함정 —
a = lst.sort()하면 a는 None이다. 정렬된 리스트를 원하면sorted(lst)(새 리스트 반환).
2.2 얕은 복사 — 2026-1회를 무너뜨린 문항 (→ 10)
def f(a):
m = [ [x] for x in a ] # [ [1], [2], [3], [4] ]
b = m[:] # ★ 얕은 복사 — 겉 리스트만 새것, 내부 리스트는 공유
for i in range(len(b) - 1):
b[i+1] += b[i] # += 는 내부 리스트를 '제자리 연장' → m에도 보인다
return sum(len(x) for x in m)
print(f([1, 2, 3, 4]))
b = m[:]는 겉 리스트만 새로 만들고 b[k]는 여전히 m[k]와 같은 내부 리스트를
가리킨다. 그래서 b[i+1] += b[i](제자리 연장)가 m에 그대로 반영된다:
| 단계 | m의 상태 | 비고 |
|---|---|---|
| 초기 | [ [1],[2],[3],[4] ] | b[k]는 m[k]와 같은 객체 |
| i=0 | [ [1],[2,1],[3],[4] ] | b[1] += b[0] → m[1] 연장 |
| i=1 | [ [1],[2,1],[3,2,1],[4] ] | b[2] += b[1] → m[2] 연장 |
| i=2 | [ [1],[2,1],[3,2,1],[4,3,2,1] ] | b[3] += b[2] → m[3] 연장 |
내부 리스트 길이 = 1, 2, 3, 4 → 합 10.
세 겹의 함정 — ①
m[:]는 깊은 복사가 아니다(내부 객체 공유). ② 리스트의+=는 새 리스트 생성이 아니라 **extend(제자리 연장)**다. ③ 그래서 b를 통한 수정이 m으로 '새어 나온다'. 셋 중 하나만 놓쳐도 답이 4(공유 무시)나 20(둘 다 합산)이 된다.
| 구분 | 코드 | 내부 객체 |
|---|---|---|
| 별칭 | b = m | 전부 공유 (같은 리스트) |
| 얕은 복사 | b = m[:], m.copy(), list(m) | 겉만 분리, 내부 공유 |
| 깊은 복사 | copy.deepcopy(m) | 전부 분리 |
꼬리질문 대비 — "
b[i+1] = b[i+1] + b[i]였다면?"(이건 새 리스트를 만들어 b에만 재대입 → m은 안 바뀜 → 답이 달라짐).+=(제자리)와= x +(재대입)의 차이가 이 문제의 핵심.
3. dict / set — 삽입 순서, 스냅샷, comprehension
3.1 items() 순회는 삽입 순서 (2026-2 → CNNLRPYT)
locations = {"NYC": "New York", "LON": "London", "PAR": "Paris", "TKY": "Tokyo"}
result = ""
for key, location in locations.items():
result += key[-1] + location[0]
| 항목 | key[-1] | location[0] | 누적 |
|---|---|---|---|
| NYC:New York | C | N | CN |
| LON:London | N | L | CNNL |
| PAR:Paris | R | P | CNNLRP |
| TKY:Tokyo | Y | T | CNNLRPYT |
- 3.7부터 dict는 삽입 순서를 언어 명세로 보장한다. 실제 시험지가 "Python 3.x 이상"이라고만 써서 "순서 미보장이면 4! = 24가지 아니냐"는 이의신청이 속출했다 — 가답안은 삽입 순서 기준 CNNLRPYT. 학습 관점에선 "3.7+ = 삽입 순서" 로 정리.
key[-1](키 끝 글자) vslocation[0](값 첫 글자) — 어느 쪽에 어떤 인덱스인지가 전부.
3.2 comprehension + 갱신 + set 교집합 (2025-2 → 2)
lst = [1, 2, 3]
dic = {x: x * 2 for x in lst} # {1:2, 2:4, 3:6}
s = set(dic.values()) # {2, 4, 6} ← 이 시점의 스냅샷
lst[0] = 99
dic[2] = 7
s.add(99)
print(len(s & set(dic.values())))
| 줄 실행 후 | dic | s |
|---|---|---|
| 초기 | {1:2, 2:4, 3:6} | {2,4,6}(스냅샷) |
lst[0]=99 | {1:2, 2:4, 3:6} (불변 — dic은 이미 생성됨) | {2,4,6} |
dic[2]=7 | {1:2, 2:7, 3:6} | {2,4,6} |
s.add(99) | {1:2, 2:7, 3:6} | {2,4,6,99} |
s(={2,4,6,99}) ∩ set(dic.values())(={2,7,6}) = {2, 6} → len 2.
시점이 전부다 —
set(dic.values())는 그 줄이 실행되는 순간의 값을 복사(스냅샷)한다. 이후 dic이 바뀌어도 s는 안 변하고, lst를 바꿔도 이미 만든 dic은 안 변한다.
3.3 enumerate + sum/len (2025-3)
data = [ [3,5,2,4,1], [4,5,1], [4,4,1,5,4], [4,5] ]
result = {}
for index, lis in enumerate(data):
result[index] = (sum(lis), len(lis))
# {0:(15,5), 1:(10,3), 2:(18,5), 3:(9,2)}
enumerate(data)는 (0, [3,5,2,4,1]), (1, [4,5,1])…을 준다. 튜플 안의 (합, 길이) 순서를
바꿔 쓰는 게 대표 오답 — 코드에서 sum이 먼저인지 len이 먼저인지 확인.
set 연산 참조 —
&교집합,|합집합,-차집합,^대칭차. dict는keys(),values(),items()/ 없는 키 접근은 KeyError,.get(k)은 None.
4. type 비교 — float는 int가 아니다 (2024-3 → 45)
def test(v):
if type(v) == type(""): return len(v) # 문자열
elif type(v) == type(100): return 101 # int '만'
else: return 20 # 나머지 전부
print(test("100.0") + test(100.0) + test((100.0, 200.0)))
| 인자 | 타입 | 걸리는 분기 | 반환 |
|---|---|---|---|
"100.0" | str | 첫 번째 | len=5 (다섯 글자) |
100.0 | float | else (int 아님!) | 20 |
(100.0,200.0) | tuple | else | 20 |
합 5 + 20 + 20 = 45.
함정 2연발 — ①
100.0은 float라type(100)(int)과 다르다. ②"100.0"은 문자열이니len(다섯 글자)이지 숫자 변환이 아니다. 보너스: 실행 경로에 안 타는 분기의 리턴값(101이든 100이든)은 답에 영향 없다 — 복원본끼리 그 값이 달라도 당황할 필요 없는 이유.
주요 타입: int, float, str, bool, list, tuple, dict, set, NoneType.
type(x) == type(y) 대신 isinstance(x, int)도 쓰지만 시험 코드는 보통 type() 비교다.
5. 문자열 처리 — 카운트, split/join, 필터
# 2024-2 → ab3 ca3 (슬라이딩 윈도 카운트)
for i in range(len(str)):
sub = str[i:i+len(p)] # 범위 넘어가면 예외 없이 짧게 잘림
if sub == p: result += 1
# "abdcabcabca"에서 "ab" 3회(인덱스 0,4,7), "ca" 3회(3,6,9)
- 슬라이스가 문자열 끝을 넘어도 예외 없이 짧게 잘린다(C의 버퍼 오버런과 다른 점).
split()인자 없음 = 연속 공백 무시 + 양끝 공백 제거. 입력에 공백이 없으면 분할 자체가 안 일어나["HumanDev"]한 조각(2026-1)."a,b".split(",")처럼 구분자 지정도 가능.''.join(리스트)= 구분자 없이 이어붙이기,",".join(리스트)= 쉼표로.- 기타 자주 쓰이는 것:
.upper()/.lower(),.replace(a,b),.find(x)(없으면 -1),.strip(),.startswith(),f"{x}"(f-string).
6. 클래스가 나와도 겁먹지 않는다 (2025-1 → 13)
nodes = [Node(i) for i in li]
for i in range(1, len(li)):
nodes[(i - 1) // 2].children.append(nodes[i]) # 부모 = (i-1)//2 : 완전이진트리
# calc: level % 2 == 1 인 노드만 합산 → 레벨 1의 5+8 = 13 (레벨 0, 2는 제외)
- 기출의 클래스는 대개 장식이고, 본질은 인덱스 계산이다.
(i-1)//2가 부모 인덱스면 완전이진트리(자식은2i+1,2i+2) — 종이에 트리를 그려 레벨을 매기면 풀린다. li = [3,5,8,12,15,18,21]이면 레벨0: 3(i0) / 레벨1: 5(i1),8(i2) / 레벨2: 12,15,18,21. 홀수 레벨(1)만 합산 → 5+8 = 13.- 클래스 속성(2026-2
locations)은 인스턴스에서 그대로 읽힌다 —obj.locations는 클래스에 정의된 그 dict.
7. Python 문법 빠른 참조 (코드 문항 어디서든 나올 수 있음)
- 정수 나눗셈·나머지·거듭제곱:
7 // 2= 3(몫),7 % 2= 1(나머지),2 ** 3= 8. 음수 나눗셈은 바닥(floor) 방향:-7 // 2= -4(‑3.5를 내림),-7 % 2= 1. - 조건 표현식(삼항):
a if 조건 else b. - 불리언:
True/False(대문자),and/or/not(키워드),0·""·[]·None은 거짓. - range:
range(n)= 0..n-1,range(a,b)= a..b-1,range(a,b,step). - comprehension:
[x*2 for x in a if x>0](리스트),{k:v for ...}(딕셔너리),{x for ...}(셋),(x for ...)(제너레이터). - 다중 대입·스왑:
a, b = b, a(임시변수 없이 교환 — 슬라이싱 뒤집기의 핵심). - 함수 인자: 기본값
def f(x=0), 가변 위치*args, 가변 키워드**kwargs. - 예외:
try / except / else / finally.raise로 발생.
8. 고난도 함정 (자체 출제 hard 세트 대비)
기출엔 아직 안 나왔지만 나올 수 있는 Python 함정. 각 항목은 hard 세트의 실제 문항이다.
8.1 가변 기본 인자 — 호출 간 공유
def add(x, acc=[]):
acc.append(x)
return sum(acc)
add(1); add(2); add(3) # acc가 공유되어 1 → 3 → 6
기본값
[]는 함수 정의 시 딱 한 번 만들어져 모든 호출이 같은 리스트를 공유한다. 매 호출마다 새로 [] 되지 않는다. 이걸 피하려면def add(x, acc=None): acc = acc or []. §2의 얕은 복사와 더불어 "가변 객체 공유"의 대표 함정.
8.2 클로저 late binding — 변수를 값이 아니라 참조로 캡처
fs = [lambda: i for i in range(4)]
[f() for f in fs] # [3, 3, 3, 3] — 넷 다 마지막 i(3)
람다는 i의 '값'이 아니라 '변수 i 자체'를 캡처한다. 나중에 호출할 때 i는 루프가 끝난 값 (3)이라 전부 3. 각 시점 값을 붙들려면 기본 인자로 즉시 캡처:
lambda i=i: i→ [0,1,2,3].
8.3 dict.fromkeys — 모든 키가 한 객체 공유
d = dict.fromkeys(['a', 'b'], []) # a·b가 같은 리스트!
d['a'].append(1) # d['b']에도 [1]
fromkeys의 기본값도 한 번만 평가되어 모든 키가 같은 가변 객체를 공유한다. 키마다 독립 리스트가 필요하면{k: [] for k in keys}(컴프리헨션은 매번 새로 만든다).
8.4 divmod로 진법 변환
n = 100; r = ''
while n:
n, rem = divmod(n, 3) # 몫·나머지 동시에
r = str(rem) + r # 나머지를 '앞에' 붙여야 자리가 맞음
# 100 → '10201' (3진법)
divmod(n, b)는(n//b, n%b). 나머지를 앞에 붙여야(뒤가 아니라) 자릿값이 올바르게 쌓인다. 뒤에 붙이면 뒤집힌 답이 나온다.
9. 시험장 체크리스트
- 슬라이스: 끝 미포함 / 음수 step은 끝에서 출발 / step 2는 건너뛰기.
[:]는 얕은 복사,+=는 제자리 연장 — "새어 나오는 수정"을 추적.- in-place 메서드(reverse/sort/append)는 반환값이 아니라 원본을 바꾸고 None을 반환.
- dict는 삽입 순서(3.7+),
set(...)은 그 순간의 스냅샷. type(100)≠type(100.0)≠type("100").//·%의 음수 방향,**거듭제곱,a,b=b,a스왑을 헷갈리지 말 것.- 각 줄 실행 후 컬렉션 내용을 옆에 적는다 — Python은 상태 추적이 곧 풀이다.
출처
- 2024-1 ~ 2026-2 실기 복원 기출 (이 트랙 수록 8회차) — 기사퍼스트·두목넷·수제비(공개 미러)·뉴비티·chobopark 교차 검증
- 본문 코드·추적 표 전량 python3(3.14) 실행 검증 완료 (얕은 복사 단계별 상태, dict 스냅샷, 슬라이스 결과 일치 확인)
- dict 순서 보장: Python 3.7 릴리스 노트 (언어 명세 편입)