Python プログラミング __name__ってなに?

 今日も見に来てくださって、ありがとうございます。石川さんです。

 お友だちから、__name__のことがよく分からない、と、質問をいただいたので、実際にどうなっているのか、実行結果を確認しつつ、説明したいと思います。

はじめに

 まず初めに、dir()という組み込み関数がありまして、コマンドインタプリターで実行すると、現在のローカルスコープにある名前のリストを返してくれます。

In [1]: print(dir())
['In', 'Out', '_', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'quit']

 出力結果の1行目の右端にありますね。こんな感じで、「__name__」という名前がPythonインタープリタの中に事前定義されています。さて、こちらがなにか、ちょっと調べてみます。組み込み関数のtype()を使うとオブジェクトの型を返してくれます。まずはtype()で型を調べます。

In [2]: type(__name__)
Out[2]: str

 なるほど、ここで、「__name__」が文字列の変数であることがわかりました。中身を見てみましょう。

In [3]: print(__name__)
__main__

 変数「__name__」の中身は「”__main__”」ということがわかりました。

ファイルをつくって確認します

 コマンドインタープリタで確認してみると、セットされた値は「__main__」でした。この値は、「__main__」以外に設定されることはあるのでしょうか?利用されている箇所で知っているのはファイルの中でした。と、いうことで、以下のように「mynameis.py」ファイルを作ってみました。

"""" __name__ にどんな値がセットされているか、確認します。 """

print("My name is " + __name__)

 単純に、値をprint()するだけの記述を含むファイルです。
さて、実行してみます。

In [4]: %run mynameis.py
My name is __main__

 フツーに実行すると、やはり「__main__」がセットされています。ちなみに%runはIPythonのマジックコマンドで、指定されたファイルを直接実行します。コマンドプロンプトだと、「python mynameis.py」と実行するのと同じ意味になります。
さて、次にこの「mnameis.py」ファイルがモジュールだということにして、importしてみます。

In [5]: import mynameis
My name is mynameis

 importすると、mynameis.pyの内容が読み込まれ、すべて上から順番に実行されます。ただし、「__name__」には、このようにモジュール名がセットされることになっています。モジュールをつくるときは、この違いを利用して、

if __name__ == "__main__":

というふうにモジュール内にif文を記載することで、importから実行されたときと、%runなどで直接実行されたときのふるまいを変更することができます。

まとめ

 モジュールを作るとき、importで呼び出して利用するだけでなく、直接実行することができるようにするために、「__name__」を利用することができます。

Python tkinter GUIプログラミング ログインダイアログ

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今回は、ログインダイアログを作ってみました。ログインダイアログは別ウィンドウなので、値の引き渡し方法をどうするのか、というのがポイントになります。

出来上がりイメージ

ログインダイアログ

ソースコード

import tkinter as tk
from tkinter.simpledialog import Dialog

class LoginDialog(Dialog):
    def __init__(self, root):
        super().__init__(root, title="Login")
        
    def body(self, master):
        self.username = tk.StringVar()
        self.password = tk.StringVar()
        userLabel = tk.Label(master, text="User:")
        userLabel.grid(row=0, column=0)
        username = tk.Entry(master, textvariable=self.username)
        username.grid(row=0, column=1)
        passwdLabel = tk.Label(master, text="Pass:")
        passwdLabel.grid(row=1, column=0)
        password = tk.Entry(master, show="*", textvariable=self.password)
        password.grid(row=1, column=1)
        self.information = tk.Label(master)
        self.information.grid(row=2,columnspan=2)
        
    def validate(self):
        self.information.configure(text="")
        if not self.username.get():
            self.information.configure(text="Input username")
            return False
        if not self.password.get():
            self.information.configure(text="Input password")
            return False
        return True

    def apply(self):
        self.result = self.username.get(), self.password.get()

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Login dialogue")
        self.geometry("400x100")
        
        show = tk.Button(self, text="login", command=self.show)
        close = tk.Button(self, text="close", command=self.destroy)
        show.grid(row=0, column=0, sticky=tk.EW, ipadx=20, padx=40, pady=20)
        close.grid(row=0, column=1, sticky=tk.EW, ipadx=20, padx=40, pady=20)
    
    def show(self):
        ret = LoginDialog(self)
        print(ret.result)

if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 ログインダイアログは、tkinter.simpledialogのDialogを継承して作成しました。4~33行目です。ログインダイアログの中身は、body()メソッドをオーバーライドして実装します。ユーザー名とパスワードとメッセージ出力用のラベルを追加しました。もし、OKとCancel以外のボタンを使いたいときは、buttonbox()メソッドをオーバーライドしますが、今回はこれで充分でしょう。

 OKボタンを押したときに、値をチェックするには、validate()メソッドをオーバーライドします。今回は、22~30行目に記載があります。ユーザー名かパスワードが入力されていないとき、Falseを戻します。成功時はTrueを戻すようにしました。30行目のTrueを戻すことを忘れると、PythonはデフォルトでNoneを戻す約束ですので、OK押してもその後続処理のapply()は実行されませんので、注意が必要です。

 OKボタンを押したときの処理は、apply()メソッドをオーバーライドします。今回は、33行目で、ダイアログボックスに入力した値をself.resultへセットしています。self.resultは、初期化時にNoneをセットされていますので、Cancel押されたかどうか、この値を判定すればわかりますね。

 呼び出し方法は、47行目のようにします。戻り値の判定は48行目のret.resultをチェックすれば完璧です。Noneのときは、キャンセルされています。

まとめ

 ログインダイアログを作成することを通して、簡単なダイアログを作成するための基本を学びました。これで、いろいろなダイアログ、作成できそうですね。

Python tkinter GUIプログラミング 透明なキャンバス

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、透明なウィンドウをつくったところで、ひらめきました。透明なキャンバスのウィンドウをつくって、それを別のウィンドウに重ねるのは、どうでしょうか。と、いうことで試しにやってみました。ちょっと実用には遠いですが、アイディアとして記録しておきます。

出来上がりイメージ

透明なキャンバスを仮想的に実現しました

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Transparent Canvas Challenge!")
        self.geometry("500x300")

        self.top = tk.Toplevel()
        self.top.wm_attributes("-topmost", True)
        self.top.overrideredirect(True)
        self.top.geometry("500x300")
        self.top.forward = tk.Canvas(self.top, background="white")
        self.top.forward.pack(fill=tk.BOTH, expand=True)
        self.top.forward.create_oval(50,50,450,250,fill="lightblue")
        self.top.wm_attributes("-transparentcolor", "white")
        
        self.back = tk.Canvas(self, background="white")
        self.back.pack(fill=tk.BOTH, expand=True)
        self.back.create_rectangle(50,50,450,250)
        self.back.create_rectangle(100,100,400,200)
        self.bind('<Configure>', self.change)
        self.back.bind("<Unmap>", self.unmap)
        self.back.bind("<Map>", self.map)
        self.bind("<1>", self.draw1)
        self.bind("<3>", self.draw3)
        self.top.bind("<1>", self.draw1)
        self.top.bind("<3>", self.draw3)
        
    def draw1(self, event):
        self.top.forward.create_oval(event.x, event.y, event.x + 30, event.y + 30)
        
    def draw3(self, event):
        self.back.create_rectangle(event.x, event.y, event.x + 30, event.y + 30)

    def unmap(self, event):
        self.top.withdraw()

    def map(self, event):
        self.lift()
        self.top.wm_deiconify()
        self.top.attributes("-topmost", True)
        
    def change(self, event):
        x, y = self.back.winfo_rootx(), self.back.winfo_rooty()
        w, h = self.winfo_width(), self.winfo_height()
        self.top.geometry(f"{w}x{h}+{x}+{y}")
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 まずは、背景になるキャンバスをつくります。そして、前面に来る透明なキャンバスをつくるのですが、そのときに、Toplevelのウィンドウをつくって、このウィンドウ用のキャンバスとします。そして、Toplvelの方に透明となる設定を施す、というのがおおまかな流れです。

 9行目でToplevelのウィンドウをつくっています。10行目「self.top.wm_attributes(“-topmost”, True)」は、割とポイントになりますが、常に一番上のウィンドウとなるようにする設定です。これで、透明なキャンバスが常に上に表示されるようになります。

 11行目、「self.top.overrideredirect(True)」では、ウィンドウのタイトル部分を消去しています。これでアイコン化や閉じるボタンなども使えなくなります。

 13行目、14行目でキャンバスをつくって、配置しています。キャンバスの背景色は、「”white”(白)」にセットしていますが、これが16行目の「self.top.wm_attributes(“-transparentcolor”, “white”)」のところで効いてきます。ここで-transparentcolorは透明にする色で、「”white”(白)」を指定しています。こうすることでキャンバスの背景色の白が透明になります。

 23行目から、いくつかイベントをバインドしています。最初のイベントは<Configure>です。ウィンドウのサイズなどが変更されたときに発生します。ウィンドウサイズが変更されたときに、透明なキャンバスのサイズも同時に変更するために指定しました。<Unmap>と<Map>はウィンドウがアイコン化から戻されたときとアイコン化されたときに発生します。こちらもウィンドウを閉じたときに透明なキャンバスのウィンドウも閉じるために指定してあります。

 25~28行目は左クリックされたときと右クリックされたときにキャンバス上に丸と四角を描くためにセットしました。ちゃんと透明なキャンバスが上になっていることを確認するためにおまけで付けてみました。

 37行目の「self.top.withdraw()」は、ウィンドウを非表示にします。40行目の「self.lift()」は、ウィンドウを上に移動します。41行目「self.top.wm_deiconify()」でアイコン化から復帰し、42行目「self.top.attributes(“-topmost”, True)」で常に一番上のウィンドウとなるように再設定しています。

 44~47行目のchangeメソッドでは、透明なキャンバスのウィンドウのサイズを合わせています。

まとめ

 ウィンドウをふたつつくって、下のウィンドウにあわせて上のウィンドウが移動したりサイズ変更したり、ということで、透明なキャンバスを実現してみました。思ったとおりに動作させるのは、けっこう難しいですね。

 

Python tkinter GUIプログラミング 透明なウィンドウ

 今日も見に来てくださって、ありがとうございます。石川さんです。

 透明なキャンバスを重ねるというアイディアを実現しようといろいろ調べまわりましたが、なかなか良いものが見つかりません。その中で見つけたちょっと面白い技を紹介します。透明なウィンドウです。

出来上がりイメージ

背景色が透明なウィンドウ

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Transparent window")
        self.geometry("500x300")
        self.config(bg="white")
        self.attributes("-transparentcolor", "white")
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 ポイントはただ一つ、9行目の「self.attributes(“-transparentcolor”, “white”)」です。これで透明になる色をセットしています。この場合、白(”white”)ですね。8行目でbg(背景色)に白を設定しています。この色が透明になる、ということで画像のようなウィンドウが出来上がります。

まとめ

 背景を透明にすることで、変わった形のウィンドウを作ったり、特殊な効果を実現することができそうですね。ちなみに、Raspbianではうまく動作しない、と、言われていました。

Python プログラミング sqlite3を使ってみる

 今日も見に来てくださって、ありがとうございます。石川さんです。

 Pythonからデータを保持するための方法のひとつ、sqlite3を利用してみます。sqlite3はPythonの標準ライブラリに含まれていますので、特別なインストールなど、不要です。それでいて、SQLが使えて、テーブルを作成してデータを簡単に保存したいというニーズに充分応えてくれます。

 ぼくは普段のおしごとでは、Oracleを専門に扱っているのですけど、他のデータベースも知っておいた方がよいでしょう、ということで、ちょっぴりチャレンジしてみました。データベースをつくって、テーブルをつくって、データを入れて、検索する、という一連の流れを実行してみました。

 ちなみに、sqlite3は、別のサーバプロセスを用意する必要はありません。その名のとおり軽量で、ディスク上のデータベースを提供してくれます。

ソースコード

import sqlite3

conn = sqlite3.connect('test.db')

curs = conn.cursor()

curs.execute("SELECT tbl_name FROM sqlite_master WHERE type = 'table'")
tables = [table[0] for table in curs.fetchall()]

if "BOOKS" in tables:
    curs.execute("DROP TABLE BOOKS")
curs.execute("CREATE TABLE BOOKS ( ID NUMBER PRIMARY KEY, TITLE VARCHAR(30), PUBLISHED DATE)")
curs.execute("INSERT INTO BOOKS (ID, TITLE, PUBLISHED) VALUES"
             " (1, '入門Python3', '2015-12-01')"
             ",(2, '実践Python3', '2015-12-01')"
             ",(3, 'Fluent Python', '2017-10-11')"
             ",(4, 'Effective Python', '2016-01-22')"
            )
conn.commit()
curs.execute("SELECT * FROM BOOKS")
rows = curs.fetchall()
print(rows)

説明

 1行目、ライブラリ名はsqlite3です。利用できるようにするため、importします。

 3行目、sqlite3のデータベースを使えるようにするためには、まずConnectionオブジェクトを作る必要があります。データは「test.db」ファイルに格納されます。「:memory:」という特殊な名前を指定すると、RAM上にデータベースが作れるそうです。ちなみに、ファイルが存在しなくてもエラーにならず、勝手にファイルが作成されます。

 5行目、データベースへの操作には、必ずカーソルが必要です。ということで、cursor()メソッドでカーソルを作成します。

 このデータベースの中に、「BOOKS」テーブルを作成しようと思ったのですけど、既にこの「BOOKS」テーブルが存在した場合はいったん削除してから作成しましょう。7行目で指定した「sqlite_master」テーブルには、このデータベースの中に入っているシステムテーブルで、作られたオブジェクトの情報が入力されています。

 7行目でSELECT文を定義して、8行目でデータを取り出しています。データはタプルで取り出されるので、リスト内包表記で一つ目の項目だけを取り出しています。10行目で比較するためです。1回目はテーブルが存在しないはずですので、12行目のテーブル削除は実行されず、12行目のCREATE TABLE文でテーブルをつくることから始めます。

 13行目のINSERT文でデータを投入しています。今回は、1行で4件データを作成しています。sqlite3はINSERT一文で複数件投入できるのですね、便利です。

 19行目、commit()です。これで、データが確定されました。

 20行目以降、SELECT文でデータを取得しています。

まとめ

 sqlie3、簡単ですね。ほとんど悩むことなく使えるようになりました。さすが標準モジュールです。

Python tkinter GUIプログラミング Canvas項目の前後切り替え

 今日も見に来てくださって、ありがとうございます。石川さんです。

 EclipseのGraphical Editing Framework(GEF)には透明なキャンバスがあって、背面にConnection layer、前面にPrimary layerを配置することで、コネクションをうまくあつかう工夫をしています。tkinterでも同じことができないかといろいろと調べていたのですが、ちょっと難しそうです。

 レイヤーが変えられないので、項目を前後に移動して重なりを制御するしかありませんね。と、いうときに使えるのが、tag_raise()、tag_lower()です。

できあがりイメージ

初期画面
クリックで長方形が上にきます

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("600x400")
        self.title("canvas tag_raise")
        
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        self.r = self.canvas.create_rectangle(90,40,510,360,fill="lightblue")
        self.o = self.canvas.create_oval(30,30,560,370,fill="lightyellow")
        
        self.item = "r"
        
        self.canvas.bind("<ButtonPress>", self.clicked)
        
    def clicked(self, event):
        if self.item == "r":
            self.canvas.tag_raise(self.r)
            self.item = "o"
        else:
            self.canvas.tag_raise(self.o)
            self.item = "r"
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 12行目、13行目でcreate_rectangle()、create_oval()でキャンバス上に四角形と楕円を描画します。このときの戻り値はidで、キャンバス上に描画した項目を管理する番号です。17行目で、クリックしたときに実行するメソッドをバインドしています。

 21行目と24行目のtag_raise()で指定されたidの項目を上に移動させます。

まとめ

 tag_raise()とtag_lower()を使って、項目をz軸方向に移動させることができます。ただし、一番上と一番下しか指定できませんので、複数項目があって、重なる順番が必要な時は、工夫が必要になりそうですね。

Python tkinter GUIプログラミング フレームの切り替え

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、キャンバスの切り替えのやり方です。透明なキャンバスを二枚重ねる方法はないかと調べていたのですが、見つからず、かわりにフレームを切り替える方法を見つけましたので、紹介します。

できあがりイメージ

 初期表示画面は以下のように三角形を描いてあります。

初期表示画面

 キャンバスをクリックすると次のように切り替わります。

キャンバスをクリックすると切り替わります。

ソースコード

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("test")
        self.frames = {}
        
        self.frames["top"] = top = tk.Frame(self,width=300,height=200)
        topcanvas = tk.Canvas(top)
        topcanvas.place(x=0, y=0)
        topcanvas.create_oval(30,30,270,170,outline="blue",fill="white")
        top.grid(row=0,column=0)

        self.frames["bottom"] = bottom = tk.Frame(self,width=300,height=200)
        bottomcanvas = tk.Canvas(bottom)
        bottomcanvas.place(x=0, y=0)
        bottomcanvas.create_polygon(150,30,30,170,270,170,fill="lightgrey")
        bottom.grid(row=0,column=0)

        self.bind("<ButtonPress>", self.clicked)
        self.frame = "top"

    def clicked(self, event):
        self.frames[self.frame].tkraise()
        if self.frame == "top":
            self.frame = "bottom"
        else:
            self.frame = "top"
        
if __name__ == "__main__":
    app = App()
    app.mainloop()

説明

 クリックしたら描かれたキャンバスが切り替わります。正確には、フレームが切り替わります。まずは、アプリケーション作成、いつものようにtkinterのTkを継承してクラスを開始しています。__init__メソッドでは、最初にsuper().__init__()と記述して、親クラスTkの__init__()初期化メソッドを呼び出しています。その後、タイトルをセットして、self.framesをディクショナリとして初期化しています。

9行目でフレームを作成、10行目でそのフレーム上にキャンバスを作成します。11行目でplaceを使って配置しています。12行目でキャンバスへ楕円を描画しています。13行目でフレームをgridを使って配置しています。作ったフレームはディクショナリにセットしています。

 15~19行目、同様にフレームとキャンバスを作成、配置しています。21行目、クリックされたときに呼び出すメソッドをセット、作ったフレームはディクショナリにセットしています。

 25行目、ここが実際のフレームの切り替えを実行している箇所です。ディクショナリに登録したフレームのtkraise()メソッドを呼び出して、フレームを最上位に移動しています。

 フレームをたくさん作って、タイマーでパカパカ切り替えたら、アニメーションが作れそうですね。

まとめ

 フレームの切り替え、やり方さえ知っていれば、簡単ですね。キャンバスを二枚重ねて表示する方法は、いまだ不明です。どなたか知っている方がいらっしゃれば、教えてください。

Pythonディクショナリ(辞書)

 今日も見に来て下さって、ありがとうございます。石川さんです。

 今日は、Pythonのディクショナリを少し紹介します。ディクショナリはキーと値のペアを格納できるコレクションです。はじめてこのディクショナリを知った時は、便利すぎて衝撃でした。

作成方法と使い方

In [1]: d = { "A": "Apple", "B": "Boy", "C": "Color" }

In [2]: d
Out[2]: {'A': 'Apple', 'B': 'Boy', 'C': 'Color'}

 ディクショナリを作るには、上記のようにキー:値のペアをカンマで区切って並べ、波括弧「{}」で囲みます。それぞれの要素を取り出すには、キーを以下のように指定していきます。

In [3]: d["A"]
Out[3]: 'Apple'

In [4]: d["B"]
Out[4]: 'Boy'

In [5]: d["C"]
Out[5]: 'Color'

 キーだけを取り出す、値だけを取り出す、キーと値のペアを取り出すには以下のように記述します。

In [6]: d.keys()
Out[6]: dict_keys(['A', 'B', 'C'])

In [7]: d.values()
Out[7]: dict_values(['Apple', 'Boy', 'Color'])

In [8]: d.items()
Out[8]: dict_items([('A', 'Apple'), ('B', 'Boy'), ('C', 'Color')])

 ちなみに、存在しないキーを指定すると、エラーになります。

In [9]: d['Z']
Traceback (most recent call last):

  File "<ipython-input-168-b7ca268d4341>", line 1, in <module>
    d['Z']

KeyError: 'Z'

 このキーが含まれているかどうかは、inを使って確認することができます。

In [10]: 'A' in d
Out[10]: True

In [11]: 'Z' in d
Out[11]: False

 値を変更するには、以下のようにします。

In [12]: d['A'] = 'Air'

In [13]: d
Out[13]: {'A': 'Air', 'B': 'Boy', 'C': 'Color'}

 存在しないキーを使用すると、要素が追加されます。

In [14]: d['D'] = 'Dictionary'

In [15]: d
Out[15]: {'A': 'Air', 'B': 'Boy', 'C': 'Color', 'D': 'Dictionary'}

 要素を削除するには以下のようにします。

In [16]: del d['C']

In [17]: d
Out[17]: {'A': 'Air', 'B': 'Boy', 'D': 'Dictionary'}

 すべての値を削除するには、以下のようにします。

In [18]: d.clear()

In [19]: d
Out[19]: {}

 ここで、「{}」が出てまいりましたが。空のディクショナリを作成するときにも「{}」が使えます。なので、すべての値を削除するもう一つのやり方として、空辞書「{}」をディクショナリ名に代入することで実現できます。

 ほかのディクショナリの作成方法としては、dict()が使えます。

In [20]: dict()
Out[20]: {}

In [21]: dict(['ab','cd','ef')
Out[21]: {'a': 'b', 'c': 'd', 'e': 'f'}

In [22]: dict((['a','b'],['c','d'],['e','f']))
Out[22]: {'a': 'b', 'c': 'd', 'e': 'f'}

 この他にも、タプルやリストの組み合わせが利用可能です。

 値を取り出す他のやり方としては、get()があります。

In [23]: d.get('A')

In [24]: print(d.get('A'))
None

In [25]: d.get('A','a')
Out[25]: 'a'

 最初の例は、キー「’A’」の値を取り出していますが、存在しないため、Noneを戻しています。インタープリターでは何も表示されないため、print()を使って確認しています。その次の例は、値がなかった時のオプションとして「’a’」を指定しています。

まとめ

 ディクショナリは、とても便利ですね。連続で取り出すときの順番は保証されませんので、そこだけ注意が必要でしょうか。(順番を保証したい場合はOrderedDict()が利用できます。)

Pythonバイナリデータの扱いかた

 今日も見に来てくださって、ありがとうございます。石川さんです。

 ここのところ、Tkinterのユニットテストの実現に向けていろいろと調査を進めているのですが、若干難航しております。。。

 ということで、本ブログの更新が滞っていたのですが、更新がないよりも、何か自分にとって当たり前のことであっても誰かの役に立つかもしれないということで、Pythonの標準モジュールの範囲内でのバイナリデータについて書いてみたいと思います。

バイト

 Pythonでは、8ビットの整数を扱うために、bytesとbytearrayを用意してくれています。扱えるのは、8ビットで表現できる符号なし数値の範囲で、0~255です。

In [1]: bytes(5)
Out[1]: b'\x00\x00\x00\x00\x00'

In [2]: bytearray(5)
Out[2]: bytearray(b'\x00\x00\x00\x00\x00')

 上記のように、数値を指定すると0x00で初期化された指定されたサイズ(今回は5バイト)のバイトとバイト列を生成してくれます。以下のようにすると、リストの値から生成可能です。

In [3]: l = [1,2,3,4,5]

In [4]: b = bytes(l)

In [5]: b
Out[5]: b'\x01\x02\x03\x04\x05'

In [6]: ba = bytearray(l)

In [7]: ba
Out[7]: bytearray(b'\x01\x02\x03\x04\x05')

 ちなみに、bytesはイミュータブルでbytearrayはミュータブルです。

In [8]: b[2] = 0
Traceback (most recent call last):

  File "<ipython-input-107-088d74b352b8>", line 1, in <module>
    b[2] = 0

TypeError: 'bytes' object does not support item assignment

In [9]: b
Out[9]: b'\x01\x02\x03\x04\x05'

In [10]: ba[2] = 0

In [11]: ba
Out[11]: bytearray(b'\x01\x02\x00\x04\x05')

 ごらんの通り、bytesは変更できません。

エンディアン

 いきなり「エンディアン」というタイトルを書いてしまいましたが、ちょうどぼくが新入社員のときにはじめてであった概念で、あまりにも衝撃的だったので、よく覚えています。ま、実際には衝撃を受けるひとはそんなにいないと思いますけど。(笑)

 当時C言語を使っていました。整数値を格納するshort型は2バイト、long型は4バイト、int型は、処理系によって、2バイトだったり4バイトだったりする、というところまではなるほどー、という感じだったのですけど、実際に数値が格納された結果、上下のバイトが入れ替わる、という話です。まったくの謎でした。

 例えば、先ほどの2バイト分について考えてみます。1バイト目に0x01、2バイト目に0x02が入力されているので、2バイトが一つの数値だと考えると、0x0102ということです。2進数に置き換えて計算するとこの数値は、(0000000100000010)2となり、
2**8+2**1 = 256+2 = 258になると思いますよね。それが、上下のバイトが入れ替わると、
(0000001000000001)2となり、2**9+2**0 = 512+1 = 513ということです。ね、意味がわかりませんよね。

 では、先ほど生成したbytesをstructモジュールを使って読み込んでみます。まずは、サンプルをご覧ください。structはC言語の構造体に似たデータを処理するのに便利に使えます。

In [12]: import struct 

In [13]: b[:2]
Out[13]: b'\x01\x02'

In [14]: struct.unpack('>H',b[:2])
Out[14]: (258,)

In [15]: struct.unpack('<H',b[:2])
Out[15]: (513,)

 ここで、'<H’、’>H’を指定していますが、最初の記号はエンディアン指定子ということで、「>」こちらがビッグエンディアン、「<」こちらがリトルエンディアンです。結果をご覧の通り、ビッグエンディアンは、直感的に正しい気がする方で、リトルエンディアンが上下のバイトが入れ替わるヤツですね。ちなみに二番目の記号「H」は2バイトの符号なし単整数を扱う書式指定子ということになります。

 実際にこのエンディアンという言葉、ぼくにはあんまりなじみがなくて、いつもビッグとリトル、どっちだったっけ?と、なってしまいます。重要なのは、時と場合によって入れ替わる可能性がある、ということを知っておくことと、バイトを扱うときにはどちらが採用されているのか、ということに気を付けなければいけない、ということですね。

structの書式文字列

 structの書式文字列はエンディアン指定子と書式指定子の二種類あり、使うときはエンディアン指定子の方を書式指定子より先に記述します。

指定子バイト順
<リトルインディアン
>ビッグエンディアン
エンディアン指定子
指定子意味バイト数
x1バイト読み飛ばし1
cchar 長さ1のバイト列1
bsigned char 符号付整数1
Bunsigned char 符号なし整数1
hshort 符号付整数2
Hunsigned short 符号なし整数2
iint 符号付整数4
Iunsigned int 符号なし整数4
llong 符号付整数4
Lunsigned long 符号なし整数4
qlong long 符号付整数8
Qunsigned long long 符号なし整数8
ffloat 単精度浮動小数点数4
ddouble 倍精度浮動小数点数8
書式指定子

 バイトをPythonデータに変換するときは、unpackを利用しますが、その逆のときは、packを利用します。

In [16]: struct.pack('>2L',500,12)
Out[16]: b'\x00\x00\x01\xf4\x00\x00\x00\x0c'

In [17]: struct.unpack('>2L',b'\x00\x00\x01\xf4\x00\x00\x00\x0c')
Out[17]: (500, 12)

 この書式指定子は、ビッグエンディアンで、2個の4バイトの符号なし整数を扱う、ということを意味しています。ちなみに、「x」の書式指定子は、指定されたバイト数分を読み飛ばす、ということを意味しています。

In [18]: struct.unpack('>2xH2xH',b'\x00\x00\x01\xf4\x00\x00\x00\x0c')
Out[18]: (500, 12)

 2バイト飛ばして2バイトの符号なし整数、2バイト飛ばして2バイトの符号なし整数、ということを意味しています。

まとめ

 バイトを扱うときは、エンディアンに気を付けて。簡単なバイトなら、structを使って、バイトとPythonデータを変換できます。

ORACLE SQL分析関数で受注を分割

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、ちょっとお仕事でどうしようかなぁ、というのをSQLで解決することにしましたので、その実験のまとめを記録しておきます。

解決したいこと

 外部から自社商品の注文明細データが送付されてくるのですが、一つの注文取引の中に、同じ商品の明細が含まれて送られてきます。こちら側のシステムでは、同じ商品を一つの受注単位で複数件登録できない仕様なので、その場合は二つ以上の受注単位に分割して登録する必要があります。

サンプルテーブル

サンプルテーブル

サンプルデータ

サンプルデータ

 解決したいことの具体例を示します。取引番号1のデータは、商品コードの重複はないので、そのまま登録可能です。取引番号2のデータについては、商品コード2と3が重複していますので、取引2-1と2-2に分割して登録する必要があります。取引番号3のデータは商品コード1が3重になっていますので取引を3-1、3-2、3-3と分割して登録する必要があります。

 この問題を解決するのにいろいろな方法がありますが、今回はORACLE分析関数のROW_NUMBER()を使いたいと思います。分析関数は、各行を取り出しつつ、全体に対する分析結果を同時に取得するための関数です。

解決方法

 以下のSQLを実行して、取引を分割するためのキー情報を作成しつつ、データを取り出します。このキー情報がこちら側の受注単位になります。

SELECT 取引番号||'-'||ROW_NUMBER() OVER (PARTITION BY 取引番号, 取引日, 商品コード ORDER BY 取引番号, 取引日, 商品コード) 取引
      ,取引番号
      ,取引日
      ,商品コード
      ,数量
  FROM 取引
 WHERE 取引日 = '20-05-27'
 ORDER BY 1;

実行結果

実行結果

 これで、取引を分割することができました。

スクリプト

今回使用したスクリプトです。

CREATE TABLE 取引 ( 取引番号 NUMBER, 取引日 DATE, 商品コード NUMBER, 数量 NUMBER);

INSERT INTO 取引 (
 SELECT 1, '20-05-27', 1, 1 FROM DUAL UNION ALL
 SELECT 1, '20-05-27', 2, 1 FROM DUAL UNION ALL
 SELECT 1, '20-05-27', 3, 1 FROM DUAL UNION ALL
 SELECT 1, '20-05-27', 4, 2 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 2, 1 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 2, 3 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 3, 4 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 4, 2 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 3, 3 FROM DUAL UNION ALL
 SELECT 3, '20-05-27', 1, 2 FROM DUAL UNION ALL
 SELECT 3, '20-05-27', 1, 1 FROM DUAL UNION ALL
 SELECT 3, '20-05-27', 1, 3 FROM DUAL
);

COMMIT;

SELECT 取引番号||'-'||ROW_NUMBER() OVER (PARTITION BY 取引番号, 取引日, 商品コード ORDER BY 取引番号, 取引日, 商品コード) BREAK
      ,取引番号
      ,取引日
      ,商品コード
      ,数量
  FROM 取引
 WHERE 取引日 = '20-05-27'
 ORDER BY 1
;

まとめ

 分析関数、いろんな用途に使えて便利ですので、ぜひ、使い方を覚えましょう。