[Work/Class/ES2015で動的Webアプリ/ES2015Basic]

ES2015の基本 - 変数とそのスコープ,関数定義,Promiseによるコールバック / 2017-07-27 (木)

'use strict';

ソースコードの初めに'usr strict';を実行しておくと,ES2015以降に厳密なモードになり,間違って宣言時にグローバル変数になってしまったり等のミスを防ぐことができる.

今まで(つまり'use strict';を実行せずに)関数内でvar等を付けずにいきなり変数名が登場すると,それはグローバル変数の宣言と見なされていた.このようなミスがなくなる.

関数宣言内部で'use strict';を実行して,効果を関数内に限定することもできるが,上記のようなミスを防ぐためにソースコード全体で'use strict';を有効にするべきである.

変数

varとlet, const

今までのJavaScriptでは,変数を宣言する時にvar a = 1;のように宣言し,初期化してきた.ES2015ではこのほかに,let宣言とconst宣言がある.

varとletのスコープ

varのスコープは,グローバル(window, global)→クラス(オブジェクトインスタンス)→関数まで,である(PythonやRubyと同じ).

letのスコープは,グローバル→クラス→関数→ブロックまで可能である(C言語やJavaと同じ).すなわちletはvarの完全上位互換宣言である.

特に今までのJavaScriptではfor文を回す時に変数のスコープを気にしなければならなかった(逆にそれを使ったテクニックもある)が,for1回分のブロック内部でletで宣言してしまえば,ブロックの外への影響は全くなくなる.

const

constはC言語と同じく,一度宣言と同時に初期化してしまうと値を変更できない変数,つまり定数として使える.2017年現在,ES2015のみならず全ての言語で,可能なものは全てconst宣言する習慣が広まりつつある.

グローバルスコープ = window(global)インスタンス

ブラウザ内で実行されるJavaScriptは全てwindowというオブジェクトインスタンスの中で動作している.(Node.jsはwindowの代わりにglobalという名前になる)

つまり「グローバル変数」に見えていても,実際はwindowオブジェクトインスタンス内の変数であるといえ,window.変数名でアクセスすることができる.

関数の定義

ES2015での関数の定義は,以下の3つの方法がある.

関数名でのアクセス(静的関数定義と同じやり方)

function 関数名(引数){
    //処理内容
    return 戻り値
}

このように関数定義した場合,

let 戻り値を受け取る変数 = 関数名(引数);

という形で呼び出す.

関数を変数に打ち込む

JavaScript(Pythonも可能だが)は第一級関数言語と呼ばれる関数型言語である.関数そのものをオブジェクトインスタンスのように取り扱うことができる.

C言語ではあくまで関数は静的に存在するものでしかなく,Javaではクラス内に静的に存在するものと,クラスのオブジェクトインスタンス内部に存在することしかできない.

イメージとしてはJavaの関数一つだけを含んでいるオブジェクトインスタンスのようなものである.これを関数オブジェクトと呼称する.(関数インスタンスとは呼称しない)

let myFunction = function(引数){ /* 処理内容 */ };

という形で変数に代入され,

let 戻り値を受け取る変数 = myFunction(引数);

という式で実行できる.

ついでに「関数オブジェクトはFunctionクラスのオブジェクトインスタンス」であるので,Functionクラスのコンストラクタの引数に,引数名と処理内容を渡すことで関数オブジェクトを生成する.

let myFucntion = new Function("引数名", "{処理内容}");

この場合上記のように書く.引数名や処理内容がダブルクオーテーションで囲まれることに注意(しかしこれは,引数や処理内容そのものを文字列として扱えることも示している).内部的にはこの記法は他の記法とは結構違いがあり,あまり積極的に使うことはない.

アロー記法

ES2015では,アロー記法と言われる関数オブジェクトの生成手法が推奨されている(ただし古いバージョンのOSXのSafariではサポートされていないので注意すること).

以下のように書く.

let myFunction = (引数) => 戻り値;

何を言っているかわからねーと思うが(以下略,具体的には以下の通りである.

従来のfunctionキーワードを使った関数オブジェクト生成の記法では,

let addTwoValue = function(x, y){
    return x + y;
};

と書くべきところを,

let addTwoValue = (x, y) => {
    return x + y;
};

つまり,(引数) => {関数の処理}と書くのがアロー記法である.

処理内容が戻り値を返すreturnの1行の時は,さらに中括弧とreturnキーワードを省略して,

let addTwoValue = (x, y) => x + y;

と書くことができる,という具合である.

クラス指定の所で出てくるが,クラスを使うプログラムの時のコールバック関数に設定する関数オブジェクトの生成手法としては,このアロー記法が推奨される.

関数の引数のデフォルト値

オーバーロードと同じような感じで,引数を省略して関数を実行した場合使われる「デフォルトの値」を関数定義時に与えることができる.

例えば,

function myFunction(a = 3, b = 4, c = 5){
    return a + b + c;
}

という関数定義があった時myFunction(10);と呼び出すと,aには実際に指定した10,bにはデフォルト値の4,cにはデフォルト値の5が自動的に入り,関数は19をreturnする.

window.onload関数とwindow.onunload関数

前述の「グローバル環境そのもの」であるオブジェクトインスタンス,windowは当然ながら変数だけでなく関数も持つことができる.一番よく使われるのが,window.onload関数とwindow.onunload関数である.(正確には次項で出てくるプロパティである.onloadプロパティに関数を登録する,と考える)

window.onload = function(){
    //その画面が最初に開かれてHTMLやJavaScript全体の読み込みが終了した時に実行される処理内容
};
window.onunload = function(){
    //その画面が閉じられたり,他のページへ遷移したりする時に呼ばれる処理内容
};

である.windowオブジェクトインスタンスは元々これらの関数(正確には関数オブジェクト)を持っていることが前提なので,これらの指定はいわゆるオーバーライドと同じ概念として捉えることができる.(windowオブジェクトが元々持っているonloadイベントハンドラ関数をオーバーライドしている,と考えると良い)

関数コールバックとPromise

関数コールバック

関数コールバックとは,「この処理が発生,終了,成功した後に,この関数の処理を実行してね」と,次に処理する内容を関数としてまとめておいて,指定することである.

今までイベント関数と呼んできた「とあるイベントが起こったら,この関数を実行せよ」というものは,だいたいこのコールバック関数である.前述のwindow.onload = function(){};, window.onunload = function(){};もコールバック関数の指定である.

例えばProcessingならsetup関数やdraw関数など「その名前をつけた関数を定義(オーバーライド)しておけば,自動的に呼ばれる」関数,Javaで使われるActionListenerインタフェースのactionPerformed関数はコールバック関数であり,SwingのGUI部品にそれを指定するaddActionListener関数は「コールバック関数を指定」する手続きのことである.

PythonではHandlerという名前がつけられることが多い.

コールバック地獄による可読性の低下

JavaScriptは前述の通り「関数オブジェクト」が存在し,またlamda関数の自由度がPythonやJava等に比べ非常に高いため,コールバック関数をその場で定義して渡すことが非常に多く,それを前提にしたライブラリの設計も多い.その結果,コールバック関数の指定にさらにコールバック関数が入ると言う多重の呼び出しの指定が何重にも起こる,通称コールバック地獄に陥ることが多かった.

特にAJAXの処理をする中では,処理の内容としてコールバック地獄は必要不可欠なのだが,コールバック地獄はコードの可読性が非常に下がってしまう.

Promiseクラスとthen, catchキーワード

そこでES2015で標準化されたPromiseというクラスと,then, catchキーワードを使い,可読性に優れたコールバック地獄を書くことができる.

例えばグローバル(window, global)のインスタンスが持っているsetTimeout関数を例に取る.これは第一引数に指定した関数を,第2引数に指定したミリ秒後に,実行する関数である.

このsetTimeout関数の第一引数に指定した関数の中で,resolve(引数);関数と,reject(引数);関数を,それぞれ実行しておく.

その関数をPromiseクラスのコンストラクタに与える,その時引数の指定をresolveキーワードとrejectキーワードにしておく.

生成したPromiseクラスのインスタンスのメンバとして追加するthen関数の最初のブロックに「成功した時に実行する処理」,それ続けるcatchブロックに「失敗した時に実行する処理」をそれぞれ指定しておくと,成功した時,失敗した時にそれぞれ処理を行うことができる.

// Promiseクラスのインスタンスを作る
let myPromise = new Promise(function(resolve, reject){
    // 実際はここで何らかの処理を行う
    window.setTimeout(function(){ 
        if(/* 上で行った処理の結果が */ true){
           resolve('成功したよ!'); //成功した時に呼ばれる関数に渡したい引数を指定する.
	   //ここでは引数は文字列.
	}
	else{
	   reject('失敗したよ!'); //失敗した時に呼ばれる関数に渡したい引数を指定する.
	   // setTimeout内部で失敗することはないので,if(true)にしてあるが,
	   // 実際のコードでは,何らかの処理を行い,
	   // その処理が成功か失敗かを適切に判別するための条件式を書いて,
	   // 成功した時にresolve()を実行し,失敗した時にreject()を実行する.
	}
     }, 1000); //1000ミリ秒待つ
});

// Promiseクラスのインスタンスにthenプロパティを追加して,
// 成功した時と失敗した時の関数を登録する.(実は既に実行されているのだが……)
myPromise.then(function(result) {
    // 成功した時に呼ばれる関数を作る.
    // Promiseのインスタンスを作る時にresolveに指定した引数が入ってくる.
    alert('1秒待つのに成功した.引数:' + result);
}).catch(function(err) {
    // 失敗した時に呼ばれる関数を作る.
    // Promiseのインスタンスを作る時にrejectに指定した引数が入ってくる.
    alert('1秒待つのに失敗した.引数:' + err);
});

多段化は以下のように,resultを引数にとってPromiseクラスのインスタンスを返す関数をあらかじめ定義しておき,thenでそれを実行するように指定すると,可読性が非常に上がる.

let multiPromiseFunction1 = function(){
    return new Promise(function(resolve, reject){
	window.setTimeout(function(){
	    if(true){
	        resolve('最初のsetTimeoutが成功したよ!');
	    }
	    else{
	        reject('最初のsetTimeoutが失敗したよ!');
	    }
	}, 1000);
    });
}

let multiPromiseFunction2 = function(result){
    return new Promise(function(resolve, reject){
	window.setTimeout(function(){
	    console.log(result);
	    if(true){
	         resolve('2番目のsetTimeoutが成功したよ!');
	    }
	    else{
	         reject('2番目のsetTimeoutが失敗したよ!');
	    }
	}, 1000);
    });
}

let multiPromiseFunction3 = function(result){
    return new Promise(function(resolve, reject){
	window.setTimeout(function(){
	    console.log(result);
	    if(true){
	         resolve('3番目のsetTimeoutが成功したよ!');
	    }
	    else{
	         reject('3番目のsetTimeoutが失敗したよ!');
	    }
	}, 1000);
    });
}

let multiPromiseFunctionFinalSuceeded = function(result){
    console.log(result);
};

let multiPromiseFunctionErrorHandler = function(err){
    console.log(err);
};

multiPromiseFunction1()
    .then(multiPromiseFunction2)
    .then(multiPromiseFunction3)
    .then(multiPromiseFunctionFinalSuceeded)
    .catch(multiPromiseFunctionErrorHandler);

変数の型……JSDocの記述の仕方

コメント中のノーテーションに記述する

「JavaScriptの変数にはintやfloatなどの型がない」などと言われることもあるが,実際には存在していて,ユーザに見えないだけである.この「ユーザに見えない」という点が複数人数開発の時には非常に問題になるため,可視化することができる.それがコメント中のノーテーションで型を指定することである.

一般的にコメントは/* コメントコメント */であるが,このノーテーションを使う時には,/** コメント */とコメント開始時のアスタリスクが2つになる.

このコメント中のノーテーションは束縛するほどの効果はないが,自分以外の人間がコードを触る時にもわかりやすく,かつエディタなどで勝手に変数リストを作ってくれて候補を上げてくれるようになる(JSDoc)というメリットがある.

アロー関数などの時は難しいが,次に出てくるクラスの中のfunction定義などでは積極的に使うこと.

変数宣言の型指定

変数の役割や意味のコメントと,@type {変数の型}を書く.

/** 
 * この変数はX座標を保持する変数です
 * @type {number}
 */
let xPos = 235;

関数宣言の引数の型指定

関数の役割や戻り値,@param {引数の型}を書く.

/**
 * この関数は,x座標を減少させる関数です
 * 戻り値は減少した後のx座標です
 * @param {number} decreseWidth X座標を減少させる幅です
 */
function decreseXPosition(decreaseWidth) {
    return xPos - decreaseWidth;
}

[Work/Class/ES2015で動的Webアプリ/ES2015WithWebSocket]

ES2015でWebSocketの基本 / 2017-07-27 (木)

残念ながらES2015書法には対応していないWebSocket

ヒジョーに残念なことに,2017年5月末現在,Google ChromeのJavaScriptエンジンはWebSocketクラスの継承等に対応していない様子である.したがって「用意されているクラスを継承し,関数をオーバーライドして自分のクラスを作成し,そのインスタンスを生成して使う」通常の書法は使えない.

そこで,旧来のJavaScriptのスタイル(プロトタイプ型オブジェクト指向)の手法である「先にインスタンスを作り,その後にインスタンスのプロパティとして上書きするイベントハンドラ関数を追加していく」という方法をとる.

手法自体はこれまで行ってきた「windowインスタンスのプロパティとして変数(グローバル変数)や関数(例えばonload関数やonunload関数)を追加していく」のと同じ方法である.

コード例

クライアント側.ボタンやテキストフィールドを配置したHTMLファイルと,JavaScriptファイル.

<!DOCTYPE html>
<!-- WSTextMessageJSClient.html -->

<html lang="ja">
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta charset="utf-8" />

  <title>WebSocketでテキストメッセージのやり取り</title>
  <script type="text/javascript" src="WSTextMessageJSClient.js">
    function 

  </script>
  
</head>
<body>
  <div id="controll_field">
    <p>
      <input type="button" value="接続開始" onclick="startConnection()" />
      <input type="button" value="接続終了" onclick="exitConnection()" />
    </p>
    <form>
      <input type="text" name="text_field" value="ここに入力して「送信」ボタンを押してください." />
      <input type="button" name="send_button" value="送信" onclick="sendMessage(this.form.elements['text_field'].value)" />
    </form>
  </div>
  <div id="view_field">
    <p>初期テキスト</p>
  </div>
</body>
</html>
// WSTextMessageJSClient.js
'use strict';
window.addEventListener('load', function(){
    // 内部で使うプロパティは全てグローバルのものなので,アローによる定義の必要はない
    // グローバル変数で(windowオブジェクトのプロパティとして)
    // WebSocketクラスのインスタンスを保持する変数を作っておく
    window.webSocket = null;
    window.wsServerUrl = 'ws://localhost:30005/dandou_ws';
}, false);

window.addEventListener('unload', function(event){
    window.exitConnection();
}, false);

window.startConnection = function(event){
    // WebSocketクラスのインスタンス化
    window.webSocket = new WebSocket(wsServerUrl);
    // Override Methods
    // WebSocketクラスのインスタンスのプロパティとして
    // WebSocket通信に必要な関数を上書きしていく
    // つまり関数オブジェクトを「プロパティ」として追加していく
    window.webSocket.onopen = function(event){
       window.document.getElementById("view_field").innerHTML =
            '<p>Success to connect WebSocket Server!</p>'
    };

    window.webSocket.onmessage = function(event){
	alert(event.data);
	window.document.getElementById("view_field").innerHTML =
	    '<p>Server: ' + event.data + '</p>'
    };

    window.webSocket.onerror = function(error){
	document.getElementById("view_field").innerHTML = 
	    '<p>WebSocket Error: ' + error + '</p>';
    };

    window.webSocket.onclose = function(event){
	if(event.wasClean){ //切断が完全に完了したかどうかを確認
	    document.getElementById("view_field").innerHTML =
	    '<p>切断完了</p><dl><dt>Close code</dt><dd>' + 
		event.Code + 
		'</dd><dt>Close Reason</dt><dd>' + 
		event.reason +
		'</dd></dl>';
	    webSocket = null;
	}
	else
	    document.getElementById("view_field").innerHTML =
	    '<p>切断未完了</p>';
    };

}

window.exitConnection = function(event){
    if(window.webSocket != null)
	window.webSocket.close(1000, '通常終了'); //onclose関数が呼ばれる
}

window.sendMessage = function(sendingMessage){
    if(window.webSocket != null)
	window.webSocket.send(sendingMessage);
}

サーバ側,Python3+Tornadoライブラリの場合

# -*- coding: utf-8 -*-
# WSTextMessageTornadoServer.py
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.template

class MyWebSocketHandler(tornado.websocket.WebSocketHandler):
    # Override Event Functions
    def open(self):
        print('connection opened...')
        self.write_message("The server says: 'Hello'. Connection was accepted.")

    def on_message(self, message):
        print('received:', message)
        self.write_message("The server return your message: " + message)

    def on_close(self):
        print('connection closed...')

    def check_origin(self, origin):
        # クロスオリジン(Cross-Origin)ポリシーの問題を解決する
	# 通常JavaScirptは「自分がダウンロードされてきたサーバのドメイン」にしか接続できない
	# ここで何もチェックしないでTrueを返すことで,
	# このサーバプログラムが動いているサイト以外(例えばローカルで実行している時)から
	# ダウンロードされたJavaSciprtプログラムからも,接続を許す
        return True

if __name__ == '__main__':
    application = tornado.web.Application([('/dandou_ws', MyWebSocketHandler)])
    application.listen(30005)
    tornado.ioloop.IOLoop.instance().start()

サーバ側.Node.jsでNode-Yawlライブラリを使う場合.

Node.js v6とnode-yawlが必要.Ubuntuでは,

$ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
$ sudo apt install nodejs node-yawl
// WSTextMessageYawlServer.js
// Yet Another Websocket Library
// https://github.com/andrewrk/node-yawl
'use strict';
global.yawl = require('yawl');
global.http = require('http');
global.server = http.createServer();

global.webSocketServer = yawl.createServer({
  server: global.server,
  origin: null, // set null to disable origin check
  allowTextMessages: true,
});

global.webSocketServer.on('connection', function(ws) {
    console.log('connection opened...');
    ws.sendText('The server says: Hello. Connection was accepted.');
    ws.on('textMessage', function(message) {
	console.log('received:' + message);
	ws.sendText('Server receive message: ' + message);
    });
    ws.on('closeMessage', function(statusCode, message) {
	console.log('connection closed...');
    });
});

global.port = 30005;
global.host = 'localhost';

global.server.listen(global.port, global.host, function() {
    console.log("Listening at " + global.host + ":" + global.port + "/");
});

サーバ側.C言語(C++)+libwebsocketsの場合.

// WSTextMessageLibWebSocketsServer.cpp
#include <iostream>
#include <string>
#include <libwebsockets.h>

#ifdef __WIN32
#include <io.h>
#include "gettimeofday.h"
#else
#include <syslog.h>
#include <sys/time.h>
#include <unistd.h>
#endif

volatile int force_exit;
struct lws* wsi;
static std::string _sendStr = "";//送る文字列の定義

//関数のプロトタイプ宣言
static int my_lws_callback_function(struct lws* wsi,
				    enum lws_callback_reasons reason,
				    void *user,
				    void *in,
				    size_t len);
static void sighandler(int sig);

//Unix Signalに送る関数の設定
void sighandler(int sig){
  force_exit = 1;
  //そもそもwsiポインタの中には何も入っていないので,ここでセグフォが起きる
  lws_cancel_service(lws_get_context(wsi));
}

//プロトコル定義
static struct lws_protocols my_lws_protocols[] = {
  {
    "my_websocket_protocol", //name
    my_lws_callback_function, //function pointer of callback function
    0, //per_session_data_size
    128 //max frame size / rxbuffer
  },
  {NULL, NULL, 0, 0} //Terminator
};

//コールバック関数の定義
static int my_lws_callback_function(struct lws* callback_wsi,
				    enum lws_callback_reasons reason,
				    void *user,
				    void *in,
				    size_t len){
  wsi = callback_wsi;
  if(reason != LWS_CALLBACK_GET_THREAD_ID){
    //起動すると常にスレッドIDが取れる状態であることを示す定数が入ってくるので,
    //それ以外の時,CALLBACKイベント定数値を表示する.
    //Callbackイベント定数値は以下を参照のこと
    //https://libwebsockets.org/lws-api-doc-master/html/group__usercb.html
    lwsl_notice("%d\n", reason);
  }
  //reasonの中にイベントの種類が勝手に入ってくるので,それを切り分けて,
  //それぞれの処理を書く
  switch(reason){
    //コネクション開始の場合
  case LWS_CALLBACK_ESTABLISHED:
    {
      lwsl_notice("connection opened...\n");
    }
    break;
    //コネクション終了
  case LWS_CALLBACK_PROTOCOL_DESTROY:
    {
      lwsl_notice("connection closed...\n");
    }
    break;
    //送信処理
  case LWS_CALLBACK_SERVER_WRITEABLE:
    {
      lws_write(callback_wsi, 
		(unsigned char*)_sendStr.c_str(), 
		_sendStr.length(), 
		LWS_WRITE_TEXT);
    }
    break;
    //受信処理
  case LWS_CALLBACK_RECEIVE:
    {
      lwsl_notice("ReceiveMessage: [%s]\n", (const char*)in);
      // (const char*)型のinという変数に入ってくる.
      // std::string型に変換する
      // std::stringクラスのコンストラクタに打ち込むだけでよい
      // ここで作ったstd::string型の変数はローカル変数なので,
      // ブロックを抜けたら勝手に解放されるはず
      std::string received_string((const char*)in);

      // 「サーバが発したメッセージであることを示す文字列を連結して
      // グローバル変数の_sendStrに書き込む
      _sendStr = 
	std::string("Server received: ") + received_string; 

      //_sendStrに書き込んだうえでこのコールバック関数そのものを呼ぶと
      //自動的にWRITABLEイベントモードに入って送信される.
      lws_callback_on_writable_all_protocol
	(lws_get_context(callback_wsi), my_lws_protocols);

    }
    break;
    //何かよくわからないイベント
  case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
    {
      //dump_handshake_info(wsi);
    }
    break;
  default:
    {
    }
    break;
  }
  return 0;
}

int main(){
  struct lws_context *context; //コンテキストというかWSインスタンスというか
  struct lws_context_creation_info info;
  memset(&info, 0, sizeof(info));
  info.port = 30005;
  info.iface = NULL; //const char* 128文字までらしい
  info.protocols = my_lws_protocols; //上で自分で作ったプロトコルの配列

  //SSLによる暗号化接続はしないので,そのためのファイルは必要ない.
  int use_ssl = 0;
  info.ssl_cert_filepath = NULL;
  info.ssl_private_key_filepath = NULL;

  info.gid = -1; //通常ユーザのプロセスとして動かす場合は-1
  info.uid = -1; //同上
  info.options = 0; //通常のコンテキストを使う場合は特にオプションは入れない

  //C言語ライブラリを設計する時に,定数タグを作るための定数に
  //ビット演算子をうまく考慮に入れると,
  //複数の状態を1変数にまとめることができる
  // "|"は「ビットごとのOR」なので,最大32状態(32bit)の状態を1変数でキープできる
#ifdef __WIN32
  int syslog_options = LOG_PID;
#else
  int syslog_options = LOG_PID | LOG_PERROR; 
#endif

  //システムログに記録するデバッグレベル
  int debug_level = 7;  
  lws_set_log_level(debug_level, lwsl_emit_syslog);
  lwsl_notice("libwebsockets server - \n");

  //Unixシグナルに反応する関数の登録
  signal(SIGINT, sighandler);
  //syslogに登録する
  setlogmask(LOG_UPTO(LOG_DEBUG));
  openlog("lwsts", syslog_options, LOG_DAEMON);
  
  //コンテキストを生成する
  context = lws_create_context(&info);
  if(context == NULL){
    //もしコンテキストの生成に失敗したら,エラーメッセージを出して異常終了.
    lwsl_err("libwebsocket init failed\n");
    return -1;
  }

  int n = 0;
  while(n >= 0 && !force_exit){
    struct timeval tv; //secとmicrosecondsの二つが入った構造体で定義
    gettimeofday(&tv, NULL);
    n = lws_service(context, 50);
    //50msecでwhileを回す?
  }

  //whileループをforce_exitで終了した時のプログラムの終了処理
  lws_context_destroy(context);
  lwsl_notice("libwebsockets-server exited cleanly\n");
  
  return 0;
}

[Work/Class/ES2015で動的Webアプリ/ES2015Basic]

ES2015の基本 - クラスの定義とインスタンス化,プロパティ / 2017-07-27 (木)

クラスの定義

基本的なクラスの定義

以下のように

class BasicBall{
    // コンストラクタの定義
    constructor(windowWidth, windowHeight){
        this.windowWidth;
	this.windowHeight;

	this.xPos = Math.floor(Math.random() * this.windowWidth);
	this.yPos = Math.floor(Math.random() * this.windowHeight);
    }

    // インスタンス関数の定義
    updateParameters(){
        this.xPos += 10;
	this.yPos += 10;
    }

    // 同じくインスタンス関数
    getXPos(){
        return this.xPos;
    }

    getYPos(){
        return this.yPos;
    }
}

クラスを定義し,

let myBall = new BasicBall(640, 480);

でインスタンス化する.

コンストラクタがconstructorという名前の関数であること以外は,基本的にはJavaと同じであるが,注意点は,クラスのインスタンスメンバ変数宣言が存在しないことである(つまりPythonと同じく関数宣言しかない).コンストラクタ内部でthis.変数名 = 1;のように必ず「このクラスのメンバ変数である」という感じで宣言する必要がある.

これは後述の「プロパティ」という仕様による.

ゲッターとセッター

コンストラクタやインスタンスメンバ関数内で宣言されたthis付きの変数は全てprivate扱いとし,直接アクセスは避けるようにする.(明示的にprivateにするためには様々な方法があるが「private扱いである」ことにしておくとよい)

ただし,

get x(){
    return this._xPos;
}

get y(){
    return this._yPos;
}

set x(xPos){
    this._xPos = xPos;
}

set y(yPos){
    this._yPos = yPos;
}

というようにgetsetというキーワードをつけて,あらかじめコンスタクタで定義した変数(の頭にアンダースコア「_」をつけたもの)の中身を取り出したりいじったりする関数(ゲッターgetterとセッターsetterと呼ぶ)を定義しておくと,

let myBall = new BasicBall(640, 480);
let drawingX = myBall.x;
let drawingY = myBall.y;
myBall.x = Math.floor(Math.random() * 640);
myBall.y = Math.floor(Math.random() * 480);

という感じで,あたかもメンバ変数がpublicであるかのように,そのままアクセスできるようになるのだが,getなんちゃら()関数,setなんちゃら()関数のように専用関数を作る方が,カプセル化として明確である.(読めるプログラマが多い)

クラスの継承

先ほどのBasicBallは,右下に単調移動するだけのクラスだったので,これを継承して跳ね返るボールのクラスを定義する.

class Bounceball extends BasicBall {
    // コンストラクタ
    constructor(windowWidth, windowHeight){
        super(windowWidth, windowHeight); 親クラスのコンストラクタを最初に呼び出す
	if(Math.random() > 0.5){
	    this.xDirection = 1;
	}
	else{
	    this.xDirection = 1;
	}
	
	if(Math.random() > 0.5){
	    this.yDirection = 1;
	}
	else{
	    this.yDireciton = -1;
	}
    }

    // 関数のオーバーライド
    updateParameters(){
        this.xPos += this.xDirection * 10;
	this.yPos += this.yDirection * 10;

	if(this.xPos > this.windowWidth){
	    this.xDirection = -1;
	}
	else if(xPos < 0){
	    this.xDirection = 1;
	}

	if(yPos > this.windowHeight){
	    this.yDirection = -1;
	}
	else if(yPos < 0){
	    this.yDirection = 1;
	}
    }
}

基本的にJavaと一緒である.「@Override」がないので,コメントでオーバーライドであることを明示すると良い.

プロパティ

プロパティの概要

JavaScriptでは「関数オブジェクト」が存在すると前項で書いたが,関数自体も変数に打ち込めることからわかる通り,変数と関数の区別がない.従ってクラスをnewでインスタンス化したものの中では,変数と関数は「プロパティ」という括りで同一に扱うことができる.

インスタンスを作った後にプロパティを追加する

Javaでは,クラスとそのインスタンスに機能(変数や関数)を追加する場合,あらかじめクラスを継承した子クラスを作っておき,その子クラスのインスタンスを生成する,と言う明快な方法をとる.

ところがJavaScriptでは「メンバ変数,メンバ関数」ではなく,「プロパティ」であるため,インスタンス化をした後にメンバ変数とメンバ関数を追加することができてしまう.

上記のクラスBounceBallのインスタンスを作った後に,「そのインスタンスに対して」色要素を追加したい場合,

let myBounceBall = new BounceBall(640, 480);
myBounceBall.rColor = Math.floor(Math.random() * 155) + 100;
myBounceBall.gColor = Math.floor(Math.random() * 155) + 100;
myBounceBall.bColor = Math.floor(Math.random() * 155) + 100;

という風に書くと,メンバ変数のようにプロパティを追加できる.

さらに,

myBounceBall.getRed = function(){
    return this.rColor || 0; //this.rColorがない時は0を返す,という意味
};

と言う風に,関数ですらプロパティとして追加できる.

window.onloadとwindow.onunloadの正体

つまりwindow.onload = function(){...};window.onunload = function(){...};という指定は,このプロパティである関数オブジェクト(を保持する変数の中身)を,新たに作った関数オブジェクトで上書きしている,ということになる.(よってクラスの継承によるオーバーライドとは原理が違う)

window.addEventListenerによるwindow.onloadで実行する関数を追加

window.addEventListener('イベントのタイプ名', '関数名'もしくは関数オブジェクト, false);

のように,addEventListener関数をwindowオブジェクトは持っているのだが,この「イベントのタイプ名」に'load'文字列を指定してやると,onload関数を,新しい関数オブジェクトで「上書き」するのではなく,新たに関数オブジェクトを「追加」することができる.

つまりonloadはプロパティの名前であり,'load'がイベントタイプである.イベントタイプには例えば「'click'文字列」などを指定することができる.(JavaScriptのボタン動作に'onclick'文字列を指定していたことを思い出すこと)

let myOnloadFunction = {alert('onloadの上書き関数');};
window.addEventListener('load', myOnloadFunction, false);

ただし,addEventListenerはInternetExplorerでは使えないので,attachEventで指定する.(こちらのイベントタイプには「on」がつく)

window.attachEvent('onload', myOnloadFunction);

と指定する.

IEと他のブラウザを両立させたい場合,

if(window.addEventListener){
    // もしwindowがaddEventListenerプロパティ関数を持っている場合,
    window.addEventListener('load', myOnloadFunction, false);
    //myOnloadFunction関数オブジェクトをonload処理に追加する
}
else if(window.attachEvent){
    // IEだと上のif文はfalseになり,こちらがtrueになる
    window.attachEvent('onload', myOnloadFunction);
}
else{
    // 両方持ってない場合,しょうがないので上書きする.
    window.onload = myOnloadFunction;
}

のように書くことができる.

deleteキーワードによるプロパティの削除

delete インスタンス.プロパティ;で,プロパティを削除することができる.

グローバルな変数や関数は,windowインスタンス(Node.jsではglobalインスタンス)がプロパティとして持っていることを思い出すと,delete window.グローバル変数名;でグローバル変数(や関数)を削除することができる.

正確には仕様としてdeleteでメモリ解放が定められているわけではなく,あくまで「プロパティの削除」なのだが,大体のブラウザは「どの変数も保持しなくなったオブジェクトは消す」というガベージコレクションが働くようになっている.

SafariはChromeよりも使えるメモリ量が少ないので,Safariでメモリが足りない場合など.

コールバック関数中におけるthisキーワードとアロー関数定義

前項で出てきた,関数オブジェクトを生成する3つの手法のうち,マジでわけがわからない記法である「let 関数オブジェクトを保持する変数 = (引数) => {関数の中身};」というアロー関数記述であるが,実は非常に重要な用法がある.

クラスのメンバ関数の定義は上記のような通常のJavaやPythonに似たクラス指定で問題ないのだが,問題は自分で設計・記述したクラスのメンバ関数中から呼ばれるコールバック関数である.

JavaScriptの標準のライブラリでは,クラスになっていない関数単体であったり,クラスになっていてもES2015的なクラス継承でのオーバーライド記述に対応できていないものが多いようで,コールバック関数の指定などは従来のようにプロパティに関数オブジェクトを入れる(上書きする)形でのみ動くものが多いようである.

コールバック関数の呼び出しは,クラスのインスタンスが行うのではなく,グローバル(window,Node.jsならglobal)のインスタンスが呼び出している.グローバルインスタンスが呼び出した場合,例えクラスのメンバ関数として宣言されている関数をコールバック関数に登録したとしても,関数定義の中で使われているthisキーワードはグローバルインスタンス自身を指すことになる.

例えば以下の例を考えてみる.

class MyClass{
    constructor(){
        this.myVariable = 'メンバ変数';
    }

    startTimer(){
        setTimeout(function(){
	    alert(this.myVariable);
	}, 1000);
    }
}

window.addEventListener('load', function(){
    let instanceOfMyClass = new MyClass();
    instanceOfMyClass.startTimer();
});

このコードを実行する(実行例)と「myVariableはundefinedになってしまう」,つまり「定義されていない」エラーが出てしまう.これはsetTimeout関数にセットしたコールバック用の関数オブジェクトを呼び出すのはグローバルのインスタンスだからであり,グローバルインスタンスはmyVariableメンバ変数やメンバ関数(二つをまとめて「プロパティ」)は持っていないからである.

そこでコールバック関数のための関数オブジェクトを生成する時にアロー記法を使うと,「その関数オブジェクトが記述された場所のスコープがthisの中に入る」.

class MyClass{
    constructor(){
        this.myVariable = 'メンバ変数';
    }

    startTimer(){
        setTimeout(() => {
	    alert(this.myVariable);
	}, 1000);
    }
}

window.addEventListener('load', function(){
    let instanceOfMyClass = new MyClass(); 
    // これの変数も「function」キーワードで関数オブジェクトを作っているので,
    // 実行時はグローバルになっていることに注意
    instanceOfMyClass.startTimer();
});

とアロー関数記述を使う(実行例)と,コールバック関数が呼ばれた時のthisにはinstanceOfMyClassというインスタンスが入るので,undefinedエラーは起きずに,ちゃんと「メンバ変数」という文字列が表示される.

さらに,以下のようにaddEventListener内で宣言した変数をグローバルインスタンスwindowのプロパティとするように明示して書いておくと,より明確になる.

... // MyClassの定義は上記の通りなので省略
window.addEveventListener('load', function(){
    window.instanceOfMyClass = new MyClass();
    window.isntanceOfMyClass.startTimer();
});

[Work/Class/Python3の基礎とデータ処理]

Python3の基礎とデータ処理

概要

近年PythonがそれまでのMATLABやR言語に代わって,データ処理に使われることが多くなった.また海外では教育用言語として90年代末から普及が始まり,現在様々なフレームワークが開発されているWebサーバ側をはじめとしたサーバ用途と,通常のデスクトップのスクリプト処理用途,両用のLL(Lightweight Language)として使われている.

Pythonの特徴として「Python本体はスクリプト言語だが,ライブラリをC言語で実装しPythonにAPIを用意することで,高速に実行可能」という点がある.近年ではデータ解析や機械学習などで頻繁にPythonが使われているが,これはこの「Cで書かれた高速ライブラリをPythonで実行」というスタイルに依るものである.

現在のPythonは大きく分けてVer.2.x系とVer.3系の2つが使われているが,本授業ではPython3を対象とする.(OSX標準のPythonは2.7なので,授業の下準備としてAnacondaをインストールしておくこと)

コンテンツ

  1. Pythonの基本と配列(list)の取り扱い
  2. 関数とクラスの定義
  3. 文字列の取り扱い
  4. lambda式,高階関数,内包表記
  5. HTMLParserクラスによるWebページの取得と分析
  6. Numpyと配列(array)とmatplotlib
  7. CSVの取得とPlot
  8. CSVデータからの単純線形回帰分析
  9. CSVデータからの重回帰分析

授業の下準備

Windowsの場合

まっさらな状態からのMSYS2+GCCでWindows上にC言語とC++の環境を整えるの通りにコンパイラ環境を整える.

その後「MSYS2 MSYS」ショートカットで起動する端末エミュレータから,

$ pacman -S mingw64/mingw-w64-x86_64-python3
$ pacman -S mingw64/mingw-w64-x86_64-python3-numpy
$ pacman -S mingw64/mingw-w64-x86_64-python3-scipy
$ pacman -S mingw64/mingw-w64-x86_64-python3-matplotlib
$ pacman -S mingw64/mingw-w64-x86_64-python3-pandas

の五つのパッケージをインストールしておく.依存関係にある必要なライブラリ(主にQt関係)も全て一緒に入ってくれる.(64bit Windowsの場合)

Pythonの実行時には,MSYS2 MSYSから,

 $ /mingw64/bin/python3 MyPythonCode.py

と実行する.

ちなみに後述のOSX用に用意したAnacondaにはWindows版もある.UnixのコマンドやCコンパイラを必要とせずPython環境だけが必要ならば,MSYS2+Cコンパイラ一式+Python一式を入れるよりも,AnacondaのWindows版を入れる方が容量的に少なくて済む.

OSX, macOSの場合

OSX,macOSはPython本体とnumpy, scipy, matplotlibは標準で入っているのだが,バージョンが2.7である.HomebrewなどでPython3を入れるという方法もあるが,Homebrewでインストールを行うとシステム全体に影響を及ぼし,元々入っていたPython2.7とごちゃ混ぜになりシステムにダメージを与える可能性が高い.

そこでデータ処理に特化したPythonのパッケージであるAnacondaを「HDDのアプリケーション」以下にインストールして使う.Anacondaは/Applications/anacondaフォルダ内で独立したファイルツリーを持つ.

Python3を使いたい場合はAnacondaのPythonを明示的に実行してやる形にすれば,通常のシステムの処理に必要なPythonは元々システムに入っていた2.7がそのまま使われることになる.この方法で,システムに必要なPython2.7と,プログラミング言語として使いたいPython3系を,安全に同居させることができる.

Anacondaのダウンロードページの「Download for macOS」から「Python 3.6 version」の「Graphical Installer」をダウンロードする.

ダウンロードされた.pkgを実行して,ライセンスに同意し,「インストール先を変更」して「特定のディスク」の「アプリケーション」フォルダにインストールする.そうするとマシンの「/Applications」に「anaconda」フォルダ内にanacondaがインストールされ,Terminal.appから実行するPythonはこの中のPythonを使うように設定される.

Anaconda3のインストールパッケージ一覧の画面 Anaconda3のライセンス表示 Anaconda3のライセンス同意 Anaconda3「インストール先を変更」 Anaconda3「特定のディスクにインストール」を選択 Anaconda3メインのシステムHDDが選択されていることを確認し「フォルダを選択」 Anaconda3 フォルダを選択.ハードディスク直下の「アプリケーション」 Anaconda3 選択終了「続ける」

「このユーザがTerminal.appを開いた時はAnacondaのPythonを使う」という設定が記述されている~/.bash_profileからexport PATH=/Applications/anaconda...という行を削除しておき,Python3を実行する時は「Terminal.app」から,

 $ /Applications/anaconda/bin/python MyPythonCode.py

と「Anacondaのpython」を明示的に実行するようにしておくと,システム的に混乱が起きない.

OSXのPython2.7には,この授業の目的である基本的なデータ解析で使うnumpy, matplotlib, scipyも標準で含まれているのだが,matplotlibが使うGUI部品(システム標準はおそらくTk, Anacondaや上記WindowsのMSYS2環境ではQt5)や,リストを使ったイテレータの言語仕様などが違うため,Python2.7と3で互換性がないプログラムもある.

Anaconda-Navigatorでの追加ライブラリのインストール

AnacondaはGUIでライブラリが追加インストールできる.今回はTwitterのAPIを使うために必要なOauth認証に必要なrequests_oauthlibをインストールする.

conda-forgeというレポジトリ(AnacondaではChannelと呼称する)がよくメンテされているようなので,それを追加して,Anacondaから使えるようにする.

まず管理者権限をもつユーザでAnaconda-Navigatorを起動する.

管理者権限を持つユーザでAnaconda-Navigatorを起動する

「Environment」(環境)を選択→「root」(自分のAnacondaのデフォルトの環境)が選択されていることを確認→「Channels」ボタンを押す.

環境を選択しChannelを追加する

ミニウィンドウが開くので「Add」

ミニウィンドウが開く

新たなフィールドに「conda-forge」チャンネルを入力.

新たなフィールドに「conda-forge」チャンネルを入力

追加したら「Update channels」

「Update channels」

もとの画面に戻るので「Update Index」すると,下のバーでIndexをアップデートしている進行状況が表示される.

「Update Index」

一番左のプルダウンメニューから「Not Installed」(まだインストールされていないライブラリの検索)→一番右のボックスに「ライブラリの名前」(ここでは「requests-oauthlib」)を入力すると,下のフィールドに検索結果が出てくる.

「Not Installed」→「ライブラリの名前入力」→検索結果がでる

ライブラリにチェックを入れて,「Apply」

ライブラリにチェックを入れて「Apply」

進行状況のミニウィンドウが出る.

進行状況のミニウィンドウ

入れたいライブラリに必要なライブラリ(依存関係と呼称する)が全て並んで出てくるので「Apply」.

依存関係の表示

プルダウンメニューを「Installed」にして,インストールされていることを確認する.

「Installed」で確認.

以上である.Environment→rootの再生マークからターミナルを呼び出しても使えるし,root環境に入れておけば,通常のターミナルから/Applications/anaconda/bin/python3でPythonインタプリタを呼び出した時もライブラリが使える.

Anacondaで環境をたくさん作り,使用するライブラリを切り替えることもできる.


[Work/Class/Python3の基礎とデータ処理/2_PythonLibraries]

CSVの取得とPlot

各種ライブラリのCSV読み込み機能

標準csvライブラリcsv.reader()による読み込み

Python3の標準CSVライブラリは,

  • 標準ライブラリのcodecs.open()関数で,文字コードを指定しつつファイルを開く
  • csv.reader()関数で「データ本体を含むインスタンス」にする.ちなみにイテレータのインスタンスである.
  • イテレータのインスタンスなので,next()関数,もしくはfor文で1行ずつ読んでいく.1行が1次元の文字列のlistとして出てくるので,処理をしつつ格納する.
  • 最後にファイルをclose()する.

という流れになる.

CSVファイル内に入っている「処理したい」ターゲットが文字列の場合,今の所この標準csvライブラリを使う方法が一番適当である.

numpy.loadtxt()による読み込み

numpyで読み込む場合,CSVの中身が数字である必要がある.

流れは標準csvライブラリと同じく,codecs.open()関数でファイルを開いて,2次元配列として一気に読み込み,close()する,という流れだが,「数字である」ことが前提なので,ヘッダ等の文字列で構成されている行をskiprows=行数と「何行読み飛ばすか」を指定してから読み込み始めなければならない.

またdelimiter='区切り文字'指定により,区切り文字を明示的に指定する必要がある.(CSVは区切り文字がTABのこともあるため)

pandas.read_csv()による読み込み

pandasも内部的にはnumpyの配列を使うので,やはりnumpyの2次元配列で読み込む.が,ヘッダを特殊扱いして文字列として取り出すことができる(もちろん後述のようにnumpyと同じくヘッダを最初から読み飛ばすしてもできる).

またnumpy.loadtxt()だと強制的にfloatのみになるが,pandas.read_csv()は全て整数だった場合intの配列を作ってくれる.

さらに,標準csvライブラリやnumpy.loadtxt()のようにファイルを最初にopenして読み込み後にcloseする必要がない,numpy.loadtxt()と同じく全てのデータを一気に2次元配列として持ってくることができるが,列だけを1次元numpy配列として容易に抽出できる,等の特徴がある.

pandas.read_csv()は区切り文字は自動判定されるようである.

どのライブラリを使うべきか

それぞれ前述のような特徴があるため,pandas.read_csv()関数を使うのが一番楽である.

しかし,pandasは入っておらずインストールに苦労するPython3環境も多い.numpyが用意されていない,もしくは容易にインストールすることができないPython3環境はほぼないため,pandasがない場合は標準csvライブラリかnumpyライブラリを使って読み込む,というようにしておくとよい.

コード例

# -*- coding: utf-8 -*-
# CSVPlotter.py

import csv, numpy, pandas
import urllib.request
import codecs
from PyQt5.QtWidgets import QApplication
import matplotlib.pyplot


class CSVPlotter():
    def __init__(self, url_string):
        self.__url_string = url_string

    def download_csv(self, decoder):
        # 引数decoderで指定した文字コードのCSVファイルをダウンロードして,
	# UTF-8に直して保存する
        self.__gotten_http_response = urllib.request.urlopen(self.__url_string)
        if self.__gotten_http_response.code == 200:
        # コード200が返っていてきたら正常に取得できている
            print('Sucsess to get CSV from Internet.')
            # ダウンロードしてきたデータを一度ファイルに書き込む
            self.__downloaded_filename = 'downloaded.csv'
            downloaded_file = codecs.open(self.__downloaded_filename, mode='w', encoding='utf-8')
            downloaded_file.write(self.__gotten_http_response.read().decode(decoder))
            downloaded_file.close()
            print('Sucsess to write downloaded CSV data to file.')
        else:
            # それ以外のコードが返ってきたら,異常終了
            print('Cannot get CSV file. Code:', self.__gotten_http_response.code, 'Exiting...')
            sys.exit(1)

    def read_csv_with_csv(self):
        # 標準ライブラリのcsvを使ってCSVファイルを読み込む
        print('標準のcsvライブラリで読んで表示')
        csv_file = codecs.open(self.__downloaded_filename, mode='r', encoding='utf-8')
        self.__csv_data = csv.reader(csv_file)
        
        # header以外は2次元のlistで出てくる
        # 外側のlistがRow(行),
        # 内側のリストがColumn(列)でデータ型は文字列
        header = next(self.__csv_data)
        # 1行目(データ本体ではなくデータの内容を示すヘッダ)
        print(header)
        for a_row in self.__csv_data:
            print(a_row)

        csv_file.close()
            
    def read_csv_with_numpy(self):
        # NumPyライブラリを使ってCSVファイルを読み込む
        print('NumPyライブラリで読んで表示')
        # NumPyの読み込みデータはNumPyの2次元array(ndarray)でくるので,分解して表示
        # データ型はfloat
	csv_file = codecs.open(self.__downloaded_filename, mode='r', encoding='utf-8')
        self.__csv_data = numpy.loadtxt(csv_file, delimiter=',', skiprows=1)
	csv_file.close()

        # 区切り文字を','とし,1行目(ヘッダ)を抜かして読み込む
        for a_row in self.__csv_data:
            print(a_row)
      
    def read_csv_with_pandas(self):
        # Pandsライブラリを使ってCSVファイルを読み込む
        print('Pandasライブラリで読んで表示')
        data_field = pandas.read_csv(self.__downloaded_filename)

	# 文字列ヘッダが含まれててもエラーを起こさない
	# ヘッダだけを取り出すことができる
        header = data_field.columns.values.tolist() #headerは標準list
        self.__csv_data = data_field.values 
        print(header)

        # Pandsの読み込みデータもNumPyの2次元arrayでくるので,分解して表示
        # データ型は,中身が整数ならintに変換される
        for a_row in self.__csv_data:
            print(a_row)

    def make_plot_csv_data_2d(self, x_index, y_index, line_color):
        # インデックスで指定されたX軸要素とY軸要素を使い
        # NumPyのndarrayに入れて
        # Matplotlib.plotで図を描く
        plot_x_array = numpy.array([]) #空のnumpy ndarrayを生成
        plot_y_array = numpy.array([])
        for a_row in self.__csv_data:
            if isinstance(a_row[x_index], str):
                # numpy ndarrayに追加していく
                plot_x_array = numpy.append(plot_x_array, float(a_row[x_index]))
            else:
                plot_x_array = numpy.append(plot_x_array, a_row[x_index])
            if isinstance(self.__csv_data[y_index], str):
                plot_y_array = numpy.append(plot_y_array, float(self.a_row[y_index]))
            else:
                plot_y_array = numpy.append(plot_y_array, a_row[y_index])
        # plotに使うデータを設定
        matplotlib.pyplot.plot(plot_x_array, plot_y_array, color=line_color)

    def plot_show(self, x_label, y_label):
        # plotの軸の名前を設定
        matplotlib.pyplot.xlabel(x_label, fontsize=10, fontname='serif')
        matplotlib.pyplot.ylabel(y_label, fontsize=10, fontname='serif')
        # plotする
        matplotlib.pyplot.show()


if __name__ == "__main__":
    # 八王子市年齢別人口
    # http://www.city.hachioji.tokyo.jp/contents/open/002/p005866.html
    csv_url = "http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2703.csv"
    csv_plotter = CSVPlotter(csv_url)
    csv_plotter.download_csv('shift-jis')
    csv_plotter.read_csv_with_csv()
    csv_plotter.read_csv_with_numpy()
    csv_plotter.read_csv_with_pandas()
    csv_plotter.make_plot_csv_data_2d(1, 2, 'gray')
    csv_plotter.plot_show('Age', 'Population Size')

応用

八王子市の年齢別人口データを年度/四半期ごとに複数持ってきて,一つのグラフに色を変えてプロットしてみる.

前述のコード例では,標準CSVライブラリで出てくるCSVのデータ形式に合わせて行ごとに読み込んだが,pandasのCSVライブラリで読み込むと,1列のみ簡単に取り出せることができる.また,ヘッダの行数を指定して読み込まない,等の処理が可能である.

コード例

# -*- coding: utf-8 -*-
# MultiCSVPlotter.py

import numpy, pandas
import urllib.request
import codecs
from PyQt5.QtWidgets import QApplication
import matplotlib.pyplot

class MultiCSVPlotter():
    def __init__(self, url_string_list):
        self.__url_string_list = url_string_list
        # numpyのndarrayで多次元配列を作るためには
        # np.array([])で初期化せずに(→append()しても連結された1次元配列になってしまう)
        # np.arange(0, x軸の最大値+1)で0〜最大値までの配列を最初に作って(X軸になる),
        # そこにvstack関数を使ってY軸になる配列を足していく形を取ると良い
        self.__csv_data_array = numpy.arange(0, 121)
        # 他のものも一応初期化しておく
        self.__column_index = 0

        return None

    def set_referring_column_index(self, column_index):
        # CSVファイルの何列目を見るか 0(1列目)~
        self.__column_index = column_index
        return self

    def download_csv_then_push_stack(self, decoder):
        # ShiftJISのCSVファイルのURL配列をダウンロードして,
        # UTF-8に直して一時保存してから,
        # pandasのCSV読み込み機能を使って読み込み,
        # ndarrayに2次元配列として入れていく
        for a_url in self.__url_string_list:
            self.__gotten_http_response = urllib.request.urlopen(a_url)
            if self.__gotten_http_response.code == 200:
                # コード200が返っていてきたら正常に取得できている
                print('Sucsess to get CSV from Internet.')
                # ダウンロードしてきたデータを一度ファイルに書き込む
                self.__downloaded_filename = 'tmp_downloaded.csv'
                downloaded_file = open(self.__downloaded_filename, mode='w', encoding='utf-8')
                downloaded_file.write(self.__gotten_http_response.read().decode(decoder))
                downloaded_file.close()
                print('Sucsess to download CSV data to file: ' + a_url)
                print('Now start to read CSV as ndarray with pandas library.')
            else:
                # それ以外のコードが返ってきたら,異常終了
                print('Cannot get CSV file. Code:', self.__gotten_http_response.code, 'Exiting...')
                sys.exit(1)

            # Pandsライブラリを使ってCSVファイルを読み込む
            # ヘッダは読み込まない指定(header=-1)
            # ヘッダは「1行」である指定(skiprows=1)
            data_field = pandas.read_csv(self.__downloaded_filename, header=-1, skiprows=1)
            
            # まず取得した2次元配列の中から,column_indexで指定した列だけを抜き出す
            # pandasは取得したCSV,ここではdata_fieldに対して,
	    # data_field[列idnex]でその列のみの
            # 1次元配列をとり出せる.
            populationSizeArray = data_field[self.__column_index]
            # できた1次元配列を,最初にX軸の配列で初期化した配列にvstack(())する.
            # vstackは2重括弧であることに注意
            # ただのstackでは同じ形(行と列が同じ量)の配列or行列同士しか足せないが,
            # vstackは要素数が同じ1次元配列をどんどん重ねていき
            # 多次元配列を作ることができる
            self.__csv_data_array = numpy.vstack((self.__csv_data_array, populationSizeArray))
            # ちなみにvstackの他に,hstack, column_stack, row_stackなどがある
            # これを,CSVファイル数分だけ回す
            print('Sucess to push new CSV column to stack')
        return self
        # return selfしないと,最後に実行した行の結果がreturnされるはず
        # この関数の場合はそれでもいいのだけれど
            
    def make_plot_csv_data_2d(self, line_color_list, line_legend_list):
        # Matplotlib.plotで図を描く
        # indexが1~shape(行列の次元数のtupleが得られる,ここでは(9, 121))[0]まで
        for index in range(1, self.__csv_data_array.shape[0]):
            # plotに使うデータを設定
            matplotlib.pyplot.plot(self.__csv_data_array[0], self.__csv_data_array[index], color=line_color_list[index-1], label=line_legend_list[index-1])
        #線に付与したlabel(凡例)を表示する
        matplotlib.pyplot.legend()

    def plot_show(self, x_label, y_label):
        # plotの軸の名前を設定
        matplotlib.pyplot.xlabel(x_label, fontsize=10, fontname='serif')
        matplotlib.pyplot.ylabel(y_label, fontsize=10, fontname='serif')
        # plotする
        matplotlib.pyplot.show()


if __name__ == "__main__":
    # 八王子市年齢別人口
    # http://www.city.hachioji.tokyo.jp/contents/open/002/p005866.html
    csv_url_list = list([])

    # matplotlibで使える色のリスト
    # http://matplotlib.org/examples/color/named_colors.html
    # 一応16進数カラーコードでも指定はできる
    line_color_list = list([])

    # 凡例(線の色がどの年のデータを表しているか)
    line_legend_list = list([])

    # H25 6月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2506.csv')
    line_color_list.append('blue')
    line_legend_list.append('H25.6')

    # H25 9月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2509.csv')
    line_color_list.append('green')
    line_legend_list.append('H25.9')

    # H25 12月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2512.csv')
    line_color_list.append('red')
    line_legend_list.append('H25.12')

    # H26 3月 (「平成25年3月末日」tと書いてあるのは25年「度」3月末……の意味らしい)
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2503.csv')
    line_color_list.append('cyan')
    line_legend_list.append('H26.3')

    # H26 6月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2606.csv')
    line_color_list.append('magenta')
    line_legend_list.append('H26.6')

    # H26 9月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2609.csv')
    line_color_list.append('yellow')
    line_legend_list.append('H26.9')

    # H26 12月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2612.csv')
    line_color_list.append('black')
    line_legend_list.append('H26.12')

    # H27 3月 (同様にH26年「度」3月末の意味のようだ)
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2703.csv')
    line_color_list.append('gray')
    line_legend_list.append('H27.3')

    csv_plotter = MultiCSVPlotter(csv_url_list)
    csv_plotter.set_referring_column_index(2)
    csv_plotter.download_csv_then_push_stack('shift-jis')
    csv_plotter.make_plot_csv_data_2d(line_color_list, line_legend_list)
    csv_plotter.plot_show('Age', 'Population Size')