time.tznameが文字化けしてるんだが

time.tzname の値がおかしい。

# Python 3.4.3 on Win32
import sys
import time
print(time.tzname[0], file=sys.stderr)  # ==> \x93\x8c\x8b\x9e (\x95W\x8f\x80\x8e\x9e)

化けてますねー (わざわざ sys.stderr に出力したのは、例外で落ちないようにするため)。


この類の文字化けは Python 2.x 時代に見たことがある文字コードが変換されないまま、内部文字列になってしまっているやつだ。

とりあえず、こうすれば元に戻せるが、

print(time.tzname[0].encode('raw_unicode_escape').decode('shift_jis'))  # ==> 東京 (標準時)

バグが直ったら動かなくなるな。


そもそも、どこでバグっているのか調べると、ロケールに依存した関数 mbstowcs を使っているのを見つけた
それなら import time する前に setlocale すれば化けないはず。

import locale
locale.setlocale(locale.LC_ALL, '')
import time
print(time.tzname[0])  # ==> 東京 (標準時)

ただ、プログラムを実行した時点で既に time モジュールが読み込まれていると効果がないので、早く直して欲しい。

warnで警告を表示させる

warnings.warn を使うと警告文を表示させることができる。

import warnings
warnings.warn('警告です')
print('hello world')  # プログラムは続行

実行するとstderrに警告文が表示されるものの、プログラムは停止せず、そのまま実行を継続する。

C:\temp\script.py:2: UserWarning: 警告です
  warnings.warn('警告です')
hello world

print関数とやっていることは同じように見えるかもしれないけど、warn は繰り返し実行しても、同じような警告は1回しか表示されないという利点がある。

import warnings

def foo():
    warnings.warn('警告です')
    print('hello world')

foo()
foo()
D:\temp\script.py:7: UserWarning: 警告です
  warnings.warn('警告です')
hello world
hello world


それから warn には stacklevel という引数があって、警告時に表示する場所を関数の呼び出し元に移動させることができる。

import warnings

def foo():
    warnings.warn('この関数を使わないでください', stacklevel=2)
    print('hello world')

for i in range(3):
    foo()
C:\temp\script.py:8: UserWarning: この関数を使わないでください
  foo()
hello world
hello world
hello world

stacklevel=2 としている理由は warn関数の内部から見て2回呼び出しをさかのぼるためである。


warn の第2引数 category にはクラス名を指定して、警告の種類を設定することができる。

warnings.warn('普通の警告', UserWarning)

よく使うクラスとしては DeprecationWarning がある。廃止された関数やモジュールを警告する時に使う。

import warnings

def foo():
    warnings.warn('この関数は廃止されました', DeprecationWarning, stacklevel=2)
    print('hello world')

for i in range(3):
    foo()
hello world
hello world
hello world

あれ? 警告が表示されないよ?
どういうことかというと、DeprecationWarning みたいに開発者向けの警告は初めから表示されないようになっているんだなあ。
この動作を変更するにはpythonのオプションを変更する。

python -W default script.py
# 以下は上の省略形
python -Wd script.py

毎回オプションをつけるのが面倒な場合は、PYTHONWARNINGS という名前の環境変数を作って default と書いておく方法もある。
設定した状態で普段使っているスクリプトを実行すると思わぬ発見があるかも。自分はこれで廃止予定になっているライブラリを使っていることに何度か気づいたことがある。


表示されない警告の種類はPythonのバージョンによって違う。最新の Python 3.4.2 だとこんな感じ。

import warnings
for i in warnings.filters:
    print(i)
('ignore', None, , None, 0)
('ignore', None, , None, 0)
('ignore', None, , None, 0)
('ignore', None, , None, 0)
('ignore', None, , None, 0)

DeprecationWarning の他にも表示されない警告があるね。

こいつらはフィルターと呼ばれていて、警告が発せられると上からマッチするものがないか見ていく。マッチするフィルターがあれば警告を無視(ignore)したりエラー(error)にしたりと所定の操作を実行する。

特定のモジュールやメッセージを限定したフィルターも書けるけど、まぁ普通は使わないと思う。興味があれば参考文献をどうぞ。

参考文献

warnings — Non-fatal alerts - Python Module of the Week
フィルターの書き方や警告の表示方法について
29.5. warnings — 警告の制御 — Python 3.4.2 ドキュメント
warningsの公式ドキュメント

tkinterでは基本多言語面しか使えない

Pythontkinter.Entry に入力した内容を取り出そうとすると、こういう例外が出る文字がある。

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xed in position 0: invalid continuation byte


何とか迂回できないものかと、対話モードで色々弄っていると、

>>> s = StringVar()
>>> s.set('\U0001F5FF')

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\bin\Python34\lib\tkinter\__init__.py", line 263, in set
    return self._tk.globalsetvar(self._name, value)
_tkinter.TclError: character U+1f5ff is above the range (U+0000-U+FFFF) allowed by Tcl
訳:文字 U+1f5ff は Tcl で扱える範囲(U+0000-U+FFFF)を上回ってますねぇ

と言われた。

基本多言語面にしか対応してないのか、Tcl。がっくしだよ。

tkinterよ、できれば通常の使用でまともなエラーメッセージを吐くようにしておいてくれると助かるんだが。

time.strftime で UnicodeEncodeError が出るんだが

WindowsのPython3で、time.strftimeに日本語を渡すと例外が出る。

>>> import time
>>> time.strftime("%Y年%m月%d日")

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'locale' codec can't encode character '\u5e74' in position 2: Illegal byte sequence

事前にlocale.setlocaleを呼んでおけば*1Shift_JISの範囲内であれば通るようになるが、

>>> import locale
>>> locale.setlocale(locale.LC_ALL, '')
'Japanese_Japan.932'

>>> time.strftime("%Y年%m月%d日")
'2014年11月03日'

他の文字はやっぱり通らない。

>>> time.strftime('\N{LATIN SMALL LETTER A WITH MACRON} %Y年%m月%d日')

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    time.strftime('\N{LATIN SMALL LETTER A WITH MACRON} %Y年%m月%d日')
UnicodeEncodeError: 'locale' codec can't encode character '\u0101' in position 0: Illegal byte sequence


ソースコードを調べてみるとCランタイムのstrftimeを使っていた。このせいで wchar_t* → char* 変換していて、おそらくこの辺りで失敗していると思われるが、具体的な場所がわからない。

そもそもPython3から文字列はUnicodeで統一されているので内部で不用意にマルチバイトに変換してはならないと思うのだが。調べてる途中に見つけたバグ報告ページによれば、どうもwhcar_t版であるwcsftimeではタイムゾーン文字列を正しく扱えないので、仕方なくstrftimeを使っているとのこと。

ま、そのうち直ると思うけど、既に報告から4年以上経ってるからな。いつになることやら。

*1:IDLEなどのIDEを使っている場合は、起動時に自動でsetlocaleされてることもあるよ。

誰がthreadingで作ったスレッドの後始末をしているのか

メインスレッドが終了すると、_threadモジュールで作ったスレッドは終了してしまう。

#! python3.3

import _thread
import time

def func():
    ident = _thread.get_ident()
    print('thread {} starts'.format(ident))
    time.sleep(5)
    print('thread {} ends'.format(ident))

print('main thread starts')
_thread.start_new_thread(func, ())
time.sleep(3)
print('main thread ends')

""" 出力:
main thread starts
thread 1756 starts
main thread ends
"""

一方、threadingモジュールで作ったスレッドは終わるまで自動で待ってくれる。

#! python3.3

import threading
import time

def func():
    ident = threading.get_ident()
    print('thread {} starts'.format(ident))
    time.sleep(5)
    print('thread {} ends'.format(ident))

print('main thread starts')
threading.Thread(target=func).start()
time.sleep(3)
print('main thread ends')

""" 出力:
main thread starts
thread 3612 starts
main thread ends
thread 3612 ends
"""

この待つ処理(join)は誰がやっているんだろうと思って、threading.pyの中身を見てみると、_MainThreadクラスの_exitfuncメソッドが実際にjoin()してた。しかし、この_exitfuncというメソッドは_shutdownという別名がついているだけで、誰が呼んでいるのか不明なんだ。__del__もないし、atexitも呼んでいない。どうやってPython終了時に実行してるんだろう?

そこで検索してみたら http://sh1.2-d.jp/b/2008-01-10-02-17.html に書いてあった。現在のPythonでは少し仕組みが変わっているが、呼び出し順はこう。

Modules/main.cのPy_Main
    ↓
Python/pythonrun.cのPy_Finalize
    ↓
同ファイルのwait_for_thread_shutdown
    ↓
threading.pyの_shutdown

しかしなあ。これ実際の中身は「もしthreadingという名前のモジュールが読み込まれていたら_shutdownを実行する」という形だからな。「もしthreadingという同名のモジュールがあったら?*1」とか「高水準なモジュールの後始末をインタプリタが名指しでするの?」とか色々な疑問が頭をよぎってしまう。まぁでも、threadingのドキュメントを見る限り、Pythonのスレッドは発展途中っぽいからな。GILの件もあるし。単に一時しのぎ的な実装になっているだけなのかもしれない。

*1:実際にやってみると呼ばれる。

winregでREG_QWORDを無理やり読み書きする方法

今のところPythonのwinregはREG_QWORDに対応していない。しかしwinregのソースを見てみたところ、未知のデータ型はバイナリ扱いにするようなので、整数をバイナリに変換すれば読み書きできることに気がついた。

具体的にはwinreg.SetValueEx()を呼ぶ前にデータをstruct.packしておく

#! python3.2
import winreg
import struct

with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, r'Software\__test__') as key:
    data = 12345678901234567890
    
    if hasattr(winreg, 'REG_QWORD'):
        regtype = winreg.REG_QWORD
    else:
        # REG_QWORD未対応の場合
        regtype = 11
        data = struct.pack('Q', data)
    
    winreg.SetValueEx(key, 'quadValue', 0, regtype, data)

また、winreg.QueryValueEx()を呼んだ後にデータをstruct.unpackする

import winreg
import struct

REG_QWORD = 11

with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\__test__') as key:
    data, regtype = winreg.QueryValueEx(key, 'quadValue')

    if not hasattr(winreg, 'REG_QWORD') and regtype == REG_QWORD:
        # REG_QWORD未対応の場合
        data = struct.unpack('Q', data)[0]

    print(data)

という方法でうまくいく。

そもそもwinregはREG_BINARYの処理をswitch文のdefaultにフォールスルーしているので、元からこういう風に書くことを狙っているのではないかという気がしてきた。

新12桁トリップなど

いつの間にかトリップコードの仕様が新しくなっていた。例のごとく情報が散逸していて仕様を調べるのに苦労したが、この記事が大いに参考になった。

まず12桁トリップから行こう。こちらは結構簡単で、キーをSHA1でみじん切りにしたあと、Base64で見た目を整えれば完成。

# Python 2.6.6
# -*- coding: shift_jis -*-
import base64
import hashlib

def trip12(key):
    """12桁トリップを計算する。
    
    >>> trip12('123456789012')
    'jZk8zfYo4m4X'
    >>> trip12('cWXV3rkHusy8')
    'V/Q./xpP.l0c'
    >>> trip12('いろはにほへと')
    '2R7QVeXeLHrO'
    """
    
    assert len(key) >= 12
    
    code = hashlib.sha1(key).digest()
    code = base64.b64encode(code, './')
    code = code[:12]
    
    return code

3.x版はこちら。

# Python 3.1.3
import base64
import hashlib

def trip12(key):
    """12桁トリップを計算する。
    
    >>> trip12('123456789012')
    'jZk8zfYo4m4X'
    >>> trip12('cWXV3rkHusy8')
    'V/Q./xpP.l0c'
    >>> trip12('いろはにほへと')
    '2R7QVeXeLHrO'
    """

    key = key.encode('cp932')
    assert len(key) >= 12
    
    code = hashlib.sha1(key).digest()
    code = base64.b64encode(code, b'./')
    code = code[:12]
    code = code.decode('cp932')
    
    return code


従来の10桁トリップも変更が加えられている。新しく「生キー指定」ができるようになった。

# Python 2.6.6
# -*- coding: shift_jis -*-
import re
import crypt

def trip10hex(key):
    """10桁トリップを16進数表現から計算する(生キー指定)。

    >>> trip10hex('#4141414141414141AA')
    'DLUg7SsaxM'

    >>> trip10hex('#0000000000000000ZZ')
    'IHp4MBMwSE'
    """
    assert re.match(r'^#[0-9A-Fa-f]{16}[./0-9A-Za-z]{0,2}$', key)
    
    key = key[1:]
    
    plain = key[:16]
    plain = plain.decode('hex')
    plain = plain.split('\0', 1)[0]

    salt = key[16:18] + '..'
    
    code = crypt.crypt(plain, salt)
    code = code[-10:]

    return code

3.x版の方は結構難しいが、無理やり書くとこんな感じ。

# Python 3.1.3
import re
import ctypes
import binascii

libcrypt = ctypes.cdll.LoadLibrary('libcrypt-2.11.3.so')
libcrypt.crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
libcrypt.crypt.restype = ctypes.c_char_p

def trip10hex(key):
    """10桁トリップを16進数表現から計算する(生キー指定)。

    >>> trip10hex('#4141414141414141AA')
    'DLUg7SsaxM'

    >>> trip10hex('#0000000000000000ZZ')
    'IHp4MBMwSE'
    """
    assert re.match(r'^#[0-9A-Fa-f]{16}[./0-9A-Za-z]{0,2}$', key)
    
    key = key[1:]
    key = key.encode()
    
    plain = key[:16]
    plain = binascii.a2b_hex(plain)

    salt = key[16:18] + b'..'
    
    code = libcrypt.crypt(plain, salt)
    code = code[-10:]
    code = code.decode()
    
    return code


こうして 2.x と 3.x のコードを比べてみると、Python3 は strbytes の使い分けがうまくできてないな。例えば binascii.a2b_hex は引数に bytes しか受け付けない。別に 0〜F しか使っていなければ str を受け付けてもいいと思うのだが(それ以前に hex_codec とか base64_encode とかが使えないままになっているのも気になるとこだが)。

>>> import binascii
>>> binascii.a2b_hex(b'1234')
b'\x124'
>>> binascii.a2b_hex('1234')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' does not support the buffer interface

それから crypt の実装がおかしい。C言語の crypt 関数は仮引数の型が char * なのだが、Python3 の crypt モジュールは不思議なことに str しか受け付けない。 bytes を渡すとエラーになる。どういうことかと思って中身を見てみると、UTF-8 で変換してから渡してやがった。これでは旧来の文字コードShift_JISEUC-JP、Latin-1 など)が置き去りではないか。

>>> import crypt
>>> crypt.crypt('hello', 'ab')
'abl0JrMf6tlhw'
>>> crypt.crypt(b'hello', b'ab')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be string, not bytes

しょうがないので上のコードでは ctypes を使って crypt を直接呼ぶことにした。おかげでマルチプラットフォームがさらに遠のいてしまった。このあたりは zipfile の問題と同じ構図だろう。

Python3 になれば文字コードから開放されると思ってたが、どうやらこれからも文字コード周りのトラブルは続きそうだな。