このBlogは移転しました。今後は aish.dev を御覧ください。

真空調理法(sous vide)の基礎調査

sous vide

  • 真空調理法または低温調理法と呼ばれる。
  • フランス語で「真空で」という意味。「スービド」って発音するのかな。
  • 肉などの食材をビニール袋に入れて空気を抜き、お湯につける調理方法。
  • 一定の温度を保ったお湯を使って、長時間加熱して調理する。
  • 空気を抜くのは、お湯と食材を直接接触させて、熱を伝えやすくするため。空気は熱伝導率が悪い。
  • 煮たり焼いたりする調理法と比べて、食材の食感や香りを損なわず、焦がしたり、茹ですぎたりすることがない。
  • ビニール袋に密閉するので、肉汁や香りを逃さない。
  • 調理中はほったらかしで良い。
  • 温度設定により、好みの焼き加減に調整できる。
  • 調理時間が長すぎても、それほど料理の完成度に影響しない。
  • おなじ温度設定で調理すれば、同じような出来栄えとなり、失敗が少ない。家庭でも、一流レストランと同じ仕上がりを期待できる。
  • 100g50円の鳥むね肉がけっこうなごちそうに化ける。

調理器具

  • 私は Anova Precision Cooker を使っている。
  • アメリカ製
  • 日本語版は販売されておらず、直輸入品しかないい。
  • 水をはった鍋に突っ込んでコンセントをつなげば、0.5℃単位で一定の温度を維持できる。
  • Bluetooth/WifiiPhoneAndroidスマホと接続し、温度や調理時間を設定できる。
  • 旧型はBluetoothのみ。下のリンクはBluetooth版。
  • 深さが15cm程度ある鍋が必要。
  • ビニール袋はジップロックを使用している。
  • ジップロックに食材を入れて、開口部以外を水に沈めて封をすれば、簡単に空気を抜いた状態にできる。

温度設定と加熱時間

  • 肉のタンパク質は50℃ぐらいから変質が始まる。
  • 65℃を越すと、タンパク質が水分を放出し始め、パサつきはじめる。
  • したがって、50℃以上、65℃未満で加熱する。
  • タンパク質のうち、コラーゲンは約65℃で収縮を始め、硬くなる。
  • コラーゲンは75~85℃で軟化をはじめる。
  • しかし、コラーゲンは低温度でも長時間加熱することでゼラチン化し、柔らかくなる。
  • コラーゲンの多い(硬い)牛肉などでも、長時間(12時間〜? 24時間〜?)加熱を続けることで、コラーゲンをゼラチン化できる。
  • 鶏肉などの柔らかい肉は、中心部まで十分に加熱されれば、それ以上加熱する必要はない。
  • フライパンで焼く場合などと違って、細かく加熱時間を気にしなくとも良い。長すぎると徐々に水分が失われていくが、それほどシビアなものではない。

低温殺菌

重要な注) 私は、食品衛生に関する専門的な知識は、一切持ち合わせていない。以下は、私が各種ウェブサイトなどを参照して得た断片的な知識を書き出したものである。単なるポエムであり、信頼できる調査結果ではない。以下の記述によってあなたがどんな被害を受けようと、当方は一切感知しない。

  • 厚生労働省の基準では、加熱食肉製品は「中心部の温度を 63°で 30 分間加熱する方法又はこれと同等以上」で殺菌しなければならない、とある。肉の厚みに応じて、63℃で 数時間加熱すれば低温殺菌として十分だろう。
  • 63℃より低温でも、大腸菌サルモネラ菌などは長時間加熱することで殺菌できる。(http://www.douglasbaldwin.com/sous-vide.html#Safety)
  • ボツリヌス菌は、低温では殺菌できない。
  • ボツリヌス菌のヤバイのは、ボツリヌス菌の毒素。ボツリヌス菌自身は、乳児以外は食べても問題はない。
  • ボツリヌス菌の毒素は、83℃5分の加熱で分解できる。
  • ボツリヌス菌の毒素は、低酸素状態でのみ発生する。通常の流通で入手した肉で、腐敗していなければ、それほど心配はいらない?
  • 真空パックされた冷凍生肉などは、加熱しないとリスクが大きい?
  • 牛肉のブロックなどでは、汚染されたとしても肉の表面のみで、肉の内部にまでは入り込む可能性は低い。したがって、低温調理の前または後にフライパンやオーブンで表面を焼く方法もある。

参考にしたURL

Python 3.6の概要 (最終回 - オプティマイズ)

Python 3.6では、長年に渡って使われてきた、Python の基本的な実装に関わる部分で、重要な変更が行われている。

辞書オブジェクトのレイアウト変更

Raymond Hettingerのアイデア を元に、稲田さん が実装したもので、古くから使われてきたPythonの辞書オブジェクトが修正され、よりメモリ効率の良い形式でデータを格納するように変更された。

この修正により、辞書オブジェクトのメモリ使用量が20〜25%程度改善されている。

バイトコードからワードコードへ

Pythonインタープリタは、ソースコードバイトコード と呼ばれる中間言語に変換し、実行する仕組みになっている。従来、バイトコード はその名の通り8bit長のデータの集まりだったが、Python3.6では、バイトコードのサイズは8bitから16bitに拡張された。

これまでのバイトコードでは 命令コード+可変長の引数 という形式の中間言語を解釈しながら実行していたが、新しい形式では、ほぼ固定長の中間言語を読み込んで実行できるようになり、Pythonインタープリタがシンプルに、効率よく動作するようになった。(https://bugs.python.org/issue26647)

PyMem_* APIが pymalloc を使用

Python C APIには、メモリの取得・開放を行うAPIPyMem*系、PyObect*系、PyMem_Raw*系の3種類がある。PyMem*系のAPIは古くからあるAPIで、C言語ランタイムライブラリの malloc()free() などをそのまま使用している。

一方、PyObject系のメモリ操作APIは、一般に pymalloc と呼ばれるPython専用のメモリアロケータを使用しており、Pythonでのメモリ利用形態に合わせて、小さくて数の多いメモリ割り当てを効率良く行えるようにしている。

pymalloc が導入された頃は、すべてのメモリ割り当てをpymallocにしてしまうとサードパーティ製の拡張モジュールなどで動作しなくなるケースが多く、完全に pymalloc に切り替えることはできなかった。そこで、PyMem*系のAPIは変更せずに malloc()を使用する実装のまま残され、新しいコードでは PyObjectAPIを使用するようになった。

しかし、pymalloc 導入からもう20年近く経過し、この間にpymallocが進化したこともあって,そろそろPyMem*APIでも pymallocを使用してもよいのではないか、ということになった。そこで3.6から PyObjectAPIと同じくpymallocを使用するように変更された。この変更により、実質的に、PyMemAPIは必要なくなっている。

既存の拡張モジュールなどでは、正しくPyMem*APIを使用していれば、この修正による不具合は発生しないはずだ。現在、C言語などによる拡張モジュールを開発している場合、正しくメモリ管理APIを利用できているか自信がなければ、デバッグビルドのPythonでテストしてみると、GILを確保しない状態でのAPI呼び出しなどの不正を検出できるので試してみると良いかもしれない。

https://bugs.python.org/issue26249

Python 3.6の概要 (その6 - ローカル時間の曖昧さを解消)

夏時間を採用している地域では、1日に2回、同じ時間となる場合がある。たとえば、アメリカの東部時間では、11月第1日曜日の午前2時に夏時間が終了すると、また同じ日の午前1時にもどる。つまり、この日は午前1時は2回存在することとなり、このタイムゾーン2016年11月1日1時0分0秒 という時間は、正確には何時なのか、特定することはできない。

そこで、PEP 495: Local Time Disambiguation では、Datetimeオブジェクトにあらたに fold 属性を追加し、最初の2016年11月1日01:00:00 なら fold0、2回めなら1となるように変更された。

>>> import os
>>> os.environ['TZ'] = 'US/Eastern'  #  タイムゾーンをUS/Easternに設定
>>> datetime.fromtimestamp(1478408400) # 最初の 2016/11/06 01:00:00
datetime.datetime(2016, 11, 6, 1, 0)
>>> datetime.fromtimestamp(1478408400+3600) # 2回目の 2016/11/06 01:00:00
datetime.datetime(2016, 11, 6, 1, 0, fold=1)

Python 3.6の概要 (その5 - ファイルシステムパス プロトコル)

ファイルシステムパス プロトコル

pathlib はPython3.4で導入されたが、pathlibで表現されるファイルパスは、 open()os.path.* などの、既存のファイル操作関連関数では使用できないため、あまり便利には使えていなかった。

Python3.6からは、pathlib.Path などのファイルパスをあらわすオブジェクトに特殊メソッド __fspath__() が実装された。ファイル名を引数として受け取る、open() などの関数は、__fspath__()を呼び出して、ファイルパスを取得するように変更された。

>>> import pathlib
>>> spam = pathlib.Path('./spam')
>>> spam.__fspath__()
'spam'
>>> open(spam, 'w')
<_io.TextIOWrapper name='spam' mode='w' encoding='UTF-8'>

また、オブジェクトからファイルパスを取得する関数として、os.fspath()が追加された。os.fspath(path)は、pathが文字列またはバイト列ならそのまま、それ以外ならpath.__fspath__()を呼び出して戻り値とする。

>>> import os, pathlib
>>> os.fspath('.')   # 文字列はそのまま
'.'
>>> os.fspath(pathlib.Path('.')) # __fspath__()メソッドがあれば呼び出す
'.'
>>> os.fspath(1)  # 以外ならエラー
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: expected str, bytes or os.PathLike object, not int

Python 3.6の概要 (その4 - クラス定義)

クラス定義のカスタマイズ

これまで、Pythonのクラス定義をカスタマイズする手段として、メタクラスが使われてきた。しかし、メタクラスを利用したカスタマイズは、Pythonのオブジェクトモデルや型システムの知識が必要で実装が難しく、また複数のメタクラスを同時に使用するのが難しい、などの問題点があった。そこで、PEP 487 -- Simpler customisation of class creation では、メタクラスを使わずにクラスをカスタマイズする手段を提供している

init_subclass() メソッド

クラスのサブクラスが作成されたときに呼び出され、引数として、派生クラスと、クラス定義の引数が渡される。__init_subclass__()メソッドは、自動的にクラスメソッドとなる。

class Spam:
    def __init_subclass__(cls, **kwargs):
         super().__init_subclass__()
         print('Spam:', cls, kwargs)

class Ham(Spam, bacon=1):
    def __init_subclass__(cls, **kwargs):
         super().__init_subclass__(**kwargs)
         print('Ham:', cls, kwargs)

class Egg(Ham, cheese=1):
    def __init_subclass__(cls, **kwargs):
         super().__init_subclass__(**kwargs)
         print('Egg:', cls, kwargs)

# prints
# Spam: <class '__main__.Ham'> {'bacon': 1}
# Spam: <class '__main__.Egg'> {'cheese': 1}
# Ham: <class '__main__.Egg'> {'cheese': 1}

ディスクリプタset_name()メソッド

従来のディスクリプタインターフェース__set_name__() メソッドが追加された。__set_name__()はクラスオブジェクトの作成時に呼び出され、引数としてクラスと属性名を受け取る。

class Descr:
    def __get__(self, instance, owner):
        return f'Descr.__get__({instance}, {owner})'

    def __set_name__(self, owner, name):
        print(f'Descr.__set_name__({owner}, {name})')

class Spam:
    descr = Descr()

のように、クラス Spamディスクリプタ Descrインスタンスを属性として作成すると、クラス Spam 作成時に Descr.__set_name__(Spam, 'descr') が呼び出される。

属性の作成順

メタクラスの一般的な使い方として、クラスの属性を、作成された順番にしたがって処理する、というケースがある。この場合、メタクラスでクラスの名前空間に使用する辞書として、collections.OrderedDict()を使用するようにカスタマイズするのが一般的だが、Python 3.6からは名前空間辞書はデフォルトで登録順序を保存するようになったため、このようなカスタマイズは不要となった。

次のように、クラスの名前空間辞書 __dict__ の要素を取得すると、常に属性が登録された順番で返される。

>>> class Spam:
...     a=1
...     b=2
...     c=3
...
>>> Spam.__dict__.keys()  # 常に同じ順番で取得できる  
dict_keys(['__module__', 'a', 'b', 'c', '__dict__', '__weakref__', '__doc__'])

Python 3.6の概要 (その3 - async関連)

非同期ジェネレータ

現在のPythonでは、ジェネレータを使って、とてもお手軽にイテレータを作成できる。例えば、奇数列を生成するジェネレータは、次のように書ける。

def odds():
    i = 1
    while True:
        yield i
        i += 2

しかし、ジェネレータが存在しなかった頃のPythonでは、わざわざ__iter__メソッドなどの特殊メソッドを実装したクラスを定義し、

class Odds:
    def __init__(self):
        self._cur = 1

    def __iter__(self):
        return self

    def next(self):
        ret = self._cur
        self._cur += 2
        return ret

などと書かなければならなかった。

Python3.5で導入された コルーチン は、イテレータと同様な概念として 非同期イテレータ をサポートしているが、ジェネレータに相当する機能が存在しないため、コルーチンを使用したイテレータを作成するのは非常に面倒な作業となっていた。

このため、Python3.6ではあらたに非同期イテレータを作成する、非同期ジェネレータが提供され、コルーチン内でも普通の関数と同様に yield 式を使って非同期ジェネレータを作成できるようになった PEP 525 -- Asynchronous Generators

import asyncio

# spam()は非同期ジェネレータ
async def spam(): 
    await asyncio.sleep(1)
    yield 'spam1'
    await asyncio.sleep(2)
    yield 'spam2'

async def spam_restrant():
    async for s in spam():  #非同期forループ
        print(s)

loop = asyncio.get_event_loop()
loop.run_until_complete(spam_restrant())

通常のジェネレータオブジェクトは、send(), throw(), close() メソッドで通信できるが、非同期ジェネレータでも同様にasend(), athrow(), aclose() メソッドを使用できる。

非同期内包

リスト内包などの内包式で、非同期for ループを指定できるようになった。PEP 530 -- Asynchronous Comprehensions

import asyncio
async def spam(n):
    for i in range(n):
       yield f'spam{i}'

async def spam_restrant():
    print([s async for s in spam(3)])

loop = asyncio.get_event_loop()
loop.run_until_complete(spam_restrant())

また、内包式中に、 await 式を指定できるようになった。

import asyncio
async def spam(n):
    await asyncio.sleep(0.1)
    return f'spam:{n}'

async def pred(n):
    return n % 2

async def spam_restrant():
    print({n:await spam(n) for n in range(5) if await pred(n)})

loop = asyncio.get_event_loop()
loop.run_until_complete(spam_restrant())

Python3.6の概要(その2 - 変数アノテーション)

変数アノテーション

PEP 484 -- Type Hintsで導入された型ヒントが、モジュールのグローバル変数や、クラスのインスタンス変数などにも指定できるようになった。PEP 526 -- Syntax for Variable Annotations

変数の型アノテーションは、mypy などではコメントとして指定するようになっていたが、Python3.6以降では、正式なPythonの構文で指定できるようになった。

>>> SPAM:str = 'global spam string'  # SPAM は str型グローバル変数

>>> class Ham:
...     EGG: int = 100  # EGGはint型クラス変数

この例では、代入文で変数を作成するのと同時に型を宣言しているが、変数の値を指定せずに、変数の型だけでも宣言できる。この場合は代入文では使用せずに、

>>> SPAM:str  # SPAM は str型グローバル変数

>>> class Spam:
    num: int   # Spam.numはint型

と指定する。デフォルト値の存在しない、クラスのメンバ変数などはこの形式で指定すると良いだろう。

値を指定せずに型だけを指定した場合、変数としては存在しないので実際に代入するまでは参照できない。

>>> HAM:int  # HAMの型宣言。宣言だけで、まだ変数は作成されていない
>>> print(HAM)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  NameError: name 'HAM' is not defined

>>> HAM = 100  # 代入文で変数名を作成
>>> print(HAM)
100

指定した型情報は、モジュールやクラスの__annotations__メンバ変数で参照できる。

>>> class Egg:
...     num: int
...
>>> Egg.__annotations__
{'num': <class 'int'>}

関数内でローカル変数にアノテーションを指定してもエラーとはならないが、指定した結果はどこにも格納されていないようだ。ローカル変数のアノテーションは、単に無視されるだけようだ。