きどたかのブログ

いつか誰かがこのブログからトラブルを解決しますように。

LightGBMの目的関数、評価関数の話

しっかり調べられてないけど、
考えを整理するためにメモっておく。


LightGBMの目的関数は、パラメーターでobjectiveを何にするかによって決まるのが原則。
binaryを選んだ場合、src/objective/binary_objective.hppが使われる。
これはBinaryLoglossになる。


このような対応関係は、src/objective/objective_function.cppを読めば分かる。


目的関数を独自のものにする場合、lgb.trainやcvにfobjを渡す。


一方で、fevalというのがある。
これは目的関数ではない。
early_stoppingの時には使われるし、
evals_resultにも関係する。

fevalを与えない場合、early_stoppingは何を使って判断するかというと、パラメーターのmetricが使われる。
ちょっと細かく書くとこうなってるはず。


feval未指定、かつmetric未指定なら、objectiveが使われる。


feval未指定、かつmetric指定かつ、first_metric_only未指定なら、metricが使われ、metricが複数指定されていた場合、どのmetricも改善され続けないといけない。


feval未指定、かつmetric指定かつ、
first_metric_only=Trueなら、
metricの1番目のもののみでearly_stoppingする


feval指定、かつmetric指定かつ、
first_metric_only未指定の場合はどうなるか?metric、fevalの全てで改善が必要なはず。


feval指定、かつmetric指定かつ、
first_metric_only=Trueの場合はどうなるか?
evals_resultはmetric、fevalの順番で作成されているはずで、metricの1番目で評価するはず。


ひょっとすると、fevalとfirst_metric_onlyは良くない組み合わせなのかもしれない。
metricを指定しない場合、objectiveが使われるはずなので、fevalを使ったearly_stoppingにはならないのではないだろうか。

LightGBM + Optuna でベストモデル保存

さいきん書いたんで、どういうコードを書いたかをテキトーに書く。

ちなみに、公式のモデル保存の例はこちら。

https://optuna.readthedocs.io/en/stable/faq.html#how-to-save-machine-learning-models-trained-in-objective-functions

嫌だ、trial数分のモデルを保存するだなんて!


では、話を戻して。。。

今回の実装の制限事項は、シリアルでしか正しく動かないことかな。

準備するもの

Objectiveクラス
Callbackクラス

Objectiveの実装

Objectiveクラスは、__call__(self, trial)を実装。
__init__で、X_trainなどに加えて、Callbackクラスも受け取る。
__call__(self, trial)内で、学習したのち、良い悪いに関わらず、出来たモデルをCallbackクラスに引き渡す、その際のキーは、trialのnumberにする。
max_binもチューニングしたいなら、lgb.Datasetは毎回作らないといけない。

class Objective:
    def __init__(self, X_train, y_train, X_test, y_test, callback):
        self.X_train = X_train
        self.y_train = y_train
        self.X_test = X_test
        self.y_test = y_test
        self.callback = callback

    def __call__(self, trial):
        # do something
        params = self.create_params(trial)
        train_set = lgb.Dataset(self.X_train, self.y_train)
        valid_set = lgb.Dataset(self.X_test, self.y_test, reference=train_set)
        pruning_callback = optuma.integration.LightGBMPruningCallback(trial, 'binary_logloss')
        model = lgb.train(params, train_set, valid_sets=[valid_set], callbacks=[pruning_callback])
        self.callback.register_model(trial.number, model)
        y_test_pred = np.rint(model.predict(self.X_test))
        auc_test = roc_auc_score(self.y_test, y_test_pred)
        return 1 - auc_test
        
    def create_params(self, trial):
        return something trial suggestion code

Callbackの実装

Callbackクラスは元来、study.optimize()に渡すものです。今回も渡しますけどね。
__call__(self, study, trial)を実装。
study.best_trial.numberがtrial.numberと一致したら、ベストが更新されてます。だったら、登録されてるはずのモデルを取り出せるはずです。ベストが更新されてないなら、要らないモデルが登録されてるはずです、消してしまいましょう。
モデルの登録はただのdictを使うだけです。
ついでに、bestが更新されたら、best_paramsをjsonにして保存したり、更新されなくても、studyのdataframeを毎回保存したりなんかも出来ます。これをしてると、何トライアル目かを高速に流れるログから探さなくてすみます。

class Callback:
    def __init__(self):
        self.models = {}
    def register_model(self, trial_number, model):
        self.models[str(trial_number)] = model
    def unregister_model(self, trial_number):
        self.models.pop(str(trial_number), None)
    
    def unregister_other_model(self, trial_number):
        model = self.models.pop(str(trial_number), None)
        self.models.clear()
        self.models[str(trial_number)] = model
    def get_model(self, trial_number):
        return self.models[str(trial_number)]
    def __call__(self, study, trial):
        if study.best_trial.number == trial.number:
            self.unregister_other_model(study.best_trial.number)
            self.save(study)
        else:
            self.unregister_model(trial.number)
            # save study.trials_dataframe()
    def save(self, study):
        model = self.get_model(study.best_trial.number)
        # save model
        # save study.best_params
        # save study.trials_dataframe()


思ったより終わらずに、止めたくなることがある。そういう時に備えて、ベスト更新時に、ファイル保存。ベスト未更新でもtrials_dataframe()を毎回ファイル保存。

利用例

callback = Callback()
objective = Objective(X_train, y_train, X_test, y_test, callback)
sampler = optuna.samplers.TPESampler(seed=1)
study = optuna.study.create_study(sampler=sampler)
study.optimize(objective, n_trials=100, callbacks=[callback])

best_model = callback.get_model(study.best_trial.number)

再現性を持たせたい時には、TPESamplerのseedを固定する必要がある。

ベストモデルを毎回保存するなら、Callbackの__call__の中で書けばいい。
最後にcallbackから取り出して保存するなら、optimize後に取り出して保存すればいい。


add_modelを明示的に呼ぶのがやはりださい。

callback = Callback()
objective = Objective(X_train, y_train, X_test, y_test, lambda x, y: callback.add_model(x, y))

こっちのパターンのほうが汎用性が高いか?
結局はobjective側で何を登録するのか決めてるので、利点はなさそう。

Go言語で、ファイルの「所有者」を拾えるか? You、DLL呼び出しちゃいなよ

発端

java.nio.file.attributeで「所有者」情報が取れる。
PowerShellでも取れる。
では、Go言語はどうだ?

調査

たとえば、C++でそれを行う場合、こういうことをする。
Finding the Owner of a File Object in C++ (Windows)


ここで登場する「GetSecurityInfo関数」はGo言語で使われているか?
調べた結果、使われていない。

大抵のdll呼び出しは、"src/syscall/zsyscall_windows.go"を読むと分かる。

「GetSecurityInfo関数」は、「advapi32.dll」に含まれています。
GetSecurityInfo function (Windows)


Go言語は、内部で「advapi32.dll」を読み込んではいるものの、「GetSecurityInfo関数」を呼び出せるようにはなっていません。
今見ているソースはGo1.8です。

挑戦

ネジがゆるい私は、「よし、DLL呼び出してみよう」と、泥船に足を突っ込んだ。

Go内部では、ロードしているDLLですが、変数が公開されていません。
しかたない、自分でロードすんぜ。

var (
    modadvapi32          = syscall.NewLazyDLL("advapi32.dll")
    procGetSecurityInfo  = modadvapi32.NewProc("GetSecurityInfo")
)

はい、ただのもろパクリです。
あ、でもsysdll.Add()を挟むのは除外しました、なぜならばinternalはコンパイルエラーになるからです。

NewLazyDLLやNewProcについては、「src/syscall/dll_windows.go」に書かれています。

Procには呼び出し用のポインタレシーバが付いてます。

func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) 

とりあえず、引数を全部uintptrで渡せってことか。

1つ目の引数 HANDLE

handleには、ファイルハンドルを渡すことになる。
os.OpenFileして、fileからFdを得ると、それはuintptrなので、そのまま渡してみる。

2つ目の引数 SE_OBJECT_TYPE

今回は、SE_FILE_OBJECTを渡したいわけです。
SE_OBJECT_TYPE enumeration (Windows)

enumで先頭が0初期化してますから、2つ目の定数SE_FILE_OBJECTは、1ってことですね。
これをuintptrで渡すとか、まどろっこしいなぁ。

3つ目の引数 SECURITY_INFORMATION

今回はOWNER_SECURITY_INFORMATIONを渡したいので、おそらく1です
DWORDとあるので、uint32にしときます。

4つ目の引数 ppsidOwner 型はPSID

これが受け取りたい。ぜひ受け取りたい。
でもPSIDって何?
どうやら、可変長な構造であるSIDをポイントしているポインターっぽい。

var psidOwner = make([]byte, uint32(50))
sid := (unsafe.Pointer(&psidOwner))
ppsidOwner := uintptr(unsafe.Pointer(sid))

ちょっと変数名をどうするか、ちぐはぐな感じになったが、こんな感じで書いてみた。
これは、「src/syscall/security_windows.go」にあるfunc LookupSIDを参考にしている。

残りの引数

どうせオプションだし、渡さないことにする。

ダメだった点

1をあらわしている引数をuintptrにして渡してるのがどうもダメっぽかったので修正。
最終的にこの形にした。1をベタ書きです。。。ひどい。なんだコレ。

  r1, r2, lastErr := procGetSecurityInfo.Call(handle, 1, 1, ppsidOwner)


なんとか結果が返ってきた。
ここからバッファ渡してるやつをポイントしなおす。

    ownerSID := (*syscall.SID)(unsafe.Pointer(&psidOwner[0]))
    str, convertErr := ownerSID.String()
    if convertErr != nil {

    } else {
        fmt.Printf("string SID = %s\n", str)
    }

    account, domain, accountType, lookUpErr := ownerSID.LookupAccount("")
    if lookUpErr != nil {

    } else {
        fmt.Printf("account = %s\n", account)
        fmt.Printf("domain = %s\n", domain)
        fmt.Printf("accountType = %d\n", accountType)
    }

ownerSID.String()すると「S-X-X-XX-XXXXXXXX」みたいな形式のSIDが取れてます。
ownerSID.LookupAccount("")するとアカウントがとれました。
このあたりのことは「src/syscall/security_windows.go」を見てください。


こまごました点は考慮していませんが、DLL呼び出せちゃいました。
.

Go言語でzipファイルを作る、ただしWindowsに限る

Go言語でzipファイルを扱うとき、とりあえず"archive/zip"を使いますよね。

しかし、使ってみて、気付いたことがある。

思っていたzipファイルを作ることができなかった。

ちなみに今は古いgo 1.6.2を使ってます。最新は1.8です。

zipのエントリ(zip.FileHeader)を作る方法が複数あるみたいだ。

  • ファイルパスから作る writer.Create(relativepath)
  • FileInfoから作る zip.FileInfoHeader(fileInfo)
  • 自分で作る &zip.FileHeader{}

ファイルパスから作ると細かいことができない。
FileInfoから作ると、unix形式(rwxrwxrxw)が強制される <- SetModeを呼ばせたくない。
仕方ないから自分で作るよ。

属性

一般的なファイルは"A"が7zで表示されます。エクスプローラーの表示を変えれば属性も出せます。

setModeを呼び出すとホストOSがUnixになり、属性もrwx形式になる。
そのため、それは迂回して、自分でExternalAttrsを設定しないといけない。

とりあえず、何を設定すればどうなるのか手で打ち込んでチェックしてみた。

D  ディレクトリ               <- fh.ExternalAttrs = 16
R  読み取り専用               <- fh.ExternalAttrs =  1
H  隠しファイル               <- fh.ExternalAttrs =  2  
A  アーカイブ                 <- fh.ExternalAttrs = 32
S  システム ファイル          <- fh.ExternalAttrs =  4 
I  非インデックス対象ファイル <- 不明。。。力尽きた。
L  再解析ポイント             <- fh.ExternalAttrs = 1024
-  その属性以外

上記の調査は、だいたいあってると思う。
src/syscall/ztypes_windows.go - The Go Programming Language
https://msdn.microsoft.com/ja-jp/library/windows/desktop/gg258117(v=vs.85).aspx


あ、これでいいのかも。

  fh.ExternalAttrs = data.FileAttributes

すみません、dataっつーのは、更新日のところで説明しますが、
fileInfo.Sys().(*syscall.Win32FileAttributeData).FileAttributesということです。

ファイル所有者

java.nio.file.attributeならファイル所有者の情報を取得できるらしい。
goの場合、データをどこから抜いてくるのか発見できてない。
できるんだろうか??

そして、どうやって詰めるんだっけ・・・。
どうも、zipにするときには詰められないように思える。
7zには所有者情報は見えないし。
とりあえず、zipのためには取得する必要はなさそうだ。

おそらく、これを実現するには、fileの情報をもとに、セキュリティ情報を拾って、
その情報をもとにルックアップするイメージなんだろう。(C++でもJavaでもそんなイメージ)

更新日時、作成日時、アクセス日時

更新日時のみであれば、FileHeader#SetModTime(info.ModTime())で十分です。
作成日時とアクセス日時は、FileHeaderのExtraを設定する必要がある。
作成日時とアクセス日時は、FileInfoのSys()から取得する。
とりあえず以下の記事は参考になるでしょう。

 data := info.Sys().(*syscall.Win32FileAttributeData)

これを使ってExtraに突っ込む。
注意事項はリトルエンディアンであるということだ。
どういう[]byteになればいいのかは適宜バイナリエディタでzipファイルを比較確認するほうがいい。
理屈的な形式は、ここが参考になったので是非ご覧いただきたい。
https://opensource.apple.com/source/zip/zip-6/unzip/unzip/proginfo/extra.fld


トルエンディアンを簡単に扱うためには、
"encoding/binary"パッケージのLittleEndianを使うほうが間違いがないが、
bufの先頭から詰めてくるので、Extraのbyte配列に直接詰め込めない。

  binary.LittleEndian.PutUint32(buf, data)

とりあえず自分はこんな感じで書いた。間違ってたらスマン。
序盤は自前でエンディアンを意識して書いたが、後半はbinaryパッケージを利用する関数を用意した。

  data := info.Sys().(*syscall.Win32FileAttributeData)
  var extra []byte
  extra = append(extra, 0x0a, 0x00)             // NTFS @Short
  extra = append(extra, 0x20, 0x00)             // TSize @Short
  extra = append(extra, 0x00, 0x00, 0x00, 0x00) // Reserved @Long
  extra = append(extra, 0x01, 0x00)             // NTFS attribute Tag @Short
  extra = append(extra, 0x18, 0x00)             // Size of attribute#1 @Short
  extra = appendUint32(extra, data.LastWriteTime.LowDateTime)
  extra = appendUint32(extra, data.LastWriteTime.HighDateTime)
  extra = appendUint32(extra, data.LastAccessTime.LowDateTime)
  extra = appendUint32(extra, data.LastAccessTime.HighDateTime)
  extra = appendUint32(extra, data.CreationTime.LowDateTime)
  extra = appendUint32(extra, data.CreationTime.HighDateTime)

あ、すみませんが、infoってのはFileInfoですよ。
filepath.Walkを呼ぶので、WalkFuncを実装していて、引数でFileInfoが渡ってきます。

ホストOS

7zで言うところのホストOSは、FileHeaderのCreatorVersionに相当する。

  fh.CreatorVersion = 11 << 8

意味不明な数字がでてくるでしょう。
goのstructを見ると数字が解るので、それを8ビットシフトして使えばなんとかなる。
この11はNTFS

Creating RHEL7.3 Vagrant Box using Packer and VitualBox on Windows10

準備するもの。
  1. VirtualBox + Extension Pack
  2. Packer
  3. Vagrant
  4. RHEL

あと、ハッシュ計算できるコマンドも必要かな。
自分の場合はpowershell立ち上げたらmd5sumなどが見つかったけど。
とりあえず、RHEL以外をインストールしてくださいな。

RHELの話

Red Hat Developer Programに参加して、
(登録は必要だけど)無償でRHELを手にいれます。
あくまで個人用で、VirtualBox上で動かします。
VirtualBoxで動かす例は、Red Hatのページでも手順が公開されていますが、
手でペチペチインストールするのが面倒臭くなるので徐々に自動化します。

はじめは手でインストール

anaconda-ks.cfgが欲しいじゃないですか。
それを手直しして、とりあえずこういう感じのにしました。
firewallを書き忘れてたり、ツッコミどころはたくさん。
ミニマムインストールなのに、レポジトリの追加もしてます。
いつか使うかもしれんから勉強のために追加してるだけです。

auth --enableshadow --passalgo=sha512
repo --name="Server-HighAvailability" --baseurl=file:///run/install/repo/addons/HighAvailability
repo --name="Server-ResilientStorage" --baseurl=file:///run/install/repo/addons/ResilientStorage
text
cdrom
firstboot --enable
ignoredisk --only-use=sda
keyboard --vckeymap=jp --xlayouts='jp'
lang en_US.UTF-8 --addsupport=ja_JP.UTF-8
network --bootproto=dhcp --device=enp0s3 --onboot=true --ipv6=auto
network --hostname=localhost.localdomain
rootpw --iscrypted $6$VzSTNp8xLdgMSJNe$Ns6mWbts4wIV9NRt.mZhDaQZLSKuzfEwZ.gXDw3hmVTJtkYbERPhB3jil.OghoOc6yEBtQe6tz9ovbMVvB18E/
services --disabled="chronyd"
timezone Asia/Tokyo --isUtc --nontp
user --name=vagrant --password=$6$hyGjqRR76Cf5KOF9$gwz6bfU8HvYB.15VjAIdKWB8jJAAJ8rlS9kuv6gWfAZNvN9JkmCCJDepiFM4eYNexD2E0.CRjXoEJwOloJUVF. --iscrypted --gecos="vagrant"
bootloader --append=" crashkernel=auto" --location=mbr --boot-drive=sda
autopart --type=lvm
clearpart --none --initlabel
eula --agreed
reboot

%packages
@^minimal
@core
kexec-tools

%end

%addon com_redhat_kdump --enable --reserve-mb='auto'

%end

%anaconda
pwpolicy root --minlen=6 --minquality=50 --notstrict --nochanges --notempty
pwpolicy user --minlen=6 --minquality=50 --notstrict --nochanges --notempty
pwpolicy luks --minlen=6 --minquality=50 --notstrict --nochanges --notempty
%end

%post

%end

ネットワークはonboot付けてないと面倒です。
付け忘れた場合はあとでks.cfgのfetchに失敗しますよ。
ユーザとパスワードは以下のとおりです。
root/vagrant
vagrant/vagrant
まあ、Vagrant用のBoxを作る前提ですからね。
日本語うざいんで、langをen_US.UTF-8をメインにしてます。

Packerのファイルを作る

ks.cfgを使う設定ファイル(rhel73.json)を書いた。

{
  "description": "creating rhel 7.3 vagrant box",
  "builders": [
	{
	  "type": "virtualbox-iso",
	  "vm_name": "packer-rhel73",
	  "guest_os_type": "RedHat_64",
	  "iso_url": "file:///C:/Users/{{user `username`}}/Downloads/rhel-server-7.3-x86_64-dvd.iso",
	  "iso_checksum": "34a65dbdfb8d9bb19b3a03d278df2a99",
	  "iso_checksum_type": "md5",
	  "ssh_username": "root",
	  "ssh_password": "vagrant",
	  "shutdown_command": "echo 'packer' | sudo -S shutdown -P now",
	  "http_directory": ".",
      "vboxmanage":[
		["modifyvm", "packer-rhel73", "--memory", "2048"],
		["modifyvm", "packer-rhel73", "--cpus", "2"]
	  ],
	  "boot_command": [
		"<esc><wait>",
         "linux ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg ksdevice=enp0s3<enter>",
		 "<wait>"
      ]
	  
	}
  ],
  "provisioners":[
	{
		"type": "shell",
		"inline": [
			"cat <<__EOF > /etc/sudoers.d/vagrant",
			"vagrant ALL=(ALL) NOPASSWD:ALL",
			"__EOF",
			"chmod 440 /etc/sudoers.d/vagrant"
		]
	},
	{
		"type": "shell",
		"inline": [
			"mkdir -p /home/vagrant/.ssh",
			"chmod 700 /home/vagrant/.ssh",
			"chown vagrant:vagrant /home/vagrant/.ssh",
			"cat <<__EOF > /home/vagrant/.ssh/authorized_keys",
			"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key",
			"__EOF",
			"chmod 600 /home/vagrant/.ssh/authorized_keys",
			"chown vagrant:vagrant /home/vagrant/.ssh/authorized_keys"
		]
	},
	{
		"type": "shell",
		"inline": [
		    "mkdir -p /media/cdrom",
			"mount /dev/cdrom /media/cdrom",
			"cat <<__EOF > /etc/yum.repos.d/rhel-dvd.repo",
			"[rhel-dvd]",
			"name=Red Hat Enterprise Linux 7.3 - x86_64 - DVD",
			"baseurl=file:///media/cdrom/",
			"enabled=0",
			"gpgcheck=1",
			"gpgkey=file:///media/cdrom/RPM-GPG-KEY-redhat-release",
			"__EOF",
			"yum -y --enablerepo=rhel-dvd install bzip2 gcc make kernel-devel",
			"yum -y --enablerepo=rhel-dvd groupinstall \"X Window System\"",
			"mkdir -p ~/guestaddition",
			"mount -t iso9660 -o loop ~/VBoxGuestAdditions.iso ~/guestaddition",
			"~/guestaddition/VBoxLinuxAdditions.run",
			"umount ~/guestaddition",
			"rm -rf ~/guestaddition ~/VBoxGuestAdditions.iso"
		]
	},
	{
		"type": "shell",
		"inline": [
			"yum -y --enablerepo=rhel-dvd install unzip lsof psmisc",
			"umount /media/cdrom",
			"eject -r /dev/cdrom"
		]
	}
  ],
  "post-processors": [
	{
	  "type": "vagrant",
	  "compression_level": 1
	}
  ]
}

vm名なんかは変えてください。
これは手インストール時に使った名前と被らないようにしただけです。
yum.repos.dへの追加には、addonの類が入ってませんが、好みで追加してくださいな。
ksdeviceを書くのは過去の経験からです。
奇妙なヒアドキュメントの書き方がありますが、これで動きます。
あの配列は改行コード付きで連結されるという仕様があるためです。

簡単に言うとpackerは自前サーバを立てて、http_directoryにあるks.cfgを公開します。
bootコマンドを受け付けた側は、ネットワーク経由でそのks.cfgを見に行きます。
そのため、このときにハマるのはたいていネットワーク系の問題です。

あと、今回メモリは2GBしか割り当ててないですが、RHELは4GB(推奨8GB)です。
おそらくそのあたりが原因でkdumpサービスが起動できてないように思います。

いざビルド
 packer build -var "username=myname" rhel73.json

packerがbootコマンドをチンタラ書いてる様子、いとおかし。
自分の記憶では、その部分はgo-vncで投げていたはずです。


あとは、vagrant box addとかしてください。

細かいことは抜きにすれば、おもちゃにできるRHELを手に入れられたので、
RHELのコアダンプを読む方法について勉強してみようかな。


追記:
反省。
いざ起動してみると光学ドライブがないという話・・・。
vboxmessageに何か加えればいけるやろ。

jmockitのDeencapsulationでstatic finalを書き換えできるの?それは使ってるバージョンによる!

今、迷っている。
書き換えられるっぽい過去の経験、書き換えられない最近の経験が混ざっている。


よし、確かめよう。
ダラダラ書く。

Deencapsulationのコードをgithubで読んだ。(もちろんコール先も読んだ)
https://github.com/jmockit/jmockit1/blob/master/main/src/mockit/Deencapsulation.java
static finalを書き換える特殊なコードは書かれていないようだった。
実際に、自宅の環境では書き換えに失敗している。


自分は、こういうことを予想していた。
modifiersの書き換えをしているんじゃないだろうか。
一旦finalを外して書き込むとか。
ところがそういうのをやっていないようだった。
過去の経験はまやかしか?


実際に、modifiers書き換えによるstatic finalフィールドへの書き込み例はネットでも見受けられる。
final修飾子を除去するコードを書いて試している。
だけども、この方法は完璧ではないと感じたことがあるので書いてみる。

①そもそも正攻法ではない。
Fieldオブジェクトのmodifiersのフィールド名称は実装に依存するから、正式な方法とはいえない。
誰でもそう感じるだろう。

②書き換えた内容が残らない(Fieldオブジェクトのmodifiersの話)
Fieldオブジェクトのmodifiersを変更したとしても、
ClassオブジェクトからgetDeclaredFieldし直すと、
そのmodifiersは本来の値に戻っている。
これはFieldオブジェクトが別物なんだから多少納得がいくだろう。
ちなみにFieldクラスのequalsメソッドはmodifiersを評価していないようだ。

③modifiersの一貫性が損なわれる(FieldオブジェクトのoverrideFieldAccessorの話)
次の例ではまた別のことを考える必要がある。
getDeclaredFieldを叩いたのちに、setAcessible(true)して、setに失敗しているコードがまず存在していて、
その後、取り直した別のFieldオブジェクトに対してmodifiers書き換え版のコードでsetを試みるとしよう。
この場合、書き換えは失敗する。

これの怖くて面白いところは、Fieldオブジェクトそのものが返すmodifiersと、
内部のaccessorが持つmodifiers関連変数の一貫性がなくなることだ。

逆にmodifierを書き換えるコードを先に成功させてしまえば、accessorは書き換え可能な状態で残る。
accessorについては、2度目のgetDeclareFieldでも同じインスタンスが設定されているようだ。

ここで言っているaccessorというのはsun.reflect.UnsafeFieldAccessorImpl系の話をしている。
内部で型に応じたUnsafeFieldAccessorImplのサブクラスが作られている。
setするときは、overrideFieldAccessorフィールドに置かれるっぽい。
final付きのFieldの場合とそうではない場合で、setされるaccessorは異なる。
isFinal変数を持つクラスと、isReadOnlyを持つクラスがいて、
isReadOnlyをfalseに書き換えるとfinal修飾子が付いたまま書き換えることができるようになる。

  1. isFinal : abstract class sun.reflect.UnsafeFieldAccessorImpl extends sun.reflect.FieldAccessorImpl
  2. abstract class sun.reflect.UnsafeStaticFieldAccessorImpl extends sun.reflect.UnsafeFieldAccessorImpl
  3. isReadOnly : abstract class sun.reflect.UnsafeQualifiedStaticFieldAccessorImpl extends sun.reflect.UnsafeStaticFieldAccessorImpl
  4. : class sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl extends sun.reflect.UnsafeQualifiedStaticFieldAccessorImpl

ちなみに、先にmodifiersを書き換えてfinalがついてない場合は、
isReadOnlyを持つクラスは使われていないようだ。(Qualifiedが付かないクラスが使われている)




うーん、Javaの実装に依存するなら、成功したのは非OracleJavaだったからかな。
OpenJDK7の実装もOracleと同じっぽい。
自宅の環境はOracleJavaのjdk1.8.0_121(最新)にあげて試した。
IBMJavaでも試したいのでWASDを用意した。8.5.5.11。
fixpack11から標準でJava8っぽいな。
IBM Java8およびJava7で試したがやっぱりできないよ。
やっぱり過去の経験はまやかしか?



ここからgradleでテストするように変更を加えた。(build.gradleを用意した)
すると、どうだろう、テストが成功し始めるではないか。
はぁぁぁぁぁぁあ!?
落ち着けオレ。
ちゃんと調べた。
凡ミス、modifierの書き換えが一番初めに動いたせいだ。


ふむ、一般的な解決策は、static finalのインスタンスを置き換えるのではなく、
static finalのインスタンスをDeencapsulationでgetして、それをMockUp(インスタンス)して期待する動作を書くとかだ。
インスタンスを引数に渡すとmockitoのspy相当になる。

jmockitのMockUpの場合、インスタンスを書き換えてるからクラスも同じ、setし直しは不要。
mockitoでspyした場合、別クラスの別インスタンスができてるのでsetし直しが必要で、static finalにsetできず困る。


static finalを書き換える方法については、他にsun.misc.Unsafeを使う方法がネットで調べられる。
まあ確かに一番強力な方法であるが、これも正攻法ではない。説明しないでおく。
正攻法でstatic finalを書き換える方法はない。


まやかしがまだ信じられないので追加調査。
やっぱりまやかしではなかったぞ。
jmockitの古いバージョンではmodifiersを書き換えとるやないか。
Dropped the support for setting static final fields through the Deenc… · jmockit/jmockit1@3819065 · GitHub
えっと、どのバージョンまで書き換え可能なんだ・・・。
1.24まで書き換え可能、1.25から書き換えできない。
1.25は2016年6月26日リリース。issues#281でdropしている。

jmockitのバージョンをあげるときの悲鳴は不可避や。

インフルエンザではなく風邪の模様

職場でインフルエンザA型感染者が2名出現。

 

火曜日の昼に、自分も不思議な熱を感じはじめたため、水曜日の午前中に内科に行ってきた。

結論は陰性で問題ない。

 

ここから先は、ふだん行かない場所(内科、処方箋受付)に行ったので、気になったことについて書く。日頃から気付いたことについて考え、なぜそう考えるのかと根源を求める性格。

 

社会人になって内科に行った記憶がない。

風邪を引いても市販の薬と大量の飯で治すタイプだ。

待ち合いはご老人が多い。

ああ、そうか、子供は小児科に行くもんな。

トイレの場所がカーテンの奥とあるが、不思議なところに誘導するんだな。

内科で言われたことで疑問に思ったこと。

インフルは初日から症状がでる。

むむむ、そんな話は聞いたことがない。

潜伏期間とかあるじゃん。

しかも、職場で流行しているのが、インフルエンザのA型なのか、B型なのかを聞いてこなかった。潜伏期間に差があるだろう。。。

医者が操作してソフトが気になった、UIが。。。聖路加病院とかはもっと綺麗なUIだったが、町のお医者さんでは無理からぬこと。

インフルエンザの検査は、鼻の穴の奥から検体を採取する。怖い。差し込まれるものが硬いのか柔らかいのか不安になった。

小さいころ耳鼻咽喉科でしょっちゅう鼻に金属製の器具を差し込まれていたので耐性がある。

 

念のため風邪薬の処方箋を受け取る。

処方箋も久しく受け取ったことがないな。

 

くすりの福太郎に向かう。

40分待ちだそうだ。

長いね。待ち行列を計算するか考えたが、今回は計算しないことにした。

ジェネリックに変更しますかという質問をされた気がする。

はあ、知るかボケ。そう思った。

日本語を話せ。何を言ってるのか分からない。

「同じ効能で低価格なお薬がございますが〜」くらいを言えるようになったほうがいい。

変更するメリット・デメリットが伝わるようにしてくれ。

また、たとえ効能が同じだとしても、医者が処方した薬以外に変更してよい法的な根拠とはどこにあるのか気になった。

 

初来店のため問診票のようなものを書く。

個人情報保護法の記載は、待ち合い室に貼ってあるとか書いてあるのだが、私の視力ではどこに貼ってあるのか確認できなかった。

ふぅ、一字一句の事実確認をしていくのは職業病だ。。。

 

 

待ち行列における女性比率の高さに驚いた。

今回はたまたまなのか。

文句をいう女性がいた。

日本には心のゆとりが足らないんじゃないか。

諸般の事情で順番が前後することがある。

小さな電光掲示板には32番まで呼びましたとある。

あれよあれよという間に私の41番まできた。

32番と思しき女性が立ち上がり文句を言いにカウンターに迫っている。

人がイライラしているのを見ると気分が悪くなる。

あと、女性比率の高さを見て、医療費の男女内訳とか統計があったりしないのかなと思った。

 

薬を配達にきた人が2回はいた。

受け渡しの確認作業をしている。

なんか、受け渡しもそうだけど、効率化できる要素はたくさんありそうだ。

カプセルとかの調合がいらない薬ならロボット任せになる時代はくるかもしれない。

 

 

待ち合いに置いてある雑誌は、なんで美容院に置いてるような雑誌なのか気になった。

男は迷わずMONOマガジンだと。

 

椅子の後ろに張り紙でQRコードがあったので、お薬手帳のアプリをダウンロードしてみた。

これのUIはとてつもなくダサい。

利用者は誰だ。比較的不健康な人だ。

年に1回くらいしか医者にかからない人には必要ないだろ。

基本、老人相手だ。

ただ、スマホを使える老人に限られる。

まず、文字は大きくあるべきだろうし、

ボタンも大きめの方がいい。

画面遷移のたびに画面下部が右からスライドしてくるのがウザい。

職場で私はスマホアプリを作ってないので、こんなところで市場調査しても意味ないのにな。