Composing Programs - 3

Defining New Functions
지금까지 파이썬을 통해서 강력한 프로그래밍 언어라면 반드시 갖추어야 할 몇가지 요소를 확인했다.
- 숫자와 산술 연산은 기본적인 내장 데이터 값 및 함수이다.
- 중첩된 함수 호출은 연산을 결합하는 수단을 제공한다.
- 이름을 값에 바인딩하는 것은 제한적인 추상화 수단을 제공한다.
이제 함수 정의에 대해 배워보자. 이는 이름을 복합 연산에 바인딩하여 이를 하나의 단위로 참조할 수 있게 해주는 훨씬 더 강력한 추상화 기법이다.
먼저 제곱이라는 개념을 어떻게 표현할지 살펴보자. 무언가를 제곱한다는 것은 그 수를 그 자체와 곱하는 것 이라고 말할 수 있다. 이는 파이썬에서 아래와 같이 표현할 수 있다.

이는 square라는 이름이 부여된 새로운 함수를 정의한다. 이 사용자 정의 함수는 인터프리터에 내장되어 있지 않다. 이 정의에서 x는 형식 매개변수라고 하며, 곱할 대상에 이름을 부여한다.
함수를 정의하는 방법은 이름을 나타내는 def문과 쉼표로 구분된 명명된 형식 매개변수 목록, 그리고 함수가 호출될 때마다 평가될 반환식을 지정하는 return 문으로 구성된다.
def (이름) (형식 매개변수) : return (반환 식)
두 번째 줄은 들여쓰기를 해야 한다. 대부분의 프로그래머는 4개의 공백을 사용하여 들여쓰기를 한다. 반환 표현식은 즉시 평가되지 않고, 새로 정의된 함수의 일부로 저장되었다가 나중에 함수가 호출될 때만 평가된다.
square를 정의 했으므로 호출 표현식을 사용하여 적용할 수 있다.

또한, square를 다른 함수를 정의하는 데 필요한 구성요소로 사용할 수도 있다. 예를 들어, 두 개의 숫자를 인수로 받아 그 제곱의 합을 반환하는 sum_squares 함수를 쉽게 정의할 수 있다.

사용자 정의 함수는 내장 함수와 정확히 동일한 방식으로 사용된다. 실제로 sum_squares의 정의만으로 square가 인터프리터에 내장된 함수인지, 모듈에서 가져온 함수인지, 아니면 사용자가 정의한 함수인지 알 수 없다.
def문과 대입문은 모두 이름을 값에 바인딩하며, 기존에 존재하던 바인딩은 모두 사라진다. 예를 들어 아래의 g는 처음에는 인수가 없는 함수를 가리키고, 그 다음에는 숫자를 가리키며, 그 다음에는 2개의 인수를 갖는 다른 함수를 가리킨다.

Environments
이제 우리가 다루는 파이썬의 하위 집합은 프로그램의 의미가 직관적으로 파악되지 않을 만큼 복잡해졌다. 만약 형식 매개변수의 이름이 내장 함수의 이름과 같다면 어떻게 될까? 두 함수가 혼동 없이 이름을 공유할 수 있을까? 이러한 의문을 해결하기 위해서는 환경을 더 자세히 알아볼 필요가 있다.
표현식이 평가되는 환경은 상자로 푯된 일련의 프레임으로 구성된다. 각 프레임에는 바인딩이 포함되어 있으며, 각 바인딩은 이름과 그에 해당하는 값을 연결한다. 전역 프레임은 하나뿐이다. 할당문과 import문은 현재 환경의 첫번째 프레임에 항목을 추가한다.
지금까지 우리의 환경은 전역 프레임으로만 구성되어 있었다.

이 환경 다이어 그램은 현재 환경의 바인딩과 각 이름에 할당된 값을 보여준다.
예를 들어, import pi를 했을 때 pi에는 3.1416이라는 값이 적용되어 있고 tau에는 6.2832가 적용되어있는 것을 오른쪽 파란색 환경 프레임에서 확인할 수 있다.
함수도 환경 다이어그램에 나타난다. import문은 이름을 내장 함수에 바인딩한다. def문은 이름을 정의에 의해 생성된 사용자 정의 함수에 바인딩한다. mul을 import하고 square를 정의 한 후에 환경은 아래와 같다.

환경 다이어그램에서 각 함수들은 func으로 시작하는 한 줄로, 구성되며 그 뒤에 함수명과 매개변수가 이어진다. mul과 같은 내장함수는 형식 매개변수 이름이 없으므로 환경에서 ...이 사용된다.
함수 이름은 코드 내에서 한 번, 함수 자체의 일부로서 한 번, 총 두 번 반복된다. 함수 내에 나타나는 이름을 내재적 이름이라고 한다. 코드 내의 이름은 바인딩된 이름이라고 한다. 이 둘 사이에는 차이가 있다. 서로 다른 이름이 동일한 함수를 가리킬 수 있지만, 그 함수 자체는 단 하나의 고유 이름만을 가진다.
코드 내에서 함수에 바인딩 된 이름이 평가 과정에서 사용된다. 함수의 고유 이름은 평가 과정에서 아무런 역할을 하지 않는다. 아래 예제에서는 이름 max가 값 3에 바인딩된 후에는 더 이상 함수로 사용되지 않음을 알 수 있다.

TypeError: 'int' object is not callable 라는 오류 메세지는 max라는 이름(현재 숫자 3에 바인딩 되어있음)이 함수가 아닌 정수이므로, 호출 표현식에서 연산자로 사용할 수 없을을 나타낸다.
Function signatures 함수는 혀용되는 인자의 개수에 따라 서로 다르다. 이러한 요구 사항을 파악하기 위해 각 함수는 환경에서 함수 이름과 형식 매개변수가 보이도록 표기한다. 사용자 정의 함수 square는 x만 받는다. 인수를 더 많이 또는 적게 전달하면 오류가 발생한다.
함수의 형식 매개변수에 대한 설명을 함수의 시그니처라고 한다.
함수 max는 임의의 개수의 인수를 받을 수 있다. 이는 max(...)로 표시된다. 인수의 개수와 관계없이 모든 내장 함수는 name(...)으로 표시된다. 이는 이러한 기본 함수들이 명시적으로 정의된 적 없기 때문이다.
Calling User-Defined Functions
연산자가 사용자 정의 함수를 참조하는 호출 표현식을 평가하기 위해, 파이썬 인터프리터는 일련의 계산 과정을 따른다. 다른 호출 표현식과 마찬가지로, 인터프리터는 연산자 및 피연산자 표현식을 평가한 다음, 그 결과로 얻은 인자에 명시된 함수를 적용한다.
사용자 정의 함수를 적용하면 해당 함수에서만 접근할 수 있는 두번째 로컬 프레임이 생성된다.
사용자 정의 함수를 인자에 적용하려면
- 새로운 로컬 프레임에서 인자를 함수의 형식 매개변수 이름에 바인딩한다.
- 이 프레임으로 시작하는 환경에서 함수의 본문을 실행한다.
함수 본문이 평가되는 환경은 두개의 프레임으로 구성된다. 첫번째는 형식 매개변수 바인딩을 포함하는 로컬 프레임이고, 두 번째는 그 외 모든 것을 포함하는 전역 프레임이다. 함수 호출의 각 인스턴스는 자체적인 독립적인 로컬 프레임을 가진다.
예를 자세히 설명하기 위해 동일한 예제의 대한 환경 다이어그램의 여러단계를 아래에서 참고해보자. 첫번째 import문을 실행한 후에는 전역 프레임에 mul이라는 이름만 바인딩 되어있다.

그 다음 squre 함수의 정의문이 실행된다. def문 전체가 한번에 처리되는 것을 유의하자. 함수의 본문은 함수가 호출될 때까지 (정의 될때까지가 아니라) 실행되지 않는다.

다음으로 square 함수가 인수 -2를 받아 호출되므로, 형식 매개변수 x 가 값 -2에 바인딩된 새로운 프레임이 생성된다.

그런 다음, x라는 이름이 표시된 두 프레임으로 구성된 형재 환경에서 검색된다. 두 경우 모두 x의 값은 -2로 평가되므로, 제곱 함수는 4를 반환한다.

square() 프레임 내의 반환값은 이름 바인딩이 아니다. 대신, 이 값은 해당 프레임을 생성한 함수 호출이 반환한 값을 나타낸다.
이 간단한 예제에서도 2개의 서로 다른 환경이 사용된다. 최상위 표현식 square(-2)는 전역 환경에서 평가되는 반면, 반환 표현식 mul(x, x)는 square 호출로 생성된 환경에서 평가된다. x와 mul은 모두 이 환경에 바인딩 되어있지만, 서로 다른 프레임에 속한다.
환경 내 프레임의 순서는 표현식에서 이름을 조회할 때 반환되는 값에 영향을 미친다.
Name Evaluation 이름은 현재 환경 내에서 해당 이름이 발견되는 가장 앞쪽 프레임에 바인딩 된 값으로 평가된다.
Example : Calling a User-Defined Function
사용자 정의 함수에 대한 호출 표현식을 평가하는 과정을 설명하기 위해 다시 한 번 두 개의 간단한 함수 정의를 살펴보고 그 절차를 예로 들어보자.

파이썬은 먼저 글로벌 프레임에서 사용자 정의 함수에 바인딩 된 이름인 sum_squares를 평가한다. 기본 숫자 표현식인 5와 12는 각각 해당 숫자로 평가된다.
그다음, 파이썬은 sum_squares를 적용하며, 이 과정에서 x를 5에 y를 12에 바인딩하는 로컬 프레임을 생성한다.

sum_squares의 본문은 아래와 같은 호출 표현식을 포함하고 있다.

이 3가지 하위 표현식은 모두 sum_squares라고 표시된 프레임에서 시작되는 현재 환경 내에서 평가된다. 연산자의 하위 표현식인 add는 전역 프레임에 있는 이름으로, 덧셈을 수행하는 내장 함수에 바인딩 되어 있다. 덧셈이 적용되기 전에 2개의 피연산자 하위 표현식이 순서대로 평가되어야 한다. 두 피연산자 모두 sum_squares 프레임에서 시작되는 현재 환경에서 평가된다.
피연산자 0의 경우 square는 전역 프레임에 있는 사용자 정의 함수를 가리키며, x는 로컬 프레임에 있는 숫자 5를 가리킨다. 파이썬은 x를 5에 바인딩하는 또 다른 로컬 프레임을 생성하여 square 함수를 5에 적용한다.

이 환경을 사용하여 표현식 mul(x, x)는 25로 평가된다. 이제 평가 절차는 피연산자 1로 넘어가며 여기서 y는 숫자 12를 가리킨다. 파이썬은 square의 본문을 다시 한번 평가하는데, 이번에는 x를 12에 바인딩하는 또 다른 로컬 프레임을 새로 생성하고 피연산자 1을 144로 평가한다.

마지막으로 인자 25와 144에 덧셈을 적용하면, sum_squares의 최종 반환 값인 169가 산출된다.

이 예제는 지금까지 우리가 발전시켜 온 많은 근본적인 개념들을 잘 보여준다. 이름들은 값에 바인딩 되며, 이 값들은 공유된 이름들을 담고 있는 단 하나의 전역 프레임과 더불어 여러개의 독립적인 로컬 프레임에 분산되어 있다. 함수가 호출될 때마다 새로운 로컬 프레임이 도입되며 이는 설령 같은 함수가 두 번 호출되더라도 마찬가지이다.
이 모든 기작은 프로그램 실행 중에 이름이 적절한 시점에 정확한 값으로 해석되도록 보장하기 위해 존재한다. 이 예제는 왜 우리의 모델이 우리가 도입한 만큼의 복잡성을 필요로 하는지 잘 보여준다.
3가지 로컬 프레임 모두, x라는 이름에 대해 바인딩을 포함하고 있지만, 그 이름은 프레임마다 서로 다른 값에 바인딩 되어있다. 로컬 프레임들은 이러한 이름들이 서로 섞이지 않도록 분리해 주는 역할을 한다.
Local Names
함수의 구현 세부사항 중 함수의 동작에 영향을 미치지 않아야 하는 한 가지는 바로 구현자가 선택한 형식 매개변수의 이름이다. 따라서 아래의 두 함수는 완전히 동일하게 동작해야 한다.

함수의 의미는 작성자가 선택한 매개변수 이름과 독립적이어야 한다는 이 원칙은 프로그래밍 언어에 있어 중요한 결과를 초래한다.
가장 단순한 결과는 함수의 매개변수 이름이 반드시 해당 함수 본문 내에서만 지역적으로 유지되어야 한다는 점이다.
만약 매개변수가 각 함수의 본문에 국한되지 않는다면, square의 매개변수 x는 sum_squares의 매개변수 x와 혼동 될 수 있다. 결정적으로, 실제로는 그런 일이 발새아지 않는다. 서로 다른 로컬 프레임에 있는 x에 대한 바인딩은 서로 아무런 관련이 없기 때문이다. 우리가 배우는 계산 모델은 이러한 독립성을 보장하도록 정교하게 설계되었다.
우리는 로컬 name의 scope가 그것을 정의한 사용자 정의 함수의 본문으로 제한된다고 말한다. 어떤 이름에 더 이상 접근할 수 없게 되면 그 이름은 out of scope라고 한다. 이러한 스코핑 동작은 우리 모델에 대한 새로운 사실이 아니라, 환경이 작동하는 방식에 따른 당연한 결과이다.
Choosing Names
이름을 서로 바꿔 쓸 수 있다는 것이 형식 매개변수의 이름이 전혀 중요하지 않다는 뜻은 아니다. 오히려 잘 선택된 함수와 매개변수 이름은 인간이 함수 정의를 이해하는데 있어 필수적이다.
아래 가이드라인은 모든 파인썬 프로그래머들의 지침서 역할을 하는 PEP8에서 인용한 것이다. 공유된 관습은 개발자 커뮤니티 구성원들 사이의 의사소통을 원화라게 해준다. 또한 이러한 관습을 따름으로써 우리의 코드가 내부적으로 더 일관성을 갖추게 된다는 붓적인 효과도 얻을 수 있다.
- 함수 이름은 소문자로 작성하며, 단어 사이는 _ 로 구분한다. 설명적인 이름 사용 권장.
- 함수 이름은 보통 인터프리터가 인자에 적용하는 동작이나 결과로 나오는 양의 이름을 떠올리게 한다.
- 매개변수 이름은 소문자로 작성하며, 단어 사이는 _ 로 구분한다. 가급적 한 단어로 된 이름 섢
- 매개변수 이름은 단순히 허용되는 인자의 종류가 아니라, 함수 내에서 해당 매개변수의 역할을 떠올리게 해야 한다.
- 역할이 명확할 때는 한 글자 매개변수 이름을 사용할 수 있지만, 숫자와 혼동 될 수 있는 l, O, I 는 피해야 한다.
물론 파이썬 표준 라이브러리조차 이러한 가이드라인에 예외가 많다. 영어 어휘와 마찬가지로, 파이썬 역시 다양한 기여자들로부터 단어들을 물려받았기 때문에 그 결과가 항상 일관된것은 아니다.
Functions as Abstractions
비록 매우 간단하지만, sum_squares는 사용자 정의 함수가 가진 가장 강력한 특성을 보여준다. sum_squares 함수는 square 함수를 기반으로 정의되었지만, 오직 square가 입력 인자와 출력 값 사이에 정의하는 관계에만 의존한다.
우리는 숫자를 제곱하는 방법에 대해 고민하지 않고도 sum_squares를 작성할 수 있다. 제곱이 계산되는 구체적인 세부 사항은 나중에 고려하기 위해 억제할 수 있다. 실제로 sum_squares 입장에서 볼 때 square는 특정한 함수의 본문이 아니라 함수의 추상화, 즉 이른바 함수 추상화이다. 이러한 추상화 수준에서는 제곱을 계산하는 어떤 함수든 똑같이 훌륭하다.
따라서 반환하는 값만 고려한다면, 제곱을 계산하는 두 함수는 서로 구별할 수 없어야 한다. 두함수 모두 수치 인자를 받아 그 숫자의 제곱을 결과값을 산출한다.

다시 말해, 함수 정의는 세부 사항을 감출 수 있어야 한다. 함수의 사용자는 함수를 직접 작성하지 않았을 수도 있고, 다른 프로그래머부터 전달 받았을 수도 있다. 프로그래머는 함수를 사용하기 위해 그 함수가 어떻게 구현되었는지 알 필요가 없어야 한다. 파이썬 라이브러리가 바로 이러한 특성을 가지고 있다. 많은 개발자가 그곳에 정의된 함수를 사용하지만, 그 구현을 직접 조사하는 사람은 거의 없다.
함수 추상화의 측면
함수 추상화를 완벽하게 다루기 위해서는 세가지 핵심 속성을 고려하는 것이 유용하다.
- Domain : 함수가 받을 수 있는 인자의 집합
- Range : 함수가 반환할 수 있는 값의 집합
- Intent : 함수가 입력과 출력 사이에 계산해내는 관계
복잡한 프로그램에서 함수 추상화를 올바르게 사용하려면 Domain, Range, intent를 통해 함수를 이해하는 것이 매우 종요하다. 예를 들어, sum_squares를 구현하기 위해 모든 square 함수는 다음과 같은 속성을 가져야 한다.
- Domain : 임의의 실수 하나
- Range : 임의의 비음수 실수
- Intent : 출력값은 입력값의 제곱이어야 함
이러한 속성들은 Intent가 어떻게 수행되는지 명시하지 않는다. 그 세부 사항은 추상화되어 감춰지기 떄문이다.
Operators
수학 연산자는 우리가 처음 접한 결합 방법 중 하나였지만, 아직 이 연산자들을 포함하는 표현식의 평가 절차를 명확히 정의하지는 않았다.
중위 연산자가 포함된 파이썬 표현식은 각각 고유한 평가 절차를 가지고 있지만, 대게 호출 표현식의 축약형이라고 생각하면 이해하기 쉽다.
예를 들어 아래와 같은 코드를 보자.

이것을 단순히 아래 코드의 축약형으로 간주하는 것이다.

중위 표기법도 호출 표현식처럼 중첩될 수 있다. 파이썬은 여러 연산자가 포함된 복합 표현식을 해석하는 방법을 결정하기 위해 일반적인 수학적 연산자 우선순위 규칙을 적용한다.

위 식은 아래의 호출 표현식과 동일한 결과를 도출한다.

호출 표현식의 중첩 구조는 연산자 버전보다 훨씬 명시적이지만, 동시에 읽기는 더 어렵다. 또한 파이썬은 일반적인 우선순위 규칙을 무시하거나 표현식의 중첩 구조를 더 명확하게 만들기 위해 괄호를 사용한 하위 표현식 그룹화를 허용한다.

위 식은 아래와 동일한 결과로 평가된다.

나눗셈의 경우, 파이썬은 2가지 중위 연산자 / 와 // 를 제공한다. 전자는 일반적인 나눗셈으로 나누어 떨어지는 경우라도 항상 float 값을 결과로 내놓는다.

반면 // 연산자는 결과를 내림하여 정수로 만든다.

이 두 연산자는 각각 truediv와 floordiv 함수의 축약형이다.

프로그램을 작성할 때 중위 연산자와 괄호를 자유롭게 사용해도 좋다. 관용적인 파이썬 방식은 간단한 수학 연산의 경우 호출 표현식보다 연산자를 사용하는 것을 선호한다.