Pythonコードを3-way mergeする場合にKDiff3とP4Mergeのどちらを使うか検討

もう何年もKDiff3ユーザーですが、無料のマージツールならP4Mergeも良いよと聞いたので、少し試してみました。
MercurialやGitのマージツールとして使うため、3-way mergeが前提です。
できるだけ自動解決したいので、うまく自動解決する手段があればそちらを優先します。

マージ対象のコード

今回試したマージ対象のコードは以下の3つです。意図的にコンフリクトが発生するような内容にはしていますが、多人数で同じモジュールを変更するような開発をしていると、しばしばこのような状況は起こるかと思います。

base.py
class User:
    """ユーザークラス
    """
    def __init__(self, name):
        self.name = name
local.py

base.pyに対してGroupクラスをファイルの末尾に追記しています。

class User:
    """ユーザークラス
    """
    def __init__(self, name):
        self.name = name


class Group:
    """グループクラス
    """
    def __init__(self, name):
        self.name = name
        self.users = []
other.py

base.pyに対してPermissionクラスをファイルの末尾に追記しています。

class User:
    """ユーザークラス
    """
    def __init__(self, name):
        self.name = name


class Permission:
    """パーミッションクラス
    """
    def __init__(self, name):
        self.name = name

3-way mergeの比較

KDiff3とP4Mergeで前述の3つのファイルをマージしてみます。

KDiff3の場合

KDiff3は0.9.96のcjk対応のものを使っています。
yuja / kdiff3-mq-cjk — Bitbucket

未解決のコンフリクトは、クラス定義とその次の行になります。また、__init__コンストラクタとself.nameへの代入の行は同一とみなされて、自動解決されています。
この場合、未解決のコンフリクト部分でB(local.py)とC(other.py)の内容を単純に選択しても、正しくマージできないため、コピー&ペーストで編集することになります。

P4Mergeの場合

P4Mergeは2015.2を使っています。
Visual Merge and Diff Tools | Perforce

P4Mergeの場合も、未解決のコンフリクト位置と同一な位置の検出はKDiff3と同様でした。
P4Mergeでもこの場合はコピー&ペーストで編集することになります。

ベースを少し工夫した場合の3-way mergeの比較

前述の通り、マージ対象のコードのままではどちらを使っても自動解決できませんでした。
あらかじめ同じモジュールを編集することがわかっているのであれば、ベースのファイル(base.py)に少し手を加えることで、マージツールの差分検出にヒントを与えられます。

base.py(工夫したやつ)

分岐前にあらかじめ、追記する部分にコメントを入れておきます。

class User:
    """ユーザークラス
    """
    def __init__(self, name):
        self.name = name

# ここにGroupクラス

# ここにPermissionクラス
local.py(工夫したbase.pyからの編集)

編集する場合には、base.pyの該当箇所のコメントを削除した上でコードを記述します。この際、追記しない部分のコメントは、残しておくことに注意します。

class User:
    """ユーザークラス
    """
    def __init__(self, name):
        self.name = name

class Group:
    """グループクラス
    """
    def __init__(self, name):
        self.name = name
        self.users = []

# ここにPermissionクラス
other.py(工夫したbase.pyからの編集)
class User:
    """ユーザークラス
    """
    def __init__(self, name):
        self.name = name

# ここにGroupクラス

class Permission:
    """パーミッションクラス
    """
    def __init__(self, name):
        self.name = name
KDiff3の場合(対象を工夫した場合の3-way merge)

KDiff3では、この場合、変更がうまく検出されてコンフリクト部分は自動解決されました。

P4Mergeの場合(対象を工夫した場合の3-way merge)

一方P4Mergeでは、大変残念な結果になりました。マージ結果にコメントが残ったり、チグハグな状態です。

P4Mergeだと、同じ内容の行が衝突の前後位置に来ると差分検出で同一と判定されてしまうのでしょうか?

結論

  • KDiff3とP4Mergeのどちらでも自動解決できないパターンはある。
  • KDiff3の場合、コメント等でマージのヒントになるキーワードを事前に入れておけば、自動解決できることがある。

やっぱりKDiff3を使い続けることになりそう。

Djangoで複数のデータベースを使う際に、モデルごとに使われるデータベース名を取得する

Djangoで複数のデータベースを使う際に、データベースルーター(Database router)を設定すると、モデル毎にデータベースを切り替えたりできます。
Multiple databases | Django documentation | Django
データベースルーターが設定されている場合に、アプリケーション側で任意のモデルがどのデータベースを使うのかを取得するには、django.db.routerを使います。
試したバージョンは、Python3.5、Django1.9。

コード

myapp/models.py
from django.db import models

class Item(models.Model):
    name = models.CharField(max_length=20)
myproject/router.py

myappという名前のアプリケーションの場合はspamdbを使うルールのデータベースルーター

class MyAppRouter:
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'myapp':
            return 'spamdb'

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'myapp':
            return 'spamdb'

    def allow_relation(self, obj1, obj2, **hints):
        if obj1._meta.app_label == 'myapp' or \
           obj2._meta.app_label == 'myapp':
           return True

    def allow_migrate(self, db, app_label, model=None, **hints):
        if app_label == 'myapp':
            return db == 'spamdb'
myproject/settings.py(抜粋)
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    },
    'spamdb': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'spamdb.sqlite3'),
    }
}

DATABASE_ROUTERS = ['myproject.router.MyAppRouter']

django.db.routerを使って、データベース名を取得してみる

django.dbモジュールのrouter変数は、django.db.utils.ConnectionRouterクラスのインスタンスです。

>>> from django.db import router
>>> from myapp.models import Item
>>> router.db_for_read(Item)
'spamdb'
>>> router.db_for_write(Item)
'spamdb'
>>> from django.contrib.auth.models import User
>>> router.db_for_read(User)
'default'
>>> router.db_for_write(User)
'default'

モデルから、使用するデータベース名を取得できました。
ちなみに、Manager.db_managerによって取得したMangerインスタンスの場合は、dbプロパティにてアクセスできます。

>>> User.objects.db_manager('spamdb').db
'spamdb'

複数のレコードをまとめて更新する際にdjango-bulk-updateが便利

既存の複数のレコードをまとめて更新する際にdjango-bulk-updateというパッケージが便利だったので紹介します。
GitHub - aykut/django-bulk-update: Bulk update using one query over Django ORM

通常のDjangoのORMを使ったレコードの更新

Djangoで既存のレコードを更新するには、モデルインスタンスのsaveメソッドを呼ぶか、クエリセットのupdateを使うのが通常の方法です(Django1.9時点)

# Itemモデル
class Item(models.Model):
    value = models.CharField(max_length=20)
    class Meta:
        db_table = 'item'

# 新規作成(id=1で保存)
Item.objects.create(pk=1, value="spam")

# id=1のレコードの更新(saveメソッド)
item = Item.objects.get(pk=1)
item.value = "更新された値"
item.save()

# id=1のレコードの更新(QuerySetのupdateメソッド)
Item.objects.filter(pk=1).update(value="更新された値1")

これは、バックエンドにもよりますが、例えばMySQLの場合だとUPDATEクエリが実行されます。
複数件をまとめて更新する場合に、更新後の値がすべて同じであれば、クエリは単純でDjangoの標準ORMでも対応できます。

# 新規作成(id=1, 2, 3で保存)
Item.objects.create(pk=1, value="spam")
Item.objects.create(pk=2, value="egg")
Item.objects.create(pk=3, value="ham")

# id=1, 2, 3のレコードの更新(すべて同じ値にする)
Item.objects.filter(pk__in=[1, 2, 3]).update(value="更新された値")

しかし、一件ごとに違う内容で保存したい場合には、モデルインスタンスのsaveメソッドか、前述のupdateメソッドで何度もUPDATEクエリを実行することになります。
大量に更新する場合には、これだと時間がかかってしまいます。

raw SQLで対応するならどうやるか

例えばMySQLの場合、レコードの更新方法だと、UPDATE文以外にREPLACE文などもあります。またLOAD DATA INFILEでREPLACEを行う方法もあります。
ただしこれは、既存のレコードを削除した上で更新となるため、一部の列の内容だけを更新したい場合には、事前にSELECTした値を使う必要があったりして少し使いづらいことがあります。
更新対象のテーブルに主キー、または一意なキーがある前提の場合、UPDATE文のSETの値部分にCASE演算子を指定することで、一回のクエリの発行でそれぞれ別の値を更新できます。

mysql> SELECT * FROM item;
+----+-------+
| id | value |
+----+-------+
|  1 | spam  |
|  2 | egg   |
|  3 | ham   |
+----+-------+
3 rows in set (0.00 sec)

mysql> UPDATE item SET value=(
    ->  CASE id
    ->   WHEN 1 THEN 'updated_spam'
    ->   WHEN 2 THEN 'updated_egg'
    ->   WHEN 3 THEN 'updated_ham'
    ->  END)
    ->  WHERE id in (1, 2, 3);
Query OK, 3 rows affected (0.01 sec)
Rows matched: 3  Changed: 3  Warnings: 0

mysql> SELECT * FROM item;
+----+--------------+
| id | value        |
+----+--------------+
|  1 | updated_spam |
|  2 | updated_egg  |
|  3 | updated_ham  |
+----+--------------+
3 rows in set (0.00 sec)

他にもまとめて更新する方法はあるかと思いますが、ここでは言及しません。

django-bulk-updateを使う

django-bulk-updateを使うと、前述のUPDATEのSET部分にCASE演算子を使うクエリの発行をSQLの記述無しでできます。
PyPI上のパッケージ名はdjango-bulk-updateです。
django-bulk-update · PyPI

(venv)$ pip install django-bulk-update

使い方は、Djangoの標準ORMのbulk_createに似ています。
Django1.9、Python3.5、django-bulk-update1.1.8で試しました。

from bulk_update.helper import bulk_update

def my_update_task():
    # 更新対象のインスタンスを用意(明示的に生成せずに、QuerySetで取得したインスタンスでも可)
    updates = [
        Item(pk=1, value="updated_spam"),
        Item(pk=2, value="updated_egg"),
        Item(pk=3, value="updated_ham"),
    ]
    # value列だけを更新
    bulk_update(updates, update_fields=['value'])
    # 結果を表示
    print(Item.objects.values_list())

実行結果は次の通り。

>>> my_update_task()
[(1, 'updated_spam'), (2, 'updated_egg'), (3, 'updated_ham')]

MySQLの場合、実行されたクエリは次の通りです。

UPDATE `item` SET `value` = (CASE `id` WHEN 1 THEN 'updated_spam' WHEN 2 THEN 'updated_egg' WHEN 3 THEN 'updated_ham' END) WHERE id in (1, 2, 3)

このようにbulk_updateを使うことで、1クエリで複数件の更新を簡単にできるようになりました。便利。

2019/2/28追記

Django 2.2で同等の機能となるbulk_updateメソッドが実装されます。

django.db.models.query.QuerySet.bulk_update

jedi-vimのrpcフォークをWindowsで使ってみた

メモ書き。jedi-vimのjedi部分をRPCで動かすforkを@nakamurayが作ってくれたのでWindowsで動かしてみてた。
https://github.com/nakamuray/jedi-vim/tree/rpc
https://github.com/nakamuray/jedi-vim/tree/rpc2
そのままではWindowsで動かなかったので、手元で少し修正して使った。

virtualenvでも問題なく動作しているのでありがたい。

Windowsで動かすために修正したところ

  • Windowsだとstderrを指定しないと例外が発生していたので指定した(os.devnull)
  • サブプロセスの起動時にコマンドプロンプトのウィンドウが表示されてしまうのでstartupinfoを指定

作業のお供にノンアルコールビール(主にドライゼロ)

この記事はpyspa Advent Calendar 2015の2日目の記事です。
Python温泉は主催者の意向により、酒を飲むのは禁止となってます。
ノンアルコールビールは、酒としてカウントされないのでOKとなっています 。
前回のPython温泉で、「ノンアルコールビールでもドライゼロはうまい」とのタレコミがあり、どれどれと飲んでみたところ、確かにうまい。
参加者たちの間で人気となり、熱海周辺のスーパーマーケットでドライゼロを買い占める集団となってしまいました。

自分も飲んでみて、「これは新しい選択肢だ」と思ったので、この記事で少し紹介したいと思います。

ビールと比べてどうなのか?

ドライゼロの成分表はウェブサイトで確認できます。
アサヒ ドライゼロ | アサヒビール
まず、ドライゼロについては、アルコール分が0.00%でノンアルコールです(0%じゃないノンアルコールもあるので注意しよう)
飲んでも酔いませんし、自動車の運転もできます。
カロリー、糖質も0gとなっています。
後味はすっきりです。ブラックのほうは少し癖がありました。
うまみについては、生ビールと比べると少し物足りなさを感じます。

清涼飲料水やいわゆるノンアルコールの飲み物としてはどうなのか?

では酒ではなく、コーヒーや紅茶、ジュースなどと比べるとどうなのか。
カロリー、糖質が0gとなっている飲み物はノンアルコールビール以外にもありますね。
ノンアルコールビールは、 甘くない(むしろ苦い) カテゴリの飲み物ですね。
コーヒーなどと比べると、 ノンカフェイン の飲み物でもあります。
また、炭酸飲料でもあります。

価格

酒ではないので酒税がかからないため、ビールよりは安いですが、同容量のジュースよりは高めになっているようです。
2015年現在、350ml缶で120円前後で販売されているようです。

飲み物の選択肢として

ノンカロリー、ノンカフェイン、甘くない、炭酸を含む、だとウィルキンソンの炭酸水などが該当しますが、ノンアルコールビールも同様に選択肢としてはありだと思います。

作業のお供に試してみてはいかがでしょうか?

プログラマーは飲み物をつい過剰摂取してしまいがちなデスクワークですし、この機会に飲み物について見なおしてみてはどうでしょうか?
炭酸水のような甘くない炭酸飲料に抵抗がないのであれば、是非ドライゼロを試してみることをおすすめします。

Delphiでインターフェースを使う

久々にDelphiを少し触ってました。かなり忘れてる。
Delphiのインターフェースを使って2つの実装クラスを作り、実行時にどちらのクラスを使うか決定してる。
試したバージョンはDelphi XE4。

Main.dpr

program Main;
{$APPTYPE CONSOLE}

uses
  System.Classes,
  System.SysUtils,
  System.Generics.Collections;

type
  IStore = interface
  { データを読み書きするインターフェース }
    procedure SetValue(Key, Value: String);
    function GetValue(Key: String): String;
  end;

  TStore = class(TInterfacedObject, IStore)
  { データを読み書きするベースクラス }
  public
    procedure SetValue(Key, Value: String); virtual; abstract;
    function GetValue(Key: String): String; virtual; abstract;
  end;

  TMemoryStore = class(TStore)
  { データをメモリ上にのみ保持するクラス }
  private
    StoredData: TDictionary<String, String>;
  public
    constructor Create;
    destructor Destroy; override;
    procedure SetValue(Key, Value: String); override;
    function GetValue(Key: String): String; override;
  end;

  TFileStore = class(TStore)
  { データをファイルに保持するクラス }
  private
    Loaded: Boolean;
    FilePath: String;
    StoredData: TDictionary<String, String>;
    procedure Load;
    procedure Save;
  public
    constructor Create(FilePath: String);
    destructor Destroy; override;
    procedure SetValue(Key, Value: String); override;
    function GetValue(Key: String): String; override;
  end;

constructor TMemoryStore.Create;
begin
  WriteLn('-- TMemoryStore --');
  StoredData := TDictionary<String, String>.Create;
end;

destructor TMemoryStore.Destroy;
begin
  StoredData.Free;
end;

procedure TMemoryStore.SetValue(Key, Value: String);
begin
  StoredData.AddOrSetValue(Key, Value);
end;

function TMemoryStore.GetValue(Key: String): String;
begin
  if StoredData.ContainsKey(Key) then
    Result := StoredData.Items[Key]
  else
    Result := '';
end;

constructor TFileStore.Create(FilePath: String);
begin
  WriteLn('-- TFileStore --');
  Loaded := False;
  Self.FilePath := FilePath;
  StoredData := TDictionary<String, String>.Create;
end;

destructor TFileStore.Destroy;
begin
  Save;
  StoredData.Free;
end;

procedure TFileStore.SetValue(Key, Value: String);
begin
  { データベースの遅延読み込み }
  if not Loaded then Load;
  StoredData.AddOrSetValue(Key, Value);
end;

function TFileStore.GetValue(Key: String): String;
begin
  { データベースの遅延読み込み }
  if not Loaded then Load;
  if StoredData.ContainsKey(Key) then
    Result := StoredData.Items[Key]
  else
    Result := '';
end;

procedure TFileStore.Load;
var
  Buffer: TStringList;
  Index: Integer;
  KeyValuePair: TArray<String>;
begin
  Buffer := TStringList.Create;
  if FileExists(FilePath) then
    try
      WriteLn('Loading database...');
      Buffer.LoadFromFile(FilePath);
      for Index := 0 to Buffer.Count - 1 do
      begin
        KeyValuePair := Buffer[Index].Split([','], 2);
        StoredData.AddOrSetValue(KeyValuePair[0], KeyValuePair[1]);
      end;
    finally
      Buffer.Free;
    end;
  Loaded := True;
end;

procedure TFileStore.Save;
var
  Buffer: TStringList;
  Key: String;
begin
  Buffer := TStringList.Create;
  try
    for Key in StoredData.Keys do
    begin
      Buffer.Add(Key + ',' + StoredData.Items[Key]);
    end;
    Buffer.SaveToFile(FilePath);
  finally
    Buffer.Free;
  end;
end;

function StoreFactory(FilePath: String = ''): IStore;
begin
  { FilePathが指定されているならTFileStore、なければTMemoryStoreを使います }
  if FilePath <> '' then
    Result := TFileStore.Create(FilePath)
  else
    Result := TMemoryStore.Create;
end;

var
  Store: IStore;

begin
  { メモリリーク検出 }
  ReportMemoryLeaksOnShutdown := True;

  { コマンドライン引数の有無で実装を切り替える }
  if ParamCount > 0 then
    Store := StoreFactory(ParamStr(1))
  else
    Store := StoreFactory;

  { 値の書き込みと取得 }
  Store.SetValue('MyKey', 'MyValue');
  WriteLn(Store.GetValue('MyKey'));
end.

実行結果

C:\MyApp>dcc32 Main.dpr
Embarcadero Delphi for Win32 compiler version 25.0
Copyright (c) 1983,2013 Embarcadero Technologies, Inc.
Main.dpr(169)
170 行, 0.14 秒, コード 919344 バイト, データ 35392 バイト

C:\MyApp>Main
-- TMemoryStore --
MyValue

C:\MyApp>Main database.txt
-- TFileStore --
Loading database...
MyValue

外部入出力の部分は実装の切り替えができると、自動テストのときに便利です。DIコンテナは大仰だなと思ったら、このぐらいでもいいかもしれませんね。
また、TStoreはTInterfacedObjectを継承しているので、明示的にFreeを呼ばなくても、開放されるようにしています。

通信料金の計算2015年秋

通信料金がいくらぐらいかかっているのか計算などやってたので、メモを残す。

現状の使い方

  • auガラケー、Nexus4、PocketWifi(GL10P)の3台を持ち歩いてる
  • Nexus4はデータ通信専用、SORACOM AirのSIMを挿してるけど、できるだけPocketWifiのほうを使ってる。
  • b-mobileのSIMをしばらく使っていたけど、期限が切れたタイミングでSORACOM Airに変更した。

通信料金

  • au使用料(ガラケー) 2060円/月
  • ワイモバイル(PocketWifi) 4535円/月
  • SORACOM Air(Nexus4/s1.standard) 約500円/月

合計: 7095円/月

感想

auガラケーは滅多に通話しないので安い。
ワイモバイルがやっぱりちょっと高いので、同等のもので安く抑える方法を模索したい。