Composing Programs - 8

Building Abstractions with Data
Introduction
우리는 이젠 Composing Programs post들에서 계산 과정과 프로그램 설계에서 함수가 하는 역할에 집중했다. 우리는 원시 데이터와 원시 연산을 사용하는 방법, 합성과 제어를 통해 복합 함수를 형성하는 방법, 그리고 과정에 이름을 부여하여 함수적 추상화를 만드는 방법을 살펴보았다. 또한 고차 함수가 일반적인 계산 방법의 관점에서 조작하고 추론할 수 있게 함으로써 우리 언어의 힘을 얼마나 향상시키는지도 확인했다.
이 장은 데이터에 초점을 맞춘다. 우리가 여기서 조사할 기술들은 다양한 분야의 정보를 표현하고 조작할 수 있게 해줄 것이다. 인터넷의 폭발적인 성장 덕분에, 방대한 양의 구조화된 정보를 온라인에서 누구나 자유롭게 이용할 수 있게 되었으며, 계산은 매우 다양한 문제에 적용 될 수 있다.
Native Data Types
파이썬의 모든 값은 그것이 어떤 종류의 값인지를 결정하는 class를 가진다. 같은 클래스를 공유하는 값들은 행동 또한 공유한다. 예를 들어, 정수 1과 2는 모두 int 클래스의 인스턴스이다. 이 두 값은 유사하게 취급될 수 있다. 예를 들면, 둘 다 부호를 바꾸거나 다른 정수와 더할 수 있다.
내장 함수인 type을 사용하면 어떤 값이든 그 클래스를 검사할 수 있다.

우리가 지금까지 사용한 값들은 파이썬 언어에 내장된 소수의 원시 데이터 유형의 인스턴스이다. 원시 데이터 유형은 다음과 같은 특성을 가진다.
- 원시 유형의 값으로 평가되는 표현식이 존재하며, 이를 리터럴이라고 부른다.
- 원시 유형의 값을 조작하기 위한 내장 함수와 연산자가 존재한다.
int 클래스는 정수를 표현하는 데 사용되는 원시 데이터 유형이다. 정수 리터럴은 int 값으로 평가된다.

파이썬은 세 가지 원시 숫자 유형을 포함한다. 정수(int) 실수(float) 복소수(complex)

Floats float라는 이름은 파이썬을 비롯한 많은 프로그래밍 언어에서 실수를 표현하는 방식인 floating point 표현식에서 유래되었다. 숫자가 내부적으로 어떻게 표현되는지 세부적인 내용은 이 주제가 아니지만, int 객체와 float 객체 사이의 몇 가지 고차원적인 차이점을 아는 것은 중요하다.
특히 int 객체는 크기에 제한이나 근사치 없이 정수를 정확하게 표현한다. 반면, float 객체는 넓은 범위의 소수들을 표현할 수 있지만, 모든 숫자를 정확하게 표현할 수 있는 것은 아니며 최소값과 최대값이 존재한다. 따라서 float 값은 실제 실수의 근사치로 취급해야 한다. 이러한 근사치는 유한한 정밀도만을 가진다. float 값들을 결합하면 근사 오차가 발생할 수 있다. 아래의 두 표현식은 근사 오차가 없다면 모두 7로 평가되었을 것이다.

위에서는 int 값들이 결합되었음에도 불구하고, 하나의 int를 다른 int로 나누면 float 값이 생성된다. 이는 두 정수의 실제 나눗셈 비율을 유한하게 잘라낸 근사치이다.

이러한 근사치로 인한 문제는 등호 비교 테스트를 수행할 때 더욱 잘 드러난다.

int와 float 클래스 사이의 이러한 미묘한 차이는 프로그램을 작성할 때 광범위한 영향을 미치므로, 프로그래머가 반드시 숙지해야 하는 세부 사항이다. 다행히 원시 데이터 유형은 몇 개 되지 않기 때문에 프로그래밍 언어에 능숙해지기 위해 외워야 할 양이 많지는 않다. 게다가 이러한 세부 사항들은 IEEE 754 부동소수점 표준과 같은 커뮤니티 가이드라인 덕분에 많은 프로그래밍 언어 전반에 걸쳐 일관되게 유지된다.
Non-numeric types
값은 소리, 이미지, 위치, 웹 주소, 네트워크 연결 등 다른 많은 유형의 데이터를 표현할 수 있다. 그중 몇 가지는 값 True와 False를 위한 bool 클래스처럼 원시 데이터 유형으로 표현된다. 대부분의 데이터 유형은 프로그래가 이번 장에서 발전시켜 나갈 결합과 추상화 수단을 사용하여 직접 정의해야 한다.
Data Abstraction
우리가 프로그램에 표현하고자 하는 세상의 수많은 것들을 고려해볼 때, 우리는 그것들의 대부분이 복합적인 구조를 가지고 있다는 것을 알게 된다. 예를 들어, 지리적 위치는 위도와 경도 좌표를 가진다. 위치를 표현하기 위해, 우리는 프로그래밍 언어가 위도와 경도를 묶어 하나의 쌍을 형성하는 능력을 갖추기를 바란다. 이는 우리 프로그램이 하나의 개념적 단위로 조작할 수 있으면서도, 동시에 개별적으로 고려할 수 있는 두 개의 부분을 가진 복합 데이터 값이다.
복합 데이터를 사용하면 프로그램의 모듈성을 높일 수 있다. 만약 우리가 지리적 위치를 하나의 전체 값으로 조작할 수 있다면, 위치를 사용해 계산을 수행하는 프로그램의 다른 부분들을 그 위치가 실제로 어떻게 표현되는지에 대한 세부 사항으로부터 보호할 수 있다. 데이터가 표현되는 방식을 다루는 프로그램의 부분과 데이터가 조작되는 방식을 다루는 부분을 서로 고립시키는 이 일반적인 기술은 데이터 추상화라고 불리는 강력한 설계 방법론이다. 데이터 추상화는 프로그램을 설계, 유지보수 및 수정하기 후러씬 쉽게 만들어준다.
데이터 추상화는 성격상 함수 추상화와 유사하다. 우리가 함수 추상화를 만들 때 함수가 어떻게 구현되었는지에 대한 세부 사항은 숨겨질 수 있으며, 특정 함수 자체는 동일한 전반적 동작을 수행하는 다른 함수로 대체될 수 있었다. 다시 말해, 함수가 사용되는 방식과 함수가 구현되는 방식의 세부 사항을 분리하는 추상화를 만들 수 있는 것이다. 이와 유사하게 데이터 추상화는 복합 데이터 값이 사용되는 방식을 그것이 구축되는 방식의 세부 사항으로부터 고립시킨다.
데이터 추상화의 기본 아이디어는 프로그램이 추상 데이터 위에서 동작하도록 구조화하는 것이다. 즉, 우리의 프로그램은 데이터에 대해 가능한 한 적은 가정을 하는 방식으로 데이터를 사용해야 한다. 동시에, 구체적인 데이터 표현은 프로그램의 독립적인 부분으로 정의된다.
추상 데이터 위에서 동작하는 부분과 구체적인 표현을 정의하는 부분이라는 프로그램의 이 두 영역은 구체적인 표현을 바탕으로 추상 데이터를 구현하는 소수의 함수 집합에 의해 연결된다. 이 기술을 설명하기 위해 우리는 유리수를 조작하기 위한 함수 집합을 설계하는 방법을 살펴볼 것이다.
Example : Rational Numbers
유리수는 정수의 비율을 나타내며, 실수에서 중요한 하위 클래스를 구성한다. 1/3이나 17/29 같은 유리수는 보통 분자/분모와 같이 표기된다.
여기서 분자와 분모는 모두 정수 값이 들어갈 자리표시자이다. 유리수의 값을 정확하게 특정짓기 위해서는 이 두 부분이 모두 필요하다. 실제로 정수를 나누면 float 근사치가 생성되어 정수의 정확한 정밀도를 잃게 된다.

하지만 우리는 분자와 분모를 하나로 결합함으로써 유리수를 오차 없이 정확하게 표현할 수 있는 방법을 만들 수 있다.
우리는 함수 추상화를 다루면서, 프로그램의 일부가 아직 구현되지 않았더라고 생산적으로 프로그래밍을 시작할 수 있다는 것을 배웠다. 우선 분자와 분모로부터 유리수를 구성하는 방법이 이미 존재한다고 가정해보자. 또한, 어떤 유리수가 주어졌을 때 그것이 분자 성분과 분모성분을 선택하는 방법도 있다고 가정하자. 나아가 이러한 생성자와 선택자들이 다음과 같은 세가지 함수로 이미 제공되고 있다고 가정해보자.
- rational(n, d) : 분자가 n이고 분모가 d인 유리수를 반환한다.
- numer(x) : 유리수 x의 분자를 반환한다.
- denom(x) : 유리수 x의 분모를 반환한다.
여기서 우리는 프로그램 설계에 있어 매우 강력한 전략인 희망사항 기반 사고를 사용하고 있다. 우리는 유리수가 내부적으로 어떻게 표현되는지, 혹은 numer, denom, rational 함수가 어떻게 구현되어야 하는지 아직 한마디도 하지 않았다. 그럼에도 불구하고 만약 우리가 이 세 함수를 정의하기만 한다면 다음과 같이 유리수의 덧셈, 곱셈, 출력, 그리고 등호 비교를 수행할 수 있다.

이제 우리는 선택자 함수인 numer, denom과 생성자 함수인 rational을 기반으로 유리수 연산들을 정의했다. 하지만 아직 이 함수들을 실제로 정의하지는 않았다. 우리에게 지금 필요한 것은 분자와 분모를 하나의 복합 데이터 값으로 끈끈하게 묶어줄 수 있는 구체적인 방법이다.
Pairs
우리가 데이터 추상화의 구체적인 단계를 구현할 수 있도록, 파이썬은 리스트라고 불리는 복합 구조를 제공한다. 리스트는 대괄호 안에 표현식들을 쉼표로 구분하여 배치함으로써 생성할 수 있다. 이러한 표현식을 리스트 리터럴이라고 부른다.

리스트의 요소에 접근하는 방법은 2가지가 있다. 첫번째 방법은 우리에게 익숙한 다중 할당 방식으로, 리스트를 각각의 요소로 풀어내어 각 요소를 서로 다른 이름에 바인딩하는 것이다.

리스트의 요소에 접근하는 두 번째 방법은 대괄호를 사용하는 요소 선택 연산자이다. 리스트 리터럴과 달리 다른 표현식 바로 뒤에 오는 대괄호 표현식은 리스트 값으로 평가되지 않고 앞선 표현식의 값에서 특정 요소를 선택한다.

파이썬의 리스트는 0부터 시작하는 인덱스를 사용한다. 즉, 인덱스 0은 첫번째 요소를 인덱스 1은 두번째 요소를 선택하는 식이다. 이러한 인덱싱 규칙을 뒷받침하는 한가지 직관은, 인덱스가 리스트의 시작점으로부터 해당 요소가 얼마나 떨어져 있는지를 나타낸다는 점이다.
요소 선택 연산자와 동일한 기능을 하는 함수는 getitem이며 이 함수 역시 0부터 시작하는 위치를 사용하여 리스트에서 요소를 선택한다.

파이썬에서 두 개의 요소를 가진 리스트가 쌍을 표현하는 유일한 방법은 아니다. 두 개의 값을 하나로 묶을 수 있는 모든 방법은 쌍으로 간주될 수 있다.
이제 우리는 유리수를 분자와 분모라는 두 정수의 쌍으로 표현할 수 있다.

앞서 정의한 산술 연산들과 결합하여, 우리는 우리가 정의한 함수들로 유리수를 조작할 수 있다.

위의 예시가 보여주듯이 우리의 유리수 구현은 유리수를 기약분수로 약분하지 않는다. 우리는 rational의 구현을 변경하여 이 결함을 보완할 수 있다. 만약 두 정수의 최대공약수를 계산하는 함수가 있다면, 쌍을 구성하기 전에 분자와 분모를 약분하여 기약분수로 만들 수 있다. 많은 유용한 도구들과 마찬가지로, 그러한 함수는 이미 파이썬 라이브러리에 존재한다.

버림 나눗셈 연산자 // 는 정수 나눗셈을 나타내며 나눗셈 결과의 소수부분을 내림한다. 우리는 g가 n과 d를 모두 나누어떨어지게 한다는 것을 알고 있으므로, 이 경우 정수 나눗셈은 오차 없이 정확하다. 이 수정된 rational 구현은 유리수가 항상 기약분수로 표현되도록 보장한다.

Abstractions Barriers
복합 데이터와 데이터 추상화의 더 많은 예시를 살펴보기 전에, 유리수 예제에서 제기된 몇 가지 도점을 짚고 넘어가자.
우리는 생성자인 rational과 선택자인 numer, denom을 바탕으로 연산들을 정의했다. 일반적으로 데이터 추상화의 근본적인 아이디어는, 어떤 종류의 값을 조작하는 모든 행위를 표현할 수 있는 기본적인 연산 집합을 식별하고, 데이터를 다룰 때는 오직 그 연산들만 사용하는 것이다. 이처럼 연산의 사용을 제한함으로써, 프로그램의 동작을 변경하지 않고도 추상 데이터의 표현 방식을 바꾸기가 훨씬 쉬워진다.
추상화 장벽 위반은 더 높은 수준의 함수를 사용할 수 있는 프로그램의 어떤 부분이 대신 더 낮은 수준의 함수를 사용할 때마다 발생한다. 예를 들어, 유리수의 제곱을 계산하는 함수는 유리수의 구현 방식에 대해 아무것도 가정하지 않는 mul_rational을 사용해 구현하는 것이 가장 좋다.

분자와 분모를 직접 참조하는 것은 하나의 추상화 장벽을 위반하는 것이다.

유리수가 두 개의 요소를 가진 리스트로 표현된다고 가정하는 것은 두 개의 추상화 장벽을 위반하는 것이다.

추상화 장벽은 프로그램을 유지보수하고 수정하기 쉽게 만든다. 특정 표현 방식에 의존하는 함수의 수가 적을수록, 그 표현 방식을 바꾸고자 할 때 수정해야 할 사항도 적어진다. square_rational에 대한 이 모든 구현은 올바르게 동작하지만, 오직 첫번째 구현만이 향후의 변경에 유연하게 대처할 수 있다.
square_rational 함수는 우리가 유리수의 표현 방식을 바꾸더라도 코드를 업데이트할 필요가 없다. 반면 square_rational_violating_once는 선택자나 생성자의 시그니처가 바뀔때마다 변경되어야 하며 square_rational_violating_once 는 유리수의 내부 구현이 바뀔 때마다 매번 업데이트 되어야 한다.
The Properties of Data
추상화 장벽은 우리가 데이터를 생각하는 방식을 형성한다. 유리수의 유효한 표현은 특정 구현에 제한되지 않는다. 그것은 단지 numer와 denom에 전달 될 수 있는 rational이 반환한 값일 뿐이다. 게다가 생성자와 선택자 사이에는 적절한 관계가 유지되어야 한다. 즉 우리가 n과 d로부터 유리수 x를 구성했다면, numer(x)/demon(x) 는 n/d와 같아야 한다.
일반적으로 우리는 추상 데이터를 일련의 선택자 및 생성자와 더불어 몇가지 동작 조건을 사용해 표현할 수 있다. 위의 나눗셈 성질과 같은 동작 조건이 충족되는 한, 선택자와 생성자는 해당 종류의 데이터에 대한 유효한 표현을 구성한다. 추상화 장벽 아래의 구현 세부 사항은 바뀔 수 있지만, 동작이 바뀌지 않는다면 데이터 추상화는 유효하게 유지되며, 이 데이터 추상화를 사용해 작성된 모든 프로그램은 계속해서 올바르게 작동할 것이다.
이러한 관점은 우리가 유리수를 구현하기 위해 사용했던 쌍 값들을 포함하여 광범위하게 적용될 수 있다. 우리는 실제로 쌍이 무엇인지에 대해 많은 말을 하지 않았고, 단지 언어가 2개의 요소를 가진 리스트를 만들고 조작할 수 있는 수단을 제공한다고만 했다. 우리가 쌍을 구현하기 위해 요구하는 동작은 두 값을 하나로 묶어주는 것이다. 이를 동작 조건으로 진술하면 다음과 같다.
만약 값 x와 y로부터 쌍 p가 구성되었다면, select(p, 0)은 x를 반환하고 select(p, 1)은 y를 반환해야 한다.
우리는 쌍을 만들기 위해 실제로 리스트 타입이 필요하지 않다. 대신 두개의 요소를 가진 리스트 못지않게 이 설명을 완벽히 충족하는 두 함수 pair와 select를 구현할 수 있다.

이 구현을 사용하더라도 우리는 쌍을 생성하고 조작할 수 있다.

이러한 고차 함수의 사용은 데이터가 무엇인지에 대한 우리의 직관적인 개념과 전혀 다르다. 그럼에도 불구하고, 이 함수들은 우리 프로그램에서 쌍을 표현하기에 충분하다. 함수만으로도 복합데이터를 표현하기에 충분하다.
쌍의 함수적 표현을 보여주는 목적은 파이썬이 실제로 이렇게 작동한다는 것이 아니라, 이렇게 작동할 수도 있다는 점을 보여주기 위함이다. 이 함수적 표현은 비록 모호해 보일지라도, 쌍이 충족해야 하는 유일한 조건들을 만족하기 때문에 쌍을 표현하는 완벽하게 적절한 방법이다. 데이터 추상화의 실천은 우리가 이러한 표현 방식 사이를 쉽게 전환할 수 있도록 도와준다.