[7일차-1] Python

송정근·2026년 5월 28일

사용자 정의 함수와 객체지향 프로그래밍 정리

파이썬에서 함수와 클래스는 코드를 구조화하는 핵심 도구이다.

함수는 중복되는 작업을 묶고, 클래스는 데이터와 동작을 하나의 객체로 표현할 수 있게 해준다.

이번 글에서는 변수의 범위, 클로저, 콜백함수, 람다 함수, 프로그래밍 방법론, 클래스와 객체지향 개념을 예제와 함께 정리한다.

사용자 정의 함수

1. 변수의 범위

변수의 범위 (scope) 는 변수를 참조하거나 변경할 수 있는 영역을 의미한다.

파이썬은 이름을 찾을 때 LEGB 규칙 을 따른다.

순서범위의미
LLocal현재 함수 내부 범위
EEnclosing내부 함수를 감싸는 외부 함수의 범위
GGlobal모듈 전체 범위
BBuilt-in파이썬이 기본으로 제공하는 내장 범위

파이썬은 변수를 사용할 때 Local ➡ Enclosing ➡ Global ➡ Built-in순서로 이름을 찾는다.

1.1 지역 변수

지역 변수(local variable) 는 함수 내부에서 정의된 변수이다. 함수가 실행되는 동안에만 사용할 수 있으며, 함수 밖에서는 접근할 수 없다.

def local_example():
  local_var = "로컬 변수"
  print(local_var)
  
local_example()

# print(local_var) # NameError: name 'local_var' is not defined

출력 결과 :

로컬 변수

local_var 는 local_example() 함수 안에서 만들어졌기 때문에 함수 내부에서만 사용할 수 있다.

1.2 전역 변수

전역 변수(global variable) 는 함수 밖, 즉 모듈 전체 범위에 정의된 변수이다. 함수 내부에서도 읽을 수 있다.

global_var = "전역 변수"

def test_global_scope():
  print(global_var)
  
test_global_scope()

출력 결과 :

전역 변수

다만 함수 내부에서 전역 변수의 값을 변경하려면 global 키워드가 필요하다.

global_var = 10

def modify_global():
  global global_var
  global_var = 20
  
modify_global()
print(global_var)

출력 결과 :

20

global 을 사용하면 함수 내부에서 전역 변수 자체를 수정할 수 있다. 하지만 전역 변수 변경은 코드 흐름을 추적하기 어렵게 만들 수 있으므로 꼭 필요한 경우에만 사용하는 편이 좋다.

1.3 둘러싼 범위와 클로저

함수 안에 또 다른 함수를 정의하면, 내부 함수는 외부 함수의 지역 변수를 참조할 수 있다. 이때 외부 함수의 범위를 둘러싼 범위(enclosing scope) 라고 한다.

def outer_function():
  enclosing_var = "둘러싼 범위 변수"
  
  def inner_function():
    print(enclosing_var)
    
  return inner_function

f = outer_function()
f()

출력 결괴 :

둘러싼 범위 변수

outer_function() 의 실행이 끝났는데도 inner_function() 은 enclosing_var를 사용할 수 있다. 내부 함수가 외부 함수의 변수를 계속 참조하기 때문에 파이썬이 해당 값을 콜로져 공간에 보관하기 때문이다.

1.4 클로저 공간

클로저(closure)는 내부 함수가 외부 함수의 변수를 기억하는 구조이다. 일반적으로 함수가 종료되면 그 안의 지역 변수는 사라진다. 하지만 내부 함수가 외부 함수의 변수를 참조하고 있다면 파이썬은 그 값을 바로 제거하지 않고 함수 객체 안에 보관한다.

def make_counter():
  count = 0
 	
  def counter():
    return count + 1
  
  return counter

counter_func = make_counter()
print(counter_func()) # 1

출력 결과 :

1

예제 :

def outer_function(msg):
  message = "hello, " + msg
   
  def inner_function():
  	print(message)
  
  return inner_function

func = outer_function("Python") # 클로저 생성

print(func()) # "hello, Python" 출력

출력 결과 :

hello Python

출력 과정 :

  1. outer_function 함수에 "Python" 를 매개변수 값으로 넘겨주며 실행한다.
  2. message 변수에 매개변수를 이용해 "hello Python" 라는 문자열을 저장한다.
  3. inner_fuction 함수가 message변수를 참조
  4. inner_function 함수 리턴
  5. func 변수가 inner_function 함수를 참조
  6. func 변수 실행(inner_function 함수 실행)
  7. func변수는 message변수를 출력

➡ 4번 단계에서 outer_function 함수는 역할을 마치고 종료되었다. 함수가 종료되면 원래 메모리에서 삭제되어야 한다. 하지만 삭제되지 않고 메모리에 남은 이유가 closure 공간 때문이다.

closure 가 성립하려면 3가지 조건이 필요하다.

  1. 함수 안에 함수가 있어야한다. (중첩 함수)
  2. 내부 함수가 외부 함수의 변수를 참조해야 한다.
  3. 외부 함수가 내부 함수를 반환해야 한다.

클로저에 저장된 값은 함수 객체의 __closure__ 속성에서 확인할 수 있다.

print(func.__closure__)
print(type(func.__closure))

출력 결과 :

(<cell at 0x108434f10: str object at 0x10849d2b0>,)
<class 'tuple'>
print(dir(func.__closure__[0]))

출력 결과 :

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']

마지막에 cell_contents 값이 있는데 이 부분을 출력해보자.

print(func.__closure__[0].cell_contents)

출력 결과 :

hello, Python

message 가 여기에 저장된다. 즉 클로저가 저장되는 경로는 __closure__[0].cell_contents 정도로 생각할 수 있다.

__closure__ 튜플은 모든 함수 객체가 가지고 있다. 다만 조건을 만족하지 않아 클로저가 생성되지 않으면, 그 값이 None 으로 고정된다.

클로저의 장점

  1. 함수가 상태를 기억할 수 있다.
  2. 전역 변수 없이 데이터 유지가 가능하다.
  3. 데이터 은닉이 가능하다.
  4. 함수별 독립 상태 생성이 가능하다.
  5. 데코레이터 구현이 가능하다.

클로저에서 외부 함수의 변수를 수정해야 한다면 nonlocal 키워드를 사용한다.

def make_counter():
  count = 0
  
  def counter():
    nonlocal count
    count += 1
    return count
 	
  return count

counter_func = make_counter()

print(counter_fun()) # 1
print(counter_fun()) # 2
print(counter_fun()) # 3

global 은 전역 변수를 수정할 때 사용하고, nonlocal 은 바깥 함수의 지역 변수를 수정할 때 사용한다.

1.5 빌트인(Built-in) 범위

빌트인(built-int) 범위에는 파이썬이 기본으로 제공하는 이름들이 들어 있다. print() , len(), type() , range() 같은 함수가 여기에 해당한다.

print(len([1, 2, 3])) # 3
print(type("Python")) # <class 'str'> 

내장 함수와 같은 이름으로 변수를 만들면 내장 함수가 가려질 수 있다.

len = 5

# print(len([1, 2, 3])) # TypeError: 'int' object is not callable

위 코드에서 len 은 더 이상 내장 함수가 아니라 정수 5 를 가리키는 변수로 인식된다. 따라서 len() 처럼 호출 할 수 있다.

내장 함수 이름인 len, list, dict, sum type 등을 변수명으로 사용하는 것은 피하는 것이 좋다.

2. 함수 객체와 메모리

파이썬에서 함수도 객체이다. 즉, 함수는 변수에 저장할 수 있고, 다른 함수에 전달할 수 있으며, 필요 없어지면 가비지 컬렉션 대상이 될 수 있다.

2.1 함수도 변수에 담을 수 있다.

def greet():
  print("안녕하세요.")
  
hello = greet

greet()
hello()

출력 결과 :

안녕하세요.
안녕하세요.

hello = greet 는 함수를 실행하는 코드가 아니다. greet 함수 객체에 hello 라는 이름을 하나 더 붙이는 코드이다.

2.2 del은 이름을 삭제한다.

del 은 함수 객체 자체를 즉시 삭제한다기보다, 해당 객체를 가리키는 이름을 제거한다.

def func1():
  print("처음으로 만드는 함수")
 
func1()

del func1

# func1() # NameError: name 'func1' is not defined

출력 결과 :

처음으로 만드는 함수

함수를 가리키는 다른 참조가 남아 있다면 함수 객체는 계속 사용할 수 있다.

def func1():
  	print("함수 객체")

alias = func1
del func1

alias() # 함수 객체

출력 결과 :

함수 객체

함수 객체를 참조하는 이름이나 자료구조가 더 이상 없으면 파이썬은 해당 객체를 나중에 메모리에서 정리할 수 있다. 단, 정확히 언제 제거되는지는 구현과 상황에 따라 달라질 수 있다.

3. 콜백 함수

콜백 함수(callback function) 는 다른 함수에 인자로 전달되어, 특정 시점에 나중에 호출되는 함수이다. 파이썬에서는 함수도 객체이므로 함수의 인자로 다른 함수로 전달할 수 있다.

3.1 기본 콜백 예제

def say_hello(name):
  print(f"{name}님 안녕하세요.")
  
del execute(callback):
  print("작업을 시작한다.")
  callback("김사과")
  print("작업을 종료한다.")
  
execute(say_hello)

출력 결과 :

작업을 시작한다.
김사과님 안녕하세요.
작업을 종료한다.

위 코드에서 say_hello가 콜백 함수이다. execute() 는 직접 인사 문구를 알 필요 없이, 전달받은 함수를 필요한 시점에 호출한다.

3.2 공통 흐름은 유지하고 세부 동작만 바꾸기

콜백 함수는 공통 작업은 그대로 두고, 세부 동작만 바꾸고 싶을 때 유용하다.

def print_score(score):
    print(f"점수: {score}")

def print_pass_fail(score):
    if score >= 80:
        print(f"{score}점: 합격")
    else:
        print(f"{score}점: 불합격")

def print_bonus_score(score):
    print(f"보너스 적용 점수: {score + 5}")

def process_scores(scores, callback):
    for score in scores:
        callback(score)

scores = [80, 95, 70, 100, 60]

print("원점수 출력")
process_scores(scores, print_score)

print("합격 여부 출력")
process_scores(scores, print_pass_fail)

print("보너스 점수 출력")
process_scores(scores, print_bonus_score)

출력 결과 :

원점수 출력
점수: 80
점수: 95
점수: 70
점수: 100
점수: 60
합격 여부 출력
80점: 합격
95점: 합격
70점: 불합격
100점: 합격
60점: 불합격
보너스 점수 출력
보너스 적용 점수: 85
보너스 적용 점수: 100
보너스 적용 점수: 75
보너스 적용 점수: 105
보너스 적용 점수: 65

process_scores() 는 점수 목록을 순회하는 공통 로직만 담당한다. 점수를 어떻게 처리할지는 콜백 함수가 결정한다.

3.3 sorted() 의 key 도 콜백이다.

파이썬의 sorted() 함수는 key 인자로 함수를 받을 수 있다. sorted() 는 각 요소를 비교하기 전에 key 함수를 호출해 정렬 기준값을 얻는다.

students = [
    {"name": "김사과", "score": 90},
    {"name": "반하나", "score": 75},
    {"name": "오렌지", "score": 100}
]

def get_score(student):
    return student["score"]

result = sorted(students, key=get_score)

print(result)

출력 결과 :

[{'name': '반하나', 'score': 75}, {'name': '김사과', 'score': 90}, {'name': '오렌지', 'score': 100}]

이 코드에서는 get_score() 가 콜백 함수 역할을 한다.

3.4 콜백 함수의 필수조건

  1. 객체로서의 전달 : 콜백 함수는 호출될 때 괄호 () 를 제외한 함수 이름(객체 자체)을 인자로 넘겨야 한다.
  2. 호출 가능(Callable) : 전달받는 메인 함수 내부에서 콜백 함수를 실행할 수 있어야 한다. (일반 함수, 람다 함수, 클래스 메서드 등 모두 가능)
  3. 서명(Signature) 일치 : 메인 함수가 콜백 함수를 호출할 때 전달하는 인자(Arguments)의 개수와 형태를 콜백 함수가 정상적으로 받을 수 있어야 한다.

4. 람다 함수

람다 함수(lambda function) 는 이름 없이 간단한 함수를 한 줄로 정의하는 익명 함수이다. 보통 짧고 일회성으로 사용할 함수를 만들 때 활용한다.

lambda 매개변수 : 반활한_표현식

4.1 일반 함수와 람다 함수 비교

def square(x):
  return x * x

print(square(5)) # 25

square_lambda = lambda x : x * x

print(square_lambda(5)) # 25

출력 결과 :

25
25

람다 함수는 표현식 하나만 가질 수 있다. 여러 줄의 복잡한 로직이 필요하다면 def 로 일반 함수를 정의하는 편이 좋다.

4.2 여러 매개변수 사용하기

multiply = lambda a, b: a * b

print(multiply(3, 4)) # 12

출력 결과 :

12

4.3 조건부 표현식과 함께 사용하기

pass_fail = lambda score: "합격" if score >= 60 else "불합격"

print(pass_fail(80)) 
print(pass_fail(45)) 
합격
불합격

4.4 sorted() 에서 람다 사용하기

짧은 정렬 기준 함수는 람다로 표현하면 간결하다.

students = [
    {"name": "김사과", "score": 90},
    {"name": "반하나", "score": 75},
    {"name": "오렌지", "score": 100},
]

result = sorted(students, key=lambda student: student["score"], reverse=True)

print(result)

출력 결과 :

[{'name': '오렌지', 'score': 100}, {'name': '김사과', 'score': 90}, {'name': '반하나', 'score': 75}]

4.5 map() 과 filter()

map() 은 반복 가능한 객체의 각 요소에 같은 작업을 적용할 때 사용한다.

scores = [70, 80, 90]

result = list(map(lambda score: score + 10, scores))
print(result)

출력 결과 :

[80, 90, 100]

filter() 조건을 만족하는 값만 골라낼 때 사용한다.

socres = [45, 70, 88, 52, 100]

passed = list(filter(lambda score: score >= 60, scores))
print(passed)

출력 결과 :

[70, 88, 100]

단순한 변환이나 필터링은 리스트 컴프리헨션으로 더 읽기 쉽게 작성할 수도 있다.

scores = [45, 70, 88, 52, 100]

bonus_scores = [score + 10 for score in scores]
passed = [score for score in scores if score >= 60]

print(bonus_scores)
print(passed)

출력 결과 :

[55, 80, 98, 62, 110]
[70, 88, 100]

출처 및 참고 자료

본 정리는 강의 내용과 아래 자료를 참고하여 학습 목적으로 작성하였습니다.

https://ryuzyproject.tistory.com/287

profile
기록하며 성장하는 개발자

0개의 댓글