파이썬에서 함수와 클래스는 코드를 구조화하는 핵심 도구이다.
함수는 중복되는 작업을 묶고, 클래스는 데이터와 동작을 하나의 객체로 표현할 수 있게 해준다.
이번 글에서는 변수의 범위, 클로저, 콜백함수, 람다 함수, 프로그래밍 방법론, 클래스와 객체지향 개념을 예제와 함께 정리한다.
변수의 범위 (scope) 는 변수를 참조하거나 변경할 수 있는 영역을 의미한다.
파이썬은 이름을 찾을 때 LEGB 규칙 을 따른다.
| 순서 | 범위 | 의미 |
|---|---|---|
| L | Local | 현재 함수 내부 범위 |
| E | Enclosing | 내부 함수를 감싸는 외부 함수의 범위 |
| G | Global | 모듈 전체 범위 |
| B | Built-in | 파이썬이 기본으로 제공하는 내장 범위 |
파이썬은 변수를 사용할 때 Local ➡ Enclosing ➡ Global ➡ Built-in순서로 이름을 찾는다.
지역 변수(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() 함수 안에서 만들어졌기 때문에 함수 내부에서만 사용할 수 있다.
전역 변수(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 을 사용하면 함수 내부에서 전역 변수 자체를 수정할 수 있다. 하지만 전역 변수 변경은 코드 흐름을 추적하기 어렵게 만들 수 있으므로 꼭 필요한 경우에만 사용하는 편이 좋다.
함수 안에 또 다른 함수를 정의하면, 내부 함수는 외부 함수의 지역 변수를 참조할 수 있다. 이때 외부 함수의 범위를 둘러싼 범위(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를 사용할 수 있다. 내부 함수가 외부 함수의 변수를 계속 참조하기 때문에 파이썬이 해당 값을 콜로져 공간에 보관하기 때문이다.
클로저(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
출력 과정 :
outer_function 함수에 "Python" 를 매개변수 값으로 넘겨주며 실행한다.message 변수에 매개변수를 이용해 "hello Python" 라는 문자열을 저장한다.inner_fuction 함수가 message변수를 참조inner_function 함수 리턴func 변수가 inner_function 함수를 참조func 변수 실행(inner_function 함수 실행)func변수는 message변수를 출력➡ 4번 단계에서 outer_function 함수는 역할을 마치고 종료되었다. 함수가 종료되면 원래 메모리에서 삭제되어야 한다. 하지만 삭제되지 않고 메모리에 남은 이유가 closure 공간 때문이다.
closure 가 성립하려면 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 으로 고정된다.
클로저의 장점
클로저에서 외부 함수의 변수를 수정해야 한다면 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은 바깥 함수의 지역 변수를 수정할 때 사용한다.
빌트인(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,sumtype등을 변수명으로 사용하는 것은 피하는 것이 좋다.
파이썬에서 함수도 객체이다. 즉, 함수는 변수에 저장할 수 있고, 다른 함수에 전달할 수 있으며, 필요 없어지면 가비지 컬렉션 대상이 될 수 있다.
def greet():
print("안녕하세요.")
hello = greet
greet()
hello()
출력 결과 :
안녕하세요.
안녕하세요.
hello = greet 는 함수를 실행하는 코드가 아니다. greet 함수 객체에 hello 라는 이름을 하나 더 붙이는 코드이다.
del 은 함수 객체 자체를 즉시 삭제한다기보다, 해당 객체를 가리키는 이름을 제거한다.
def func1():
print("처음으로 만드는 함수")
func1()
del func1
# func1() # NameError: name 'func1' is not defined
출력 결과 :
처음으로 만드는 함수
함수를 가리키는 다른 참조가 남아 있다면 함수 객체는 계속 사용할 수 있다.
def func1():
print("함수 객체")
alias = func1
del func1
alias() # 함수 객체
출력 결과 :
함수 객체
함수 객체를 참조하는 이름이나 자료구조가 더 이상 없으면 파이썬은 해당 객체를 나중에 메모리에서 정리할 수 있다. 단, 정확히 언제 제거되는지는 구현과 상황에 따라 달라질 수 있다.
콜백 함수(callback function) 는 다른 함수에 인자로 전달되어, 특정 시점에 나중에 호출되는 함수이다. 파이썬에서는 함수도 객체이므로 함수의 인자로 다른 함수로 전달할 수 있다.
def say_hello(name):
print(f"{name}님 안녕하세요.")
del execute(callback):
print("작업을 시작한다.")
callback("김사과")
print("작업을 종료한다.")
execute(say_hello)
출력 결과 :
작업을 시작한다.
김사과님 안녕하세요.
작업을 종료한다.
위 코드에서 say_hello가 콜백 함수이다. execute() 는 직접 인사 문구를 알 필요 없이, 전달받은 함수를 필요한 시점에 호출한다.
콜백 함수는 공통 작업은 그대로 두고, 세부 동작만 바꾸고 싶을 때 유용하다.
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() 는 점수 목록을 순회하는 공통 로직만 담당한다. 점수를 어떻게 처리할지는 콜백 함수가 결정한다.
파이썬의 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() 가 콜백 함수 역할을 한다.
() 를 제외한 함수 이름(객체 자체)을 인자로 넘겨야 한다.람다 함수(lambda function) 는 이름 없이 간단한 함수를 한 줄로 정의하는 익명 함수이다. 보통 짧고 일회성으로 사용할 함수를 만들 때 활용한다.
lambda 매개변수 : 반활한_표현식
def square(x):
return x * x
print(square(5)) # 25
square_lambda = lambda x : x * x
print(square_lambda(5)) # 25
출력 결과 :
25
25
람다 함수는 표현식 하나만 가질 수 있다. 여러 줄의 복잡한 로직이 필요하다면 def 로 일반 함수를 정의하는 편이 좋다.
multiply = lambda a, b: a * b
print(multiply(3, 4)) # 12
출력 결과 :
12
pass_fail = lambda score: "합격" if score >= 60 else "불합격"
print(pass_fail(80))
print(pass_fail(45))
합격
불합격
짧은 정렬 기준 함수는 람다로 표현하면 간결하다.
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}]
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]
본 정리는 강의 내용과 아래 자료를 참고하여 학습 목적으로 작성하였습니다.