본문 바로가기
TYLER SONGBlog
블로그 목록
LLM Research

함수 · scope · 인자 · lambda: 코드를 값처럼 다루는 법 배우기

오늘은 반복되는 코드를 함수로 묶어서 재사용하는 법을 배운다. 함수 안팎의 변수가 서로 어떻게 영향을 주는지(스코프), 인자를 넘기는 여러 방법(위치·키워드·기본값·*args·**kwargs), 그리고 함수를 변수처럼 주고받는 lambda까지 다룬다. 이 감각은 나중에 모델 학습 함수, 평가 함수를 설계할 때 그대로 쓰인다.

송민성7분 읽기

오늘의 목표

같은 코드를 여러 번 복사-붙여넣기 하지 않고, "이 계산을 반복해서 쓸 거야"라는 부분을 함수로 뽑아낼 수 있게 된다. 그리고 그 함수를 다른 함수에 재료(인자)로 넘기는 것까지 할 수 있게 된다.

개념과 직관

함수는 "미리 만들어둔 요리 레시피"

def로 함수를 만드는 건 요리 레시피를 종이에 적어두는 것과 같다. 레시피 자체는 아무것도 안 하지만, 그 레시피대로 "실행"(함수 호출)하면 결과(요리, return 값)가 나온다.

python
def add(a, b): return a + b result = add(3, 5) # 레시피 실행 -> 8 print(result)
  • def 함수이름(인자들): 로 시작
  • return은 "결과를 밖으로 돌려준다"는 뜻. return이 없으면 함수는 None을 돌려준다.

지역 스코프(local scope) vs 전역 스코프(global scope)

함수 안에서 만든 변수는 그 함수 "안에서만" 존재하는 물건이다. 함수가 끝나면 사라진다. 반면 함수 밖에서 만든 변수는 전역 변수라서 어디서든 보인다(단, 함수 안에서 값을 바꾸려면 특별한 처리가 필요).

비유: 함수는 방(room)이고, 함수 안에서 만든 변수는 그 방 안에 있는 물건이다. 방문을 나가면(함수 종료) 그 물건은 못 쓴다. 하지만 집 전체(전역)에 있는 물건은 어느 방에서든 볼 수 있다.

python
x = 10 # 전역 변수 def show_x(): print(x) # 전역 변수를 읽는 건 가능 show_x() # 10 def change_x(): x = 20 # 이건 새 지역 변수를 만든 것! 전역 x와는 다른 변수 print(x) # 20 change_x() print(x) # 여전히 10 (전역 x는 안 바뀜)

여기서 초보자가 자주 헷갈리는 지점: 함수 안에서 x = 20이라고 쓰는 순간, 파이썬은 그걸 "새로운 지역 변수를 만든다"로 해석한다. 전역 x를 진짜로 바꾸고 싶으면 global x를 써야 하는데, 실무에서는 이렇게 전역 변수를 직접 바꾸는 걸 잘 안 한다(코드 추적이 어려워지기 때문). 대신 함수는 return으로 결과를 돌려주고, 호출하는 쪽에서 그 값을 받아 쓰는 방식을 쓴다.

인자(argument)를 넘기는 여러 방법

1) 위치 인자(positional argument): 순서대로 매칭

python
def greet(name, age): print(f"{name}, {age}살") greet("철수", 20) # name="철수", age=20

2) 키워드 인자(keyword argument): 이름을 지정해서 순서 상관없이

python
greet(age=20, name="철수") # 순서 바뀌어도 OK

3) 기본 인자(default argument): 값을 안 주면 쓸 기본값

python
def greet(name, age=0): print(f"{name}, {age}살") greet("영희") # age는 자동으로 0

4) ***args**: 몇 개가 올지 모르는 위치 인자들을 튜플(tuple)로 묶어서 받음

python
def add_all(*numbers): total = 0 for n in numbers: total += n return total print(add_all(1, 2, 3, 4)) # 10

5) `kwargs`**: 몇 개가 올지 모르는 키워드 인자들을 딕셔너리(dict)로 묶어서 받음

python
def print_info(**info): for key, value in info.items(): print(f"{key}: {value}") print_info(name="철수", age=20, city="서울")

비유: *args는 "이름 없는 짐 여러 개를 순서대로 받는 택배함", **kwargs는 "이름표 붙은 짐을 받는 사물함"이라고 생각하면 된다.

lambda: 이름 없는 짧은 함수

def로 함수를 만들 정도로 거창하지 않은, 한 줄짜리 간단한 함수가 필요할 때 쓴다.

python
square = lambda x: x ** 2 print(square(5)) # 25 # 아래 def와 완전히 같은 동작 def square2(x): return x ** 2

lambda의 진짜 쓸모는 함수를 다른 함수에 재료로 넘길 때다. 예를 들어 리스트를 정렬할 때 "무엇을 기준으로 정렬할지"를 함수로 전달할 수 있다.

python
students = [("철수", 90), ("영희", 85), ("민수", 95)] # 점수(두 번째 값) 기준으로 정렬 sorted_students = sorted(students, key=lambda s: s[1]) print(sorted_students) # [('영희', 85), ('철수', 90), ('민수', 95)]

여기서 key=lambda s: s[1]이 "각 항목에서 정렬 기준값을 뽑아내는 함수"를 통째로 넘긴 것이다. 함수를 숫자나 문자열처럼 "값"으로 취급해서 다른 함수의 인자로 넘길 수 있다는 게 핵심 개념이다.

코드로 직접 해보기

1단계: 통계 함수 모음 만들기

평균, 분산, 표준편차를 각각 함수로 만든다. 하나를 만들 때마다 이전 함수를 재사용한다.

python
def mean(numbers): return sum(numbers) / len(numbers) def variance(numbers): m = mean(numbers) squared_diffs = [(x - m) ** 2 for x in numbers] return mean(squared_diffs) def std_dev(numbers): return variance(numbers) ** 0.5 data = [2, 4, 4, 4, 5, 5, 7, 9] print("평균:", mean(data)) print("분산:", variance(data)) print("표준편차:", std_dev(data))

variance 함수가 mean 함수를 두 번 호출하는 걸 눈여겨보자. 이게 함수를 쪼개서 만드는 이유다 — 한 번 만든 로직을 재사용한다.

2단계: 정렬 기준을 인자로 받는 함수

"정렬 기준 자체를 바꿔가며 쓸 수 있는" 함수를 만든다.

python
def sort_by_key(items, key_func): return sorted(items, key=key_func) words = ["banana", "kiwi", "apple", "fig"] # 길이 기준 정렬 by_length = sort_by_key(words, lambda w: len(w)) print(by_length) # ['fig', 'kiwi', 'apple', 'banana'] # 알파벳 역순 기준 정렬 by_reverse_alpha = sort_by_key(words, lambda w: w[::-1]) print(by_reverse_alpha)

sort_by_key 함수는 "무엇을 정렬할지"뿐 아니라 "어떤 기준으로 정렬할지"까지 인자로 받는다. key_func가 함수를 담는 변수라는 게 핵심이다.

3단계: *args, **kwargs로 유연한 통계 함수 만들기

python
def summarize(*numbers, **options): result = { "mean": mean(list(numbers)), "count": len(numbers), } if options.get("include_std", False): result["std"] = std_dev(list(numbers)) return result print(summarize(1, 2, 3, 4, 5)) print(summarize(1, 2, 3, 4, 5, include_std=True))

오늘의 실험

정해진 실험은 없지만, 아래를 직접 바꿔보면서 스코프 감각을 확인해보자.

  • change_x() 예제에서 global x를 추가해보고 전역 x가 실제로 바뀌는지 확인
  • sort_by_key 함수에 lambda w: -len(w) (음수로 만들어서 내림차순) 같은 다른 기준을 넣어보고 결과 비교
  • mean 함수에 빈 리스트 []를 넣으면 어떤 에러가 나는지 직접 확인하고 에러 메시지를 읽어보기 (ZeroDivisionError가 왜 나는지 설명할 수 있어야 함)

결과와 에러 메시지를 짧게 메모해두면, 나중에 비슷한 에러를 만났을 때 "예전에 본 적 있다"는 감각이 생긴다.

왜 이걸 배우나 (LLM / Post-training 연결)

학습 루프(training loop), 평가 함수(evaluation function), 데이터 전처리 함수는 전부 오늘 배운 패턴으로 만들어진다. 예를 들어 "어떤 지표로 모델을 평가할지"를 함수로 넘기는 구조(오늘의 sort_by_key처럼)는 나중에 loss_fn이나 metric_fn을 인자로 받는 학습 코드에서 똑같이 나타난다. 또한 **kwargs는 PyTorch나 Hugging Face 라이브러리의 함수 시그니처에서 흔히 보게 되는데, 오늘 원리를 모르면 "저게 왜 저렇게 생겼지"를 이해하지 못하고 그냥 외우게 된다.

초보자가 막히는 지점

  • `return` 없이 끝나는 함수: 함수가 뭔가 계산은 하는데 return을 안 써서 결과를 못 받는 실수가 흔하다. print로 화면에 보여주는 것과 return으로 값을 돌려주는 건 완전히 다르다.
  • 지역 변수와 전역 변수 이름이 같을 때 헷갈림: 함수 안에서 전역 변수와 같은 이름으로 새 변수를 만들면, 그 순간부터 함수 안에서는 그 이름이 지역 변수를 가리킨다. "왜 전역 값이 안 바뀌지?"라는 질문이 나오면 오늘 배운 스코프 규칙을 떠올릴 것.
  • ***args, **kwargs 순서 실수**: 함수 정의할 때 def f(a, *args, b=1, **kwargs)처럼 순서가 있다. 일반 인자 → *args → 기본값 인자 → **kwargs 순서를 지켜야 한다. 순서를 틀리면 SyntaxError가 난다.
  • lambda 안에 여러 줄 코드나 `if`문(완전한 형태)을 넣으려는 시도: lambda는 표현식(expression) 하나만 담을 수 있다. 조건이 필요하면 lambda x: "짝수" if x % 2 == 0 else "홀수"처럼 조건 표현식(conditional expression)은 되지만, 여러 줄 로직이 필요하면 def로 바꿔야 한다.

알고리즘 (병행 1시간)

LeetCode에서 아래 두 문제를 각각 풀어본다.

  • Easy 1문제: 배열/문자열 구현 문제 중 하나 선택 (예: 배열을 순회하며 조건에 맞는 값을 세거나 찾는 유형)
  • Medium 1문제: 구현 또는 수학 유형 중 하나 선택 (예: 나머지·배수 관계를 이용한 계산 문제)

풀 때 아래 세 가지를 반드시 기록한다.

  1. 접근: 문제를 어떻게 쪼개서 풀었는지, 오늘 배운 함수 분리 습관을 적용했는지(예: 헬퍼 함수로 로직을 나눴는지)
  2. 시간복잡도: 내 풀이가 데이터 크기 n에 대해 대략 몇 단계(n, n제곱 등)로 도는지 어림잡아 적기
  3. 틀린 이유: 한 번에 안 풀렸다면 어디서 틀렸는지(엣지 케이스 누락, 인덱스 실수, 스코프 문제 등) 구체적으로 적기 — 이 기록이 나중에 비슷한 실수를 줄여준다.
© 2026 Tyler Song