今川館

都内勤務の地味OLです

Goはパッケージトップレベルの関数をreflectで取り出せない

ことの始まりはtimeモジュールのFormat()の挙動が理解できなかったことなのだが、
time.nextStdChunkという関数をどうしても実行したくなり、reflectで取り出そうとしてはまった。

まず、小文字で始まる名前をつけたものは同一パッケージ内からしかアクセスできない。

なので、time.nextStdChunkはtimeパッケージ外から利用できない。

そういうとき、リフレクションを使えばnextStdChunkを取り出せるだろうと思いreflectパッケージの使い方を調べたのだが、以下のコードが動かなくて困った。

import "reflect"
import "time"

reflect.ValueOf(time)  // これができない

どうやら、↓こんな難しいことしないとダメらしい。

Adventures in Go: Accessing Unexported Functions
http://www.alangpierce.com/blog/2016/03/17/adventures-in-go-accessing-unexported-functions/

go getで利用できるようにGithubに上げてあった
https://github.com/alangpierce/go-forceexport

Pythonだとモジュールもインポートしたらただのオブジェクトみたいに使えて、トップレベルに定義したものも難なく取り出せるので意外だった。

Goの日付フォーマットの謎

日付や日時を文字列に変換する方法を調べていて、Goは%Y-%m-%dではなく、"2006-01-02"というフォーマットを採用していることを知る。

年は「2006」、月は「01」、日は「02」と決まっている。
これは、「2016」、「12」、「31」ではいけないのである。

import time

fmt.Println(time.Now()) // 2016-11-17 11:46:44.630877067 +0900 JST

// わかる。
fmt.Println(time.Now().Format("2006/01/02")) // => 2016/11/17

// なぜなのか?
fmt.Println(time.Now().Format("2016/12/31")) // => 17116/1117/1111
fmt.Println(time.Now().Format("2005/01/02")) // => 17044/11/17

ここに各種トークンの意味が定義されている
https://golang.org/src/time/format.go

const (
	_                        = iota
	stdLongMonth             = iota + stdNeedDate  // "January"
	stdMonth                                       // "Jan"
	stdNumMonth                                    // "1"
	stdZeroMonth                                   // "01"
	stdLongWeekDay                                 // "Monday"
	stdWeekDay                                     // "Mon"
	stdDay                                         // "2"
	stdUnderDay                                    // "_2"
	stdZeroDay                                     // "02"
	stdHour                  = iota + stdNeedClock // "15"
	stdHour12                                      // "3"
	stdZeroHour12                                  // "03"
	stdMinute                                      // "4"
	stdZeroMinute                                  // "04"
	stdSecond                                      // "5"
	stdZeroSecond                                  // "05"
	stdLongYear              = iota + stdNeedDate  // "2006"
	stdYear                                        // "06"
	stdPM                    = iota + stdNeedClock // "PM"
	stdpm                                          // "pm"
	stdTZ                    = iota                // "MST"
	stdISO8601TZ                                   // "Z0700"  // prints Z for UTC
	stdISO8601SecondsTZ                            // "Z070000"
	stdISO8601ShortTZ                              // "Z07"
	stdISO8601ColonTZ                              // "Z07:00" // prints Z for UTC
	stdISO8601ColonSecondsTZ                       // "Z07:00:00"
	stdNumTZ                                       // "-0700"  // always numeric
	stdNumSecondsTz                                // "-070000"
	stdNumShortTZ                                  // "-07"    // always numeric
	stdNumColonTZ                                  // "-07:00" // always numeric
	stdNumColonSecondsTZ                           // "-07:00:00"
	stdFracSecond0                                 // ".0", ".00", ... , trailing zeros included
	stdFracSecond9                                 // ".9", ".99", ..., trailing zeros omitted

	stdNeedDate  = 1 << 8             // need month, day, year
	stdNeedClock = 2 << 8             // need hour, minute, second
	stdArgShift  = 16                 // extra argument in high bits, above low stdArgShift
	stdMask      = 1<<stdArgShift - 1 // mask out argument
)

非常に不運なことに、わたしはこのサンプルコードを11月17日の午前11時に動かしていたため、「11」と「17」が不規則に並んで出力され(!)、ことさら混乱を招いた。
ちなみに、11月11日は日本を代表する歌手である吉幾三さんの誕生日である。

"2005"を指定すると、「2:日」+「0:無視」+「05:秒」と解釈される。
よって、「17(日)」+「(空文字列)」+「044(秒)」=「17044」となる。

同様に、"2016"は「2:日」+「01:月」+「6(末尾文字はそのまま結合)」解釈され、「17」+「11」+「6」=「17116」となるのである。

はっきり言って、%Y-%m-%dの方が慣れているのでわかりやすい*1
なまじ具体的な日時の値に即したフォーマットなので、任意の日時を書式指定するとうまく解釈してくれるものと誤解してしまった。

*1:所詮、わかりやすい理由は「慣れているから」にすぎないから、新たに慣れれば良いだけなのだが。

Goのファイル・ファイルライクオブジェクトもろもろ

ファイルの読み取り

Goでファイルを1行ずつ読み出したい場合はbufio.NewScannerを使うらしい。

そのNewScannerはio.Reader型の引数を取り、ファイルやファイルライクオブジェクトを渡す。

では、Goでファイルやファイルライクオブジェクトを扱う方法を調べてみた(以下3つ)。

Go Python3
1 strings.NewReader/bytes.NewReader io.StringIO
2 ioutil.TempFile tempfile.NamedTemporaryFile
3 os.Open ビルトインのopen関数

1. strings.NewReader/bytes.NewReaderを使う方法 (PythonのStringIOに相当)

文字列をファイルのように扱うファイルライクオブジェクトとして、Goではstrings.NewReaderを使うらしい。

[追記: 2016/12/6]

stringを反復する場合はstrings.NewReader, byteを反復する場合はbytes.NewReaderと、2種類用意されている。
テキストファイルの代わりに使うならstrings.NewReaderが妥当。

x := bufio.NewScanner(strings.NewReader(text))
// または
// x := bufio.NewScanner(bytes.NewReader([]byte(text)))

for x.Scan() {
	fmt.Println(x.Text())
}

2. ioutil.TempFileを使う方法 (PythonのNamedTemporaryFileに相当)

Goにも一時ファイルをパァーッっと作ってくれるライブラリがあった。ioutil.TempFileがこれに当たる。

// import "io/ioutil"
tmp, _ := ioutil.TempFile("", "_")
tmp.Write([]byte("| わたしは\n"))
tmp.Write([]byte("| い*がわ\n"))
tmp.Write([]byte("| よ**と"))
defer os.Remove(tmp.Name()) // PythonのNamedTemporaryFileと違い、ファイルの削除は自分でやる。

tmp.Seek(0, 0)
x = bufio.NewScanner(tmp)

for x.Scan() {
	fmt.Println(x.Text())
}

3. os.Openを使う方法 (Pythonのビルトインopen関数に相当)

普通にファイルを開くときは、os.Openを使う。

fp, _ := os.Open(tmp.Name())
defer fp.Close()
x = bufio.NewScanner(fp)
for x.Scan() {
	fmt.Println(x.Text())
}

まとめ

当然ながら、これらはすべてio.Reader型の変数に代入できる(以下)。

package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"bufio"
	"bytes"
	"os"
)

func main(){
	var reader io.Reader
	var scanner *bufio.Scanner

	reader = bytes.NewReader([]byte("Hello, world."))
	scanner = bufio.NewScanner(reader)
	fmt.Println(scanner)

	tmp, _ := ioutil.TempFile("", "_")
	defer os.Remove(tmp.Name())
	tmp.Write([]byte("I have a pen."))
	tmp.Seek(0, 0)
	
	reader = tmp
	scanner = bufio.NewScanner(reader)
	fmt.Println(scanner)

	fp, _ := os.Open(tmp.Name())
	defer fp.Close()
	reader = fp
	scanner = bufio.NewScanner(reader)
	fmt.Println(scanner)
}

Goのtype宣言はすごい

Goのtype宣言を使うと、特定の型を元にして別の型を定義できる。

一番単純な例: 基本データ型から別の名前の型を作る

type Foo int
var x Foo
x = 18
fmt.Printf("%T\n", x)  // => main.Foo

これはわかる。intからFooという別の型を作るだけである。

関数に型名を付与する使い方

ところが、このGoのtypeってやつは関数にも利用できることに驚いた。

type NumFunc func(xs []int) int

これは全然okで、構文エラーにはならない。

つまり、

// intのスライスを引数に取って合計値を「文字列で」返す
func NumString(xs []int) string {
	acc := 0
	for i := 0; i < len(xs); i++ {
		acc += xs[i]
	}
	return strconv.Itoa(acc)
}

// intのスライスを引数に取って合計値を返す
func Sum(xs []int) int {
	acc := 0
	for i := 0; i < len(xs); i++ {
		acc += xs[i]
	}
	return acc
}

type NumFunc func(xs []int) int

var fn NumFunc

fn = Sum // これはok

// fn = NumString // これはコンパイルエラー
// cannot use NumString (type func([]int) string) as type NumFunc in assignment

このように、関数を任意の型として宣言し、所定のシグネチャを守る関数しか代入できなくさせられる。

動くコードはhttps://play.golang.org/p/Em_52ORgYqこちら

関数のファクトリを簡潔に記述する

そもそもこの話に興味を持ったのは、Pythonのデコレータに相当することをGoでやる方法を考え始めたときだった。

Pythonのデコレータは関数のファクトリだから同じことをすれば良いのだが、最初以下のような安直なやり方でしか書けないと思っていた。

package main

import "fmt"

func main() {
	var foo = func(x int) int {
		return x + 5
	}

	var deco = func(fn func(int) int) func(int) int {
		return func(x int) int {
			return fn(x)
		}
	}

	var fn = deco(foo)
	fmt.Println(fn(100)) // => 105
}

とにかくdecoの定義の部分がひどい。

「func(int) int」という記載が3回も並ぶと実に読みにくい・・

なので、この重複の多い部分をtypeでくくり出す。

package main

import "fmt"

func main() {
	type F func(x int) int

	deco := func(fn F) F {
		return func(x int) int {
			return fn(x)
		}
	}

	var fn = deco(func(x int) int { return x + 5 })
	fmt.Println(fn(100)) // => 105
}


すると、decoの記載がだいぶスッキリするじゃないか。というところでこのtypeってやつはすごいと思ったのであった。

参考リンク

Go for Pythonistas

SQL ServerのIDENTITY値

基本事項

IDENTITY値にはSEED(初期値), INCR(増分)という概念がある。
また、IDENT_????という関数が用意されている(以下)

  • IDENT_CURRENT 現在値
  • IDENT_SEED 初期値
  • IDENT_INCR 増分
CREATE TABLE foo (
  [ID] INT IDENTITY(25, 2) PRIMARY KEY,
  [NAME] NVARCHAR(16) NOT NULL,
  [AGE] INT
);

INSERT INTO foo (NAME) VALUES (N'あ'), (N'い'), (N'う');

SELECT * FROM foo;

ID NAME
25 あ
27 い
29 う

SELECT
  IDENT_CURRENT('foo') [Current],
  IDENT_SEED('foo') [Seed],
  IDENT_INCR('foo') [Incr]
;

Current Seed Incr
29 25 2

IDENTITY列に明示的に値を指定して挿入する

通常、IDENTITYが設定されている列に値を明示的に指定して挿入はできない。
これがやりたいときは SET IDENTITY_INSERT ON を予め実行する必要がある。

オートインクリメント列を事前に採番しておきたい場合

テーブルに共有ロック(SELECTは許すがUPDATE, INSERT, DELETEを許さない)をかけてから先述のIDENT_????関数を使って採番。

DECLARE @items TABLE ([ID] INT NOT NULL, NAME NVARCHAR(16) NOT NULL, AGE INT);

BEGIN TRANSACTION;

WITH X (NAME, AGE) AS (
  SELECT N'わ', NULL
  UNION ALL SELECT N'を', NULL
  UNION ALL SELECT N'ん', 13
)
INSERT INTO @items ([ID], NAME, AGE)
SELECT
  IDENT_CURRENT('foo') + (IDENT_INCR('foo') * ROW_NUMBER() OVER (ORDER BY [NAME])),
  X.NAME,
  X.AGE
FROM
  X WITH (TABLOCK, HOLDLOCK)  -- 共有ロック。トランザクション終了まで有効
;

SET IDENTITY_INSERT foo ON;

INSERT INTO foo ([ID], NAME, AGE) SELECT [ID], NAME, AGE FROM @items;

COMMIT;

名称を指定せずテーブルのIDENTITY列を参照する -- $IDENTITY

MSDNのサンプルの中に $IDENTITY というキーワードがあって、最初何を表すのかわからなかった。

これは対象としたテーブルのIDENTITY列を指すキーワードらしい。

SELECT $IDENTITY FROM foo

ID
25
27
29
31
33
35

requestsを使ってInsecurePlatformWarningが出てしまうときの対処

requestsを使っていたらいつの間にかInsecurePlatformWarningという警告が出るようになってしまった。

/home/echizen/pecan/local/lib/python2.7/site-packages/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
InsecurePlatformWarning

SoftLayer - Python実行時にInsecurePlatformWarningが出る場合の対応方法 - Qiita

この記事を読むと

  • requestsパッケージを2.5.3以前に戻す
  • Pythonのバージョンを2.7.9以降に上げる

上記ふたつの対処方法が紹介されているが、どちらも採用しがたいと思い見送った。

困ったのでstackoverflowを見たら別の解決方法が書いてあった。

python - InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately - Stack Overflow

"requests[security]"をインストールすると良いとのこと。

手元の環境がUbuntuだったので

$ sudo apt-get install libffi-dev libssl-dev
$ . ./bin/activate
$ pip install requests requests[security]

これで確かに警告が消えた。

django-pyodbcでselect_for_updateは機能しないので要注意 (2015/4時点)

django-pyodbcはselect_for_updateメソッドを呼び出しても普通のSELECT文を発行する

Django1.4以上でモデルマネージャやクエリセットにselect_for_updateというメソッドが使えるけれども、django-pyodbc(=SQLServer)だとこれが使えないので注意が必要。

続きを読む