# open 関数ってなに?

Python は open 関数を使いファイルの書き込みや読み込みを行うことができます。 このとき open 関数は

与えられた文字列から

こんにちは

ビット列に変換して

111000111000000110010011111000111000001010010011111000111000000110101011111000111000000110100001111000111000000110101111

からファイルに保存しています。

わざわざ文字で表現されているものを、 数字で表現されてもわからないしという感じです。 なんのために存在しているのでしょうか?

どうやってファイルに保存するのか?ということを考えます。 コンピュータの内部的には という文字を のまま保存はできないのです。 コンピュータは 0, 1 のビット列しか保存できないのです。

このときの文字列からビット列への変換方式は色々種類があるらしいです。 ただ自分がよく目にするのは、以下の4種類くらいです。

  1. ASCII
  2. UTF-8
  3. Shift_JIS
  4. EUC

例えば UTF-8 では という文字に e3 81 93 という数字を割り当てています。 数字であればビット列に変換可能です。 11100011 10000001 10010011 というビット列に変換して、コンピュータに保存できるという訳です。

言葉で説明されても、ピンとこないので以下のサイトで遊ぶと感覚が掴めるかもしれません。 "Enable Spacing Between Bytes" のチェックを外すと雰囲気が出ます。

# 1. open 関数

この節は Python の標準ドキュメントの open 関数の劣化版です。 もし「何言ってるんだこいつ?」とか「記述が曖昧なだな」と感じたら、

お手数ではありますが、以下の公式ドキュメントを当たって見てください。

# Step 1. open 関数でエンコーディング

ファイルを読み書きする時に使われています。 なにも指定していない場合は、で指定された値に自動的にエンコードしてくれます。

#
# 対話モード >>> に
# コピペで実行できます。
#
old_poetry = """\
さよなら
さんかく
またきて
しかく
"""

# 1) ファイルに書き込み
with open('sample4649.txt', 'w') as writer:
    writer.write(old_poetry)

# 2) ファイルに読み込み
with open('sample4649.txt', 'r') as reader:
    new_poetry = reader.read()

print(new_poetry, end='')
>>> print(new_poetry, end='')
さよなら
さんかく
またきて
しかく
>>>

一見、エンコーディングを気にしていないようです。 しかし、内部的にはエンコーディングされています。 何にエンコーディングされているかは、以下のコードで確認できます。

import locale
locale.getpreferredencoding(False)
>>> locale.getpreferredencoding(False)
'UTF-8'
>>>

テキストモードでは、 encoding が指定されていない場合に 使われるエンコーディングはプラットフォームに依存します: locale.getpreferredencoding(False) を使って現在のロケールエンコーディングを取得します。
open - Python 標準ライブラリ

# Step 2. open 関数でエンコーディング(引数を明示)

modet を追加しました。 encodingutf-8 を追加しました。 これは Step 1 のコードと同じです。

#
# 対話モード >>> に
# コピペで実行できます。
#
old_poetry = """\
さよなら
さんかく
またきて
しかく
"""

# 1) ファイルに書き込み
with open('sample4649.txt', 'wt', encoding='utf-8') as writer:
    writer.write(old_poetry)

# 2) ファイルに読み込み
with open('sample4649.txt', 'rt', encoding='utf-8') as reader:
    new_poetry = reader.read()

print(new_poetry, end='')

modet を追加しました。 これはテキストモードを表しています。 テキストモードだと encoding で指定されたエンコードで  変換しながら  、 書き込み write、読み込み read をしてくれるようになります。

# Step 3. str.encode メソッドでエンコーディング

open 関数でテキストモードだと変換しながら書き込んでくれます。 この変換しながら、というのがポイントです。

逆に変換しないで直接書き込むことができます。 それがバイナリモードです。 バイナリモードを使うには引数 mode'b' を追記します。

#
# 対話モード >>> に
# コピペで実行できます。
#
old_poetry = """\
さよなら
さんかく
またきて
しかく
"""

encoded_old_poetry = old_poetry.encode('utf-8')
print(encoded_old_poetry)

with open('sample4649.txt', mode='wb') as writer:
    writer.write(encoded_old_poetry)

with open('sample4649.txt', mode='rb') as reader:
    encoded_new_poetry = reader.read()
    print(encoded_new_poetry)

new_poetry = encoded_new_poetry.decode('utf-8')
print(new_poetry, end='')

またここでのポイントは、3つあります。

1つ目は、エンコードさせない方式なので encoding には何も指定しません。 指定するとエラーで弾き返されます。

(rawバイト列の読み書きには、バイナリモードを使い、encoding は未指定のままとします) 指定可能なモードは次の表の通りです。
oepn - Python 標準ライブラリ

2つ目は、エンコードされたものを表示すると意味不明の長文が出力されること。

>>> print(encoded_old_poetry)
b'\xe3\x81\x95\xe3\x82\x88\xe3\x81\xaa\xe3\x82\x89\n\xe3\x81\x95\xe3\x82\x93\xe3\x81\x8b\xe3\x81\x8f\n\xe3\x81\xbe\xe3\x81\x9f\xe3\x81\x8d\xe3\x81\xa6\n\xe3\x81\x97\xe3\x81\x8b\xe3\x81\x8f\n'
>>>

3つ目は、こんな意味不明な長文でも write して、そして read すると、 ちゃんとまた元の文字列が表示されることです。

>>> print(new_poetry, end='')
さよなら
さんかく
またきて
しかく
>>>

この意味不明な長文が何かについて、次節の bytes 型で見ていきたいと思います。

# 2. bytes 型

int 型の集まり

変数 m には bytes 型が代入されています。 そして bytes 型は int の集まりであることがわかります。

m = 'こんにちは'.encode('utf-8')
m[0]
m[1]
m[2]
type(m)
>>> m = 'こんにちは'.encode('utf-8')
>>> m[0]
227
>>> m[1]
129
>>> m[2]
147
>>> type(m)
<class 'bytes'>
>>> 

bytes 型は、直接目にすることあまりないかも知れません。 もし目にするとしたら encode メソッドを使ったときではないでしょうか。

m = 'こんにちは'.encode('utf-8')
type(m)
>>> type(m)
<class 'bytes'>
>>>

で、これはなにをしているの?という話です。 UTF-8 にエンコードしてな encode('utf-8') としたのに、 なぜか出力されたのは意味のわからない文字の羅列です。

m = 'こんにちは'.encode('utf-8')
m
>>> m
b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf'
>>>

実際には int の集まりです。

m = 'こんにちは'.encode('utf-8')
m[0]
m[1]
m[2]
>>> m = 'こんにちは'.encode('utf-8')
>>> m[0]
227
>>> m[1]
129
>>> m[2]
147
>>>

ちょっと書き換えてみます。16 進数にしてつなぎ合わせてみます。 意味のわからない長い文字列は、どうやら int をつなぎ合わせただけのもののようです。

m = 'こんにちは'.encode('utf-8')
m
''.join(hex(c) for c in m).replace('0x', '|x')
>>> m
b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf'
>>> ''.join(hex(c) for c in m)
'|xe3|x81|x93|xe3|x82|x93|xe3|x81|xab|xe3|x81|xa1|xe3|x81|xaf'
  vvvvvvvvvvv ^^^^^^^^^^^ vvvvvvvvvvv ^^^^^^^^^^^ vvvvvvvvvvv
       こ         ん          に           ち         わ
>>>

bytes 型の要素3つでひらがな1文字が表現されています。 表がこちらにあります。

さっきからバイナリ、バイナリと連呼していますが、こいつは一体何者でしょうか。 ちなみに「バイナリ」という言葉については、以下の記事で雰囲気がつかめるような気がしました笑

あとは公式ドキュメントによると bytes 型は、バイナリデータを使う時にも使ったりするらしいです。

バイナリデータを操作するためのコア組み込み型は bytes ... です。
組み込み型 - Python 標準ライブラリ

# 3. bytearray 型

bytes 型は immutable なので変更できません。

m1 = 'こんにちは'.encode('utf-8')
m2 = 'さようなら'.encode('utf-8')
m1[0] = m2[0]
>>> m1[0] = m2[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
>>>

bytes はバイトの不変なシーケンスです。
組み込み型 - Python 標準ライブラリ

そこで bytearray を使います。

#
# 対話モード >>> に
# コピペで実行できます。
#
m1 = bytearray('こんにちは'.encode('utf-8'))
m2 = 'さようなら'.encode('utf-8')
m1.decode()
for i in range(len(m1)):
    m1[i] = m2[i]

m1.decode()
>>> m1.decode()
'こんにちは'
>>> for i in range(len(m1)):
...     m1[i] = m2[i]
...
>>> m1.decode()
'さようなら'
>>>

bytearray オブジェクトは bytes オブジェクトの可変なバージョンです。
bytearray オブジェクト - Python 標準ライブラリ

# ◯ 制限

なので正確には int の集まりといえば集まりだけど、0 から 255 までの間しか保存することができません。

m1 = bytearray('こんにちは'.encode('utf-8'))
m1[0] =  -1  # <--- ValueError
m1[0] =   0
m1[0] = 255
m1[0] = 256  # <--- ValueError
>>> m1[0] =  -1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: byte must be in range(0, 256)
>>> m1[0] =   0
>>> m1[0] = 255
>>> m1[0] = 256
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: byte must be in range(0, 256)
>>>

# 4. memoryview 型

長大な bytes 型あるいは bytearray 型のコピーを高速に作れるようになります(正確にはコピーではなく参照らしいです)。

from timeit import timeit
N = 5

b = b's' * 10**N
timeit('b[1:]', globals=globals())

mb = memoryview(b)
timeit('mb[1:]', globals=globals())

a = bytearray(b)
timeit('a[1:]', globals=globals())

am  = memoryview(a)
timeit('am[1:]', globals=globals())
>>> b = b's' * 10**N
>>> timeit('b[1:]', globals=globals())
4.346901617999947
>>> 
>>> mb = memoryview(b)
>>> timeit('mb[1:]', globals=globals())
0.14340937999986636
>>> 
>>> a = bytearray(b)
>>> timeit('a[1:]', globals=globals())
4.345922583000174
>>> 
>>> am  = memoryview(a)
>>> timeit('am[1:]', globals=globals())
0.15383407199988142
>>> 

One reason memoryviews are useful is because they can be sliced without copying the underlying data, unlike bytes/str.
What exactly is the point of memoryview in Python - Stackoverflow

memoryview objects are great when you need subsets of binary data that only need to support indexing. Instead of having to take slices (and create new, potentially large) objects to pass to another API you can just take a memoryview object.
What exactly is the point of memoryview in Python - Stackoverflow

# ◯ Step By Step で

組み込み型に memoryview 関数というのがあります。

memoryview(obj)
与えられたオブジェクトから作られた "メモリビュー" オブジェクトを返します。 詳しくは メモリビュー を参照してください。

メモリービューオブジェクトってなんだ?「コピーすることなく」とあり、おそらく参照を返しているものだと思われます。

メモリービュー
memoryview オブジェクトは、Python コードが バッファプロトコル をサポートするオブジェクトの内部データへ、 コピーすることなくアクセスすることを可能にします。

バッファプロトコルってなんだ?Python のプロトコルは Java のインターフェイスに相当します。 すいません、いい説明が思いつきませんでした...

バッファプロトコル (buffer Protocol)
Pythonで利用可能ないくつかのオブジェクトは、下層にあるメモリ配列または buffer へのアクセスを提供します。このようなオブジェクトとして、組み込みの bytes や bytearray 、 array.array のようないくつかの拡張型が挙げられます。

リスト list や文字列とかどうなんだろう?と思ったら、やっぱり TypeError で弾き返されました。 リストや文字列は、バッファプロトコルを実装していないということです。

l = [0] * 10**5
memoryview(l)  # TypeError

s = 'a' * 10**5
memoryview(s)  # TypeError
>>> l = [0] * 10**5
>>> memoryview(l)  # TypeError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: memoryview: a bytes-like object is required, not 'list'
>>> 
>>> s = 'a' * 10**5
>>> memoryview(s)  # TypeError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: memoryview: a bytes-like object is required, not 'str'
>>> 

# ◯ そのほかの利用例

Cython で GIL を外すみたいなことをできたりするらしいです。 一切理解できない...orz

# ◯ まとめ

bytes型, bytearray型, memoryview型について見てきました。 公式ドキュメントによると、これら3つは、まとめて「バイナリシーケンス型」と呼ばれているようです。

バイナリシーケンス型 --- bytes, bytearray, memoryview
バイナリデータを操作するためのコア組み込み型は bytes および bytearray です。 これらは、別のバイナリオブジェクトのメモリにコピーを作成すること無くアクセスするための バッファプロトコル を利用する memoryview でサポートされています。

# 5. str 型

ここまでバイナリシーケンス3兄弟を見てきました。ここで改めてテキストシーケンス str を見ていきたいと思います。 単純に文字列なのですが...

テキストシーケンス型 --- str
Python のテキストデータは str オブジェクト、すなわち 文字列 として扱われます。  文字列は Unicode コードポイント  のイミュータブルな シーケンス です。 文字列リテラルには様々な記述方法があります:

文字列は「コードポイント」です。たんなる数字だということです。

[ord(c) for c in 'こんにちは']
>>> [ord(c) for c in 'こんにちは']
[12371, 12435, 12395, 12385, 12399]
>>>

ord(c)

1 文字の Unicode 文字を表す文字列に対し、 その文字の Unicode コードポイントを表す整数を返します。 例えば、 ord('a') は整数 97 を返し、 ord('€') (ユーロ記号) は 8364 を返します。 これは chr() の逆です。

# ◯ あれ、UTF-8 いらなくない?

ファイルに保存する際、UTF-8 などの方式に従ってエンコーディング、0, 1 のビット列にしてから保存しています。 しかし、Python の str も内部では数字を使って Unicode を使って文字を取り扱っています。

じゃあ、UTF-8 はいらないんじゃないの?という話です。 そのまま Unicode を保存すればおしまいだと思いました。

Unicode を bit 列に変換してしまうと、それはそれであたりのありそうな雰囲気があります。 まだ理解できていないのですが Wikipedia の記事が参考になります。

UTF-8
バイトストリーム中の任意の位置から、その文字、前の文字、あるいは次の文字の先頭バイトを容易に判定することができる。

Shift_JIS
また特定のバイトを取り出しても、それが厳密に1バイト目か2バイト目かを判断できないため、日本語を扱うソフトウェアを作る際の厄介な問題となっている。

# 6. 文字コード

文字コードを覚える必要はないと思うのですが、だいたい UTF-8, ASCII, EUC, Shift_JIS の4種類は見かけます。 この4種類あるんだなくらいの感覚で知っていてもいいかな...と。

問題 基本情報技術者平成18年春期 午前問69

コンピュータで使われている文字符号の説明のうち,適切なものはどれか。

ア ASCII符号はアルファベット,数字,特殊文字及び制御文字からなり,漢字に関する規定はない。

イ EUCは文字符号の世界標準を作成しようとして考案された16ビット以上の符号体系であり,漢字に関する規定はない。

ウ Unicodeは文字の1バイト目で漢字かどうかが分かるようにする目的で制定され,漢字をASCII符号と混在可能とした符号体系である。

エ シフトJIS符号はUNIXにおける多言語対応の一環として制定され,ISOとして標準化されている。

# 7. 参考文献

# 8. おわりに

ここまで以下のように見てきました。

ここでは、バイナリシーケンスと総称される bytes型, bytearray型, memoryview型について見てきました。 そして最後に慣れ親しんでいるテキストシーケンス str 型について復習しました。 以上になります、ありがとうございました。

Last Updated: 11/11/2019, 11:17:29 PM
x 消す
銀河英雄伝説が無料!