더보기
C#을 공부한 이후 Python으로 다시 복귀했을 때, 함수의 기본 구조가 상당히 달라 해맸었다. 그래서 일부러 아주 기본적인 사항까지 정리하기로 했다.
1. 함수 (Function)
- 반복되는 코드를 모아 이름을 붙인 것
- 특정 목적의 작업을 수행하기 위해 독립적으로 설계된 코드의 집합
1 - 1. 함수의 특징
- 모듈성(재사용성) : 한 번 정의한 함수는 프로그램 내 여러 위치에서 반복 사용 가능
-
더보기Python 함수 정의와 변수 선언 순서 :
C#과 달리 Python은 인터프리터 언어라 한 가지 큰 차이가 생기는데, Python에서는 항상 함수를 정의한 위치보다 아래에 사용해야 한다. C#같은 컴파일링 언어는 함수를 위에서 사용하고 나중에 사용자 정의 함수들을 아래 쪽에, 아니면 객체나 기능 별로 한 곳 몰아두어도 문제가 되지 않지만, Python은 위에서 정의하지 않은 함수를 아래에 사용하려고 하면 정의된 적이 없다며 에러가 발생하기 때문이다.
그런데 가독성을 위해 함수 정의부를 한 곳에 몰아놓고 싶은데, 함수에서 외부 변수를 사용하도록 만들었다고 하자. 변수를 호출하려면 변수가 선언된 것보다 위여야 하고, 함수에서 변수를 선언하려면 함수는 변수보다 아래여야 하기 때문에 (변수 선언) - (함수 정의) - (함수 사용)으로 위치가 고정되야 할 것처럼 보인다. 이게 무슨 문제일까 싶을 수 있지만, 다르 사람이 짠 코드를 읽는데 로직 중간에 갑자기 다른 함수를 정의하고 있으면 가독성이 떨어진다는 걸 느낄 수 있다. 하지만 Python은 이 상황을 방지하기 위해, 함수 내에 아직 선언하지 않은 변수가 있어도 함수를 정의할 수 있다. 즉, 아직 사용하지 않은 변수가 사용되도 기다리고 있다가, 함수 사용 이전엔 해당 변수가 선언되어 있다면 그 변수를 찾아가서 참조할 수 있게 구현되어 있다. 그래서 적어도 (변수 선언 / 함수 정의) - (함수 사용)에서 변수 선언과 함수 정의의 순서는 바꿔도 상관 없다.
사실 위같은 상황이 없도록, 그리고 함수의 독립성을 위해 방지하기 위해 함수에서 사용하는 모든 변수를 함수에 인수로 전달하는 방법이 권장된다. 하지만 이 경우 인수를 잔뜩 전달해야 하고, 함수를 사용할 때에도 그 모든 인수를 다 전달해야 하므로, 당장 코드가 복잡하고 길어진다. 특히 코딩 테스트를 준비할 때 이 차이를 실감하게 되는데, 시험용으로 자주 쓰는 코드를 암기하거나 당장 시험 중 한시가 급한 상황에서는 그냥 외부 변수를 호출하도록 만들게 될 것이다.
추가로, 함수에서 함수 외부 변수를 호출한 경우에는 값을 읽어올 수만 있고, 값을 변경할 순 없다. 가령 함수 내에서 새로운 지역 변수 y에 대해 외부 변수(전역 변수) x를 사용해 y = x**2 같은 식으로 x를 읽어와 사용할 순 있지만, x = y**2 처럼 x에 새로운 값을 넣으려고 하면 UnboundLocalError 예외를 발생시킨다. (참고로 UnboundLocalError는 NameError의 하위 클래스이다. NameError는 변수에 접근 가능한 범위 문제에 관한 예외다.) 이는 의도치 않게 외부 변수의 값을 훼손해버리는 문제를 막기 위해서이다. 함수에서 강제적으로 외부 변수를 수정하고 싶다면 아래에 설명할 global 키워드를 사용해 함수 내에서 해당 변수를 수정할 것임을 미리 선언해야 한다. 다만, 역시나 권장되는 프로그래밍 방식은 아니다.
-
- 가독성 : 코드를 기능 단위로 분리 → 해당 코드의 역할을 이해하기 용이해짐
- 유지보수성 : 기능 수정이 필요할 경우, 정의된 함수만 수정하면 호출되는 모든 위치에서 동일하게 적용됨
- 추상성 : 함수 내부의 복잡한 처리 과정을 모두 이해할 필요 없이 사용하여 원하는 작업 수행 가능
1 - 2. Python 함수의 특징
- def 키워드로 함수 정의
- 매개변수(Parameter)와 반환값(return)에 타입을 미리 지정하지 않음
-
더보기함수의 타입 힌트 :
Python을 다시 공부할 때 가장 인상적이었던 부분인데, C#에서는 매개변수와 반환값에 모두 타입을 지정해줘야 하는데, Python은 그렇지 않다는 것이다. C#은 정적 타입 언어로, 컴파일 시점에서 변수와 연산의 타입을 미리 확정하여 어떤 연산이 가능한지, 사용된 변수의 타입이 맞는지, 그래서 코드에 잘못된 부분이 있는지 미리 확인할 수 있다. 그래서 IDE를 사용할 때 C#에서는 함수 내에서 매개변수, 반환값의 타입과 맞지 않는 코드를 작성하면 미리 알고 빨간 줄로 경고를 하고, 컴파일 자체가 되지 않는다. 그러나 Python에서는 함수를 정의할 땐 매개변수와 반환값의 타입을 지정하지 않고, 실행 단계에서 해당 변수로 불가능한 코드를 만나는 경우 예외를 발생시킨다.
Python에서도 미리 매개변수와 반환값의 변수 타입을 지정할 수 있는 방법이 있는데, 이를 타입 힌트(Type Hint)라고 한다. 매개변수의 경우 매개변수 뒤에 (매개변수) : (타입)로 타입을 미리 지정할 수 있으며, def 함수명(매개변수) -> (반환 타입) :를 통해 어떤 타입을 넣어 어떤 타입을 반환 받는지 미리 지정할 수 있다. IDE에서 함수를 사용할 때 help( ) 등에서 확인할 수 있는 매개변수, 반환값의 타입이 기능을 사용한 것이다. 컴파일링 언어들과는 달리 기본적으로 타입으로 인해 발생이 예상되는 예외들을 미리 확인할 순 없지만, 이것도 pytest 모듈을 통해 코드를 미리 테스트하여 확인할 수는 있다. 나중에 리팩토링에 관해 글을 쓸 일이 생기면 자세히 정리해 보자.
-
- 메서드 오버로딩을 지원하지 않음
-
더보기메서드 오버로딩 :
C#에서 메서드를 사용할 때 보면, 같은 메서드라도 인수를 전달하는 방법이 여러가지인 걸 알 수 있다. C#에서는 메서드를 정의할 때 같은 이름의 메서드를 여러개 정의해놓고 매개변수 구성을 다르게 할 수 있다. 이렇게 하면, 실제 컴파일 시에는 각 메서드들을 따로따로 인식해서 만들어 놓는다. 이게 가능한 이유는 C#에서는 메서드의 변수의 타입을 미리 정해놓고 사용하기 때문에, 같은 이름의 메서드를 사용하더라도 전달된 인수에 따라서 컴파일 단계에서 어떤 함수를 호출할지 컴퓨터는 미리 알 수 있기 때문이다. 즉 '같은 메서드를 사용한다'는 건 코드를 짜는 개발자 입장에서만 그렇게 느낄 뿐이고, 컴파일러 입장에서는 구별되는 별개의 메서드인 것이다.
그러나 functools.singledispatch라는 기능으로 인수 타입에 따라 다른 함수를 실행하는, C#의 메서드 오버로딩과 비슷한 기능을 만들 순 있다. 그러나 C#의 컴파일 타입 메서드 오버로딩과 원리는 다르다고 한다.
-
1 - 3. 함수의 종류
- 내장 함수(built-in) : 파이썬 자체에 포함되어 있는 함수
- input( ), print( ), len( ), str( ), int( ), float( ) 등, 외부 모듈 호출 없이 사용 가능
- 모듈 (Module) 함수 : 외부 모듈(관련된 함수 코드를 모아놓은 Python 파일)에 포함된 함수
- 사용자 지정 함수 : 프로그램 내에서 직접 정의한 함수
2. 네임스페이스 (Namespace : 이름 공간)
- 이름과 객체의 대응 관계를 저장해 두는 공간
- 하나의 네임스페이스 안에는 같은 이름 하나가 한 시점에 하나의 객체에만 바인딩 됨 → 같은 이름 중복 사용 불가
네임스페이스 종류
- Python의 네임스페이스는 일반적으로 LEGB 규칙으로 정리함
- Python이 이름을 읽을 때는 기본적으로 Local → Enclosing → Global → Built-in 순으로 찾음
- Local Namespace (지역 이름 공간) :
- 현재 실행 중인 함수 내부의 이름 공간
- 함수 호출 종료 시 메모리 상에서 제거됨
- Enclosing Namespace (바깥 함수의 이름 공간) :
- 중첩 함수가 있을 때 바깥 함수의 네임스페이스
- 바깥 함수의 변수를 수정하려면 nonlocal 키워드를 사용해야 함 (nonlocal x)
- Global Namespace (전역 이름 공간) :
- 현재 모듈(Python 파일) 전체의 이름 공간
- 전역 네임스페이스의 변수를 함수 내부에서 수정하려면 glbal 키워드를 사용해야 함 (global x)
- 프로그램 종료 시 메모리 상에서 제거됨
- 모든 함수에서 접근 가능
- Built-in Namespace (내장 이름 공간) :
- Python이 기본적으로 제공하는 이름들이 들어있는 이름 공간
- print( ), len( ), int( ), str( ), range( )등 Built-in 함수들이 사용함
- 같은 이름으로 변수를 재정의할 순 있으나, 기존의 Built-in 함수 사용이 막힘
2 - 1. global
- 함수 내부에서 외부 전역 변수에 값을 변경할 때 사용하는 키워드
- 함수 내부에서 해당 이름의 지역 변수 생성을 차단
x = 10
def func():
global x # 이하 사용하는 x가 전역 변수 x임을 선언
x = 20 # 전역 변수 x에 새로운 값 할당
print(x) # >>> 20
2 - 2. nonlocal
- 중첩 함수에서 바로 바깥 함수 영역의 변수의 값을 변경할 때 사용하는 키워드
def outer():
x = 10
def inner():
nonlocal x # 이하 사용하는 x가 outer의 x임을 선언
x = 20 # outer의 x에 새로운 값 할당
inner()
print(x)
outer() # >>> 20