オタク日記
(Mac と Linux, 2015Q3)

目次

2015-10-09 (Fri): Django の進化 (その 3)—mod_wsgi
2015-09-26 (Sat): Django の進化 (その 2)—DB I/F
2015-09-19 (Sat): Django の進化 (その 1)—Time Zone 再訪
2015-09-16 (Wed): pyvenv と readline
2015-09-12 (Sat): Pip で pyvenv
2015-08-29 (Sat): Pip-tools の様変わり
2015-08-22 (Sat): 重い Python ……

古い日記:
2015Q2   2015Q1   2014Q4   2014Q3  
2014Q2   2014Q1   2013Q4   2013Q3   2013Q2   2013Q1  
2012 年   2011 年   2010 年   2009 年   2008 年   2007 年  
2006 年   2005 年   2004 年   2003 年   2002 年   2001 年


2015-10-09 (Fri): Django の進化 (その 3)—mod_wsgi

これは、Django そのものの進化ではないが、私にとっては、mod_wsgi は 「Django 専用」みたいなものなので…… (私と同じように)そもそも WSGI (Web Server Gateway Interface) って何?という人は、 PEP-3333 を御覧下さい(Python のドキュメントの常ながら、とってもよくできています。) しかし、かつてはこれ (mod_wsgi) はかなり使い辛いものだった——一年前にも 相当難儀したのを憶えている。 この時の「なんだかなぁ」は、つまるところ
  1. Python 3k 未対応
  2. dev-server と、static files (css, java, img, etc) の扱いが全く違う。
  3. Apache の側で WSGI 対応にしないといけない(httpd.conf との格闘が必要という事)
くらいに纏められるだろう。しかるに、最新の mod_wsgi (-4.4.15) は、 これらの問題を殆んどクリアしている。

但し、Django と一緒に使うに当っては、まだいくつかの「考慮」が必要なようだ。 (真の「必要条件」かどうかは確認していない。)

  1. Static files を置くディレクトリは「標準」のものを使う: 要は tts/static/tts/{css|img|js}/ のようにする、という事。('tss' は Django application の名前。) 実は 'tss' が重なるを嫌って後の方を省いていた事があるが、これは話をとても面倒にする。
  2. db.sqlite3 と、その直上のディレクトリの group は Apache デーモンの group にする必要がある……(OSX MacPorts では、'_www', Ubuntu では 'www-data')
  3. dev_server は DEBUG=True で、mod_wsgi + Apache は DEBUG=False で: 逆の組合せは原理的に不可能ではない筈だが、開くポートが well-known かどうか(すな わち、起動するのに root privilege が必要かどうか)と相俟って、なかなかうまく行かない。

Wsgi 専用の settings.py を作る

最後の「考慮」はなかなか難物で、かなり四苦八苦させられたが、 今のところ、次のような「策」で満足している。
  1. settings.py: django-admin startproject my_project でできるディフォルトのファイル。これは、DEBUG = True のままで、dev_server 専用とし、必要な他の設定をする。
  2. mod_settings.py: 上の settings.py のコピーを編集して、 DEBUG = False とし、mod_wsgi 専用の settings とする。肝心な差分は
        
    (pvenv) fukuda@tts5:~/pvenv/wrm_cwt/mysite$ diff mod_settings.py settings.py
    26c26
    < DEBUG = False
    ---
    > DEBUG = True
    28c28
    < ALLOWED_HOSTS = ['localhost', 'ubuntu', 'falcon', 'tts2', 'tts3', 'tts4', 'tts5', 'tts6']
    ---
    > # ALLOWED_HOSTS = ['localhost', 'ubuntu', 'falcon']
    .....
    ここで、ALLOWED_HOSTS は uncomment するだけでなく、 実際にこのアプリケーションが動いているホストの hostname が含まれていないといけない。
  3. wsgi.py: これも、'startproject' の際に作られる mysite/wsgi.py を編集して、
    import os
    from django.core.wsgi import get_wsgi_application
    
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.mod_settings")
    application = get_wsgi_application()    
    のようにする。('mysite.settings' を 'mysite.mod_settings' にした。)
こうしておくと、dev_server を走らせたままで、mod_wsgi/Apache を起動し、両方同時にアクセスする、なんて事も可能になる(勿論、port# は違えておく必要あり。)

Mod_wsgi のインストール

これまで難航したので今回もしっかり身構えて取り掛かったが、 どうもはなから様子が違う…… 最新版が PyPI に有るってのもさる事ながら、それより、mod_wsgi を Apache ではなくて「Python にインストールする」ってのが驚き。(何だか、「Apache の module」っていうイメージがまるっきり間違っていたような気がしてきた。)

fukuda@tts5:~$ sudo apt-get install apache2-mpm-prefork apache2-threaded-dev
(pvenv) fukuda@tts5:~/pvenv$ nano requirements.in
(pvenv) fukuda@tts5:~/pvenv$ cat requirements.in
django
.....
mod-wsgi   # new
.....
(pvenv) fukuda@tts5:~/pvenv$ pip-compile
(pvenv) fukuda@tts5:~/pvenv$ pip-sync
とするだけ。

これで、Django の devserver が動くのを確認できている Django project へ行ってから、次のようにする:

(pvenv) fukuda@tts5:~/pvenv/wrm_cwt$ python manage.py collectstatic  #1)
(pvenv) fukuda@tts5:~/pvenv/wrm_cwt$ python manage.py runmodwsgi     #2)
Successfully ran command.
Server URL         : http://localhost:8000/
Server Root        : /tmp/mod_wsgi-localhost:8000:1001
Server Conf        : /tmp/mod_wsgi-localhost:8000:1001/httpd.conf    #3)
Error Log File     : /tmp/mod_wsgi-localhost:8000:1001/error_log (warn)
Request Capacity   : 5 (1 process * 5 threads)                       #4)
Request Timeout    : 60 (seconds)
Queue Backlog      : 100 (connections)
Queue Timeout      : 45 (seconds)
Server Capacity    : 20 (event/worker), 20 (prefork)
Server Backlog     : 500 (connections)
Locale Setting     : en_US.UTF-8 
ここで
  1. #1) Apache が static file を扱えるようにするために、一箇所に集める
  2. #2) これで、まるで dev-server が走るように、Apache を起動できる
  3. #3) httpd.conf を自動生成している(新しい mod_wsgi の最大のメリット)
  4. #4) 望み通り multi-threading が実現されている
ここまでで、browser で http://localhost:8000 にアクセスして確認する事ができる。

Root 権限で Apache を走らせる

Port:80 で待ち受けるように設定した Apache を起動するには root 権限が必要になる。
(pvenv) fukuda@tts5:~/pvenv/wrm_cwt$ sudo ../bin/python manage.py runmodwsgi \
    --port=80 --user www-data --group www-data 
Successfully ran command.
Server URL         : http://localhost/
Server Root        : /tmp/mod_wsgi-localhost:80:0
Server Conf        : /tmp/mod_wsgi-localhost:80:0/httpd.conf
Error Log File     : /tmp/mod_wsgi-localhost:80:0/error_log (warn)
Request Capacity   : 5 (1 process * 5 threads)
Request Timeout    : 60 (seconds)
Queue Backlog      : 100 (connections)
Queue Timeout      : 45 (seconds)
Server Capacity    : 20 (event/worker), 20 (prefork)
Server Backlog     : 500 (connections)
Locale Setting     : en_US.UTF-8
sudo では、元の PATH を引き継がないので pvenv の python を指定するには 明示的に ../bin/python とする必要がある。また、--user, --group の指定も必須(security からの要求?)

Mod_wsgi/Apache をデーモンに

上の #3) の「httpd.conf の自動生成」も大したものであるが、 このディレクトリを恒久的なものにして、daemon にする方法も準備されている。 (いや、もう、感心するのみ。) 上のコマンドに --server-root= として、サーバの root directory を指定し、さらに --setup-only オプションを付け加えるだけ。
(pvenv) fukuda@tts5:~/pvenv/wrm_cwt$ sudo ../bin/python manage.py runmodwsgi \
    --port=80 --user www-data --group www-data \
    --server-root=/etc/mod_wsgi-express-80 --setup-only 
Successfully ran command.
Server URL         : http://localhost/
Server Root        : /etc/mod_wsgi-express-80
Server Conf        : /etc/mod_wsgi-express-80/httpd.conf        #1)
Error Log File     : /etc/mod_wsgi-express-80/error_log (warn)
Environ Variables  : /etc/mod_wsgi-express-80/envvars
Control Script     : /etc/mod_wsgi-express-80/apachectl         #2)
Request Capacity   : 5 (1 process * 5 threads)
.....
Locale Setting     : en_US.UTF-8    
ここで、
  1. #1) /tmp の代りに /etc に「恒久的」な、root directory が作られる。
  2. #2) apachectl まで作ってくれるので、以降はこれを使って daemon を起動できる。(このコマンドは飽く迄 --setup-only なので、daemon の起動はしない。)
この apachectl を使って daemon を立ち上げる。
(pvenv) fukuda@tts5:~/pvenv/wrm_cwt$ sudo /etc/mod_wsgi-express-80/apachectl start
(pvenv) fukuda@tts5:~/pvenv/wrm_cwt$ ps ax | grep httpd
 19409 ?        Ss     0:00 apache2 (mod_wsgi-express) -f /etc/mod_wsgi-express-80/httpd.conf -DMOD_WSGI_MPM_ENABLE_EVENT_MODULE -DMOD_WSGI_MPM_EXISTS_EVENT_MODULE -DMOD_WSGI_MPM_EXISTS_WORKER_MODULE -DMOD_WSGI_MPM_EXISTS_PREFORK_MODULE -k start
 19410 ?        Sl     0:00 (wsgi:localhost:80:0)      -f /etc/mod_wsgi-express-80/httpd.conf -DMOD_WSGI_MPM_ENABLE_EVENT_MODULE -DMOD_WSGI_MPM_EXISTS_EVENT_MODULE -DMOD_WSGI_MPM_EXISTS_WORKER_MODULE -DMOD_WSGI_MPM_EXISTS_PREFORK_MODULE -k start
 19411 ?        Sl     0:00 apache2 (mod_wsgi-express) -f /etc/mod_wsgi-express-80/httpd.conf -DMOD_WSGI_MPM_ENABLE_EVENT_MODULE -DMOD_WSGI_MPM_EXISTS_EVENT_MODULE -DMOD_WSGI_MPM_EXISTS_WORKER_MODULE -DMOD_WSGI_MPM_EXISTS_PREFORK_MODULE -k start
これで、 http://localhost/tts/ (localhost から)および http://tts5/tts/ (local network から) でアクセスできる事を確認した。

2015-09-26 (Sat): Django の進化 (その 2)—DB I/F

Database を使うには、SQL をもっと知らねば、と思った事も有ったが、 Django からアクセスする限り、それもあまり必要ないようだ——甘いかな。

Query Set

再び、次のような cwt/models.py (つまり、table の構成)を前提にする。
# cwt/models.py
from django.db import models
from timezone_field import TimeZoneField     
....
class Area(models.Model):
    .....
    area_name = models.CharField(max_length=80, unique=True)
    time_zone = TimeZoneField(default='UTC')  
    .....
    def __str__(self):
        return "{0}".format(self.area_name)
    
class Party(models.Model):
    .....
    started_on = models.DateTimeField(blank=True, null=True)  
    finished_on = models.DateTimeField(blank=True, null=True)
    area = models.ForeignKey(Area, null=True, blank=True)    
    .....
    def __str__(self):
       return self.party_name
このデータベースにアクセスするには、
import wct.models import Aerea, Party

area1 = Area.objects.get(area_name="Kitayatsu")
party = Party.objects.create(area=area, party_name="Kitadake Sangakukai")
party.started_on = timezone.now()
party.save()
.....
とするのが基本。ClassName.objects.{get|create}() が key であるが、これらはいずれも、その object を返す。 ここで、ClassName.objects.{all|filter}() とすると、 単なる object ではなく、その list (のようなもの) である query set を返す。 これはとても便利に扱えて、例えば
parties = Party.objects.filter(area=area1)               #1)

party_active = parties.filter(finished_on=None)          #2)

party_newest = parties.order_by('registered_on').last()  #3)
    
for party in parties:                                    #4)
    party.finished_on = timezone.now()
    party.save()
ここで、
  1. #1) Party class の全体から、area == area1 となる object の set (query set) を返す。
  2. #2) parties は query set なので、さらにフィルタをかける事もできる。
  3. #3) ソートも可能。その結果も query set で、さらに例えば、 last() (最後の要素を取り出す)というメソッドを適用できる。
  4. #4) query set は for loop のイテレータとしても使える。
ClassName.objects.get() の代りに ClassName.objects..filter() を使う事のメリットと注意事項としては……

Migrate/Make Migration

上述のように、Django を使うと非常にスマートに DB へアクセスできる。 しかし、DB の構造(テーブルカラム?)の変更は、なかなかそうは行かない。 かつては、その DB の種類に応じた、SQL コマンドで「変更」の内容を記述する必要が有った。 これは SQL をよく知らない私にとっては「鬼門」で、 散々悩まされたあげく、models.py でのクラス定義は、 設計の段階でよく考え、もし迷ったらアトリビュート (field) はとりあえず付けておく、なんて事をしていた。

しかし、Django の開発者達もさすがにこれでは可哀想だと思ったのか (Django-1.5 の頃から?)このステップを自動化する方策を考え出してくれた。 例えば、cwt/models.py の Party class に次のように area というフィールドを付け加えたとすると、

class Party(models.model):
    .....
    area = models.ForeignKey(Area, null=True, blank=True)    
    .....
manage.py を使って
(PyVenv) fukuda@falcon:~/PyVenv/wrm_cwt% python manage.py makemigrations cwt
Migrations for 'cwt':
  0003_party_area.py:
    - Add field area to party
(PyVenv) fukuda@falcon:~/PyVenv/wrm_cwt% python manage.py migrate cwt
Operations to perform:
  Apply all migrations: cwt
Running migrations:
  Rendering model states... DONE
  Applying cwt.0003_party_area... OK
というように、自動で、migration-file を生成し、それを使って DB の変更を実施してくれる。

おおー簡単になったなぁ、目出度し、目出度し……だと良いのだが、 実はこれ、いつもうまく行くとは限らない。 どんな時失敗するかについては確実な事は分ってないが、今のところ、

のような場合に、失敗する確率が高いようだ。(Migrate の際に失敗が解ればまだ良いが、後になって、 そのフィールドにアクセスして初めて解る、という事もある。)

そのような場合、対策としては、

  1. cwt/migrations 以下を全部消して、再度 migrations file を作るところから始める。
  2. models.py で追加したフィールドを一旦コメントアウトし、 makemigrations/migrate を実行、 その後またアンコメントして、再度 makemigrations/migrate を実行。
等が有効なようだ。

Git との併用

弊社では git を活用して共同開発をしている……と言いたいところだが、 実はまだそこまで行ってなくて、 テストのために、git でサーバの clone を幾つかのプラットフォームに作っているだけ。

これは、まことに具合が良い……が、Django の場合は db.sqlite3 を、tracking file にするかどうか、という問題があった。 要は、テスト用のサイトでは、ソースを触る事は稀だが、 db.sqlite3 だけは試験している間にどんどん変わる。 remote/local 両方の reposite で変更があると勿論の事ながら git pull だけでは同期できない。 あと、local settings も、元の reposite と完全に同期して欲しくない。

随分試行錯誤したが、今のところ db.sqlite3(双方でしょっちゅう変わる) と local_settings.py(サイト固有の設定をする)については、 sync_db.sqlite3sync_local_settings.py というファイルを作る事で凌いでいる。つまり……

fukuda@lark:/var/lib/git/wrm_cwt.git% sudo git init --bare
fukuda@lark:/var/lib/git/wrm_cwt.git% sudo chown -R gitdaemon:root ..
として作った remote repository に対し、開発しているサイトで
fukuda@falcon:~/PyVenv/wrm_cwt% cp cwt/local_settings.py cwt/sync_local_settings.py
fukuda@falcon:~/PyVenv/wrm_cwt% cp db.sqlite3 sync_db.sqlite3
fukuda@falcon:~/PyVenv/wrm_cwt% git add sync_db.sqlite3 
fukuda@falcon:~/PyVenv/wrm_cwt% git add cwt/sync_local_settings.py
fukuda@falcon:~/PyVenv/wrm_cwt% git commit -a -m "first release"
fukuda@falcon:~/PyVenv/wrm_cwt% git remote add origin git://otacky.jp/wrm_cwt.git
fukuda@falcon:~/PyVenv/wrm_cwt% git push origin master   
とする。(local_settings.pydb.sqlite3.git-ignore に入っている、と前提。) 公開するサイトで
fukuda@ubuntu:~/pvenv% git clone git://otacky.jp/wrm_cwt.git
fukuda@ubuntu:~/pvenv% cd wrm_cwt
fukuda@ubuntu:~/pvenv/wrm_cwt% git clone git://otacky.jp/wrm_cwt.git
fukuda@ubuntu:~/pvenv/wrm_cwt% cp sync_db.sqlite3 db.sqlite3
fukuda@ubuntu:~/pvenv/wrm_cwt% cp tts/sync_local_settings.py tts/local_settings.py 
この時点で、tts/local_settings.py, mysite/settings.py を編集。 その後試験を実施し、その後ソースを update するには
fukuda@ubuntu:~/pvenv/wrm_cwt% git pull
とする。もし、データベースそのものを、更新するなら
fukuda@ubuntu:~/pvenv/wrm_cwt% cp sync_db.sqlite3 db.sqlite3
とする。(ubuntu で集積したデータは失われる)

2015-09-19 (Sat): Django の進化 (その 1)—Time Zone 再訪

「進化」と言っても、何しろ比較の対象が、Django-0.9x の頃なんだから、 「Django が進化した」というより「ようやく自分に理解できかけた」というのが正しい……

前にも書いたが、当初「少々ドラスティック過ぎでは」と思われた

  1. Database の datetime は 'timezone aware' の UTC 一本で
  2. 表示の際にのみlocal time zone を使う
という「原則」は、 実際に実行してみると、 「もはやこれしかない」と思える程具合が良い。(Django document の「文字コードを UTF-8 にするのと同じくらい効用があるよ」とのキャッチフレーズは誇大広告ではなかった。) が、しかし、「設定」「出力」など、実世界と時刻をやりとりする段になると、 「あれっ、これどうやるんだったけ」となる事が、まま有る。

なので、「逆引き風」に要点を纏めておく。

設定・定義

新環境を作る時など、意外に忘れがちだが、これをやっておかないと、 以下の説明は全く無意味となる。
# mysite/settings.py    
INSTALLED_APPS = (
    'django.contrib.admin',
    .....
    'timezone_field',
    .....
)
# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Tokyo'       #1)
.....
USE_TZ = True                  #2)
.....
  1. #1) local_time のディフォルトの TZ となる。
  2. #2) これを True にする事で、datetime object が TZ-aware になる。

一方、modles(つまり、テーブル)の定義の方は

# cwt/models.py
from django.db import models
from timezone_field import TimeZoneField     #3)
....
class Area(models.Model):
    .....
    area_name = models.CharField(max_length=80, unique=True)
    time_zone = TimeZoneField(default='UTC')  #4)
    .....
    def __str__(self):
        return "{0}".format(self.area_name)
    
class Party(models.Model):
    .....
    started_on = models.DateTimeField(blank=True, null=True)  #5)
    finished_on = models.DateTimeField(blank=True, null=True)
    .....
    def __str__(self):
       return self.party_name
  1. #1) TimeZoneField クラスの import
  2. #2) このカラムのアトリビュートを TimeZoneField に(詳細は後述)
  3. #3) 従来通りの DateTimeField だが、TZ-aware になる。
以上を前提に、Django の shell を立ち上げる。pyvenv に入っている事、 Django の設定(上記の settings.py)が読み込まれている事に注意。
(PyVenv) fukuda@falcon:~/PyVenv/wrm_cwt% python manage.py shell             
Python 3.4.3 (default, Aug 26 2015, 18:29:14) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)    

TZ-aware

datetime.datetime class object が、Time-Zone aware かどうかは、 つまるところ
>>> import datetime
>>> from django.utils import timezone
>>> now_naive = datetime.datetime.now()
>>> now_naive
datetime.datetime(2015, 9, 20, 6, 1, 39, 820225)
>>> type(now_naive)
<class 'datetime.datetime'>
>>> now_aware = timezone.now()
>>> now_aware
datetime.datetime(2015, 9, 19, 21, 2, 8, 245347, tzinfo=<UTC>)
>>> type(now_aware)
<class 'datetime.datetime'>
のように、「tzinfo が付加されるかどうか」に尽きる。この効果は大きいが、 しかし、これがなかなか厄介。例えば、この表示をそのまま使って、TZ-ware な datetime object を作ろうとしても駄目で、
>>> now_by_hand = datetime.datetime(2015, 9, 19, 21, 2, 8, 245347, tzinfo=<UTC>)
  File "<console>", line 1
    now_by_hand = datetime.datetime(2015, 9, 19, 21, 2, 8, 245347, tzinfo=<UTC>)
                                                                          ^
SyntaxError: invalid syntax
などと言われる。ここは、
>>> import pytz
>>> now_by_hand = datetime.datetime(2015, 9, 19, 21, 2, 8, 245347, tzinfo=pytz.UTC)    
>>> now_by_hand = datetime.datetime(2015, 9, 19, 21, 2, 8, 245347, tzinfo=pytz.timezone('Asia/Tokyo'))
    
とする必要がある。('UTC' と 'Asia/Tokyo' で、「扱い」がこんなに違うのがなんとも……)

Time Zone の設定・表示

このように、単に「TZ を指定するだけ」でも面倒なのに、これを DB とやりとりするとなるとちょっと絶望的な感じがしてくるが、 それを救ってくれるのが、上の #3), #4) で導入される TimeZoneField。
>>> from cwt.models.py import Area
>>> yatsu = Area.objects.get(area_name="Yatugatake")
>>> yatsu.time_zone
'UTC'
>>> yatsu.time_zone = 'Asia/Tokyo'
>>> yatsu.time_zone
<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>
>>> now_by_hand = datetime.datetime(2015, 9, 19, 21, 2, 8, 245347, tzinfo=yatsu.time_zone)
>>> type(yatsu.time_zone)
<class 'pytz.tzfile.Asia/Tokyo'>  
つまり、TimeZoneField と定義された *.time_zone attribute は、文字列で設定(入力)できるが、その実体は tzinfo に相当する class object である、という事。('*.time_zone' という名前自体に意味が有る訣ではない事に留意。)

なので、出力の方は、(yatsu.time_zone が既に tzinfo class なので)

>>> yatsu.time_zone.tzname(datetime.datetime(2015,1,1))   #6)
'JST'
>>> yatsu.time_zone.zone
'Asia/Tokyo'
>>> timezone.localtime(now_aware)                    
datetime.datetime(2015, 9, 20, 6, 2, 8, 245347, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
>>> timezone.localtime(now_aware, yatsu.time_zone)
datetime.datetime(2015, 9, 20, 6, 2, 8, 245347, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
>>> timezone.localtime(now_aware, pytz.utc)
datetime.datetime(2015, 9, 19, 21, 2, 8, 245347, tzinfo=<UTC>)    
#6) の引数は daylight saving time の判定に用いるためのものらしく、 それと関係ない TZ では何を入れても良いようだ。 (このあたり、もうちょっと熟れてくれても良いと思うのだが。) あと、ここでも、TIME_ZONE で指定したディフォルトの TZ が顔を出す。 localtime を求めるのに tzinfo を指定しなければ、その値になるという事。 また、これを変更する事もできて、
>>> timezone.activate(pytz.timezone('Asia/Kathmandu'))
>>> timezone.localtime(now_aware)
datetime.datetime(2015, 9, 20, 2, 47, 8, 245347, tzinfo=<DstTzInfo 'Asia/Kathmandu' NPT+5:45:00 STD>)
>>> timezone.activate(yatsu.timezone)
>>> timezone.localtime(now_aware)
datetime.datetime(2015, 9, 20, 6, 2, 8, 245347, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
のようにできる。

2015-09-16 (Wed): pyvenv と readline

Pip + pyvenv 万歳、みたいな事を書いたばっかりだけど、 実は「Ubuntu での依存性解決」の他にも、「なんだかなあ」は有って……

Zsh や Bash におけるのと同様、Python でも readline は、コマンド入力での行編集から、コマンド・ヒストリー、 「コマンド補完」まで、「なくてはならない」ものだけど、Python ではなかなか微妙なところがある。 (ちょっと前に悩まされていた、.python_history の扱いの問題もその一つ。)

OSX では、それに加えて、python-readline モジュールが、(ディフォルトだと) readline (gnu-readline) ライブラリではなく、libedit ライブラリを使うという「ひねり」が入る——両者の間に然程差はないのだけど、 後者は、ちょっと機能が低くて、例えば(かつては)Ctrl-R で incremental search ができなかった。

とは言え、MacPorts できちんと、 Python-3.4 と py34-readline を入れておくと、問題なく Gnu-readline を使った readline (module) が import できる

# OSX に組み込みの Python
fukuda@falcon:~% /usr/bin/python
Python 2.7.10 (default, Jul 14 2015, 19:46:27) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import readline
>>> readline.__doc__
’Importing this module enables command line editing using libedit readline.’
>>> # Ctrl-R は無視される
# MacPorts の Python3.4 と py34-readline
fukuda@falcon:~% python  
Python 3.4.3 (default, Aug 26 2015, 18:29:14) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import readline
>>> readline.__doc__
’Importing this module enables command line editing using GNU readline.’
>>> 
(reverse-i-search)‘imp’: import readline
と、gnu-readline が読み込まれ、Ctrl-R もちゃんと動作する。 ところが、pyvenv に入ると
(PyVenv) fukuda@falcon:~/PyVenv% python
Python 3.4.3 (default, Aug 26 2015, 18:29:14) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import readline
>>> readline.__doc__
>>> ’Importing this module enables command line editing using libedit readline.’
>>> import readline
bck:imp
となり、libedit-readline が読み込まれる。Ctrl-R (reverse-i-search) は無視はされないが動作が違う。 pip で readline をインストールしてもこれは変わらない。 しかし、easy_install を使って gnureadline をインストールすると
(PyVenv) fukuda@falcon:~/PyVenv% easy_install ~/Downloads/gnureadline-6.3.3-py3.4-macosx-10.9-x86_64.egg
(PyVenv) fukuda@falcon:~/PyVenv% python
Python 3.4.3 (default, Aug 26 2015, 18:29:14) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import readline
>>> readline.__doc__
’Importing this module enables command line editing using GNU readline.’
>>> 
(reverse-i-search)‘im’: import\040readline 
となる。(最後の Ctrl-R で、'\040' が挿入されているのは、前の libedit-readline のせい。)

実は、ここまで来るのに長い道程が有ったのだが、 イキサツを一々書いていると話が長くなるので要約すると、

  1. MacPorts 環境:
    • MacPorts で入れた Python3.4 + py34-readline: 問題なく gnu-readline。
    • 同 Python3.4 + pip で入れた readline: gnu-readline
  2. pyvenv 環境:
    • pyvenv の Python3.4 (MacPorts の Python3.4 のコピー)のみ: libedit-readline
    • 同 Python3.4 + pip の readline: libedit-readline
    • 同 Python3.4 + pip の gnureadline: libedit-readline
    • 同 Python3.4 + easy_install の gnureadline-6.3.3: gnu-readline.
つまり、要は pyvenv 下で readline をまともに使おうとすると、 gnureadline モジュールを easy_install でインストールしないといけないって事。 これは、「pip-tools で Python-module のバージョン管理の一本化」の精神に大いに背くが、 上述のように、libedit-readline が .python_history へコマンドをセーブする時、空白の代りに '\040' という文字列を挿入するので、 二つの readline を混在させる訣には行かないのだった。

2015-09-12 (Sat): Pip で pyvenv

Pyvenv (Python Virtual Environment) と Git、 どちらもちょろっと触ってみてはすぐに撃退される、の繰返しだったが、 このところかなり真面目に使って、有難味を再認識している。 というか、(すぐに諦めたりしないで)使い続けないと、 道具の善し悪しは分らない、という事かな?

とはいえ、pyvenv の方は環境そのものがかなり洗練されてきた、 というのもあるだろう。 素人が最初の「うう、わけわか、もうやってられねぇ」バリアを超えるには、 それをかなり氐くしておいてくれないと難しい :-)

MacPort 上なら pyvenv はとっても簡単・便利

前にも書いたが、Python 3.x から、pyvenv を最初から含むようになり、これと、新しい pip-tools (pip-compile と pip-sync) を使って

fukuda@hawk:~% sudo port install git
fukuda@hawk:~% pyvenv-3.4 pvenv                          #1)
fukuda@hawk:~% cd pvenv   
fukuda@hawk:~/pvenv% . bin/activate
(pvenv) fukuda@hawk:~/pvenv% pip install --upgrade pip   #2) 
(pvenv) fukuda@hawk:~/pvenv% pip install pip-tools       #3)
(pvenv) fukuda@hawk:~/pvenv% cat requirements.in         #4)  
django
pytz
django-timezone-field
pip-tools
nose
pyparsing
python-dateutil
mod-wsgi
django-gmapi-new
numpy
readline
matplotlib
(pvenv) fukuda@hawk:~/pvenv% pip-compile                 #5)
(pvenv) fukuda@hawk:~/pvenv% pip-sync    
(pvenv) fukuda@hawk:~/pvenv% pip freeze               
click==4.1
Django==1.8.3
django-gmapi-new==1.0.1.1
django-timezone-field==1.2
first==2.0.1
matplotlib==1.4.3
mod-wsgi==4.4.13
nose==1.3.7
numpy==1.9.2
pip-tools==1.1.3
pyparsing==2.0.3
python-dateutil==2.4.2
pytz==2015.4
readline==6.2.4.1
six==1.9.0    
まで、比較的簡単に来られる。とは言え、まあ、ちょっとなんだかなぁ、は有って、
  1. #1) かつては Python の版が固定されていた方が安定するだろうという事で、 pyvenv --copies としていたが、一度、readline か何かで互換性の問題が出てから、 pyvenv として、link (default) を使う事にした。 もう半年経ったが、今のところ、OSX + port でも Ubuntu-14.04, -15.04 でも問題は出ていない。(Ubuntu-14.04 は最近は未確認 :-)
  2. #2) pyvenv がディフォルトで "bootstrap" する pip は -6.0.x で、pip-tools の pip-review が使えない。
  3. #3) 前にも書いたが pip-tools の最新版に pip-review は無い。
  4. #4) これを書いておけば、pip-compile, pip-sync が、依存モジュールの推定、モジュールのバージョンアップを自動でやってくれる。

とは言え、かつて MacPorts でもかなり苦労した numpy や matplotlib, readline が、 pip-compile, pip-sync であっさりインストールできたのにはちょっと感激。

Pip-compile は Ubuntu では結構大変

上の「一見とってもラクチン」は、どうやら、MacPorts で依存関係で苦労した時に、library 群を片っ端から(自動で) port してきたせいらしい。

しかし、「素の Ubuntu」で同じ事をしようとしても、 pyvenv を作るところまでは同様にやれるが、 pip-compile では、例えば matplotlib や readline で大量のエラーを吐く。どうやら、pip-compile はシステムレベルのライブラリまではインストールしないようだ。 おまけに、このエラーメッセージを見ても、 どのライブラリ・パッケージが不足しているのかよく分らない。 (Ubuntu はライブラリ・パッケージが極端に細分化されている。)

「pyvenv 環境では、Python のパッケージ管理は pip で」なんて決心した事を後悔し始めた…… が、さすが Ubuntu、悩みがあるところ、必ず解決策が有る。 Matplotlib で途方に暮れていた時、 askubuntu.com に回答がありました。(しかも、例が matplotlib ドンピシャリだった!ちょっと感激したよ。) 要は、mod-wsgi と matplotlib を pip でインストールするためには、その前に apt-get build-dep {package} で、ビルドに必要なライブラリ群を揃える、という事。

#1)    
fukuda@ubuntu:~$ sudo apt-get install python3.4-dev python3.4-venv         
#2)
fukuda@ubuntu:~$ sudo apt-get install apache2-dev  
fukuda@ubuntu:~$ sudo apt-get install apache2-mpm-worker apache2-threaded-dev
#3)
fukuda@ubuntu:~$ sudo apt-get build-dep mod-wsgi
#4)
fukuda@ubuntu:~$ sudo apt-get build-dep matplotli
とすれば良い。ここで、
  1. #1) Ubuntu ではこれをやらないと (pip 以前に) そもそも pyvenv が無い。
  2. #2) 次の 2 行は、mod-wsgi が apache の multi-threading を使うために必要。 (何故か build-dep mod-wsgi はこれをカバーしてくれない。)
  3. #3), #4) mod-wsgi, matplotlib の難物の依存性を解決してくれる。 (実は、何十もの dpk を捜し出してインストールする。)
ここまでやっておくと、後は、上のように pyvenv に入って、pip-compile, pip-sync だけで環境構築が可能になる。

他の Python パッケージについて、apt-get build-dep が必要か、はたまた、build-dep だけで、依存性の解決が可能か (mod-wsgi みたいに、別途 apt-get install が必要ではないか) については、 実はあまりよく分らない。でもまあ、お蔭様で「pip で通す」という決断が、 然程悪くなかった、という気はしている。


2015-08-29 (Sat): Pip-tools の様変わり

あんまり時間が経たないうちに、pyvenv と git の話を書き残しておこうと思っているのだが、なかなかその本題に入れない。

virtualenv は何が良いのか、どうしたら良いのがさっぱり分らず、 早々に撃退されてしまい、その後しばらくは羹に懲りていたのだが、 Python-3.x になって、pyvenv というコマンド一発で Python の仮想環境が構築できる(つまり、virtualenvwrapper とか考えなくて良い)事になり、またぞろ使い始めた。

簡素化のもう一つの軸は、パッケージ管理が pip で統一された事。(そもそも pyvenv で環境を作ったら、ディフォルトで pip もインストールされる。) 使ってみると、これはなかなか良い。特に、Linux の上で使う Python アプリを OSX の上で開発したい、なんて時にはとっても便利。

が、使い続けていると不満も出てくる。 最大の「なんだかなぁ」は、port upgrade outdatedapt-get upgradeみたいに、「コマンド一発で環境を upgrade」ができない事。しかし、同じ不満を持つ人が多かったのか pip-tools という PyPI パッケージが提供されていて、その中の pip-review を使うと

(PyVenv) fukuda@falcon:~/PyVenv% pip-review
Django==1.8.3 is available (you have 1.8.2)
(PyVenv) fukuda@falcon:~/PyVenv% pip-review -i
Django==1.8.3 is available (you have 1.8.2)
Upgrade now? [Y]es, [N]o, [A]ll, [Q]uit y 
てな具合に、半自動でアップグレードできる事がわかり、 とりあえずそれで満足していた。(常用の Python モジュールが何百も有る訣ではないので、これで十分。)

……という具合に、そこそこ幸せな生活をしていたのだが、 或る日アップグレードしようとしたら、 「pip-review コマンドが無い」と言われるようになった…… pip-tools の基本的な構成が(知らぬ間に)すっかり変ってしまった、 という事らしい。pip-review の代りに、pip-compile pip-sync というコマンドを使って、

(PyVenv) fukuda@falcon:~/PyVenv% pip freeze > requirements.in
(PyVenv) fukuda@falcon:~/PyVenv% emacs requirements.in  #1)
(PyVenv) fukuda@falcon:~/PyVenv% pip-compile 
#
# This file is autogenerated by pip-compile
# Make changes in requirements.in, then run this to update:
#
#    pip-compile requirements.in
#
click==5.1                # via pip-tools      #2)
django-gmapi-new==1.0.1.1
django-timezone-field==1.2
django==1.8.4
first==2.0.1              # via pip-tools
matplotlib==1.4.3
    ....
pytz==2015.4
readline==6.2.4.1
six==1.9.0                # via matplotlib, pip-tools, python-dateutil
(PyVenv) fukuda@falcon:~/PyVenv% pip-sync 
Collecting pip-tools==1.1.4
  Downloading pip_tools-1.1.4-py2.py3-none-any.whl
    ....
Installing collected packages: pip-tools
  Found existing installation: pip-tools 1.1.3
    Uninstalling pip-tools-1.1.3:
      Successfully uninstalled pip-tools-1.1.3
Successfully installed pip-tools-1.1.4    
    
てな具合に使う。上で、
  1. #1) 編集して、"==" 以下のバージョン情報を外す。 (ついたままにしていると、その版に固定 (pin down) する、という意味になる。)
  2. #2) requirements.in に無くても、依存関係を推定して、 自動で requirements.txt に加えてくれる。
Ports (MacPorts) なんかに比べると、若干「不自然」な感じもするが、 とりあえず、「コマンド一発(実は二発)」で、 環境を最新版に保つ事ができるようになった。 しかも MacPorts のパッケージと完全に切れている。

この tools も大したもんだが、でも使い勝手改善の一番の貢献者は、PyPI パッケージの「安定性」だろう。Readline や Numpy, matplotlib など、 MacPorts で結構悩まされた大物パッケージも、 大抵ノータッチでアップグレードされてきている。


2015-08-22 (Sat): 重い Python ……

Pip や pyvenv にどっぷり首までつかっている毎日だけど、まさか今更 Python 本体で悩む事になろうとは……

事のおこりは、Python の起動と停止にやたら時間がかかるようになってしまった事。 現在の Mac-mini は結構速くて、Python についても"% python" とタイプしてリターンキーを半分押したあたりでプロンプトが出てるんじゃないか、 と思う程だったのに :-)。 起動も停止も問題で、どちらも 30 秒から 1 分くらいもかかるようになってしまった。 今思えば、よくまあこんなになるまで放置していたな、と思うが、 問題の進行がゆっくりだったせいで我慢できてしまった……

さて、対策。 Py-27, py-34 両方で起きるし、 お決まりの Python 本体や module 群の再インストール等でもまったく改善せず、ちょっと焦ったが、 そのうち、.pyhistory.python_history が異常に大きくなっている事に気がついた。

fukuda@hawk:~% ls -l .py*
-rw------- 1 fukuda staff 610299904 Aug 10 15:21 .pyhistory
-rw-r--r-- 1 fukuda staff       390 Jul 22  2014 .pystartup
-rw------- 1 fukuda staff 714716651 Aug 10 15:20 .python_history
(これは MacBook (Mavericks) での結果で、Mac-mini (Yosemite) では、1GB を超えていた。) 試しにこれを小さくしたら覿面に起動が早くなった……。

まあ、それはそうだろう……Python ならずとも、1GB のファイルの読み書きは大 変だから。しかし、真の問題は、なんでこんなに history file が成長してしまったのか?だけど、こっちはどうもよく分らない。 .pyhistory.python_history の中身は全て正しい Python の命令のようなので、 どうやら、Python アプリの実行中にその命令が .pyhistory.python_history に流れ込んだようだ。 本来それは有り得ない(.pystartup は対話モードでだけ実行される)ので、 readline module か Python 本体のバグではないかと想像している。 (そもそも、.pyhistory.python_history の両方が作られて大きくなる、というのが摩訶不思議。やっぱり readline の問題かな。)

しかし今となっては根本原因は「藪の中」になってしまった。 件の「急成長」が、その後全く再現しないから。 とりあえず、history file の長さを制限するために、.pystartup

# 2015-08-28 (Fri) 補足: この設定は、Python-2.x でのみ有効(というか必要)
import atexit
import os
import readline

histfile = os.path.expanduser("~/.python_history")

try:
    readline.read_history_file(histfile)
except FileNotFoundError:
    pass 
readline.set_history_length(100000)
atexit.register(readline.write_history_file, histfile) 
として、使っているが、一月余りの使用で、
fukuda@falcon:~% wc .python_history
 278  367 4360 .python_history    
くらいにしかなってない。(278 行、4.4 kB。この調子では、「リミット」の 100,000 行 (2 MBくらい?) には一生届かない :-)
補遺: 2015-08-28 (Fri): また ~/.python_historyが「急成長」を始めたので、 今度は、ファイルが小さいうちに中身を眺めてみた。 すると、何故か同じ「コマンドのシーケンス」が何度も表われる…… 最初は訣が分らなかったが、ふと思いついて確かめてみたら、 Python を終了する度に、.python_history が二度書かれている。 当然ながら、その度にサイズも倍になる訣ですぐに「指数関数的爆発」になる。 要は、上の
atexit.register(readline.write_history_file, histfile) 
の行が、「ディフォルトの動作」で書き込んだ .python_history の後に、さらに同じ内容を書き足したと思われる。

ややこしいのは、Python-2.7.10 と Python-3.4.3 では、この「ディフォルトの動作」が違っている事で、前者では何もしない。 つまり、Python-2.7.x で、command history を使うために付け加えた上の .pystartup のせいで、Python-3.4.x では history file の倍々ゲームになっていた訣だ。

対策としては本来なら、「Python のバージョンを検出して……」 とやるべきなのだろうけど、今や Python-2.7.x とはトンと御無沙汰なので、 先々ひょっこり出てくるかも知れない面倒を避けるために .pystartup を空にしてみる事にした。 (全く無くしてしまうと、PYTHONSTARTUP がこれを指している場合は却って面倒。)

首尾は上々で、.python_history がちゃんと更新され、 倍々ゲームも無くなった。(但し、当然の事ながら Python-2.7.x ではヒストリーが使えない。)


167/1,729,066 Valid CSS! Valid HTML 5.0
Taka Fukuda
Last modified: 2016-01-25 (Mon) 17:16:13 JST