TL;DR
- Python3 から非ASCII文字(ひらがな等の半角英数以外の文字)も識別子(変数名、関数名、クラス名など)につかえるようになったが、入力した文字がそのまま使われるわけではない
- 通常の用途では殆ど問題にならないが、知っておかないと極稀に想定通りでない挙動をすることがある
試した環境
- Python 3.7.3
- この記事のnotebook
Python3 の識別子の仕様
非ascii識別子(non-ascii identifier)
- Python3になって、非ascii文字(もともと使えていた半角英数と一部記号以外の文字)も変数や関数の名前として使えるようになった
- 使える文字一覧については こちらhttps://qiita.com/yukinoi/items/e521e8f6b085a51de90bの記事を参照
- ただし、この記事では2文字目以降にのみ使える文字(数字など)や他の文字とくっついて使える文字(゙など)は含まれていないので、実際にはもっと多い
- ざっくりいうと、「文字」として扱われるものはだいたい使えて、「記号」として扱われるものはだいたい使えない
- 使える文字一覧については こちらhttps://qiita.com/yukinoi/items/e521e8f6b085a51de90bの記事を参照
- 例えば平仮名や漢字はもちろんのこと、ヒエログリフや楔形文字も変数として使える
- 使えるというだけで、可読性やフォントの問題があるので、本当に必要な場合のみ使いましょう
あ = "ひらがな"
漢 = "漢字"
𓃻 = "ヒエログリフ"
def 𒄀():
return "楔形文字"
print(あ, 漢, 𓃻, 𒄀())
ひらがな 漢字 ヒエログリフ 楔形文字
別の変数が上書かれる?
例えば、以下のようなコードを書いてみる
A = "半角A"
A = "全角A"
print(A, A)
すると出力はこうなる
全角A 全角A
このように、半角A
にも'全角A'
という文字列が入ってしまっている。
これ以外にも
ガ = "全角ガ(1文字の変数名)"
ガ = "半角ガ(2文字の変数名)"
print(ガ, ガ)
半角ガ(2文字の変数名) 半角ガ(2文字の変数名)
変数名の文字数も違うのに、2行目の代入で1行目の代入が上書かれている
PEP 3131
何が起きているのか調べるために、PEPを読んで見る。非ASCIIな識別子についてのPEP 3131 -- Supporting Non-ASCII Identifiersを見ると以下のように書かれている
2 . The entire UTF-8 string is passed to a function to normalize the string to NFKC, and then verify that it follows the identifier syntax.
(意訳)UTF-8文字列全体はNFKCへ正規化する関数に渡され、その後識別子の文法に従っているか確かめられる
どういうことかというと、非ASCIIな識別子はUnicode正規化という処理がなされ、NFKCという正規化形式に変換した上で使われる、ということらしい。
Unicode正規化やNFKCについては上記Wikipedia参照。ちなみに、どんな文字が正規化されるのかについては下のブログ記事で紹介されている(先程参照したQiitaと同じ方の記事)。
https://expectorate.hatenadiary.org/entry/20131230/1388433282
どういう処理がされるか
Unicode正規化の内容
実際にどういう処理がされているのか詳しく見てみる。Unicode正規化については、pythonではunicodedata.normalize関数で試せる。
先程の変数名は
import unicodedata
def print_normalize(before):
after = unicodedata.normalize("NFKC", before)
print(f"{before} -> {after}")
print_normalize("A")
print_normalize("ガ")
A -> A
ガ -> ガ
と変換される。ガ
は2文字が1文字に変換されたが、逆に1文字が2文字以上に変換される例もある。
print_normalize("㍍")
㍍ -> メートル
ただし、変換後の文字列が識別子に使えるからといって、変換前の文字列が識別子に使えるというわけではなく(シンタックスのチェックは変換前にも行われる)、例えば先程の㍍
は識別子には使えない(メートル
は使える)。他にも、①
は1
に変換されるが、2文字目以降でも識別子としては使えない。
x1 = 10 # OK
x① = 10 # SyntaxError: invalid character in identifier
代入、評価のされ方
具体的にどのように変数として保持されているか見てみる。pythonのグローバル変数はglobals
関数で参照できる。
ガギグゲゴ = 12345
globals().keys()
dict_keys(['__name__', '__builtin__', ~~中略~~, 'ガギグゲゴ'])
この通り、既にUnicode正規化された形で保持されている。
しかし、正規化前の変数を評価しても
print(ガギグゲゴ)
12345
正常に評価される。つまりは代入する時も評価する時も、Unicode正規化された形で使われていることがわかる。
直接宣言していない、正規化後を評価しても同様になる。
print(ガギグゲゴ)
12345
意図しない動作
この仕様の影響で、プログラムが意図しない動作をする可能性があり、注意が必要である。自分が思ったのは以下の2つ
- 想定外のところで変数が上書かれる
- 識別子を文字列で参照したときに、動作が異なる
1については上で説明したとおりなので、2について説明する。
識別子を文字列で参照する
globals, locals
上述の通り、pythonの変数テーブルはglobalsやlocals関数で参照できるが、これらは辞書を返すため、代入を介さずに直接値をつっこんだり、評価を介さずに直接値を参照することが可能である。(ただし、普通はやらない)
代入や評価を介さないと、Unicode正規化されることもないため、正規化されていない状態で使用することになり、代入・評価した時と別のものを指すことになる。
ザジズゼゾ = "代入"
globals()["ザジズゼゾ"] = "直接追加"
print(ザジズゼゾ)
print(globals()["ザジズゼゾ"])
代入
直接追加
その他の方法
globals()やlocals()を直接触ることは滅多にないが、スクリプト言語であるpythonでは識別子を文字列で触る機会は他にもある。(どこまでが推奨されない書き方なのか、正直わかっていないです)
例えばsetattr
, getattr
でオブジェクトのアトリビュートを追加、参照するケース
class SomeClass(object):
def __init__(self, ガ=100,**kwargs):
self.ガ = ガ
for k,v in kwargs.items():
setattr(self, k, v)
obj = SomeClass(**{"ガ":200,})
print(obj.ガ)
print(getattr(obj, "ガ"))
100
200
や、そのの特殊ケースとして、pandasのDataFrameのカラムを文字列参照する時とドット参照する時で結果が変わる場合
import pandas as pd
table = pd.DataFrame(index=range(3),columns=["A1", "A1"])
table["A1"] = "半角"
table["A1"] = "全角"
0 半角
1 半角
2 半角
Name: A1, dtype: object
0 全角
1 全角
2 全角
Name: A1, dtype: object
は注意が必要。
まとめ
- Python3で非ASCIIな識別子を使うときの挙動について調べた
- そもそも、たとえ日本語ネイティブだけで開発する場合でも、ASCIIでない識別子は極力避けるべき
- PEP 3131でも、standard libraryに関しては"MUST use ASCII-only identifiers"と書いてある
- どうしても使う場合は最小限に抑えて、上記のような不具合が起きないようにしましょう
(おまけ1)この記事を書いたきっかけ
- Juliaでは、円周率を表すπや自然対数の底(ネイピア数)を表すℯがあらかじめ値が入った形で用意されている
- jupyterでは
\pi
や\euler
と入力してtabキーを押せば入力できるので、imeを切り替える必要がなく便利
- jupyterでは
- pythonでもスタートアップスクリプトで代入しておけば便利なのでは?
実際に試そうとした結果
from math import pi as π
from math import e as ℯ
e = 10
print(ℯ)
10
このように、pythonの代入・評価ではe
とℯ
の区別がされないことが発覚した。
(おまけ2 )他言語の仕様
少なくともJuliaでは正規化されず、Python3では正規化されることがわかったので、他の言語でも試してみる。paiza.ioで実行してみた結果が以下。
言語 | 使える | 区別される |
---|---|---|
bash | ✕ | - |
C | ○ | ○ |
C# | ○ | ○ |
C++ | ○ | ○ |
Go | ○ | ○ |
Java | ○ | ○ |
Javascript | ○ | ○ |
Kotlin | ○ | ○ |
Perl | ✕ | - |
PHP | ○ | ○ |
Python2 | ✕ | - |
Python3 | ○ | ✕ |
R | ○ | ○ |
Ruby | ○ | ○ |
Swift | ○ | ○ |
Unicode正規化されるのはPython独自の仕様?
以下試したコード
bash
A="hankau A"
A="zenkaku A"
echo $A
echo $A
代入エラー(ASCIIしか変数名に使えない)
C
#include <stdio.h>
int main(void)
{
char *A = "hankau A";
char *A = "zenkaku A";
printf("A = %s\n", A);
printf("A = %s\n", A);
}
A = hankau A
A = zenkaku A
区別される(以下結果略)
C Sharp
public class Hello{
public static void Main(){
var A = "hankau A";
var A = "zenkaku A";
System.Console.WriteLine($"A = {A}");
System.Console.WriteLine($"A = {A}");;
}
}
C++
#include <iostream>
using namespace std;
int main(void){
char *A = "hankaku A";
char *A = "zenkaku A";
std::cout<<"A="<<A;
std::cout<<"A="<<A;
}
Go
package main
import "fmt"
func main(){
// Your code here!
A:= "hankaku A"
A:= "zenkaku A"
fmt.Println("A = ", A)
fmt.Println("A = ", A)
}
Java
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
// Your code here!
var A = "hankaku A";
var A = "zenkaku A";
System.out.printf("A = %s\n", A);
System.out.printf("A = %s\n", A);
}
}
Javascript
const A = "hankaku A";
const A = "zenkaku A";
console.log("A = ", A);
console.log("A = ", A);
全角変数はシンタックスハイライトされない
Kotlin
fun main(args: Array<String>) {
// Your code here!
var A = "hankaku A";
var A = "zenkaku A";
println("Kotlin");
println("A = " + A);
println("A = " + A);
}
Perl
$A = "hankaku A";
$A = "zenkaku A";
print "A = $A";
print "A = $A";
代入エラー(ASCIIしか変数名に使えない)
PHP
<?php
$A="hankau A";
$A="zenkaku A";
echo "A = $A\n";
echo "A = $A\n";
?>
R
A <- "hankaku A"
A <- "zenkaku A"
cat("A = ", A, "\n")
cat("A = ", A, "\n")
Ruby
A = "hankaku A"
A = "zenkaku A"
puts("A = " + A)
puts("A = " + A)
全角変数はシンタックスハイライトされない
Swift
var A = "hankaku A"
var A = "zenkaku A"
print("A = ", A)
print("A = ", A)