Last Updated: 2/6/2024, 5:44:57 AM

# open 関数ってなに?

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

# 1. なんで open したり close するの?

なんで、わざわざ open したり close しないといけないのでしょうか? どうやら  ファイルディスクリプタ  を確保するためのようです。

ファイル(への通り道)に割り振られる番号で、ファイルを識別するための目印

Linuxプログラミングの概念 (opens new window)

  • ファイルディスクリプタ
    • オープンしたファイルを使用する際にはディスクリプタを使用する。
    • ディスクリプタは一意な識別子で、オープンしたファイルのメタデータとファイルを対応させるものです。
    • Linuxカーネル内では、ディスクリプタは整数で表現され、ファイルディスクリプタまたは省略してfdと呼ばれる。

ファイルディスクリプタを open して確保して close して解放しているようです。 そして、この  ファイルディスクリプタ  という管理用の番号は有限個らしく、 Python で close し忘れて枯渇すると、大変なことになるらしいです。

# 2. ファイルってなに?

バイトがリニアに並んだデータだそうです。数字の 0, 1 の並びということでしょうか。

Linuxプログラミングの概念 (opens new window)

  • 一般的に「ファイル」と呼ぶものは、Linuxでは"通常ファイル"と名付けている。
  • 「通常ファイル」はその内容として  バイトがリニアに並んだデータ(バイトストリーム、バイト列)  を持っている。Linuxのファイルにはこの構造しかありません
    • 他のOSではレコード等の高度に構造化されたファイルを備えたものがあるが、Linuxにはありません。

引用したものの正直よくわかっていません... orz ファイルは難しいらしいです。

以下のツイートでは、ファイルの話をしているのに、なぜ "TCP" というネットワークの用語が出てきたのでしょうか?

Linux は、すべてのものをファイルとして扱っているからです。 Python でプログラムをしているときは、ファイル経由でデータを取得していると考えて良いのでしょうか。

Linuxプログラミングの概念 (opens new window)

  • ファイルはLinuxの中でも、もっとも基本的かつ重要な概念。
  • Linuxには、  すべてものはファイルである(everything-is-a-file)  という思想がある。

ファイルディスクリプタについて(1) ~ ファイルディスクリプタの概要 (opens new window)
ファイルディスクリプタは、プログラムの外部との入出力を行う抽象的なインタフェースです。 Unix/Linuxのファイルディスクリプタは、一般的なファイルだけでなく  デバイス   ソケット   パイプ  も対象としています。

上記でいう「デバイス」とは、例えば Python で言えば input, print 関数と関係があります。

「あれ? でもキーボードから打ち込んで…というのもファイルなのか?」と違和感を感じられるかも知れません。 が、これはある意味Linux ( UNIX系OS ) の流儀です。 通常のファイルも含め、バイト列としてデータを読み込んだり書き込んだり、どちらかができるものをファイルとして扱うという決めごとなのです。
標準入力・標準出力ってなに? - Qiita (opens new window)

上記でいう「ソケット」とは、ネットワークと関係があります。

ソケットはネットワーク通信に用いる際のファイルディスクリプタです。
Webサーバにおけるソケット周りの知識 (opens new window)

上記でいう「パイプ」とは、コマンドと関係があります。

$ history | grep hoge

# 3. エンコーディング

open 関数は、与えられた文字列から

こんにちは

ビット列に変換して

111000111000000110010011111000111000001010010011111000111000000110101011111000111000000110100001111000111000000110101111

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

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

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

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

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

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

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

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

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

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

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

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

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

# 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 標準ライブラリ (opens new window)

# 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 標準ライブラリ (opens new window)

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 型で見ていきたいと思います。

# 4. 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 標準ライブラリ (opens new window)

# 5. 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 標準ライブラリ (opens new window)

そこで 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 標準ライブラリ (opens new window)

# ◯ 制限

なので正確には 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)
>>>

# 6. 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 (opens new window)

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 (opens new window)

# ◯ Step By Step で

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

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

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

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

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

バッファプロトコル (buffer Protocol) (opens new window)
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 (opens new window)
バイナリデータを操作するためのコア組み込み型は bytes および bytearray です。 これらは、別のバイナリオブジェクトのメモリにコピーを作成すること無くアクセスするための バッファプロトコル を利用する memoryview でサポートされています。

# 7. str 型

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

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

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

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

ord(c) (opens new window)

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

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

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

じゃあ、UTF-8 はいらないんじゃないの?という話です。 そのまま Unicode を保存すればおしまいだと思いました。 Unicode を bit 列に変換してしまうと、それはそれであたりのありそうな雰囲気があります。

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

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

まだ理解できていないのですが Wikipedia の記事が参考になります。

# 8. 参考文献

# 9. おわりに

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

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