2021年5月アーカイブ

前の記事で,Pythonの識別子のUnicodeの扱いについて取り上げました。では,同じくUnicodeの様々な文字を識別子に使えるJavaとはどのように類似しているか,あるいは違っているでしょうか。

Javaの識別子の仕様

Javaは最初からUnicodeを前提に設計されており,変数名やメソッド名等の識別子にも世界の様々な文字を使えるようになっています。

識別子の仕様は言語仕様Java Language Specification 3.8. Identifiersに見えます。が,読んでみると実は,このセクションにはほとんど中身がなく,実質的にはJava APIのCharacterクラスの2つのstaticメソッド isJavaIdentifierStart(int) および isJavaIdentifierPart(int) にて,識別子に使える文字が規定されています。前者が識別子先頭,後者が2文字目以降です。

前者のメソッドのAPIドキュメントにはこう記されています:

  • isLetter(codePoint) が次を返す: true
  • getType(codePoint) が次を返す: LETTER_NUMBER
  • 参照される文字が通貨記号である ('$' など)
  • 参照文字が連結句読点文字である ('_' など)

一方,後者のメソッドつまり2文字目以降の定義は次のように記されています:

  • 汎用文字である
  • それが通貨記号である ('$' など)
  • それが連結句読点文字である ('_' など)
  • 数字である
  • 数値汎用文字である (ローマ数字文字など)
  • 連結マークである
  • 非スペーシングマークである
  • 文字の isIdentifierIgnorable(codePoint) が true を返す

少々とっつきにくく見えるかもしれませんが,Unicodeの文字カテゴリを前提としているので,その知識があれば「あれのことだな」と思いあたるでしょう。例えば最初の「汎用文字」はカテゴリLo, Luなどをまとめたもの,「数字」はカテゴリNd,「非スペーシングマーク」はMnといった具合です。最後の項目のメソッドは,制御文字の類いかどうかを判定するものです。

Pythonの識別子

Python3では,言語仕様の2.3. Identifiers and keywordsにて識別子の字句構造が定義されています。形式的に定義されているところを引用します:

identifier   ::=  xid_start xid_continue*
id_start     ::=  <all characters in general categories Lu, Ll, Lt, Lm, Lo, Nl, the underscore, and characters with the Other_ID_Start property>
id_continue  ::=  <all characters in id_start, plus characters in the categories Mn, Mc, Nd, Pc and others with the Other_ID_Continue property>
xid_start    ::=  <all characters in id_start whose NFKC normalization is in "id_start xid_continue*">
xid_continue ::=  <all characters in id_continue whose NFKC normalization is in "id_continue*">

Pythonの仕様書では,識別子の仕様は Unicode Standard Annex UAX-31 "Unicode Identifier and Pattern Syntax" に準拠していることが明示されています。これは一般的な言語の識別子の仕様(の雛形)として利用できるものです。Unicode仕様があまりに巨大なので,言語設計者の役に立つようなガイドラインとして作られたものだろうと思います。

実はJavaの方も内容的にはこれに準じているように見えます。ただし仕様書から参照されてはいません。

というわけで,JavaとPythonとで使える文字にあまり違いはありません。通貨記号の扱いが異なるのと,連結句読点(カテゴリPc)かアンダースコアのみかの違い,またUAX-31で互換性のために用意されている "Other_ID_{Start|Continue}" を含むか否かのような細かな出入りはあるようですが,大筋では大体同じものが使えると思ってよさそうです。

正規化の問題

ただし,JavaとPythonの小さくない違いとして,Pythonの方はUnicodeのNFKC正規化にて同一性をチェックされるということがあります。つまり,識別子の文字に,「é」のような文字を含む際に,「e + 合成用アクセント記号」としているか,あるいは合成済みの「é」の符号位置を用いているかという違いの取り扱いです。Pythonではこれらは正規化されて同じものとみなされます。

Javaにはこの規則はありません。Javaは正規化の仕様ができる前のUnicode仕様に基づいて設計されたので,比較的最近Unicode対応したPythonとは事情が異なります。

Javaで以下のようなコードを書いてみると実験できます。

public class JavaIdentifierTest {
    public static void main(String[] args) {
	int é = 0;
	int e\u0301 = 1;
	System.out.println(é);  // -> 0
	System.out.println(e\u0301);  // -> 1
    }
}

コード中のUnicodeエスケープ \u0301 は合成用アキュートアクセントです。JavaのUnicodeエスケープはコンパイルの早い段階で処理されるので,文字列リテラル専用ではなく,このように識別子の一部に用いることもできます。2つ変数を定義しており,どちらも同じ「é」を表しますが,片方は合成済みの符号位置,もう片方は結合文字を使っています。可能であれば,Unicodeエスケープを用いずに「基底文字e + 合成用アキュートアクセント」をエディタから入力してみても良いでしょう。Javaではこの2つの変数は同一視されずに別物として扱われます。

Pythonで同じことを試そうとすると,エスケープでなしに合成列をどうにかしてターミナルにペーストしてやる必要があります。私のMacの環境では,pythonコマンドでインタプリタを起動して,

>>> print('e\u0301')

として出力された é をマウスで選択してコピーすると,「e + 合成用アクセント」としてコピーできるようです (どこかで勝手に正規化されないことを祈りましょう)。確かめるには,

>>> len('é')
2

のように長さが2になっていればOK。これを変数名として,

>>> é = 1

のように変数として用いてみます。一方,キーボードから普通に入力したéは合成済みの符号位置になるので,Macの場合なら入力方式「ABC - 拡張」から Option-e e と打鍵して,

>>> é
1

とすることで,さっき代入した値が出てくることがわかります。つまり,文字éの符号化として合成済みの単一の符号位置を用いても,結合文字を用いて2つの符号位置で符号化しても,識別子としては同一のものとして扱われていることがわかります。

プログラミング言語Pythonで,変数名のような識別子に使える文字の種類は何だったかなとウェブを検索したところ,出てきたページに,UnicodeのカテゴリMn (結合文字)が使えないという意味の説明がありました。おや,そんなことあるかな,とちょっと調べてみました。

カテゴリMnとは

Unicodeは符号位置ごとに文字のカテゴリを属性として与えています。例えばラテンアルファベット大文字AであればUppercase Letter (Lu),ドル記号($)であればCurrency Symbol (Sc)などです。漢字や平仮名のように大文字小文字の区分のない文字はOther Letter (Lo)に分類されます。個々の符号位置のカテゴリはUnicode Consortiumが配布しているUnicodeData.txtに記されています。

これらのカテゴリのうち,結合文字はNonspacing Mark (Mn), Spacing Mark (Mc), Enclosing Mark (Me)になります。日本での使用で見るものはMnが多いでしょう。フランス語に用いるCOMBINING ACUTE ACCENT (U+0301)や,片仮名の濁点COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK (U+3099)などがこれです。

Unicodeに言語仕様から対応しているプログラミング言語では識別子に英語以外の様々な言語の文字に対応していることがあります。最初からUnicode前提に設計されたJavaでは変数名に「あ」「字」のような平仮名や漢字を使うこともできます。Pythonのバージョン3系も同様です。

ここで,もしカテゴリMnつまり結合文字が使えないと,多言語対応に支障が生じることになります。上記のフランス語のアクセント記号などは基底文字のアルファベットと合成済みの符号位置,例えばé (U+00E9)が用意されているので結合文字がなくても表現できますが,全てにおいてそうとは限りません。初期のUnicodeとは異なり,今日では結合文字を使って表現可能な文字は独立した符号位置に収録されない傾向にあります。例えば比較的最近Unicodeに追加された文字に,日本語でかつて使われていた変体仮名がありますが,濁点つきの文字は結合文字を使って表現できるため収録されていません。

手っ取り早く試してみる

Pythonのインタプリタを使うと手っ取り早く試すことができます。pythonコマンドを実行して,

>>> ㇷ゚ = 1
>>> ㇷ゚
1

よし,大丈夫ですね。ここで,最初の行は変数「ㇷ゚」(プの小書きです) に整数1を代入しています。ここでエラーにならなければOK。次の行は変数の値を確認しています。「ㇷ゚」はブラウザでは1文字に見えていると思いますが,実際には2つの符号位置の並び,つまり基底文字「ㇷ」と結合半濁点とで実現されています。「ㇷ」のカテゴリがLo, それに続く半濁点がMnです。つまり,この変数がエラーにならないということは,変数名としてカテゴリMnの結合文字が使えたことになります。

小書きの「ㇷ゚」を入力するのには,Macではアイヌ語入力方式にして「p [リターン]」と打鍵すればよし。Windows 10では「ぷ」から変換キーを何度か叩けば出てくるはずです。コマンドプロンプトでは半濁点が1文字分の幅をとってしまうようですがプログラムの動作には支障ありません。

言語仕様

きちんと確認するにはPythonの言語仕様にあたる必要があります。変数名のような識別子については,2.3. Identifiers and keywordsに記載されています。

ここを見ると,識別子の2文字目以降にはカテゴリMnの文字が使えることがわかります。識別子は先頭に数字が使えない言語が多く,先頭とそれ以降とで扱いが異なるのが普通ですが,特に結合文字は2文字目以降でないと意味がありません。

私が最初に見たウェブの記事は,識別子先頭と後続との違いを考慮していなかったようです。

ちなみにPython2では識別子はASCIIの英数字とアンダースコアに限定されていたようです。

どういうときに使われるか

さて,変数名にアルファベット以外が使えるといっても,通常のプログラムでは用いないことがほとんどでしょう。ではどういうときに使われるか。

フレームワークなどで,入力ファイルから自動生成するような場合が一例に挙げられます。Pythonのポピュラーなライブラリpandasでは,CSVファイルを簡単に読み込んで様々な処理ができますが,その中で以下のようにデータ項目にアクセスできます。

>>> import pandas as pd
>>> df = pd.read_csv('sample.csv')
>>> df
   ID      名前        生年月日
0   1    Taro  1980-10-11
1   2  Hanako  1981-11-12
>>> for row in df.itertuples():
...   print(row.名前)
...   print(row.生年月日)
... 
Taro
1980-10-11
Hanako
1981-11-12

最後のforループで,「row.生年月日」のように,named tupleの機能によって読み込んだCSVファイルの列名からデータにアクセスしています。

余談

Unicodeにいろいろな文字があるので風変わりな見た目のコード,例えば楔形文字を使って 𒐪 = 8 のようなコードを書くこともできますが,あとで困るかもしれないのでほどほどに。

広告