6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Python3でNon-ASCII識別子はUnicode正規化される

Last updated at Posted at 2019-07-16

TL;DR

  • Python3 から非ASCII文字(ひらがな等の半角英数以外の文字)も識別子(変数名、関数名、クラス名など)につかえるようになったが、入力した文字がそのまま使われるわけではない
  • 通常の用途では殆ど問題にならないが、知っておかないと極稀に想定通りでない挙動をすることがある

試した環境

Python3 の識別子の仕様

非ascii識別子(non-ascii identifier)

  • Python3になって、非ascii文字(もともと使えていた半角英数と一部記号以外の文字)も変数や関数の名前として使えるようになった
    • 使える文字一覧については こちらhttps://qiita.com/yukinoi/items/e521e8f6b085a51de90bの記事を参照
      • ただし、この記事では2文字目以降にのみ使える文字(数字など)や他の文字とくっついて使える文字(゙など)は含まれていないので、実際にはもっと多い
      • ざっくりいうと、「文字」として扱われるものはだいたい使えて、「記号」として扱われるものはだいたい使えない
  • 例えば平仮名や漢字はもちろんのこと、ヒエログリフや楔形文字も変数として使える
    • 使えるというだけで、可読性やフォントの問題があるので、本当に必要な場合のみ使いましょう
 = "ひらがな"
 = "漢字"
𓃻 = "ヒエログリフ"
def 𒄀():
    return "楔形文字"


print(, , 𓃻, 𒄀())
出力
ひらがな 漢字 ヒエログリフ 楔形文字

別の変数が上書かれる?

例えば、以下のようなコードを書いてみる

A = "半角A"
 = "全角A"
print(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("")
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. 識別子を文字列で参照したときに、動作が異なる

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"] = "全角"
table.A1
0    半角
1    半角
2    半角
Name: A1, dtype: object
table["A1"]
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を切り替える必要がなく便利
  • 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"="zenkaku A"
echo $A
echo $A
出力
代入エラー(ASCIIしか変数名に使えない)

C

#include <stdio.h>
int main(void)
{
    char *A = "hankau A";
    char * = "zenkaku A";
    printf("A = %s\n", A);
    printf("A = %s\n", );
}
出力
A = hankau A
A = zenkaku A

区別される(以下結果略)

C Sharp


public class Hello{
    public static void Main(){

    var A = "hankau A";
    var  = "zenkaku A";
    System.Console.WriteLine($"A = {A}");
    System.Console.WriteLine($"A = {}");;
    }
}

C++

#include <iostream>
using namespace std;
int main(void){
    char *A = "hankaku A";
    char * = "zenkaku A";
    std::cout<<"A="<<A;
    std::cout<<"A="<<;
}

Go

package main
import "fmt"
func main(){
    // Your code here!
    A:= "hankaku A"
    := "zenkaku A"
    fmt.Println("A = ", A)
    fmt.Println("A = ", )
}

Java

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // Your code here!
        var A = "hankaku A";
        var  = "zenkaku A";
        System.out.printf("A = %s\n", A);
        System.out.printf("A = %s\n", );
    }
}

Javascript

const A = "hankaku A";
const  = "zenkaku A";
console.log("A = ", A);
console.log("A = ", );

全角変数はシンタックスハイライトされない

Kotlin

fun main(args: Array<String>) {
    // Your code here!
    var A = "hankaku A";
    var  = "zenkaku A";
    println("Kotlin");
    println("A = " + A);
    println("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"
 <- "zenkaku A"
cat("A = ", A, "\n")
cat("A = ", , "\n")

Ruby

A = "hankaku A"
 = "zenkaku A"
puts("A = " + A)
puts("A = " + )

全角変数はシンタックスハイライトされない

Swift

var A = "hankaku A"
var  = "zenkaku A"

print("A = ", A)
print("A = ", )

参考文献

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?