IronPythonを使ってみる

概要

今回も引き続き、実際に動かすことができるように、できるだけ各サンプルコード単体で動くように心がけています。 環境などについても同様です。

1. ファイルの形式について

特にファイル名に'utf-8'などと入っていない限りは、ファイルはShift_JISでエンコーディングしています。

2. ディレクトリ構成

作業フォルダは前回同様にC:\IronPython26フォルダの直下にフォルダを作成しました。

フォルダ構成

C:\IronPython26\proj.class
|-- a01_formclass.ipy
|-- a02_formclass.ipy
|-- a02_formclass2.ipy
|-- a02_formclass2_dir.txt
|-- a02_formclass_dir.txt
|-- a02_lib
|   |-- __init__.py
|   |-- mylib.py
|   `-- mylib2.py
|-- a02_tooltipbutton.ipy
...
|-- a06_form_flowlayout.ipy
|-- a06_lib
|   |-- __init__.py
|   `-- mylib.py
`-- a06_unittest.ipy

3. 参考書

MSDNの.NETライブラリとO'REILLYの「Python Pocket Reference 3rd Edition」を使っています。 その他オンラインで検索したものもありますが、直接参考にしたものは随時リンクを張ります。

オブジェクトがどのようなメソッドを持っているかはdir()メソッドで確認することができます。 マニュアルのどこを読むべきか、これであたりをつけることができる場面も多いと思います。

4. 説明の流れ

ここではサンプルを走らせる時には触れる必要がなかったものの中から、自分でスクリプトを作成する際に必要になった事柄を説明していきます。

私はWindowsプログラミングについてはVBを触ったぐらいであまり前提がないので、作業内容をまとめておくことで(初級者と中級者の間にあるような)ギャップを埋めることができればと思っています。

5. この文書の目的

実際に必要なスクリプトを作成する時には一つのスクリプトファイルに全部の処理を記述するのではなく、適当なボリュームで分割して作成していくはずです。 Pythonらしい方法はクラスに分割する方法だと思います。

そこでクラスの作成のサンプルとそのファイルを読み込む方法を中心に、より実践的な方法についてまとめていきます。

6. IronPython 2.6に添付されるCPythonライブラリについて

IronPython 1.0の時代に書かれた文書ではCPythonのライブラリが不足しているため導入するように指示があります。 しかしIronPython 2.xではMSI配布パッケージにCPython 2.6のライブラリの一部が添付されるようになっています。 csv.pyなどCPythonに附属するオブジェクトコード(DLLファイル)を利用するものは正常に動かないため添付されていません。

添付されるライブラリのためにサイズが大きいですが、この文書ではMSIパッケージからIronPythonを導入したものとして説明しています。

クラスの作成

前回一番最初に作成した空のFormを表示するスクリプトをクラスを使って作成します。

1. MyFormクラスの作成

同じ動きをする2つのファイルを比べると次のようになり、今回作成したクラスファイルの方が1.5倍ほど行数が増えています。

変更前:01_form.ipyファイル

import clr
clr.AddReferenceByPartialName("System.Windows.Forms")
clr.AddReferenceByPartialName("System.Drawing")
import System
from System.Windows.Forms import Form
from System.Drawing import Size

form = Form()
form.Size = Size(300,200)
form.Text = "Hello World!"

## main ##
System.Windows.Forms.Application.Run(form)

変更後:a01_formclass.ipyファイル

import clr
clr.AddReferenceByPartialName("System.Windows.Forms")
clr.AddReferenceByPartialName("System.Drawing")
import System
from System.Windows.Forms import Form
from System.Drawing import Size

class MyForm(Form):
  def __init__(self):
    self.Size = Size(300,200)
    self.Text = "Hello World!"
    pass

  def run(self):
    System.Windows.Forms.Application.Run(self)
  pass

## main ##
form = MyForm()
form.run()

このままでは書き方が冗長になっただけで、おもしろくありません。 初心者向けにはより短い方が好まれるため、入門書によく載っているのは短い方です。

1-1. 本体処理の簡略化

新しく作成したファイルの本体処理は2行に収まっています。 別々のファイルにするとライブラリの呼び出しが加わって3行程度が本体処理になるはずです。

まだ簡単な初期化を行なっているだけですが、ここからはアプリケーション固有の設定はクラス定義に押し込めて、イベントハンドラなど、軟性を確保したい定義は本体処理側で定義する方向を考えてみようと思います。

1-2. 今後の展開

スクリプトの規模が大きくなってくると、別ファイルにクラスを作成する方が効率が高まります。 この場合の効率は作成する際に手分けをするという意味だけではなくて、設計やテストをファイル単位で考える事で最終的には作り手の覚えておくべきことを整理できるという意味も含みます。

入門書を終えた状態だと、同じようなスクリプトのコピーに頼ってしまいがちです。 次からはクラスを別ファイルに書き出し、ライブラリとして扱うことで、扱いが楽になるものなのか様子を眺めてみようと思います。

スクリプトの分割 〜本体とライブラリ〜

まずは先ほどの例をそのままメインスクリプトとライブラリファイルに分割します。

1. MyFormライブラリの作成

まずライブラリを入れておくためのフォルダとして"a02_lib"を作成しました。 先ほど使った"a01_formclass.ipy"ファイルを元に本体とライブラリのファイルに分けていきます。

変更前:a01_formclass.ipyファイル

import clr
clr.AddReferenceByPartialName("System.Windows.Forms")
clr.AddReferenceByPartialName("System.Drawing")
import System
from System.Windows.Forms import Form
from System.Drawing import Size

class MyForm(Form):
  def __init__(self):
    self.Size = Size(300,200)
    self.Text = "Hello World!"
    pass

  def run(self):
    System.Windows.Forms.Application.Run(self)
  pass

## main ##
form = MyForm()
form.run()

変更後:空のa02_lib\__init__.pyファイル

## empty

変更後:a02_formclass.ipyファイル

from a02_lib.mylib import *

form = MyForm("Hello World!")
form.run()

変更後:a02_lib\mylib.pyファイル

import clr
clr.AddReferenceByPartialName("System.Windows.Forms")
clr.AddReferenceByPartialName("System.Drawing")
import System
from System.Windows.Forms import Form
 from System.Drawing import Size

class MyForm(Form):
  def __init__(self, title):
    self.Size = Size(300,200)
    self.Text = title
    pass

  def run(self):
    System.Windows.Forms.Application.Run(self)
  pass

変更前は一つのファイルだったのに、3つのファイルが作成されました。 最後のファイルは空なので、実質的に2つのファイルに分かれたことになります。 変更のポイントを順番に説明していきます。

2. 本体:from a02_lib.mylib import *文

新しく作成したライブラリファイルは"a02_lib\mylib.py"ですが、これを呼び出すためにfrom a02_lib.mylib import *文を使っています。

これを実行した後は、a02_lib.mylib.MyFormではなくMyFormのようにクラスの名前でアクセスできるようになります。 どのクラスが名前で呼べるのか、その様子はdir()コマンドで確認する事ができます。

> ..\ipy.exe
>>> from a02_lib.mylib import *
>>> dir()
['Form', 'MyForm', 'Size', 'System', '__builtins__', '__doc__', '__name__', 'clr']  

"MyForm"の他にも、FormSizeなどもクラスの名前だけで呼べる事がわかります。

ただし開発の規模が大きくなってくるといろいろなライブラリを使うため、MyFormだけを使いたいのに余計なものも読み込まれ、もし他の人がそのライブラリで違う"MyFormクラス"を定義して読み込んでいた場合は混乱を引き起す事になります。

できるだけ読み込むライブラリは少ない方がよく、その制御をする方法が__all__変数です。

3. __all__変数

この変数はimport *で読み込まれる際にアスタリスクに何が入るのか指定する事ができます。 次のようにファイルを作り直す事ができます。

__all__行の追加

...
 __all__ = ['MyForm']
...

mylib.pyファイルに、この変更を加えた後でdir()を実行してみます。

>>> from a02_lib.mylib2 import *
>>> dir()
['MyForm', '__builtins__', '__doc__', '__name__']  

引き続きライブラリの内部ではFormSizeはクラス名だけで利用できますが、ライブラリをimportして利用する側で はMyFormクラスの1つだけがみえるようになりました。

4. __init__.pyファイルの役割

空のファイルで作成した__init__.pyファイルの役割は、先ほどの__all__行の管理が中心です。

ライブラリを開発していくと、フォルダの中にいろいろなファイルができていきます。 そのライブラリ本来の動きをサポートするためのユーティリティ的なクラスも多数できるでしょう。

またライブラリが提供するクラスの数が増えていった時に、form a02_lib.mylibのようにフォルダ名とファイル名を組み合せるのはファイルの数だけ行数が増えるので非効率です。 またファイル名を覚えておくことも、難しくなるでしょう。

そこでフォルダ名だけを使ってimportできるようにする仕組みの土台を提供するのが__init__.pyファイルです。

4-1. __init__.pyファイルを利用したスクリプトとライブラリ

先ほど作成したコメントだけの__init__.pyファイルを変更し、処理本体の一行目を__init__.pyに合うように変更します。

オリジナル:空のa02_lib\__init__.pyファイル

## empty

オリジナル:a02_formclass.ipyファイル

from a02_lib.mylib import *

form = MyForm("Hello World!")
form.run()

変更後:a02_lib\__init__.pyファイル

__all__ = ["MyForm"]
from a02_lib.mylib import *

変更後:02_formclass.ipyファイル

from a02_lib import *

form = MyForm("Hello World!")
form.run()

4-2. 変更のポイント

本体スクリプトの変更部分はfrom a02_lib import *の部分だけです。 この文にはライブラリファイル'mylib.py'に関連する名前は一切でてきません。 そして結果としてMyFormクラスだけに名前でアクセスできるようになります。

将来、mylib.pyファイルを複数のファイルに分割しても、__init__.pyファイルの変更だけが必要になります。

__all__行は無理に使う必要はなく、代替手段がある方法です。実際に標準ライブラリの多くで使われていません。 しかし、それは__all__行が直接使えるほど単純な場合が少ないからのようで、ほとんどは__init__.pyファイルでクラス名のロードが制御されています。 ライブラリを眺めてクラスを使おうとした時に、本体をどうやって呼び出したらよいのか分からなくなるところなので、__init__.pyファイルには慣れておいた方が良さそうです。

もう少し複雑な例

これまでばらばらに実装してきた機能をまとめて"mylib.py"ファイルを作成しようと思います。

1. 実行例

$alt

$alt

2. プログラムの比較

これまではスクリプトの本体は部品となるFormやButtonの作成と配置だけを行なってきましたが、今回はイベントハンドラも実装しているため、本体全体で一つのクラスを作成しています。 実質的な本体処理は最後の2行で行なわれています。

本体スクリプトだけで以前作成したスクリプトと同程度の分量がありますが、これは動的に配置する"No. X"ボタンをクリックした時に図のようなボタン名入りのMessageBoxを表示するようにしているためです。

オリジナル:03_04_form_flowlayout.ipy

# coding=Shift_JIS
# @author: YasuhiroABE <yasu@yasundial.org>
#
import clr
clr.AddReferenceByPartialName("System.Windows.Forms")
clr.AddReferenceByPartialName("System.Drawing")
import System
from System.Windows.Forms import Form
from System.Windows.Forms import FlowLayoutPanel
from System.Windows.Forms import Button
from System.Windows.Forms import MessageBox
from System.Windows.Forms import DockStyle
from System.Drawing import Size

## init Form object
form = Form()
form.Text = "Hello World!"
form.Size = Size(300,200)

## init FlowLayoutPanel object
flowLayout = FlowLayoutPanel()
flowLayout.Size = form.Size
flowLayout.Dock = DockStyle.Fill
#flowLayout.AutoScroll = True

## 生成したButtonオブジェクトを記憶するリストです
buttonList = []
## setup add button
addButton = Button()
addButton.Text = "Add"
def addButton_click(sender, arge):
  b = Button()
  b.Text = "No. " + str(len(buttonList))
  flowLayout.Controls.Add(b)
  pass
addButton.Click += addButton_click
flowLayout.Controls.Add(addButton)

## setup del button
delButton = Button()
delButton.Text = "Delete"
def delButton_click(sender, arge):
  if(len(buttonList) == 0): return
  b = buttonList.pop()
  flowLayout.Controls.Remove(b)
  pass
delButton.Click += delButton_click
flowLayout.Controls.Add(delButton)

form.Controls.Add(flowLayout)
System.Windows.Forms.Application.Run(form)

a07_lib\__init__.py

__all__ = ["MyForm", "MyButton", "MyFlowLayoutPanel"]
from a07_lib.mylib import *

スクリプト本体:a07_flowlayout.ipy

# coding=Shift_JIS
# @author: YasuhiroABE <yasu@yasundial.org>
#
import System
from a07_lib import *

class Main:
  def __init__(self):
    self.form = MyForm("Hello World!")
    self.layout = MyFlowLayoutPanel()
    self.callback_buttonList = []
    self.putButtons()
    pass

  def putButtons(self):
    addButton = MyButton("Add", "add new button")
    addButton.Click += self.callback_addButton
    self.layout.Controls.Add(addButton)

    delButton = MyButton("Del", "delete the last button")
    delButton.Click += self.callback_delButton
    self.layout.Controls.Add(delButton)
    pass

  def callback_addButton(self, sender, arge):
    b = MyButton("No. " + str(len(self.callback_buttonList)), "")
    b.Click += self.callback_button
    self.callback_buttonList.append(b)
    self.layout.Controls.Add(b)
    pass

  def callback_delButton(self, sender, arge):
    if(len(self.callback_buttonList) == 0): return
    b = self.callback_buttonList.pop()
    self.layout.Controls.Remove(b)
    pass

  def callback_button(self, sender, arge):
    msg = "Button: " + sender.Text + " clicked"
    System.Windows.Forms.MessageBox.Show(msg, "Notice")
    pass

  def run(self):
    self.form.Controls.Add(self.layout)
    self.form.run()

## main ##
main = Main()
main.run()

ライブラリ: a07_lib\mylib.py

# encoding=Shift_JIS
# @author: YasuhiroABE <yasu@yasundial.org>
#
import clr
clr.AddReferenceByPartialName("System.Windows.Forms")
clr.AddReferenceByPartialName("System.Drawing")
import System
from System.Windows.Forms import Form
from System.Windows.Forms import Button
from System.Windows.Forms import ToolTip
from System.Windows.Forms import FlowLayoutPanel
from System.Windows.Forms import DockStyle
from System.Windows.Forms import ToolStripMenuItem
from System.Windows.Forms import MenuStrip
from System.Windows.Forms import MessageBox
from System.Drawing import Size
from System.Drawing import Point

__all__ = ['MyForm','MyButton','MyFlowLayoutPanel']

size = Size(300,200)

class MyForm(Form):
  def __init__(self, title):
    self.Size = size
    self.Text = title
    self.IsMdiContainer = True
    pass

  def setupMenu(self):
    self.ms = MenuStrip()
    self.windowNewMenu = ToolStripMenuItem("New", None)
    self.windowNewMenu.Click += self.windowNewMenu_onclick
    self.windowMenu = ToolStripMenuItem("Window")
    self.windowMenu.DropDownItems.Add(self.windowNewMenu)

    self.ms.MdiWindowListItem = self.windowMenu
    self.ms.Items.Add(self.windowMenu)
    self.ms.Dock = DockStyle.Top
    self.MainMenuStrip = self.ms
    self.Controls.Add(self.ms)
    pass

  def windowNewMenu_onclick(self, sender, arge):
    MessageBox.Show("Thank you for clicking on me!", 
                                   "windowNewMenu_onclick")
    pass

  def run(self):
    self.setupMenu()
    System.Windows.Forms.Application.Run(self)
  pass

class MyButton(Button):
  def __init__(self, name, tooltip):
    self.Text = name
    self.toolTip = MyToolTip()
    self.toolTip.SetToolTip(self, tooltip)
    pass
  def setLocation(self, x , y):
    self.Location = Point(x,y)
  pass

class MyToolTip(ToolTip):
  def __init__(self):
    self.AutoPopDelay = 5000
    self.InitialDelay = 1000
    self.ReshowDelay = 500
    self.ShowAlways = True
    pass
  pass

class MyFlowLayoutPanel(FlowLayoutPanel):
  def __init__(self):
    self.Size = size
    self.Dock = DockStyle.Fill
    self.AutoScroll = True
    pass
  pass

## end ##

3. コールバックメソッドのデザイン

今回はスクリプト本体にイベントハンドラ用の(call_back_*)メソッドを作成しました。 メニューのようにMyFlowLayoutPanel側にメソッドを作成することもできましたが、柔軟性を確保するためにイベントハンドラは基本的にライブラリ側には置かないのが良いのだろうと思います。

4. 閑話休題:プログラミングスタイル

Pythonはインデントに依存するため、if文やメソッド定義をくくるためにブレースなどの特別な記号は使いません。 その代りにスクリプトではpass文を使っています。 これ自体に機能はありませんが、インデントが崩れたり、インデントレベルの違うコードをコピーしてきた時でもpassを目印にインデントを調整する事ができるようになります。

エディタが狂ってしまい行頭の空白が全て潰れてしまった様子を想像してみてください。 pass文は非常にコストの低い予防策になるでしょう。 全てのインデントが下がる手前に挿入しなければいけないので、少し難しいですけどね。

5. このセクションのまとめ

勝手に追加したコードのせいもありますが、以前作成したスクリプトとの行数での比較でみると2倍前後の差があります。 これで効率が高まったといえるのかどうか微妙な数字ですが、個人的には余計な機能を追加するぐらいの余裕はでてきたのかなと感じています。

ライブラリを作成する意義に迫るためにも、次回はテストについて考えていきます。

作成したライブラリのテスト

プログラミングをする上でのテストには、いろいろな考え方がありますが、フェーズ(時期)の区切りで単体テスト、統合テストという概念があります。 そして、テストの入出力(内容)に注目してホワイトボックステストやブラックボックステストという考え方があります。 後はだいたいそのバリエーションで、フェーズによって役割りが変化しますが、テストの目的は常に対象がその時期で必要とされている状態にあるか確認するもので、テストの方法自体にはそれほど違いはありません。

最終的な統合テストは必要ですが、比較的規模の小さいプログラミングでは、開発しながら行なう単体テスト+ホワイトボックステストの役割が大きいと思います。 少なくとも設計した本人は部品がどのような形で使われるのか把握していますから、テストを短い間隔で網羅的に実行できれば効率が高まると考えられます。

そんな概念を土台にするとGUI系のプログラミングでは、自動化できるテスト範囲は非常に限られています。 とはいえIronPythonでは動的に内部状態にアクセスできますから、特別なツールを使わなくても確認できる範囲は静的な言語と比べれば広いといえます。

今回はプロパティが正しければ表示は正しく行なえているはず、という立場を取り、プロパティをチェックする方向で自動化を行なってみました。 そのため実際に画面に部品を表示させる事は行なっていません。

1. unittestスクリプトの作成と実行

IronPython 2.6に標準に添付されているCPythonのunittest.pyファイル(C:\IronPython26\Lib\unittest.py)を使いました。

a07_unittest.ipyファイル

# coding=Shift_JIS
# @author: YasuhiroABE <yasu@yasundial.org>
#
import unittest
import a07_lib
from a07_lib.mylib import *
import System


class MyTestCase(unittest.TestCase):
  def setUp(self):
    self.formtitle = "Hello World"
    self.form = MyForm(self.formtitle)
    self.layout = MyFlowLayoutPanel()
    self.tooltip = a07_lib.mylib.MyToolTip()

  def test_formtitle(self):
    self.assertEqual(self.form.Text, self.formtitle)

  def test_formwidth(self):
    self.assertEqual(self.form.Size.Width, 300)

  def test_layoutwidth(self):
    self.assertEqual(self.layout.Size.Width, 300)

  def test_formheight(self):
    self.assertEqual(self.form.Size.Height, 200)

  def test_layoutheight(self):
    self.assertEqual(self.layout.Size.Height, 200)

  def test_layoutdockstyle(self):
    self.assertEqual(self.layout.Dock, System.Windows.Forms.DockStyle.Fill)

  def test_layoutscroll(self):
    self.assertTrue(self.layout.AutoScroll)

  def test_tooltippopdelay(self):
    self.assertEqual(self.tooltip.AutoPopDelay, 5000)

  def test_tooltipinitdelay(self):
    self.assertEqual(self.tooltip.InitialDelay, 1000)

  def test_tooltipreshowdelay(self):
    self.assertEqual(self.tooltip.ReshowDelay, 500)

  def test_tooltipshowalways(self):
    self.assertTrue(self.tooltip.ShowAlways)

unittest.main()

2. テスト結果と解説

実行結果は次のようになります。

C:\IronPython26\proj.class>..\ipy a07_unittest.ipy
...........
----------------------------------------------------------------------
Ran 11 tests in 0.531s

OK  

このスクリプトは"a07_lib.mylib"からimportするように宣言する事で、__init__.pyの中の__all__行を無視するようにしています。 しかし"mylib.py"の中にある__all__行を参照するため、"MyToolTip"クラスにはアクセスできません。

そこで__all__行で公開していないMyToolTipクラスにアクセスするために、フルネーム(完全修飾名)でアクセスしています。

...
import a07_lib
...
    self.tooltip = a07_lib.mylib.MyToolTip()
    ...

作成したスクリプトは最後のunittest.main()によって、そのまま実行することができます。

2-1. まとめ

これまで作成してきたスクリプトはそれなりの規模になってきました。 次章ではいろいろまとめてみようと思います。

ここまでの作業のまとめ

ここでこれまで作成してきたライブラリは、行数でみた時にどれひとつオリジナルよりも短かくなったものはありませんでした。 そこでこれまでの作業内容を振り返ってみようと思います。

1. 比較方法を変えてみる

元々作成していたサンプルコードは、それぞれに共通しているコードがあまり含まれていませんでした。 ライブラリを利用して作成したサンプルではボタンには全てToolTipを付けて、フォームにはメニューが含まれています。

そこで以前作成したものにもToolTipやメニューをつけた上で比較をしてみます。

2. とりあえず行数で比較

比較の対象はLines of Code(LOC)として、コメントと空行を含むファイルの全行数を比較しました。 全体で比較するとライブラリを含めても15%ほど行数の削減ができています。

無駄に機能を増やすのは若干後ろめたい気もしますが、小さいプログラムを分割するのは、あまり効果が期待できないことはこれまで見てきたとおりです。 しかしアプリの数が増え、共通化が進み、コードの重複部分が増えるに従ってコード全体が軽量になります。

問題は効率的なライブラリの作り方なんですけどね。 さて、ここからは個別のプログラム毎の比較を載せていきます。

2-1. メニュー付き空ウィンドウ

$alt

1ファイルに全ての処理を書いたスクリプト

ライブラリを利用したスクリプト

2-2. FlowLayoutPanelを使い動的にボタンを配置する

$alt

1ファイルに全ての処理を書いたスクリプト

ライブラリを利用したスクリプト

2-3. CSVを読み込みDataGridViewに表示する

$alt

1ファイルに全ての処理を書いたスクリプト

ライブラリを利用したスクリプト

CSVファイル

2-4. ライブラリファイル

ライブラリ

CSVファイルの正しい取り扱いについて

ここまで作成してきたスクリプトではカンマを区切り記号としてsplitしただけで、CSVを正しく取り扱ってきませんでした。 IronPythonにはCPythonの標準ライブラリに含まれるcsv.pyは添付されません。 そこで.Netライブラリに含まれるクラスを使います。

1. CSVファイルについて

これまで作成したファイルに次のようなCSVファイルを処理させると画面が崩れてしまいます。

カンマを含むCSVファイル

name,age,address
"yasu,hiro",20,AizuWakamatsu
abe,25,"Yokohama Aoba-ku"

$alt

まずはコマンドライン上で動くアプリケーションからMicrosoft.VisualBasic.FileIO.TextFieldParserクラスを使用します。 このクラスはC#のサンプルがAPIリファレンスにありIronPythonからも問題なく使えそうです。

2. ライブラリ間のクラス参照

同じディレクトリに配置されたファイルから、他のファイルに書かれたクラスを呼びたい場合には、そのライブラリファイルからみたパスでimport文を使うことになります。 以下の例ではMyDataTableクラスのloadCSV()メソッドから"a09_lib\mycsv.py"で定義されたMyCSVクラスを参照するために、"a09_lib\mydata.py"に次のような宣言を利用しています。

...
import mycsv
from mycsv import MyCSV
...

sys.pathにあるフォルダ('.')からの相対パスではなくて、ライブラリの配置されている場所からのパスで書く必要のあるところが少し変っていると思います。

3. サンプルコード全体

次のスクリプトと下記4つのライブラリ系ファイルを配置して動かすことができます。

a09_csvparser.ipyスクリプト

# coding=Shift_JIS
# @author: YasuhiroABE <yasu@yasundial.org>
#
from a09_lib import *

## csv filename
filedialog = MyOpenFileDialog()
filename = filedialog.open()
csv = MyCSV(filename)
for r in csv.eachRow():
  for i in r.eachItem():
    print i + "\t",
    pass
  print ""

3-1. その他のライブラリ

メインは今回作成した"mycsv.py"ファイルです。 その他に"mydata.py"ファイルからも"mycsv.py"で定義されたMyCSVクラスを参照しています。

4. 実行結果

次のような出力が得られます。

$alt

次はDataGridViewに、このファイルを読み込ませてみます。

CSVファイルの正しい取り扱いについて 其の弐

1. MyDataGridViewへの追加

以前作成したMyDataTableクラスを修正し、CSVファイルを正しく扱えるようにしました。 先ほどと同じCSVファイルを読み込ませると次のような表示になります。

CSVファイルではデータに改行文字を含める事もできますが、DataGridViewはデータの中の改行コードを無視されてしまいます。

$alt

2. スクリプトについて

スクリプト本体は次のようになりました。

a09_filedialog.ipyファイル

# coding=Shift_JIS
# @author: YasuhiroABE <yasu@yasundial.org>
#
from a09_lib import *
form = MyForm("Hello World!")

## csv filename
filedialog = MyOpenFileDialog()
filename = filedialog.open()

## setup datatable
dt = MyDataTable()
dt.loadCSV(filename)

## setup layout
datagridview = MyDataGridView()
datagridview.DataSource = dt

form.Controls.Add(datagridview)
form.run()

MyDataTableクラスの修正個所は次の通りです。

変更前:loadCSVメソッド処理の抜粋

...
  def loadCSV(self, filename):
    reader = StreamReader(filename)
    cols = []
    for c in reader.ReadLine().split(","):
      self.Columns.Add(c, System.Type.GetType("System.String"))
      cols.append(c)
      pass
    l = reader.ReadLine()
    while(l != None):
      row = self.NewRow()
      counter = 0
      for item in l.split(","):
        row[cols[counter]] = item
        counter += 1
      self.Rows.Add(row)
      l = reader.ReadLine()
      pass
    reader.Close()
    pass
  pass
...

変更後:loadCSVメソッド処理の抜粋

...
  def loadCSV(self, filename):
    parser = MyCSV(filename)
    cols = []
    rownum = 0
    for r in parser.eachRow():
      tablerow = None
      itemnum = 0
      for item in r.eachItem():
        if rownum == 0:
          self.Columns.Add(item, 
                  System.Type.GetType("System.String"))
          cols.append(item)
	else:
          if tablerow is None: tablerow = self.NewRow()
          tablerow[cols[itemnum]] = item
	  pass
        itemnum += 1
	pass
      rownum += 1
      if tablerow is not None:
        self.Rows.Add(tablerow)
      pass
    pass
  pass
...

元々は1行目をヘッダと認識して別に処理を行なっていましたが、作成したMyCSVクラスは効率化のためにyield文を使っているため1行だけの処理を抜き出すのが難しくなっています。

特別なメソッドを準備しようかとも思いましたが、とりあえず呼び出し側でカラム、行の数を数えるカウンターを準備する事で回避しています。

3. MyCSVクラス

今回作成したmycsv.pyライブラリファイルの内容は次の通りです。

# coding=Shift_JIS
# @author: YasuhiroABE <yasu@yasundial.org>
#
import clr
clr.AddReferenceByPartialName("Microsoft.VisualBasic")
import Microsoft
from Microsoft.VisualBasic.FileIO import TextFieldParser
from Microsoft.VisualBasic.FileIO import FieldType
import System

class MyRow:
  def __init__(self, row):
    self.row = row
    pass

  def eachItem(self):
    for item in self.row:
      yield item
      pass
    pass
  def getItem(self):
    return self.row
  pass

class MyCSV:
  def __init__(self, filename):
    self.parser = TextFieldParser(filename,
		           System.Text.Encoding.GetEncoding("Shift_JIS"))
    self.parser.TextFieldType = FieldType.Delimited
    self.setDelimiters(',')
    pass

  def setDelimiters(self, delimiter):
    self.parser.SetDelimiters(delimiter)
    pass

  def eachRow(self):
    array = self.parser.ReadFields()
    while(array):
      row = MyRow(array)
      yield row
      array = self.parser.ReadFields()
      pass
    pass
  pass

行のデータにアクセスする時にはyieldするのが良いとは限らないので、getItem()メソッドを準備しています。

このクラスはCSVファイルの各列のデータは全て文字型だと仮定しています。 これはCSVファイルのエンコーディングを保持する限りは正しい仮定ですが、IronPythonで日本語を正しく扱うためには少し設定が必要です。

4. 日本語を含む文字列の取り扱いについて

coding行と揃っていれば、Shift_JISでもUTF-8でもスクリプトに直接記述した文字をコントロールに表示させる事は可能です。 MyCSVクラスでは.NETの機能を使って、CSVファイルがShift_JISエンコーディングの前提で読み取っています。

単純にファイルの内容を1行づつ表示させるような処理であれば、decodeを使うこともできます。

CSVファイル: a10_readfile.csv

name,age,address
"yasu,裕",20,AizuWakamatsu
abe,25,"Yokohama Aoba-ku"

decodeを使った例:a10_readfile.ipy

f = open("a10_readfile.csv")
for line in f:
  print line.decode("Shift_JIS")

IronPython 1.xの頃の解説を読むとコマンドプロンプトに出力される文字が化けてしまうのを避けるために"sys.setdefaultencoding()"を使っていたようですが、いまはそんな名前のメソッドはsysモジュールにはありません。

ファイルから読み込んだ文字列をコマンドプロンプトやGUIに渡したい場合には、ファイルのエンコーディングと同じエンコーディングをdecodeメソッドに渡せば扱えるようです。 ファイルはUTF-8でもShift-JISでも正しく指定すれば大丈夫なようです。

テキストファイルから読み込んだ文字列をラベルとコマンドプロンプトに出力するスクリプト:a11_readline_decode.utf8.ipy

# encoding: UTF-8
# @author: YasuhiroABE <yasu@yasundial.org>
#
import System
import clr
clr.AddReference("System.Windows.Forms")
from System.Windows.Forms import *
clr.AddReference("System.Drawing")
from System.Drawing import *

form = Form()
form.Size = Size(240,100)
label = Label()
label.Size = form.Size

f = open("a11.utf8.txt")
for line in f:
  print line.decode('utf8')
  label.Text += line.decode('utf8')

form.Controls.Add(label)
Application.Run(form)

確認のためだけなので横着をしてライブラリは使わずにスクリプトを組み立てました。

4-1. スクリプトの実行結果

$alt

スクリプトを変更し、"all.sjis.txt"を読み込みline.decode('Shift_JIS')で変換しても同じ結果が得られます。

まとめ

とりあえず自分がスクリプトを始める前に調べておきたい内容はだいたいまとまったので、ここで区切りを付けようと思います。

次はデバッグ用に入出力にパイプを付けた簡易Webブラウザでも作ってみようと思います。

戻る


Created: 2010-03-13, Last modified: 2010-03-19

2009,2010 © Yasuhiro ABE <yasu@yasundial.org>

Valid XHTML + RDFa RDFa it (RDF/XML)!

正当なCSSです!

Creative Commons License www.yasundial.org by Yasuhiro ABE is licensed under a Creative Commons Attribution 2.1 Japan License. Permissions beyond the scope of this license may be available at http://www.yasundial.org/info/license.html.