LiveScript 살펴보기 – 03 함수

LS에서 함수는 일반 문법 편에서 잠깐 언급했듯이 화살표를 써서 간단히 정의할 수 있다. 이 함수 표현에서 중요한 점 두 가지는 첫 째 우변은 하나 이상의 표현식이라는 점과 표현식이 순서대로 나열되는 경우 맨 마지막 표현식의 결과가 자동으로 리턴된다는 것이다.

함수

LS는 함수형 프로그래밍 언어의 스타일을 많이 도입했다고 하였다. 비록 LS가 진짜 순수한 함수형 언어는 아니지만, 함수형 언어의 스타일을 도입한다는 것은 LS내의 함수라는 것은 가급적 아래와 같은 특징을 갖도록 디자인되어야 한다는 것이다.

  1. 순수성 : 함수의 결과값이 순수하게 파라미터에만 의존할 것. 따라서 입력된 인자가 같다면 항상 리턴될 출력값도 같음을 보장한다.
  2. 간결성 : 사실 억지로 만든 말이기는 한데, 함수에 대한 연산 (커링, 바인딩, 파이핑, 합성)이 다양하고, 함수 자체가 1급 시민인 점(이는 JS로부터 자연스럽게 물려받는 특징이다.)에 착안하여, 가능한 간결하고 명료한 함수들을 정의하고, 이러한 함수들을 조합하여 필요한 함수를 생성하는 방법을 지향하는 것이다.

이전에 예에서 리스트의 최대값을 찾는 getMax 함수를 잠깐 언급한 바, 있는데 이 함수는 JS로 쓴다면 아래와 같을 것이다.

function getMax(arr) {
  if(arr.length < 1) { return null }
  var x = arr[0];
  var i = 1;
  var result = x;
  while(i < arr.length) {
    if (x < arr[i] ) {
      x = arr[i];
      result = x;
    }
    i += 1;
  }
  return result;
}

그리고 LS에서는 다음과 같이 쓴다.

get-max = (arr) -> arr.reduce (>?)

너무 심하게 부풀려서 비교한다고 생각할 수 있는데, 물론 JS에서도 함수형 스타일로 맵/필터/리듀스를 할 수 있는 API가 Array 타입에 존재하고 있다. 따라서 위 LS 코드를 JS로 쓴다면 (그것은 마치 함수형 스타일의 JS 코드겠지만) 다음과 같이 쓸 수 있다.

var getMax = function(arr){ 
  return arr.reduce(function(x, y){ 
    return x > y ? x : y; 
  }
);

다만, 여기서 말하고자 하는 것은 LS로 쓰면 함수를 엄청 짧은 코드로 쓸 수 있다는 점이 아니라, “두 수 중에서 큰 수를 판단하는 함수”를 리스트의 각 원소에 대해서 순차적으로 적용하면 리스트 내에서 가장 큰 수를 찾을 수 있다”는 간단한 아이디어에 관한 것이다. 두 개의 연산(함수)에 대해 이해하고 그것을 연관지어 원하는 기능을 쉽게 조합할 수 있는 것이 함수형 스타일의 가장 큰 특징이라 할 수 있겠다.

리턴

함수의 본체가 하나의 표현식인 경우에는 LS의 함수 정의 문은 one-liner로 작성된다. 두 개 이상의 표현식이 함수의 본체를 구성하는 경우에는 -> 뒤에서 줄을 바꾸고 들여써서 블럭을 구분하여 표기할 수 있다.

times = (x, y) -> x * y
sum = (arr) ->
  s = 0
  for i in arr
    s += i
  s

마지막 표현식은 자동으로 리턴되므로, 명시적으로 리턴이 없는 함수를 정의하고 싶다면 !-> 기호를 써서 정의한다.

호출

함수 호출 시, ()를 생략한다. 또한 인자들은 callable하지 않다면 콤마를 생략하고 나열할 수 있다. 인자를 받지 않는 함수는 !를 써서 호출됨을 표시한다. 그리고 인자 뒤에 오는 and, or, xor, 한 칸 띈 후의 ., ?. 등은 모두 암묵적으로 호출 구문을 완료한다. 따라서 공백을 기준으로 체이닝 표기를 간단히 할 수 있다.

$ \h1 .find \a .text! #=> h1 a 의 내용
# $('h1').find('a').text()

f!
[1 2 3].reverse!slice 1 #= [2, 1]

익명함수의 경우, 인자가 없는 케이스에는 do를 -> 에 쓰면 이를 호출한다. 간단하게 익명함수를 이용해서 여러 표현식을 묶은 블럭을 만들고 실행하는 방법으로 이해.

do -> 3 + 2 
#=> 5
#(function(){
#  return 3 + 2;
#})();

do를 이름이 있는 함수1와 썼을 때, 그리고 do가 표현식에 쓰인게 아니라면 이름지은 함수는 유지된다. 즉, do를 평가하기 전에 함수를 참조할 수 있고, do 문을 만났을 때 한 번 더 실행하는 것이다.

i = 0
f 9 #f를 한 번 실행. i == 1
i #=> 1
do function f x # 여기서 한 번 더 실행
  ++i
  x
i # => 2

축약된 객체 블럭 문법은 함수 호출 구문에서 사용할 수 없다. 단, 한 줄에 쓰여진 리터럴은 인식된다. 함수의 인자 중 하나로 객체 블럭을 쓰고자 한다면 do를 이용해서 블럭을 명시해야 한다.

func
  a:1
  b:2
## 컴파일 되지 않음

## 대신 이렇게 쓸 수 있다.
func a:1, b:2

## 보통은 do를 사용한다.
func do
  a: 1
  b: 2

이처럼 do는 함수 호출 시에 요긴하게 많이 쓰인다. 특히 개별 인자를 각 라인의 표현식으로 사용하려할 때 유용하다.

함수의 중위표현

파라미터를 2개 받는 함수는 백팃으로 둘러 싸서 중위연산자처럼 쓸 수 있다.

add = (x, y) -> x + y
2 `add` 3 #=> 5

g = (a, b) ->
  add ...     # 함수 내에서 쓰이는 `...`은  인자를 그대로 넘긴다는 의미로 해석할 수 있다. 
# 11

그리고 함수의 본체 내에서 …을 쓰면 암묵적으로 모든 인자의 리스트로 인식한다. 이는 특히 super를 호출할 때 유용하다.

파라미터

파라미터는 표현식을 쓰는 표기로도 확장된다. 즉 객체나 다른 파라미터에 관한식으로 파라미터를 받으면, 해당 값으로 맵핑된다는 이야기이다. 이를 통해서 함수 본체에서 각 파라미터간의 관계를 재설정해야 하는 부담을 줄 일 수 있다. 아래의 예제를 보자.

set-person-params = (
  person
  person.age        # 두 번째 인자는 첫 번째 인자의 .age 키에 배당된다.
  person.height ) -> person

p = set-person-params {}, 21, 180cm
# {age: 21, height: 180}

# 'this' 확장하기
set-text = (@text) -> this
# var setText = function(text){ this.text = text; return this; }

위의 setPersonParams() 함수는 세 개의 인자를 받는데, 그 중 두 번째, 세 번 째 인자는 첫 번째 인자의 프로퍼티로 명시되어 있다. 따라서 함수 호출 시에 해당 값이 주어지면, 이는 첫번째 인자로 넘겨진 객체의 프로퍼티로 자동으로 세팅된다. 따라서 함수 본체 내에서 person.age = age와 같은 처리를 따로 하지 않고 인자 자체를 확장하여 person.age로 쓰는 것으로 대체가능하다.

특히 이 확장은 객체의 메소드나 함수를 작성할 때, 특히 this를 다룰 때 매우 간결하게 쓰일 수 있다.

디폴트 값

파라미터에는 디폴트 값을 미리 지정해줄 수 있으며, (파이썬 스타일), 좀 헷갈릴 수는 있는데, 논리 연산을 수행하여 디폴트값/오버라이드를 적용할 수 있다.

add = (x && 4, y || 3) -> x + y
# x는 무조건 4로 오버라이드되고
# y는 없으면 3이 된다.

또한 객체를 통으로 받아서 특정 키를 분해해 낼 수 있다.

set-coords = ({x, y}) -> "#x, #y"
set-coords y:2, x:3 #=> "3, 2"

# 그리고 그 와중에 다시 디폴트 값을...
set-coords = ({x = 1, y = 3}) -> "#x, #y"

그리고 ...y 등과 같이 일련의 인자들을 하나의 리스트로 취급하는 것도 가능하다.

f = (x, ...ys) -> x + ys.1
f 1 2 3 4 #=> 4

이외에도 캐스팅 연산자를 파라미터에 붙일 수 있는데, 그러면 자동으로 평가된 후 들어간다.

f = (!!x) -> x
f 'truely' # true

g = (+x) -> x
g ' ' # 0

obj = {prop: 1}
h = (^^x) ->
  x.prop = 99
  x
h obj
obj.prop # 1

파라미터의 생략

JS의 함수는 호출 시 파라미터 개수에 크게 구애받지 않는다. 즉 선언된 파라미터보다 부족한 개수의 인자가 넘겨지면, 빈 인자는 undefined를 갖게되고, 인자가 과하게 많이 넘겨지면 함수의 로컬 스코프에 매핑되지 못한 인자는 모두 무시된다.

LS의 함수에 있어서 정의를 파라미터 없이 함수를 만들었다 하더라도 함수 본체에서는 파라미터를 참조하는 것이 가능하다. 사실 이는 JS의 스펙에 정의된 arguments2 객체에 의한 것이다.

단 인자 함수의 파라미터는 it으로 지칭한다. 그외의 파라미터는 모두 &0, &1 과 같은 식으로 번호 순서대로 참조하여 처리할 수 있다.

바운드 함수

바운드 함수는 특정 객체에 소유권이 묶인 함수를 말한다. 보통 함수 내에서 this를 참조하는 것은 해당 함수를 호출한 문맥이 되는데, 바운드 함수는 처음 바운드한 시점의 문맥이 유지된다. 바운드 함수의 자세한 내용에 대해서는 별도로 찾아보도록 하고, 바운드 함수를 작성하는 것은 ~> 를 써서 웨이브진 화살표를 쓴다는 점만 기억하자.

obj = new
  @x = 10
  @normal = -> @x  
  @bound = ~> @x

obj2 = x: 5
obj2.normal = obj.normal
obj2.bound = obj.bound

obj2.normal! # this.x 이고 이 때 this는 obj2 이므로 5
obj2.bound # 10. bound메소드는 obj에 바인딩되어 있으므로, 내부의 this는 obj를 가리킨다.

흔히 바운드 함수는 특정 객체의 메소드를 이벤트 핸들러를 사용하며, 그 내부에서 this를 참조할 때 유용하다.

- 대신 ~를 쓰는 것이 바운드 함수를 의미한다는 것은 매우 일관적으로 적용되며, !~>, ~~>, ~function 등의 표현이 그대로 사용될 수 있다.

커링

커링은 하스켈 커리의 이름을 따서 명명되었는데, 모든 다 인자 함수는 단인자 함수들이 합성된 상태로 볼 수 있다는 것을 말한다. 예를 들어 두 정수를 더하는 add 라는 함수가 있다면 다음과 같이 정의하고 호출할 수 있다.

add = (x, y) -> x + y
add 3 2

이 때, 이 호출식을 (add 3) 2라고 생각하는 것이다. 그러면 add 3은 정수 하나를 받아서 3을 더한 값을 리턴하는 단인자 함수가 된다.

그렇다면 다시, adda 라는 정수를 받아서 “b라는 정수를 받아 여기에 a를 더해서 리턴하는 함수”를 리턴하는 함수라고 생각할 수 있다.

add = (a) ->
  (b) -> a + b

add-one = add 1
add-one 2
# 3

이렇게 커링을 이용하면 쉽게 부분적용된 함수를 만드는 것이 가능해진다. 그리고 LS에서는 자동으로 커리되는 함수를 만들 수 있는 선언법으로 -->를 쓰는 것을 지원한다. 역시 같은 맥락에서 바운드된 커리드 함수는 ~~> 으로 선언할 숭 있다.

add = (a, b) --> a + b
add-one = add 1
add-one 2 #=> 3

접근자 단축

접근자 메소드/함수의 경우에 맵이나 필터 동작에 적용되는 경우 예를 들어 (x) -> x.prop과 같은 식으로 처리하는 것은 (.prop)으로 줄여 쓸 수 있다. 이는 특정 프로퍼티를 호출하는 것을 괄호로 둘러싼 것이기 때문에 메소드 호출 역시 같은 식으로 처리할 수 있다.

map (.length) <[ hello there you ]> #=> [5, 5, 3]
filter (.length < 4), <[ hello there you ]> #=> ['you']

map (.join \|) [[1 2 3], [7 8 9]]
#=> ['1|2|3', '7|8|9']

반대로 (obj.) 이라고 쓰는 것은 (it) -> (obj.it)의 단축 표현이 된다.

obj = one:1, two:2, three:3
map (obj.) <[one, three]> #=> [1, 3]

부분 적용

표현식 내에 언더스코어(_)를 사용하여 해당 표현식을 1개 인자로 받는 함수로 간주하고, 언더 스코어는 해당 인자의 위치를 표시하는 플레이스 홀더로 생각할 수 있다. 이는 커리드 함수에서 적절한 위치에 있지 않은 파라미터를 가변으로 남기고 싶을 때 유용하다.

# 여기서 쓰인 filter는 별도로 정의된 top-level의 함수라 가정한다.
filter = (fn, arr) --> arr fn
# 이 떄, 특정한 리스트에 대해서 고정하고 필터링 함수만 변경하려할 때,
# 다음과 같이 쓰게 되는데
filter-nums = (fn) -> filter fn, [1 to 5] # 
# 인자로 받게되는 fn의 위치를 `_`를 이용해서 표시해주면 된다.
filter-nums = filter _, [1 to 5]


filter-nums (<3) # [1, 2]

이러한 부분적용된 함수는 특히 고차 함수를 파이핑으로 연결할 때 유용하다. 아래 예제는 underscore.js를 이용해서 특정한 리스트를 조작하는 코드이다. “인자로 받은 객체가 다시 인자로 전해질 때”를 상정하기 때문에, _의 사용이 혼동되지 않고 해석될 수 있다.

[1 2 3]
|> _.map _, (* 2)
|> _.reduce _, (+), 0
# => 12

백 콜

콜백으로 주어지는 표현식들은 결국 블럭으로써, 들여쓰기를 적용해야 한다. 백 콜은 이러한 방식을 거꾸로 표현하여 콜백을 들여쓰지 않고 표현할 수 있게 해준다.

map (-> it * 2), [1 to 3] 

# 백 콜로 전환
x <- map _, [1 to 3]
x * 2  # 여기서부터는 _ 에 들어갈 표현을 쓸 수 있다. 

백 콜은 콜백을 들여쓰지 않게 해주기 때문에, 중첩되는 콜백지옥을 깔끔하게 처리해주는 장점을 가지고 있다. 만약 top레벨의 코드 중간에 백 콜을 쓰게 된다면, 이후 끝라인까지의 모든 내용이 콜백 내의 코드로 간주된다. 백 콜의 코드가 중간에서 끝나야 한다면 미리 do를 사용해서 들여쓰기 블록을 시작해주자.

do
  data <-! $.get 'ajaxtest'
  $ '.result' .html data  # $.get \ajaxtest의 콜백이며, 콜백의 인자는 data
  processed <-! $.get 'ajaxprocess', data  # 콜백 내에서 `ajaxprocess`에 대한 처리를 또 호출
  $ '.result' .append processed # 여기는 콜백 내의 콜백이지만, 들여쓰기는 더 이상 없다.

alert \hi

위 코드는 다음과 같이 컴파일 된다.

$.get('ajaxtest', function(data){
  $('.result').html(data);
  $.get('ajaxprocess', data, function(processed){
    $('.result').append(processed);
  });
});
alert('hi');

LET/NEW

함수와 관련하여 let, new에 대한 표현을 짚고 마무리하도록 하겠다. let은 특정한 익명함수를 생성하는데, 해당 함수 내에서의 특정한 문맥을 생성해준다. 즉 let A = B { 블럭} 의 형태이며, 이 때 블럭 내에서 언급되는 A는 모두 B로 치환된다.

let $ = jQuery
  $.isArray []

위의 이 표현은 $jQuery가 되는 블럭 스코프를 생성한 후에 $.isArray []를 평가하였으므로 true가 된다. 이 때 외부 스코프에 $이 있더라도 여기서는 자체 스코프만을 참조할 것이다. 비슷하게 아래와 같은 코드도 작성할 수 있다.

x = let @ = a:1, b:2
  @b ^ 3
x #=> 8

new는 새로운 익명 컨스트럭터를 만들어서 즉시 호출하는 개념이다.

doc = new
  @name = \spot
  @mutt = true

# {name: 'spot', mutt: true}

보너스

다음은 nodejs를 통한 간단한 HTTP 서버의 기본 구현을 LS로 작성한 것이다. 콜백속에서 또 콜백을 전달하는 형태의 함수호출 패턴이 존재하고, 체이닝이 쓰인다. 이를 do와 백콜을 이용하여 깔끔하게 작성할 수 있다.

require! <[ fs http url ]>
const ROOTDIR = 'html/'

do
  (req, res) <-! htttp.create-server
  url-obj = url.parse req.url, yes no
  do 
    err, data <-! fs.read-file ROOTDIR + url-obj.pathname
    if err? 
      res.write-head 404
      res.end <| JSON.stringify err
    else
      res.write-head 200
      res.end data
.listen 8080

참고자료

  • LiveScript.net
  • HTTP 서버 구현: https://mylko72.gitbooks.io/node-js/content/chapter7/chapter7_4.html

  1. named function. 여기서는 function 키워드를 써서 정의한 함수를 말한다. 
  2. arguements에 대한 MDN 설명 참조. 

[C] 함수로 전달된 포인터

포인터를 함수의 인자로 받는 경우, 함수내에서 원본을 변경하는가.

내용이 너무 두서 없어서 포스트 전체를 수정합니다.

처음 의문이 든 부분은, 문자열을 가리키는 포인터를 함수로 넘겨주고 문자열을 변형하면 포인터의 값이 변하는가?라는것이었는데. 이는 사실 그리 어려운 문제가 아니다. 포인터는 메모리의 주소를 가리키는 타입의 변수이고 이는 사실 unsigned int 나 unsinged int64 등의 정수형과 비슷하게 숫자를(메모리 번지도 결국 숫자값이므로) 담는 변수이다.

그리고 함수에 선언된 인자는 함수 내부에서만 사용하는 지역변수이고, 표준 타입의 변수를 함수에 전달하면 그 값이 인자로 선언한 변수에 들어가는데, 이건 그냥 변수의 값이 인자에도 들어가는 것이므로 값이 복사되는 것이다. 따라서,

  • 함수 외부에서 특정한 구조체나 배열, 문자열에 대한 포인터를 저장한 변수 A가 있고 이를 함수의 인자로 넘기면
  • 함수의 인자 B는 A와 동일한 값을 갖는다. 다만 값이 같을 뿐, B가 A를 가리키는 포인터가 될 수는 없다.
  • A가 포인터라면 B도 포인터이므로, 이 포인터가 가리키는 곳의 값을 조작할 수는 있다. 이러한 조작은 함수의 종료 후에도 변경을 유지하게 된다.
  • B는 A의 사본으로서, 함수의 실행 중에만 유지되고 함수 실행 종료시점에 파괴된다. A에 들어있는 값 자체는 아무런 영향을 받지 않는다.

로 정리할 수 있겠다. 땅땅땅.

[Python101] 005 함수,모듈, 그리고 도움이 되는 도구들

지난 시간까지 기본적인 프로그래밍에 필요한 “입출력”을 다루는 부분을 간단하게 나마 살펴보았다. 실제로 지금까지의 내용은 뭔가 설명이 필요하거나 개념적인 이해를 도모하는 부분과는 조금 거리가 있었고, 마치 조리법처럼 “이렇게 하면 이렇게 이렇게 됩니다.” 정도였기에 경우에 따라서는 조금 재미도 떨어지고 지겨운 부분이 없지 않아 있었을 것으로 생각된다.

이번 시간부터는 프로그램을 구성하는 단위와 이를 어떻게 만드는지, 그리고 프로그램의 흐름은 어떤 식으로 만들어지는지를 살펴보고자 한다.

모듈

모듈(Module)이라는 것은 다른 것으로 대체 가능한 어떤 구성 요소를 뜻하는데, 파이썬에서는 미리 만들어두었다가 필요할 때 꺼내 쓰는 레고 조각같은 프로그램의 조각을 말한다. 보통 프로그램의 소스 코드가 몇 백줄에서 몇 천줄까지 커지는 크고 복잡한 프로그램을 만든다면 이를 하나의 파일에 모두 작성하게 되면 사소한 오타로 인해 발생하는 에러를 잡기도 힘들어지고, 나중에 어떤 부분을 수정해야할 때 큰 범위를 일일이 찾아야 하므로 상당히 불편하고 어려워진다. 파이썬 프로그램 소스는 그 단위 단위가 모두 ‘모듈’이고, (뒤에서 설명할) 단위 프로그램 내의 함수나 클래스 같은 것들도 한 편으로는 모듈이라고 할 수 있다.

예를 들어 운영 체제에서 어떤 파일이나 폴더가 있는지를 검사하고, 파일을 복사, 삭제하거나, 폴더에 파일이 몇 개나 들어있는지를 알아내는 함수들을 만들었다고 하자. 나중에 이런 기능을 다른 프로그램에서도 사용해야 한다면, 그 때가서 똑같은 기능을 일일이 구현하는 것은 상당히 번거로운 일이 될 수도 있다.

그래서 이런 여러 개의 함수를 path(경로)라는 모듈로 묶어준다. 그리고 이런 경로와 관련된 것외에 운영체제의 특성이나 명령과 관련되는 모듈들을 다시 묶어서 os라는 모듈로 만들어둘 수 있겠다. 그리고 이게 파이썬이 기본적으로 제공하는 os 라이브러리의 정체가 된다.


from os.path import exists

라는 구문을 통해서 파일의 존재여부를 알아낼 수 있는 exists()라는 함수를 이미 사용해 본 바가 있다. (이는 우리가 구현한 것이 아니라 이미 구현되어 있는 함수를 가져다 사용한 예이다.)

파이썬을 설치하면 기본적으로 함께 설치되는 표준 라이브러리에도 활용가치가 높은 모듈들이 많이 있으며, 또 이미 많은 개발자들이 유용한 모듈들을 만들어서 공개해두고 있어 엄청나게 많은 활용 가능한 모듈들이 있다. 그래서 파이썬으로 작은 유틸리티를 만드는 일은 무척이나 쉬울 수 있다. (거의 필요한 기능들은 찾아보면 다 만들어져 있어서) 마치 레고로 자동차를 조립하는 것처럼 핸들, 바퀴, 차대 등의 부속품을 import 하여 필요한 모듈들이 함께 동작하도록 만들어주면 되는 것이다.

import / from… import

지금 편집하고 있는 소스코드 파일이 아닌 다른 곳에서 모듈을 가져와서 사용할 때 import 명령을 사용한다. import 명령은 다른 모듈 파일의 전체 혹은 일부를 현재 프로그램으로 반입해 오는 명령이다. 반입한 객체는 마치 미리 정의한 것과 같은 효과를 갖게 된다. 덕분에 프로그램 소스 코드가 간결해지고, 만들어야 하는 코드가 적어진다. (게다가 이렇게 모듈로 만들어둔 프로그램은 다른 프로그램에서 쉽게 재사용할 수 있게 된다.)

또한 모듈로 작성된 파일 내에서 그 일부만 반입해오는 경우에는 from 모듈이름 import 실제 반입할 모듈과 같은 식으로 코드를 반입할 수 있다.

모듈을 통째로 반입한 경우에는 그 하위에 있는 함수를 사용할 때 모듈.함수()와 같은 식으로 . 구분자를 써서 이들을 연결해 줘야 하지만 from 모듈 import 함수한 경우에는 함수 이름을 그대로 사용해주면 된다.

실제로 모듈을 만들어서 사용하는 예는 이 글의 말미에서 다시 다루기로 하고, 이번에는 함수에 대해 알아보도록 하겠다.

함수

함수는 프로그래밍에서 가장 중요한 개념 중의 하나이다. 함수는 “가장 작은 온전한 프로그램의 최소 단위”이다. 이 말이 무슨 뜻인고 하니, 우리가 흔히 ‘함수’라고 하면 y=2x+5와 같이 중고등학교 수학시간에 배운 함수를 떠올리게 된다. 음… 맞다. 함수는 딱 저렇게 생겨서 동작하게 된다. 여전히 감이 잘 오지 않는데, 이 함수를 잘 살펴보도록 하자.

x = 1 을 넣으면 y = 7이 된다.` x = 2` 를 넣으면 y = 9가 된다. 즉 어떤 값을 입력 (x에 대입)해 주면 이 값을 처리하여 그 결과값을 돌려주고 있다. 그리고 그 결과는 “입력된 값을 2배하고, 거기에 5를 더한다”는 처리를 한 결과값이다.

프로그래밍에서의 함수는 “가장 작은 프로그램의 단위”라고 했다. 프로그램 자체가 입력을 받아 이를 처리하고 그 결과를 출력해주는 기계 장치이므로, 이러한 프로그램의 특성을 함수는 그대로 가지고 있다. 그래서 프로그램 그 자체도 함수이며, 프로그램은 작고, 많은 함수들이 결합된 형태로 만들어진다.

그럼 어떤 기능을 함수로 만들면 좋을까? 답은 “반복적으로 하게 되는 작업”을 함수로 만들면 된다. 그 역시 감이 잘 오지 않는다면 “함수는 타이핑을 줄여주는 테크닉“이라고 처음에 생각해도 크게 무리가 없다.

함수 만들기

실제로 함수를 한 번 만들어 보자. 함수를 만드는 키워드는 def 이다. 함수는 다음과 같이 생성한다.

def 함수이름(인자값, ...):
    # << 어떤 동작을 하는 코드들 >>
    return 반환값 

def 키워드 다음에 함수의 이름을 쓴다. 그런 다음 함수가 받는 인자값의 이름을 써준다. 문장의 끝은 콜론으로 끝나고, 실제 함수가 해야 하는 동작은 모두 들여쓰기 해준다. 위에서 이야기한 y = 2x + 5를 파이썬 함수로 만들어보자.

def doublePlusFive(x):
    y = 2*x + 5
    return 5

print(doublePlusFive(1)) #==> 7
print(doublePlusFive(2)) #==> 9
print(doublePlusFive(5)) #==> 15

물론 위의 예제에서는 함수를 만드는 예를 보이다보니 저럴 거면 왜 함수를 쓰느냐고 할 수 있는데, 만약 섭씨나 화씨 온도를 서로 변환해야 하는 경우를 생각해보자. 섭씨온도를 화씨 온도로 바꾸는 식은 다음과 같다.

℉ = ℃ * 9 / 5 + 32

반대로 화씨 온도를 섭씨온도로 바꾸는 식은 다음과 같다. (위의 식을 바꾼 것에 지나지 않는다.)

℃ = (℉ - 32) * 5 / 9

그럼 화씨와 섭씨를 각각 바꿔주는 함수는 다음과 같이 작성할 수 있을 것이다.

#ex12.py
def FtoC(fh):
    return (fh-32) * 5 / 9.0

    def CtoF(cd):
        return cd*9/5.0 + 32

이렇게 함수로 만들어두면 나중에는 FtoC(45) 라고만 쓰면 화씨 45도가 섭씨 몇 도에 해당하는지 쉽게 알 수 있게된다. 만약 코드상으로 저 공식을 바로 사용하는 것도 “똑같은” 방법이라 할 수 있겠지만 1) 시간이 지나고 나서는 이 공식이 뭔지 기억이 안날 수도 있고, 2) 나중에 다시 같은 공식을 써야할 때 공식이 생각나지 않는 경우가 생길 수 있다.

대신에 함수를 사용하면 1)함수의 이름으로 이게 무슨 처리를 하는지 유추할 수 있고1 2) 함수를 모듈로 만들어 놓으면 굳이 공식을 기억하지 않아도 함수를 바로 사용할 수 있다.

함수와 모듈

위의 소스 코드를 ex12.py로 저장하고 이를 모듈로 사용하는 ex13.py를 새로 작성해보도록하자.

#ex13.py
#이 파일은 ex12.py와 같은 폴더에 저장해야 함.

import ex12

celsius = 25
farenheit = 45

print("섭씨 %d도는 화씨로 %f입니다." % (celsius, ex12.CtoF(celsius))
        print("화씨 %d도는 섭씨로 %f입니다." % (farenheit, ex12.FtoC(farenheit))

ex13.py는 ex12.py의 내용을 ex12라는 이름의 모듈로 입수한다. 우리가 이미 작성한 ex12.py 파일내의 모든 내용이 이 부분에 덧붙는 다고 생각하면 된다.

또한 우리가 반입한 내용이 ex12 전체이기 때문에 그 속에 들어있는 함수를 사용할 때는 ex12.FtoC(), ex12.CtoF()와 같은 식으로 반입한 모듈의 이름을 앞에 붙여주고 점(.)으로 구분해 준다. 만약 FtoC(), CtoF()의 이름을 그대로 사용하고 싶다면 다음과 같이 import 구문을 바꿔주면 된다.

from ex12 import FtoC
from ex12 import CtoF

모듈의 위치

모듈로 반입해 올 수 있는 파일의 위치는 특정한 범위로 제한된다. 현재 실행되는 프로그램 소스와 같은 폴더에 있거나, 파이썬의 표준 모듈 폴더에 있어야 한다. 이 표준 모듈 폴더는 sys.path라는 곳에 저장되어 있는데 여기에 특정한 폴더를 추가해 줄 수도 있다. 이 방법에 대해서는 나중에 다시 살펴볼 기회가 있을 것이다.

대화형 쉘 활용하기

지금까지 대화형 쉘은 그저 “계산기” 정도의 용도로 쓴 것이 사실이다. 단순히 하나의 명령이 어떤 일을 하는지 이미 알고 있는 상황에서 print 명령을 쓴다거나 하는 일이 그리 ‘효율적’으로 느껴질리도 없다. 지금부터는 대화형 쉘을 어떻게 활용할 수 있는지 살펴보겠다.

함수를 테스트하기

대화형 쉘은 기본적으로 한 번에 한 줄의 명령을 실행할 수 있도록 만들어져 있다. 즉 명령문을 입력하고 엔터키를 누르면 해당 명령이 바로 실행되어 즉시 그 결과를 볼 수 있는 것이다.

하지만 함수를 만드는 def 구문을 쓸 때는 엔터를 누르더라도 바로 실행되지 않는다. 위에서 예를 들었던 doublePlusFive() 함수를 대화형 쉘 모드에서 입력해보라. def doublePlusFive(x): 하고 엔터를 누르면 프롬프트가 나오지 않고 들여쓰기 상태가 된다. 이 상태에서 함수의 나머지 코드를 입력하고, 엔터를 두 번 누르면 결과가 출력되지 않고 다시 프롬프트가 표시된다.


>>> def doublePlusFive(x):
y = 2*x+5
return y

>>> doublePlusFive(7)
19

다시 프롬프트에서 doublePlusFive(7)을 입력하고 엔터를 치면 해당 함수가 실행되어 계산 결과를 표시하게 된다. 즉 대화형 쉘은 메모리에 만들어진 수식이나 읽어들인 외부 모듈을 계속 가지고 있으므로 간단한 ‘테스트’를 해보는 데 큰 도움이 된다. 특히 앞으로 배우게 될 리스트와 관련하여 애매한 부분들은 대화형 쉘에서 간단히 시험해봄으로써 정확한 명령문을 결정하는 데 도움이 될 수 있다.

모듈 함수의 도움말을 보기

help() 명령은 특정한 모듈 혹은 함수의 도움말을 표시해주는 명령이다. 백문이 불여일견, help(max)라고 대화형 쉘에서 실행해보자. max()는 이름에서 유추할 수 있지만 최대 값을 구하는 함수이다. 이 함수의 help 결과는 다음과 같다.


>>> help(max)
Help on built-in function max in module __builtin__:

max(...)
max(iterable[, key=func]) -> value
max(a, b, c, ...[, key=func]) -> value

With a single iterable argument, return its largest item.
With two or more arguments, return the largest argument.

help에서 표시하는 정보는 max__builtins__ 라는 특별한 이름의 모듈에 속해 있는 함수임을 알 수 있고, max(a, b, c, d...) 이런 식으로 쓰면 그 중에서 가장 큰 값을 반환한다는 설명을 볼 수 있다.

그러면 한 번 테스트해보자.


>>> someNumbers = (1,2,3,4,5)
>>> max(someNumbers)
    5

someNumber라는 변수에 (1,2,3,4,5) 라는 숫자 묶음을 대입해주고, max를 통해 someNumber 중에서 가장 큰 숫자를 구하는 명령을 실행해보면 최대 값을 구해 반환하는 것을 볼 수 있다.

모듈이 어떤 기능을 수행할 수 있는지를 살펴보는 방법

우리는 방금 ‘최대값’을 구하는 함수를 어떻게 사용하는지 살펴보았다. 그렇다면, 반대로 최대값을 구하는 함수도 있지 않을까? 이름으로 유추해보자면 min()이라는 함수가 있을 것 같다. 그럼 그런 함수가 있는지 없는 지 어떻게 알 수 있을까?

이럴 때 유용하게 사용할 수 있는 함수가 있다. 바로 dir()이라는 내장 함수이다. 이 함수는 인자로 받은 모듈에 포함되어 있는 하위 기능을 모두 열거해준다.

우리는 조금전 help(max)에서 이 기능이 “builtins“라는 특별한 모듈에 속해있다고 하는 것을 보았다. 양끝에 언더바(_)가 두 개씩 붙은 것은 “내부적으로”라는 의미라고 이해하면 되고 실제 글자로는 built-in된 모듈이라는 의미이다. dir 함수는 모듈의 내부 구성 요소를 볼 수 있는 명령이라 하였으니 이 builtins 모듈의 내부에 정말 min 이라는 함수가 있는지 살펴보도록 하자.


>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError',
'BaseException', 'BufferError', 'BytesWarning',
'DeprecationWarning', 'EOFError', 'Ellipsis',
'EnvironmentError', 'Exception', 'False', 'FloatingPointError',
'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
'ImportWarning', 'IndentationError', 'IndexError', 'KeyError',
'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError',
'None', 'NotImplemented', 'NotImplementedError', 'OSError',
'OverflowError', 'PendingDeprecationWarning', 'ReferenceError',
'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration',
'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit',
'TabError', 'True', 'TypeError', 'UnboundLocalError',
'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError',
'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning',
'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError',
'_', '__debug__', '__doc__', '__import__', '__name__',
'__package__', 'abs', 'all', 'any', 'apply', 'basestring',
'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable',
'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex',
'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod',
'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter',
'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr',
'hash', 'help', 'hex', 'id', 'input', 'int', 'intern',
'isinstance', 'issubclass', 'iter', 'len', 'license',
'list', 'locals', 'long', 'map', 'max', 'memoryview', 'min',
'next', 'object', 'oct', 'open', 'ord', 'pow', 'print',
'property', 'quit', 'range', 'raw_input', 'reduce',
'reload', 'repr', 'reversed', 'round', 'set', 'setattr',
'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super',
'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']

여기서 표시되는 이름들은 거의 모두가 내장 함수 혹은 내장 모듈들이다. 앞쪽에 대문자로 시작하는 것들은 거의 내장 모듈이나 상수들을 의미하며, 중간에 __로 둘러 싸여진 이름은 모듈 내에서 다시 “내부적으로” 사용되는 기능이다. 마지막에 abs… 부터가 실제 우리가 자주 사용하는 내장 함수 들의 이름이다.

이러한 내장 함수의 이름이나 사용법을 절대로 다 외울 필요가 없다. 앞으로 공부를 해 나가면서 필요한 함수들은 그만큼 자주 사용하게 될 것이니 자연스럽게 익히게 될 것이다. 대신, 뭔가 새로운 모듈이나 함수를 앞으로 접하게 된다면 dir 및 help 명령을 통해 사용법을 확인하고 다른 연관된 명령이 있는지 살펴보는 것은 큰 도움이 된다. 반드시 이런 식으로 공부를 해 나갈 것을 권한다.

1 그래서 함수의 이름을 잘 정하는 것은 엄청나게 중요한 일이다.