4.1 if文

(1/1)
条件分岐
Python など多くのプログラミング言語には制御という仕組みが備わっており、条件によって計算式を切り換えたり、同じ計算式を繰り返し実行することができる。
制御を活用することで、Excel関数だけでは実現できなかった処理が可能になり、プログラムの威力が目に見えてくる。

今回は、条件によって制御を切り替える if文の使い方を学ぶ。Python のブロックについて説明する。プログラムの内容も、少しずつ実用的なものにステップアップしてゆく。

目次

サンプル・プログラム

4年に一度のうるう年

4年に一度のうるう年――入力した西暦年がうるう年かどうかを判定するプログラムが "isLeapYear1.py" である。ユーザー定義関数の部分を抜粋して説明する。
なお、calendarモジュールの isleap関数を使えば、うるう年の判定は容易にできるのだが、今回は if文 の教材として、関数を手作りする。
def isLeapYear1(year):
	"""
	うるう年かどうかを判定する(不完全版)
	@param	int year	西暦年
	@return	bool True:うるう年である/False:ではない
	"""
	if (year % 4 == 0):
		res = True
	else:
		res = False

	return res
これまで何度か出てきた if文を使って、「4年に一度」の判定をプログラムに実装した。
if文if (式) のように用い、式が True のとき、それに続くブロック (インデントのある部分)を実行する。式が False のときは、else に続くブロック を実行する。

正確なうるう年判定

「4年に一度、うるう年」というのは、じつは不正確である。
現在の西暦であるグレゴリオ暦で、うるう年は次のように定義されている。
  1. 4で割り切れる年はうるう年である。
  2. ただし、100で割り切れる年は平年である。
  3. ただし、400で割り切れる年はうるう年である。
西暦2000年は400で割り切れたので、うるう年だった。西暦2100年は100で割り切れるので、平年である。これをプログラムにしたのが "isLeapYear2.py" だ。
def isLeapYear2(year):
	"""
	うるう年かどうかを判定する(完全版)
	@param	int year	西暦年
	@return	bool True:うるう年である/False:ではない
	"""
	if (year % 400 == 0):
		res = True
	elif (year % 100 == 0):
		res = False
	elif (year % 4 == 0):
		res = True
	else:
		res = False

	return res
あらたに elif が登場した。一般的に、if文 は次のような構造をとる。
if (式1):
    ブロック1
elif (式2):
    ブロック2
else:
    ブロック3
  1. もし式1がTrueならブロック1を実行する。
  2. 式1がFalseであり、式2がTrueならブロック2を実行する
  3. 式1,式2がすべてFalseなら、ブロック3を実行する。
  4. if文は必須である。
  5. elif句は2つ以上書くことができる。必要がなければ省略できる。
  6. else句は省略できる。
ここで if文 の順序に気をつけよう。if (year % 4) を先頭に持ってくると、西暦2100年もうるう年になってしまう。
まず、グレゴリオ暦の定義を次のように並べ替える必要がある。
  1. もし西暦年を400で割り切れれば、うるう年である。
  2. そうでなければ、西暦年を100で割り切れれば、平年である。
  3. そうでなければ、西暦年を4で割り切れれば、うるう年である。
  4. そうでなければ、平年である。
フロー図にすると下図のようになる。
うるう年の判定
うるう年の判定

早期リターン

"isLeapYear2.py" では、どんな西暦年に対しても3回の剰余算を行う。
一方、「3.7 数値型の誤差と範囲」で紹介したように、Python の処理速度はそれほど早い方ではない。
少しでも早く結果を得るには、算術演算は少ないに越したことはない。
そこで、プログラム "isLeapYear3.py" をご覧いただきたい
def isLeapYear3(year):
	"""
	うるう年かどうかを判定する(早期リターン版)
	@param	int year	西暦年
	@return	bool True:うるう年である/False:ではない
	"""
	if (year % 400 == 0):
		return True
400で割り切れたら、その時点で return してしまうのである。100, 4についても同様だ。
これを早期リターンと呼ぶ。Python では1つの関数に複数の return を書くことが可能という仕様を利用した。

入れ子になったif文

20世紀以降(1901年~)の西暦年月日を元号に変換するプログラムを作ってみる。
まず、20世紀以降で元号の切り替わった年月日を整理しておこう。
1912年7月30日大正
1926年12月25日昭和
1989年1月8日平成
2019年5月1日令和
明治と大正の切り替わり時期
明治と大正の切り替わり時期
令和を除けば、月の途中で切り替わっているため、条件分岐が複雑になる。
たとえば、1911年以前であれば「明治」で確定だが、1912年は――月が7以下で、日が29以下の時は「明治」、日が30以上なら「大正」。月が8以上なら「大正」――というように、元号の切り替わりの年は、月と日を加えた3段階の条件分岐が必要になる。
これを Python のプログラムにすると、次のように if文が入れ子になる。
def gregorian2gengo1(year, month, day):
	"""
	西暦年を元号に変換する(20世紀以降)
	@param	int year, month, day 西暦年月日
	@return	str 元号表記
	"""
	if (year <= 1900):
		gengo = "変換不能"
	elif (year <= 1911):
		yy = year - 1868 + 1
		gengo = "明治"
	elif (year == 1912):
		if (month <= 7):
			if (day <= 29):
				yy = year - 1868 + 1
				gengo = "明治"
			else:
				yy = year - 1912 + 1
				gengo = "大正"
		else:
			yy = year - 1912 + 1
			gengo = "大正"
	elif (year <= 1925):
		yy = year - 1912 + 1
		gengo = "大正"
	elif (year == 1926):
		if (month <= 12):
			if (day <= 24):
				yy = year - 1912 + 1
				gengo = "大正"
			else:
				yy = year - 1926 + 1
				gengo = "昭和"
		else:
			yy = year - 1926 + 1
			gengo = "昭和"
	elif (year <= 1988):
		yy = year - 1925 + 1
		gengo = "昭和"
	elif (year == 1989):
		if (month <= 1):
			if (day <= 7):
				yy = year - 1926 + 1
				gengo = "昭和"
			else:
				yy = year - 1989 + 1
				gengo = "平成"
		else:
			yy = year - 1989 + 1
			gengo = "平成"
	elif (year <= 2018):
		yy = year - 1989 + 1
		gengo = "平成"
	elif (year == 2019):
		if (month <= 4):
			yy = year - 1989 + 1
			gengo = "平成"
		else:
			yy = year - 2019 + 1
			gengo = "令和"
	else:
		yy = year - 2019 + 1
		gengo = "令和"

	if (yy == 1):
		yy = "元"

	return gengo + str(yy) + f"年{month}月{day}日"
プログラムをよく見てもらうと、year == 2019、つまり、平成から令和への切り替わりのところだけは、入れ子が2段で済んでいる。前述の通り、5月1日に元号が切り替わったため、条件式で年と月の2つを見ておけば切り替わるタイミングが判断できるのだ。また、5月1日であれば、4月の年度初めともぶつからないし、ゴールデンウィーク中なので基幹業務系システムを変更する影響も小さい。このように、社会の隅々にまで普及していたシステムへの影響を少しでも減らそうと、5月1日に改元が行われたという背景がある。
Python は他言語と同じように、if文の入れ子を幾らでも深くすることができる。しかし、いかにも読みにくい。
フロー図で表そうとすると、プログラムの流れが曲がりくねり、まるでスパゲッティの麺が絡んでいるように見えることから、スパゲッティ・プログラムと忌み嫌われる。スパゲッティ・プログラムは、あとで読んだときに理解に時間がかかることも問題なのだが、入れ子になっている分だけテストケースを増やさなくてはならず、余分な工数(コスト)がかかるし、テストケース漏れがあった場合には不具合が見つからないまま本番稼動を始めてしまう危険性がある。

どうしたら麺が絡まないようにすることができるか、よく考えてみよう――入れ子の原因は、年・月・日を独立した変数として見ていることにある。
ここで、年4桁+月2桁+日2桁という8桁の整数にまとてみよう。そうすれば、1つの変数で条件判定できる。つまり、1次元の数直線上で切り替わるタイミングを判定することができるようになる。この変数には yyyymmdd という名前を付けることが多い。もっとも、19262345(1926年23月45日)のような値は存在しないので、数直線といっても連続しているわけではない。
明治と大正の切り替わり時期(yyyymmdd)
明治と大正の切り替わり時期(yyyymmdd)
def gregorian2gengo2(year, month, day):
	"""
	西暦年を元号に変換する(20世紀以降)
	@param	int year, month, day 西暦年月日
	@return	str 元号表記
	"""
	yyyymmddStr = f"{year:04d}{month:02d}{day:02d}"		#1次元数にする
	yyyymmdd = int(yyyymmddStr)							#整数化

	if (yyyymmdd < 19010000):
		gengo = "変換不能"
	elif (yyyymmdd <= 19120729):
		yy = year - 1868 + 1
		gengo = "明治"
	elif (yyyymmdd <= 19261224):
		yy = year - 1912 + 1
		gengo = "大正"
	elif (yyyymmdd <= 19890107):
		yy = year - 1926 + 1
		gengo = "昭和"
	elif (yyyymmdd <= 20190430):
		yy = year - 1989 + 1
		gengo = "平成"
	else:
		yy = year - 2019 + 1
		gengo = "令和"

	if (yy == 1):
		yy = "元"

	return gengo + str(yy) + f"年{month}月{day}日"
これをプログラムにしたものが "gengo2.py" である。if文の入れ子がなくなり、流れが上から下へ直線的に落ちるようになった。
もし、プログラムを作っていて if文を入れ子にしなければならない場面に出会ったら、いったん立ち止まって、条件式に使っている変数を一元化できるか工夫してみてほしい。

練習問題

次回予告

Python には、条件分岐として、他言語にある switch文が存在しない。if文で代用できるというのがその理由なのだが、代わりに Python 3.10では match文が実装された。
次回は、switch文とはひと味違う match文の書き方について学ぶ。

コラム:フロー図

制御の流れを表すのに、よくフロー図(流れ図、フローチャート)を使う。本文中でも紹介した。
コラム:プログラム電卓から人工知能へ」で紹介したプログラミング電卓付属の「プログラム・ライブラリ」にもフロー図が掲載されており、とても参考になった。
プログラミングに慣れるまでは、手間を惜しまず、自力でフロー図を書くことをお勧めする。スパゲッティ・プログラムになっていないか点検する上でも役に立つからだ。

また、プログラミングに慣れた方でも、プログラムをフロー図にして見直すことで、条件式を正規化することなどして、制御の流れをシンプルにすることができる場合がある。
短くシンプルなプログラムほどバグが少ない。一度、自作プログラムを見直してみてはいかがだろうか。

コラム:スパゲッティプログラム

空飛ぶスパゲッティ・モンスター教
空飛ぶスパゲッティ・モンスター教
FORTRANやBASICでプログラムを組んでいた時代、GOTO文グローバル変数を多用することがスパゲッティプログラムの代表例とされた。
PythonJavaScriptといったモダンな手続き型言語では、GOTO文がなくなり、クラスのプロパティ(メンバシップ変数)を活用することでグローバル変数も使わずに済むようになっている。

だがしかし、安易な継承を繰り返せばスパゲッティプログラムになってしまうし、関数型を目指して不用意にコールバック関数を使えば、やはりスパゲッティプログラムになってしまう。さらに、処理速度を上げるために何でもマルチスレッドにすると、それもスパゲッティプログラムになってしまう。

新しいプログラミング言語を使えば、スパゲッティプログラムを防止できるというわけではない。
複雑なロジックが予想されるモジュールでは、初心にかえり、設計段階でフロー図を描いた方がいいだろう。マルチスレッドにするならタイミングチャートも必要だ。フロー図やタイミングチャートを眺めて、流れが直線的になるよう設計段階で配慮しなければいけない。

コラム:ExcelのIF関数、他言語の制御文

Excelアイコン
ExcelにもIF関数が備わっている。
うるう年の判定は、=IF(MOD(A1,400)=0,"うるう年",IF(MOD(A1,100)=0,"平年",IF(MOD(A1,4)=0,"うるう年","平年"))) とすることで実現はできるが、かなり読みにくくなってしまう。JavaScriptのように複数行にわたって書くことができるブロック文を制御する方が、読みやすい。
まずは、1954年にIBM 704にリリースされた世界最初の高水準プログラミング言語「FORTRAN」で書いてみよう。初期の FORTRAN は大文字と小文字の区別がなく、行頭から5文字目まではコメントや行番号用に空けておく。
!     うるう年の判定:FORTRAN
      SUBROUTINE IS_LEAP_YEAR(YEAR, RESULT)
      INTEGER YEAR, RESULT
 
      IF (MOD(YEAR, 4) .EQ. 0) THEN
         IF (MOD(YEAR, 100) .NE. 0 .OR. MOD(YEAR, 400) .EQ. 0) THEN
            RESULT = 1
         ELSE
            RESULT = 0
         ENDIF
      ELSE
         RESULT = 0
      ENDIF
 
      RETURN
      END
 
      PROGRAM MAIN
      INTEGER YEAR, RESULT
      YEAR = 2023
      CALL IS_LEAP_YEAR(YEAR, RESULT)
      IF (RESULT .EQ. 1) THEN
         WRITE(*,*) YEAR, '年はうるう年である.'
      ELSE
         WRITE(*,*) YEAR, '年はうるう年ではない.'
      ENDIF
      STOP
      END
つづいて、1959年に文系事務員でもプログラミングできる言語としてアメリカ政府主導で開発されたプログラミング言語「COBOL」で書いてみた。
*> うるう年の判定:COBOL
IDENTIFICATION DIVISION.
PROGRAM-ID. LeapYearProgram.
 
DATA DIVISION.
WORKING-STORAGE SECTION.
01 YEAR   PIC 9(4).
01 RESULT PIC 9(4) VALUE 0.
01 DIV PIC 9(4) VALUE 0.
01 MOD PIC 9(3) VALUE 0.
 
PROCEDURE DIVISION.
    MOVE 2024 TO YEAR
 
    PERFORM IS-LEAP-YEAR.
 
    IF RESULT = 1
        DISPLAY YEAR "年はうるう年である."
    ELSE
        DISPLAY YEAR "年はうるう年でない."
    END-IF.
 
    STOP RUN.
 
IS-LEAP-YEAR.
    MOVE 0 TO RESULT
    DIVIDE YEAR BY 4 GIVING DIV REMAINDER MOD.
    IF MOD = 0
        ADD 1 TO RESULT
        DIVIDE YEAR BY 100 GIVING DIV REMAINDER MOD
        IF MOD = 0
            MOVE 0 TO RESULT
            DIVIDE YEAR BY 400 GIVING DIV REMAINDER MOD
            IF MOD = 0
                ADD 1 TO RESULT
            END-IF
        END-IF
    END-IF.
1970年に、プログラミング教育を意識した言語として誕生した「Pascal」で書いてみた。FORTRANCOBOL とは違い、ブロックの概念が導入された。
{ うるう年の判定:Pascal }
program LeapYearProgram;
uses SysUtils;
 
function isLeapYear(year: integer): boolean;
begin
    if year mod 400 = 0      then isLeapYear := true
    else if year mod 100 = 0 then isLeapYear := false
    else if year mod 4 = 0   then isLeapYear := true;
end;
 
var
    year: integer;
begin
    year := 2024;
    if isLeapYear(year) then writeln(year, '年は うるう年である.')
    else                     writeln(year, '年は うるう年でない.');
end.
Pascal より1年早く誕生したのが C言語である。データ型が明確という違いがあるものの、JavaScriptに似ている。
/* うるう年判定:C */
#include <stdbool.h>
#include <stdio.h>
 
bool isLeapYear(int year) {
    bool result = false;
    if (year % 400 == 0) {
        result = true;
    } else if (year % 100 == 0) {
        result = false;
    } else if (year % 4 == 0) {
        result = true;
    }
    return result;
}
 
int main(void) {
    int year = 2023;
    if (isLeapYear(year)) {
        printf("%d年は うるう年である.", year);
    } else {
        printf("%d年は うるう年ではない.", year);
    }
}
1983年に C言語の改良版として登場した C++言語で書いてみる。三項演算子や文字列が扱えるようになり、JavaScriptに近い書き方ができる。
//うるう年判定:C++
#include <iostream>
 
bool isLeapYear(int year) {
    bool result = false;
    if (year % 400 == 0) {
        result = true;
    } else if (year % 100 == 0) {
        result = false;
    } else if (year % 4 == 0) {
        result = true;
    }
    return result;
}
 
int main(void) {
    int year = 2023;
    std::cout << year << "年は うるう年" << (isLeapYear(year) ? "である." : "ではない.") << std::endl;
}
CGIとして古くからWebアプリケーションに使われてきた Perlで書くとこうなる。
#うるう年判定:Perl
sub isLeapYear {
    my ($year) = @_;
    if ($year % 400 == 0) {
        $result = 1;
    } elsif ($year % 100 == 0) {
        $result = 0;
    } elsif ($year % 4 == 0) {
        $result = 1;
    } else {
        $result = 0;
    }
    return $result;
}
 
my $year = 2023;
if (isLeapYear($year) == 1) {
    print "$year 年はうるう年である.";
} else {
    print "$year 年はうるう年ではない.";
}
RubyPerlに似ている。
#うるう年判定:Ruby
def isLeapYear?(year)
    if (year % 400 === 0)
            result = true
        elsif (year % 100 === 0)
            result = false;
        elsif (year % 4 === 0)
            result = true
        else
            result = false
        end
    return result
end
 
year = 2023
result = isLeapYear?(year) ? 'である.' : 'ではない.'
puts "#{year}年は うるう年#{result}"
サーバサイド・プログラムとして人気のある PHPで書くとこうなる。変数名の頭に $ が付くのが特徴的だ。動的型付けなので、データ型まで含めて厳密に等価であることを計算するのに厳密等価演算子 === が用意されている。
<?php
//うるう年判定:PHP
function isLeapYear($year) {
    if ($year % 400 === 0)          $result = true;
    else if ($year % 100 === 0)     $result = false;
    else if ($year % 4 === 0)       $result = true;
    else                            $result = false;
    return $result;
}
 
$year = 2023;
$result = isLeapYear($year) ? 'である.' : 'ではない.';
print $year . '年は うるう年' . $result;
?>
サーバサイド・プログラムとして歴史の長い Javaで書くとこうなる。JavaScript と似た名前だが、別物であることが分かる。
//うるう年判定:Java
import java.util.*;
 
public class Main {
    //うるう年判定:Java
    public static boolean isLeapYear(int year) {
        boolean result = false;
        if (year % 400 == 0)        result = true;
        else if (year % 100 == 0)   result = false;
        else if (year % 4 == 0)     result = true;
        return result;
    }
 
    public static void main(String[] args) throws Exception {
        int year = 2023;
        String result = Main.isLeapYear(year) ? "である." : "ではない.";
        System.out.println(year + "年は うるう年" + result);
    }
}
JavaScriptをサーバサイドでも利用できるようにした TypeScriptで書くとこうなる。JavaScript と比べると、データ型が明示されている。
//うるう年の判定:TypeScript
function isLeapYear(year: number):boolean {
    let ret: boolean;   //戻り値
 
    if (year % 400 === 0) {
        ret = true;
    } else if (year % 100 === 0) {
        ret = false;
    } else if (year % 4 === 0) {
        ret = true;
    } else {
        ret = false;
    }
 
    return ret;
}
 
const year: number = 2100;
const isLeap: boolean = isLeapYear(year);
 
if (isLeap) {
    console.log(`${year}年はうるう年である.`);
} else {
    console.log(`${year}年はうるう年ではない.`);
}
2009年にGoogleが開発したプログラミング言語 Goで書くとこうなる。C++Java に似ているが、より簡明な書き方ができる。
//うるう年判定:Go
package main
import ("fmt")
 
func isLeapYear(year int) bool {
    var result bool
    if (year % 400 == 0) {
        result = true
    } else if (year % 100 == 0) {
        result = false
    } else if (year % 4 == 0) {
        result = true
    } else {
        result = false
    }
	return result
}
 
func main() {
    var year int
    year = 2023
    if isLeapYear(year) {
        fmt.Printf("%d年はうるう年です。\n", year)
    } else {
        fmt.Printf("%d年はうるう年ではありません。\n", year)
    }
}
WWWを動的に表示するためのスクリプト言語として、1995年に登場した JavaScript で書くとこうなる。
//うるう年判定:JavaScript
function isLeapYear(year) {
    if (year % 400 == 0) {
        result = true;
    } else if (year % 100 == 0) {
        result = false;
    } else if (year % 4 == 0) {
        result = true;
    } else {
        result = false;
    }
    return result;
}
 
let year = 2023;
if (isLeapYear(year) == true) {
    result = "である.";
} else {
    result = "ではない.";
}
console.log(year, "年は うるう年", result);
C言語に Smalltalk型のオブジェクトし高機能を持たせたプログラミング言語 Objecctive-C は1984年に発表され、NeXTやmacOSの開発言語となった。
//うるう年判定:Objective-C
#import 
 
BOOL isLeapYear(NSInteger year) {
    BOOL ret = NO;
    if (year % 400 == 0) {
        ret = YES;
    } else if (year % 100 == 0) {
        ret = NO;
    } else if (year % 4 == 0) {
        ret = YES;
    } else {
        ret = NO;
    }
    return ret;
}
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger year = 2024;
        if (isLeapYear(year)) {
            NSLog(@"%ld年はうるう年である.", (long)year);
        } else {
            NSLog(@"%ld年はうるう年でない.", (long)year);
        }
    }
    return 0;
}
Objective-C の後継として、2014年にAppleがiOSやMac向けのネイティブアプリケーションを開発するために発表したオープンソースのプログラミング言語 Swiftで書くとこうなる。書き方がだいぶシンプルになった。
//うるう年判定:Swift
func isLeapYear(_ year: Int) -> Bool {
    var ret: Bool
    if year % 400 == 0 {
        ret = true
    } else if year % 100 == 0 {
        ret = false
    } else if year % 4 == 0 {
        ret = true
    } else {
        ret = false
    }
    return ret
}
 
let year = 2023
if isLeapYear(year) {
    print("\(year)年はうるう年でらる.")
} else {
    print("\(year)年はうるう年ではない.")
}
関数型プログラミング言語の祖先 LISPで書くとこうなる。
;うるう年判定:Lisp
(defun isLeapYear (year)
    (if (zerop (mod year 400)) t
    (if (zerop (mod year 100)) nil
    (if (zerop (mod year 4))   t
    nil)))
)
(setq year 2023)
(if (isLeapYear year)
    (format t "~A 年はうるう年である.~%" year)
    (format t "~A 年はうるう年ではない.~%" year))
同じく関数型プログラミング言語 Haskellで書くとこうなる。
--うるう年判定:Haskell
isLeapYear :: Int -> Bool
isLeapYear year
    | (year `mod` 4 == 0) && (year `mod` 100 /= 0) = True
    | year `mod` 400 == 0 = True
    | otherwise = False
 
main = do
    let year = 2023 ::Int
    if isLeapYear year
        then putStrLn $ show year ++ "年はうるう年です。"
        else putStrLn $ show year ++ "年はうるう年ではありません。"
人工知能言語として一世を風靡した論理型プログラミング言語の Prologでは条件分岐の概念がなくなり、こんなプログラムになる。
%うるう年判定:Prolog
isLeapYear(Year) :- 0 is Year mod 400, !.
isLeapYear(Year) :- 0 is Year mod 100, !, fail.
isLeapYear(Year) :- 0 is Year mod 4, !.
main(Year) :- (isLeapYear(Year) ->
    format('~w 年はうるう年です。', [Year]);
    format('~w 年はうるう年ではありません。', [Year])
    ).
:- main(2023).
SQL はデータベースを処理するためのスクリプトだが、PROCEDUREを備えているRDBMSでは、プログラミング言語のようなことができる。下記は MySQL で実行できる。
--うるう年判定:MySQL
DELIMITER //
 
CREATE PROCEDURE isLeapYear(IN year INT, OUT message VARCHAR(50))
BEGIN
    DECLARE result BOOLEAN DEFAULT FALSE;
 
    IF (year % 400 = 0) THEN
        SET result = TRUE;
    ELSEIF (year % 100 = 0) THEN
        SET result = FALSE;
    ELSEIF (year % 4 = 0) THEN
        SET result = TRUE;
    END IF;
 
    IF result THEN
        SET message = CONCAT(year, '年はうるう年です');
    ELSE
        SET message = CONCAT(year, '年はうるう年ではありません');
    END IF;
END //
 
DELIMITER ;
 
SET @year = 2024;
SET @message = '';
 
CALL IsLeapYear(@year, @message);
 
SELECT @message;

参考サイト

(この項おわり)
header