[プログラミング演習 --- 情報基礎演習[全学向] >  関数]

関数

プログラムにおけるデータ処理は,たとえば「ファイルからデータを読み込んで解釈する処理」「データに対する分析の処理」「データを所定の形式で保存する処理」など,一連のまとまった処理を部品とみなして,データ処理全体の過程がそれらの部品によって構成されていると考えられます. プログラムにおいては,同一の部品が何度も利用されることもあります. それらの部品は最初から使える場合もありますし,プログラムで新たに作成して使うこともあります.

ここでは,プログラムにおいて新たな機能を有する部品を定義して利用する仕組みである「関数」の基本について説明します.

もくじ

  1. はじめに
  2. 例題
  3. 関数の定義
  4. 関数の利用
  5. 関数の階層構造化 ー 下請け関数の定義と利用
  6. 関数の値(返り値) ー return
  7. グローバル変数とローカル変数
  8. [参考] データとしての関数
  9. サンプルプログラム

はじめに

まず最初にプログラムにおける「関数」とはどういうものか,なぜプログラムで「関数」を使うのかについて説明します.

さて本科目で扱っているタートルグラフィクスでは,「線を描く」「円を描く」あるいは「色を指定する」などの描画機能が「forward」「circle」「color」などの部品によって提供されています.そのようにタートルグラフィクスに備わっている「ビルトインの部品」の機能を利用することで,たとえば次の図のような絵を描くことができます(参考:flower_garden.py).

フラワーガーデン

それでは,このような絵を描くプログラムは,ビルトインの部品のみを用いて記述することになるのでしょうか. たとえば上の図のような絵を描くとして,その一部として,花を一つ描く処理をビルトインの部品のみで次のように記述することはできます.


    n = 5   # 花弁の枚数
    sz = 80 # 花のサイズ
    theta = 360.0/n # 花弁の中心角
    r_c = 0.2*sz    # 花芯のサイズ
    r = sz*math.sin(theta*math.pi/360)
    phi = theta/2
    
    mark() # 最初の状態をマーク
    fill_color(100,37+12*rand(3),37+12*rand(3))
    # 花弁をすべて描く
    for _ in range(n):
        begin_fill()
        turn(-phi)
        forward(sz)
        turn(phi)
        arc(r,180)
        turn(phi)
        forward(sz)
        turn(180-phi)
        end_fill()
        turn(theta)

    # 花芯を描く
    fill_color(100,85,25)
    begin_hover()
    forward(r_c)
    turn(90)
    end_hover()
    begin_fill()
    circle(r_c)
    end_fill()
    begin_hover()
    back()
    end_hover()

しかし,このように「forward」「turn」等々のビルトインの部品のみでプログラム全体を記述したとすれば,プログラムが長くなって,どこでどのような処理をしているのか,プログラム全体の構成が把握しにくくなってしまうでしょう. また,たとえば縦横の比率の異なる多数の長方形で構成される図形を描くとしたとき, 長方形はビルトインの部品で描けるものの,それらを何度も記述すると, プログラムに同じような描画手順の記述が何度も出てきて,冗長になってしまうでしょう.

さて上図のような絵の描き方を(プログラミング言語ではなく)言葉を使って説明するのであれば,「サイズ○○で花弁が☆枚ある花を描く」と言ったり, さらに花の描き方自体を説明するのであれば,「サイズが○○の花弁を放射状に☆枚並べて描く」「花芯を中央に描く」などと言ったりするでしょう. このように「サイズ○○で花弁が☆枚ある花を描く」といった(ビルトインではない)機能を有する部品をプログラムで利用できるとすれば,次のような流れでプログラムが記述できるでしょう(ここでは言葉で表現しています).


  線を描かずに花を描画する位置まで移動する
  塗りつぶしの色を紫とする
  サイズ90で花弁が5枚ある花を描く
  線を描かずに画面中央まで戻る

たとえばこのように「花を描く処理」を1つの部品として記述できるのであれば,ビルトインの部品のみで記述する場合に比べて,プログラムの見通しがはるかによくなるでしょう.

実際,タートルグラフィクスに限らず,プログラムにおいてはビルトインでは提供されていない部品,いわば「カスタムメイドの部品」を導入して用いることができます. カスタムメイドの部品は,ビルトインの部品や他のカスタムメイドの部品を用いて設計します. たとえば上の例であれば, 「花を描く」部品は「花弁を描く」部品と「花芯を描く」部品があれば,それらを使って構成できて,また「花弁を描く」部品,「花芯を描く」部品は,ビルトインの部品(forward,arc,turnなど)を使って構成できることが想像できるでしょう.

Pythonでは,そのようなカスタムメイドの部品を「関数」と呼びます(数学の「関数」と同様のものと言えますが,完全に同じではありません). 以下では,Pythonの関数を定義する(作る)方法,利用する方法などについて,例も使って,その基本を説明します(基本にとどめて,関数の仕様について網羅的には説明しません).

Pythonでの「関数」は,他の言語では「メソッド」「プロシージャ」「サブルーチン」などと呼ばれたりすることもあります(Pythonでも関数が「メソッド」と呼ばれる場合もあります).

例題

タートルグラフィクスで次のように正方形で構成される図形を描くプログラムを作成する.
正方形で構成される図形

この図形は,外側の2つの正方形のそれぞれの中央に小さな正方形を1つずつ配置することで描いています.小さな正方形の辺の長さは大きな正方形の辺の長さの1/3倍としています. 次にプログラムを示します. なおプログラムでは,各正方形を上図とは異なる明るさのグレーで描いています(上図は,実際に描かれるものとは明るさを意図的に変えています).

関数の定義

例題のプログラムでは,塗りつぶした正方形を描く関数「square」を定義して,利用しています. squareで描画する正方形のサイズは関数のパラメタとして指定できるようになっています. 関数squareは次のように定義しています.


  # 1辺sの塗りつぶした正方形を描く
  # 最初の向きに辺を描くところから開始
  # 左回りか右回りで正方形を描いて
  # 最初の状態に戻る
  def square(s,d):
      if d == "right":
          theta = -90
      else:
          theta = 90
    
      begin_fill()
      for _ in range(4):
          forward(s)
          turn(theta)
      end_fill()

まず最初のdefの行で関数の名前と関数のパラメタが決められています.

この行以降ではインデントが1レベル深くなっています. そのインデントによって定まるブロックで関数の処理が定義されています. このsquareでは正方形の描画手順が定義されています. またその手順の中でパラメタs,dが用いられています. パラメタを用いた処理によって関数が定義されていることから,Pythonでの関数は数学での「関数」と同様のものと理解できるでしょう.

さてパラメタsは正方形の1辺の長さを表しています. sにさまざまな値を指定(代入)することで,その値に合わせたサイズの正方形が描けるようになっています. この関数squareでは,もう一つのパラメタdを用いるように定義していて, dの値によって,正方形が2通りの異なる配置で描かれるようになっています. なおパラメタdには文字列が指定されることを想定しています(パラメタは数値とは限りません).

  1. dが"right"のとき   → 亀の右前方に正方形を描く(亀は最初に正方形の左下角にいるものとして,右回りに一周して正方形を描く)
  2. dが"right"以外のとき → 亀の左前方に正方形を描く(亀は最初に正方形の右下角にいるものとして,左回りに一周して正方形を描く)

パラメタs,dに適切に値が指定されるとして, 関数の処理手順を一通り見てみれば,上に示したように正方形が描かれることが分かるでしょう. なお関数の処理においては,パラメタに加えて,新たな変数を定義して利用することもできます.たとえば関数squareでは,変数thetaを関数内で定義して利用しています.

[参考] 関数squareを(少しだけ)改良する

関数squareは次のように定義することもできます. この定義は最初のバージョンに比べて少しだけ記述が短くなっています.


  # 1辺sの塗りつぶした正方形を描く
  # 最初の向きに辺を描くところから開始
  # 左回りか右回りで正方形を描いて
  # 最初の状態に戻る
  def square(s,d):
      theta = -90 if d == "right" else 90
      begin_fill()
      for _ in range(4):
          forward(s)
          turn(theta)
      end_fill()

このように書き直したsquareを用いたプログラムを次に示します.

関数定義の基本形式

関数の定義方法の基本を示します.


  def 関数名(パラメタ0, パラメタ1, ...):
      # 関数の処理手順をインデントされたブロックとして記述する
      # (正式には関数のブロックの先頭には関数の説明を「docstring」とよばれる文字列として書く.docstringは省略できる)
      処理手順0
      処理手順1
      処理手順2
         :
         :

パラメタは関数内で使われる変数で,関数の処理が実行されるときにはパラメタには値が代入されます. その後,関数のブロックの処理が順に実行されます.

関数のパラメタの設定方法には,上には述べていないバリエーションがありますが, 以上では,基本形式に限って説明しています.

関数の利用

この関数squareは,次のようにdrawで利用されています(drawも関数です).

drawは関数で,その関数をlaunchに渡すようにしています. タートルグラフィクスでは,launchで指定される関数を亀の動作の定義として受け取って,描画処理を実行するようになっています.

  def draw():
  
      speed("normal")
      s0 = 300    # 外側の正方形のサイズ
      s1 = s0/3   # 内側の正方形のサイズ
      b0 = 50     # 内側の正方形の明るさ
      b1 = 10     # 外側の正方形の明るさ(左)
      b2 = 100-b1 # 外側の正方形の明るさ(右)

      # 左右に外側の正方形を描く
      color(b1,b1,b1)    # 明るさをb1に設定
      square(s0,"left")  # 正方形(辺の長さs0)を左回りで描画する
      color(b2,b2,b2)    # 明るさをb2に設定
      square(s0,"right") # 正方形(辺の長さs0)を右回りで描画する

      # 左右の正方形の内側に同じ明るさの
      # 小さい正方形を描く
      color(b0,b0,b0)
      for _ in range(2):
          mark()            # 最初の状態をマーク
          begin_hover()
          toward(s1,-s1)    # 向きを保って,内側の正方形の一番近い頂点へ
          end_hover()
          square(s1,"left") # 正方形(辺の長さs1)を左回りで描画する
          back(hover=True)  # 線を描かずに最初の状態に戻る
          turn(-90)

(定義済みの)関数を利用することを,しばしば「関数を呼び出す」といいます. 関数は定義してから利用します. 関数は,定義する前に呼び出すことはできません. そこで次のプログラムを実行するとエラーが発生します.


  f(1) # ==> Error! ('f' is not defined)

  # 関数fの定義
  def f(x):
      print(f"x={x}")

ところで例題のプログラムでは,関数drawより後に関数squareが定義されていますが,問題ないのでしょうか. 上の例とは異なって,これは問題ありません. 例題のプログラムにおいて,drawが呼び出されてdrawの中でsquareが呼び出されるのはプログラムのすべての定義が読み込まれた後です(プログラムの末尾でlaunchが呼び出されて,ウインドウが開いた後,描画開始の合図があった時点でlaunchで指定したdrawが実行されます). つまりその時点ではsquareは定義済みです.

関数を呼び出すときには,関数名につづいて,定義に合わせて各パラメタに渡す値を式で指定します. 関数を呼び出すときにパラメタに渡す値を引数(ひきすう; argument)と呼びます. 数学の関数と同様に,定義の各パラメタと呼び出しの際に指定する引数は順に対応付けられます. より具体的には,まず引数の式の値が計算されて,その結果の値が関数の対応する位置のパラメタに代入されます.


  sz = 100

  # 関数squareが「s = 100; d = "left"」で実行される
  square(sz,"left")

  # 関数squareが「s = 200; d = "right"」で実行される
  square(2*sz,"right")

パラメタがない(パラメタが0個である)関数については,呼び出す際には空括弧()を指定します(括弧をつけないと意味が変わってしまいます). たとえば本科目のタートルグラフィクスではbegin_fill,end_fillなどはパラメタがない関数で,()をつけて呼び出しています.


    begin_fill() # パラメタがない関数の呼び出し
    for _ in range(4):
        forward(s)
        turn(90)
    end_fill()   # パラメタがない関数の呼び出し
    

なお関数への引数の渡し方には,上には述べていないバリエーションがあります.

関数に渡される引数は,指定された式を評価した値であることに注意してください.

  
  a = 1

  def foo(x):
    x = x + 1

  foo(a)
  print(a) # 「1」と表示される(2ではない)
  

上の例で,関数fooのパラメタxとfooの呼び出しで引数として指定されているaは,変数としては別々のものです. 「foo(a)」として関数fooを呼び出したとき,パラメタxにはaを評価した値である「1」が代入されます. fooの内部でxの値を変更したからといって,aの値が変わったりはしません.

[参考] 関数を使わずに記述した例題のプログラム

例題のプログラム(正方形で構成される図を描くプログラム)は関数を使わなくても作成できます.

関数squareを「begin_fill」「for」「forward」「turn」「end_fill」の組み合わせに置き換えています. 関数を使ったプログラムに比べて,drawの定義が長くなっていて, 同じような処理が繰り返し記述されていることが分かるでしょう. ただこの程度であれば,(関数squareがシンプルであるため)関数を使わなくても問題ないと思えるかもしれません. しかし関数で記述される処理がもっと長くなった場合を考えれば, 関数の有用性が想像できるのではないでしょうか.

(独白) 理解のしやすさを考えれば,複雑な例題は避けた方がよいが,簡単な例題にすると,関数のありがたみが見えにくくなるという.

関数の階層構造化 ー 下請け関数の定義と利用

関数は必ずしもビルトインの部品だけで定義するわけではありません. 複雑な処理を行う関数は,下請けとなる関数を作っておいて, そのような関数を組み合わせて構成します. いわば関数を階層構造化して定義できるわけです. 「はじめに」で示した花の例(flower_garden.py)では,「花を描く関数」を定義するために,下請けとして「花弁を描く関数」「花芯を描く関数」を定義して利用しています. 次に「花を描く関数」「花弁を描く関数」「花芯を描く関数」の関係を,詳細は省略しつつ示します(実際の定義はプログラムで確認してください).


  # 花を描く(n:花弁の枚数, sz:サイズ, p_c:花弁の色)
  def flower(n,sz,p_c):
             :
      for _ in range(n):
          petal(sz,theta) # 花弁を描く関数を利用
             :
      core(sz*CORE_RATIO) # 花芯を描く関数を利用
             :


  # 花弁を描く(s:サイズ,theta:花弁の中心角)
  def petal(s,theta):
      r = s*math.sin(theta*DEGRAD_2)
      phi = theta/2
           :

  # 花芯(円)を描く(r:半径)
  def core(r):
      begin_hover()
      forward(r)
           :

複雑な処理を一つの関数で記述しようとすると,得てして長くなりがちです. 長い関数は全体の構造を把握することが難しくなります. 長い処理は,より小さな単位の処理に分割して表現できることが少なくないでしょう. そのような場合には,下請け関数を作って,構造が分かりやすくなるように処理を分割することが重要です. このようなことから,下請け関数を分割して,さらに下位の下請け関数で構成することも一般的です.

関数の値(返り値) ー return

数学の関数では,入力として与えるパラメタに対して出力としての値が一つ定められます. 数学の関数と同様に,Pythonの関数でも関数の値を定義できます. 関数の値は「return」で定義できます. 次に簡単な例を示します.


  def t_cmp(x,y):
      if x < y:
          v = -1
      elif x > y:
          v = 1
      else:
          v = 0
      return v

  a = t_cmp(1,2) # a == -1
  b = t_cmp("hello","everyone") # b == 1 (辞書式順序)
  c = t_cmp(True,True)  # c == 0
  d = t_cmp(False,True) # d == -1 (False < True)

returnの後に式を指定することで,その式の値を関数の値とすることができます. 関数の値は「返り値」(return value)などと呼ばれます. 関数を実行する(呼び出す)と,呼び出し側では返り値が得られて, その値を利用できます.

returnは,関数の末尾だけでなく,どこでもいくつでも記述できます. 上の関数t_cmpは次のようにも定義できます.


  def t_cmp_another(x,y):
      if x < y:
          return -1
      elif x > y:
          return 1
      else:
          return 0

2つ以上の値を返すこともできます.


  def sort2(x,y):
      if x <= y:
          return x,y
      else:
          return y,x

  s,t = sort2(1,-1) # s == -1, t == 1
  

また返り値を指定しないでreturnのみを書くこともできます. そのように記述することで(関数から明示的には値を返さずに)関数の処理を途中で終了するように記述できます.


# 正方形を描く(1辺:s)
  def sq(s):
      # s ≦ 0なら何もしないで直ちに終了する
      if s <= 0:
          return

      # s > 0なら正方形を描く
      for _ in range(4):
          forward(s)
          turn(90)

なおreturnのみを書いた場合,返り値は「None」(値がないことを表すデータ)となります.

返り値を指定しない関数

Pythonの関数では,すでに関数squareの例で見たとおり,必ずしもreturnで返り値を設定しなくても構いません(squareを描画した結果の「値」は考えないでしょう). Pythonでは,パラメタを使って一連のまとまった処理を行う部品を一般に「関数」と呼んでいるわけです.

なおreturnがない関数の返り値は「None」(値がないことを表すデータ)となります.

グローバル変数とローカル変数

変数は関数の外部でも内部でも定義できます. 変数は,関数の外側で定義されるか,内側で定義されるかによって, 有効範囲(利用可能な場所)が異なってきます. 関数の外側で定義される変数をグローバル変数, 内側で定義される変数をローカル変数とよびます.

グローバル変数は定義された後,プログラムのファイルの末尾まで有効です. 関数の内側でも参照できます. 一方で,ローカル変数(関数の内部で代入の左辺に現れる変数),関数内で定義されてから,同じ関数の末尾まででのみ利用可能です. 関数の外側,あるいは他の関数では利用できません. 関数のパラメタもローカル変数で,関数の内部全体で利用可能です.

さて関数の内部と外部で同じ名前の変数を定義することもできます. この場合,名前は同じであっても,それらは別々の変数として扱われます. 関数の内部ではローカル変数が参照されます. 関数の外部ではグローバル変数が参照されます. ローカル変数とグローバル変数は次のように使い分けられます. 関数の内部で,ある名前の変数が参照されると,まずは関数の内部で,その名前の変数の定義を探します.見つかれば,その変数の値を使います. このとき関数の外部の変数は検索しません. そこで外部に同じ名前の変数が定義されていても,定義されてなくても何も影響はありません. 一方で,関数の内部で変数の定義が見つからなければ, 関数の外部で変数を探します. このように関数の内部と外部に同じ名前の変数が定義されていると,関数の内部ではローカル変数が優先されて,グローバル変数は見えなくなります.

関数の内部と外部に同じ名前の変数が定義されている場合に, 関数の内部でグローバル変数を利用する方法もあります.

次にグローバル変数とローカル変数を利用したプログラムの例を示します.これはサンプルプログラム「variable_scope.py」を簡略化したものです.


# グローバル変数x,yの定義
x = 1
y = 2

def foo(a):
    # a,bはローカル変数(aは関数のパラメタ)
    a = a + 1
    b = x + y # x,yはグローバル変数
    return b

def bar(x):
    # xは関数のパラメタ(ローカル変数)
    # yはグローバル変数
    x = x + y 
    z = y # zはローカル変数

v = foo(x)
bar(y + 2)

上で説明したように,グローバル変数とローカル変数が定まることを確認してください.

サンプルプログラム「variable_scope.py」では,グローバル変数,ローカル変数,関数のパラメタ,関数呼び出しでの引数の利用例をいろいろと示しています. 関数内部で「定義される」(代入の左辺に現れる)変数がローカル変数であること, 変数は定義されてから利用すること, 関数呼び出しでの引数は,呼び出しで与えられた式の計算結果の値として関数のパラメタに代入されること, 変数には有効範囲があることなどを確認してみてください.

Pythonでは関数の内部に関数を定義できます.その場合も新たなスコープが導入されます. そのとき外側の関数のローカル変数は内側でも参照可能です. プログラムが2つ以上のファイルで構成される場合に, グローバル変数は他のファイルでは,そのままでは参照できません.

[参考] データとしての関数

Pythonでは関数をデータとして扱うことができます. そこで関数を変数に代入して利用したり, 関数をパラメタとする関数を定義したり, 関数を返す関数を定義したりできます.

ちなみにタートルグラフィクスのlaunchという関数は, 関数を引数として呼び出すことで,その関数をパラメタとして利用して, 描画処理を行うように定義されています.

  from pyturtle import *

  def draw():
    speed("fastest")
    color(0,0,100)
      :
      :
      :

  # 関数drawを引数として関数launchを呼び出す
  launch(draw)

参考までに関数をデータとして扱っているプログラムの例を示します.

サンプルプログラム

次に関数を定義して用いているサンプルプログラムを紹介します.

[プログラミング演習 --- 情報基礎演習[全学向] >  関数]