파이썬 기초 다지기 5

류한선·2025년 7월 5일

실기연습-2

목록 보기
92/95

좋아! 지금부터 아까 낸 심화 복습 문제 1번부터 5번까지,
각 줄마다 무슨 역할을 하는지, 왜 그런 결과가 나오는지, 실제로 어떤 값이 처리되는지,
혼동 포인트와 실전 트랩까지 모두 포함해서 해부해줄게.


✅ 문제 1: 얕은 복사 + 정렬 + 내부 변경 트랩

def modify_and_sort(lst):
    lst2 = lst[:]  # 얕은 복사
    lst2.sort()
    lst2[0][0] = 999
    return lst, lst2

a = [[3], [1], [2]]
b, c = modify_and_sort(a)
print("a =", a)
print("sorted_copy =", c)

🔍 코드 해부

  1. a = [[3], [1], [2]]

    • 리스트 안에 또 리스트가 들어있는 구조 (2차원 리스트)
    • 구조: a → [[3], [1], [2]]
  2. lst2 = lst[:]

    • 리스트의 얕은 복사
    • 얕은 복사는 바깥 리스트는 새로 만들지만, 내부 요소(여기선 [3], [1], [2])는 같은 참조를 유지함
    • 즉: lst2[0] is lst[0]True
  3. lst2.sort()

    • 내부 리스트들을 오름차순 정렬함
    • 정렬 기준은 내부 리스트의 첫 번째 값
    • 정렬 결과: lst2 → [[1], [2], [3]]
  4. lst2[0][0] = 999

    • lst2[0][1] → 내부 값 1999로 바꿈
    • 이 때 lst[1][1]을 참조 중이므로 lst[1][0] = 999가 됨

🔄 최종 구조 시각화

변수내용
lst (== a)[[3], [999], [2]]
lst2 (== c)[[999], [2], [3]]

✅ 출력

a = [[3], [999], [2]]
sorted_copy = [[999], [2], [3]]

정답: 없음 (선택지 수정 필요)
→ 만약 보기 중에 이런 선택지가 있었다면:

정답: a = [[3], [999], [2]], sorted_copy = [[999], [2], [3]]


✅ 문제 2: defaultdict + split + join

from collections import defaultdict

text = "apple banana apple grape banana apple"
word_count = defaultdict(int)

for word in text.split():
    word_count[word] += 1

sorted_words = sorted(word_count.items(), key=lambda x: (-x[1], x[0]))
result = " ".join([f"{word}:{count}" for word, count in sorted_words])
print(result)

🔍 동작 흐름

  1. text.split()['apple', 'banana', 'apple', 'grape', 'banana', 'apple']

  2. defaultdict(int) → 존재하지 않는 key에 접근하면 기본값 0

  3. 반복하면서 카운트:

    'apple' → 1
    'banana' → 1
    'apple' → 2
    'grape' → 1
    'banana' → 2
    'apple' → 3
  4. sorted(..., key=lambda x: (-x[1], x[0]))

    • 등장 횟수 내림차순, 단어 알파벳 오름차순
      → 결과: [('apple', 3), ('banana', 2), ('grape', 1)]
  5. " ".join(...)'apple:3 banana:2 grape:1'


✅ 정답:

D. 'apple:3 banana:2 grape:1'


✅ 문제 3: *args + lambda + sort 조건

def custom_sort(*args, reverse=False):
    return sorted(args, key=lambda x: (len(x), x), reverse=reverse)

print(custom_sort("apple", "cat", "banana", "bee", reverse=True))

🔍 동작 흐름

  1. *args로 받은 값들: args → ("apple", "cat", "banana", "bee")

  2. key=lambda x: (len(x), x)

    • 정렬 기준 1: 문자열 길이 (길이가 긴 게 우선)
    • 정렬 기준 2: 알파벳순
  3. reverse=True → 정렬 기준 반대 (긴 게 먼저)


정렬 기준 리스트:

문자열길이알파벳순튜플
apple5-(5, 'apple')
cat3-(3, 'cat')
banana6-(6, 'banana')
bee3-(3, 'bee')

정렬:

  • (6, 'banana')
  • (5, 'apple')
  • (3, 'cat')
  • (3, 'bee')

reverse=True → 위 순서대로


✅ 정답:

A. ['banana', 'apple', 'cat', 'bee']


✅ 문제 4: Counter + [::-1]

from collections import Counter

s = "mississippi"
cnt = Counter(s)

items = list(cnt.items())[::-1]
print(items)

🔍 흐름

  1. 문자열: "mississippi"

  2. 카운트 결과:

문자횟수
m1
i4
s4
p2

→ 사전 순서대로 저장 (dict는 3.7+부터 순서 유지함)

  1. .items()[('m', 1), ('i', 4), ('s', 4), ('p', 2)]

  2. [::-1][('p', 2), ('s', 4), ('i', 4), ('m', 1)]


✅ 첫 번째 요소:

B. ('p', 2)


✅ 문제 5: 클래스 __init__ 기본값 트랩

class Node:
    def __init__(self, children=[]):
        self.children = children

n1 = Node()
n2 = Node()
n1.children.append("leaf")

print(n2.children)

🔍 동작 원리

  • children=[]한 번만 생성됨 (함수 정의 시)
  • n1, n2 모두 같은 리스트를 공유함

따라서 n1이 append("leaf") 하면
n2도 그 리스트를 보고 있기 때문에 영향을 받음


✅ 출력:

B. ['leaf']


✅ 총정리

문제정답핵심 트랩
1(직접 분석 결과)얕은 복사 + 내부 리스트 공유
2Ddefaultdict, split + 정렬 key
3A정렬 기준 튜플 + reverse
4BCounter + 역순 슬라이싱
5B기본값 리스트 공유 트랩

좋아, 문제 1번은 파이썬 리스트의 **얕은 복사(shallow copy)**와
내부 리스트 참조 공유, 그리고 정렬 후의 구조 변화를 이해해야 풀 수 있는 문제야.

우리가 이 문제를 **1줄씩 "실제로 무슨 일이 벌어지는가?"**를 따라가며
진짜로 메모리에 뭐가 저장되는지, 어떤 값이 바뀌는지를 디버깅처럼 해부해볼게.


✅ 다시 문제 코드:

def modify_and_sort(lst):
    lst2 = lst[:]  # 얕은 복사
    lst2.sort()    # 내부 리스트 정렬
    lst2[0][0] = 999  # 내부 값 변경
    return lst, lst2

a = [[3], [1], [2]]
b, c = modify_and_sort(a)
print("a =", a)
print("sorted_copy =", c)

✅ 1단계: 초기 상태

a = [[3], [1], [2]]
  • 이건 2차원 리스트야 (리스트 안에 리스트가 있음)

메모리 구조는 이렇게 생겼어:

a -->   [   [3],     [1],     [2]   ]
         ↑       ↑       ↑
       리스트1  리스트2  리스트3
       id1      id2     id3

a[0], a[1], a[2]는 서로 **다른 객체(리스트)**이고, 각각 [3], [1], [2]를 담고 있어.


✅ 2단계: lst2 = lst[:]

이 줄은 **얕은 복사(shallow copy)**야. 무슨 말이냐면:

  • lst2라는 새로운 리스트 객체를 만들되
  • 그 안의 원소들은 **lst에 있던 원소들과 같은 객체(=참조 공유)**를 넣는 거야

그래서 이렇게 됨:

lst2 -->  [   [3],     [1],     [2]   ]
           ↑       ↑       ↑
         같은 객체들 (id1, id2, id3)

즉, lst2[0] is lst[0]True


✅ 3단계: lst2.sort()

이건 리스트 안의 요소를 정렬해.

  • 정렬 기준: 내부 리스트의 첫 번째 값 (기본값)
  • 원래 순서: [ [3], [1], [2] ]
  • 정렬 결과: [ [1], [2], [3] ]

중요한 점:
정렬은 lst2의 바깥 리스트 순서만 바꾸지, 내부 객체는 그대로 유지돼.

즉, 구조는 이렇게 됨:

lst2 -->  [   [1],     [2],     [3]   ]   ← 정렬된 순서
lst  -->  [   [3],     [1],     [2]   ]   ← 원래 순서

하지만 여전히 lst2[0] is lst[1]True (참조는 공유된 상태)


✅ 4단계: lst2[0][0] = 999

지금 lst2[0] == [1] 이니까, lst2[0][0]1이야.
여기에 999를 넣으면 lst2[0] == [999]가 돼.

하지만!!!
lst2[0]lst[1]과 같은 객체였지?

그래서 이 순간 리스트 구조가 이렇게 변해버려:

lst   → [[3], [999], [2]]
lst2 → [[999], [2], [3]]

내부 객체 하나를 바꿨더니, 원래 리스트도 같이 바뀌었어.
→ 이게 바로 얕은 복사의 핵심 트랩


✅ 최종 상태 요약

변수구조설명
lst (a)[[3], [999], [2]]내부 요소가 바뀜
lst2 (c)[[999], [2], [3]]정렬된 후, 첫 원소 내부 값 변경됨

✅ 출력 결과

a = [[3], [999], [2]]
sorted_copy = [[999], [2], [3]]

✅ 이 문제에서 반드시 알아야 할 포인트 정리

개념설명자주 나오는 트랩
lst[:]얕은 복사내부 객체는 여전히 공유됨
sort()원본 리스트 자체를 정렬정렬 후 참조 순서 변경 가능
리스트 안의 리스트내부 객체는 별개로 관리됨얕은 복사 시 같이 바뀔 수 있음

✅ 연습: 이런 상황을 피하려면?

import copy

lst2 = copy.deepcopy(lst)

→ 깊은 복사를 하면 내부 객체까지 새로 만들어지니까,
lstlst2는 완전히 독립적인 리스트가 돼서 이런 일이 안 생겨.


좋아, 그럼 지금부터 **sort()sorted()**를 완전히 확실하게 이해하도록
"한 줄도 빠짐없이", 예제와 함께,
언제 값이 변하고, 언제 None이 나오는지, 왜 그런 일이 발생하는지를 디버깅처럼 따라가며 설명해줄게.


✅ 핵심 개념 요약 먼저 (틀리는 이유부터)

항목sort()sorted()
원본 리스트바뀐다 ✅안 바뀐다 ❌
리턴값None정렬된 새 리스트 ✅
사용 가능 대상리스트만 가능모든 반복 가능한 것
용도정렬만 하고 저장은 안 함정렬 결과를 리턴받아 저장 가능

✅ 1. sort() – 리스트 객체의 메서드 (원본을 정렬, 반환값 없음)

코드 예제:

a = [3, 1, 2]
result = a.sort()
print("a:", a)
print("result:", result)

💡 실행 순서 설명:

  1. a.sort()

    • 이 줄은 a 자체를 정렬함
    • 반환값은 None임 (아무것도 리턴하지 않음!)
  2. print("a:", a)

    • 출력: [1, 2, 3]
    • 이유: a는 정렬되었기 때문
  3. print("result:", result)

    • 출력: None
    • 이유: sort()값을 리턴하지 않음

❌ 실전 실수 예시

a = [3, 1, 2]
b = a.sort()
print(b[0])  # ❌ TypeError: 'NoneType' object is not subscriptable
  • 왜 터지냐?

    • a.sort()None을 반환하니까
    • bNone이 되고
    • b[0]None[0]이 되어 에러

✅ 2. sorted()새로운 정렬된 리스트를 반환하는 함수

코드 예제:

a = [3, 1, 2]
b = sorted(a)
print("a:", a)
print("b:", b)

💡 실행 순서:

  1. sorted(a)a를 복사해서 정렬한 새 리스트를 리턴
  2. 원본 a는 바뀌지 않음
  3. b[1, 2, 3]

🔄 시각적으로 보면

a = [3, 1, 2]       # 원본
b = sorted(a)       # 새로운 리스트 [1, 2, 3] 생성

✅ 실제 문제 예제로 정리

예제 1: 둘의 차이

a = [5, 2, 9]
b = a.sort()
print("a =", a)
print("b =", b)

출력:

a = [2, 5, 9]
b = None

예제 2: 올바른 정렬 저장 방법

a = [5, 2, 9]
b = sorted(a)
print("a =", a)  # [5, 2, 9]
print("b =", b)  # [2, 5, 9]

✅ key, reverse 옵션

둘 다 다음 옵션 사용 가능:

sorted(list, key=..., reverse=...)
list.sort(key=..., reverse=...)

예:

a = ['ccc', 'a', 'bb']
a.sort(key=len)
print(a)  # ['a', 'bb', 'ccc']

같은 결과:

a = ['ccc', 'a', 'bb']
b = sorted(a, key=len)
print(b)  # ['a', 'bb', 'ccc']

✅ 언제 sort()를 쓰고 언제 sorted()를 써야 할까?

상황선택이유
결과를 새로 저장하고 싶다sorted()새 리스트 반환
리스트 자체를 바꿔도 괜찮다sort()메모리 효율
리스트가 아닌 것을 정렬sorted()문자열, 튜플 등도 가능

✅ 실전에서 자주 나오는 트랩

🔥 트랩 1

a = [5, 3, 2]
b = a.sort()
print(b)  # ❌ None

b = sorted(a)로 바꿔야 원하는 결과가 저장됨


🔥 트랩 2: 내부 구조를 정렬하는 경우

a = [[3], [1], [2]]
a.sort()
print(a)  # [[1], [2], [3]]
  • 내부 리스트를 비교해서 정렬함 ([1] < [2] < [3])
  • 내부 구조가 커질수록 key=lambda x: x[0] 등을 써줘야 안전

✅ 복습 요약표

항목sort()sorted()
원본 변경✅ 함❌ 안 함
리턴값None정렬된 리스트
메모리 효율좋음새 리스트 만듦
사용 대상리스트 전용모든 반복형
실수 위험높음 (리턴 없음)낮음

좋아! 아주 좋은 질문이야. 지금 헷갈리는 포인트는 정말 많은 사람들이 실수하는 핵심 트랩이기도 해.
너가 말한 이 부분:

lst2.sort()  # 이거 때문에 None 나와야 하는 거 아니야?

이 문장 때문에 None이 나올 것처럼 헷갈린 이유를 완전히 정확하게 짚어서 설명해줄게.


✅ 핵심 결론 먼저:

lst2.sort()
  • lst2.sort() 자체는 None을 반환하지만,
  • 우리는 그 "리턴값"을 쓰지 않았기 때문에 아무 문제 없음!
  • lst2 리스트는 정상적으로 정렬되고, 값도 그대로 유지됨

✅ 왜 None이 안 나오냐면:

❌ 이런 식으로 써야 None이 들어감:

lst2 = lst2.sort()  # ← 이러면 lst2가 None이 됨 ❌

이건 정말 잘못된 코드야. 이렇게 쓰면 lst2가 아예 None으로 바뀌기 때문에 이후에 에러 발생함.


✅ 지금 코드에서는 이런 흐름임:

lst2 = lst[:]        # 얕은 복사
lst2.sort()          # 정렬하되, 반환값은 무시 (아무 변수에도 저장 안 함)
lst2[0][0] = 999     # 내부 값을 변경
  • lst2.sort()는 단지 lst2 리스트 자체를 정렬만 하고,
  • 리턴값은 어디에도 저장하지 않았기 때문에 lst2는 여전히 리스트다.

✅ 직접 비교 예시:

✅ 잘 된 코드:

a = [3, 1, 2]
a.sort()
print(a)   # ✅ [1, 2, 3]
  • a.sort()는 정렬만 함
  • a는 여전히 리스트

❌ 틀린 코드:

a = [3, 1, 2]
a = a.sort()   # ❌ 여기서 a가 None으로 바뀜
print(a)       # None
  • a.sort()None을 리턴함
  • 그걸 다시 a = 로 받아서 aNone이 되어버림

✅ 다시 네 코드 분석 요약

def modify_and_sort(lst):
    lst2 = lst[:]         # 얕은 복사 → lst2는 리스트
    lst2.sort()           # 정렬만 하고 리턴값은 안 씀 → lst2 여전히 리스트
    lst2[0][0] = 999      # 내부 요소 직접 변경
    return lst, lst2      # 둘 다 리스트 상태

어디에서도 .sort()의 리턴값을 변수에 저장하지 않았기 때문에, lst2는 None이 되지 않아!


✅ 핵심 요약 비교

코드설명
lst2 = lst[:]얕은 복사 (리스트 복사)
lst2.sort()정렬, 리턴값 무시
lst2 = lst2.sort()❌ lst2가 None이 되어버림
sorted(lst2)정렬된 새 리스트 리턴 (lst2 안 바뀜)

✅ 마지막 시각화 정리

lst2 = lst[:]        # 얕은 복사 → ✅ 새로운 리스트
lst2.sort()          # ✅ 정렬 (리턴값 사용 X)
lst2[0][0] = 999     # ✅ 내부 요소 변경

lst2는 여전히 리스트이고 정상 출력됨.


0개의 댓글