これは何?
クラス変数の持ち主が、 python と ruby で違うことに気がついたので、他の言語どうなってるんだろと思って調査したのでその記録。ジェネリクスとテンプレートを交えながら。
ruby の場合
class B
def set_v(x); @@v=x; end
def v(); @@v; end
end
class C < B;end
class D < B;end
c = C.new.set_v("C#set_v")
d = D.new.set_v("D#set_v")
p [ C.new.v, D.new.v ] #=> ["D#set_v", "D#set_v"]
クラス変数 @@v
の持ち主は、 class B
。
C
経由で変更しても、 D
から見た @@v
の値が変わる。
Python3 の場合
class B:
v = "in-class-def"
@classmethod
def setV(cls,x):
cls.v = x
class C(B):...
class D(B):...
print(B().v, C().v, D().v) #=> in-class-def in-class-def in-class-def
C().setV("C.setV")
D().setV("D.setV")
print(B().v, C().v, D().v) #=> in-class-def C.setV D.setV
Python のクラス変数はcls.v
のようにするので、v の持ち主は cls
で指定される型。なので、クラス変数を参照しているメソッドが書かれているクラスとは関係がない。
最初の print
内にある C.v
なんかは、C
の基底クラスを見に行っている。
わりと珍しい作戦だと思う。
この作戦だと、深い継承ツリーで偶然同じ名前を使ってしまったために大惨事、みたいなことが起きると思う。
※ 初出時間違ったことを書いてましたすいません。
Java の場合
Generics なしで
まずは Generics なしで。
import java.io.*;
class B{ public static String s; }
class C1 extends B {};
class C2 extends B {};
class Foo
{
public static void main (String[] args) throws java.lang.Exception
{
C1.s = "c1";
C2.s = "c2";
System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2
}
}
ここは ruby と同じく、クラス変数の持ち主は、その変数が定義されたクラス。
Generics を使うと
Generics を使って、型引数を変えてみると
import java.io.*;
class B<T>{ public static String s; T dummy; }
class C1 extends B<Integer> {};
class C2 extends B<Long> {};
class Foo
{
public static void main (String[] args) throws java.lang.Exception
{
C1.s = "c1";
C2.s = "c2";
System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2
}
}
異なる型引数でもクラス変数は同じだということがわかる。
なので、型引数の型を static 変数には使えない。なるほどこれが型消去かという感じ。
C++ の場合
テンプレートなし
まずはテンプレートなしで。
#include <iostream>
#include <string>
struct B{ static inline std::string s; };
struct C1 : public B{};
struct C2 : public B{};
int main(){
C1{}.s = "c1";
C2{}.s = "c2";
std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c2 c2
return 0;
}
ruby, Java と同様、static変数(クラス変数) の持ち主は、その変数が定義されたクラス。
テンプレートあり
テンプレートクラスの場合。
#include <iostream>
#include <string>
template<int n>
struct B{ static inline std::string s; };
struct C1 : public B<0>{};
struct C2 : public B<1>{};
int main(){
C1{}.s = "c1";
C2{}.s = "c2";
std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c1 c2
return 0;
}
テンプレート引数が異なるクラスは別クラスになる。なので、Java と違って static 変数の型にテンプレート引数が使える。
C# の場合
Generics なし
using System;
class B{ public static String s; }
class C1 : B{};
class C2 : B{};
public class Test
{
public static void Main()
{
C1.s = "c1";
C2.s = "c2";
Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c2 C2.s=c2
}
}
まあそうだよね。Java と同じ。
Generics あり
Generics ありだと
using System;
class B<T>{ public static String s;}
class C1 : B<int>{};
class C2 : B<long>{};
public class Test
{
public static void Main()
{
C1.s = "c1";
C2.s = "c2";
Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c1 C2.s=c2
}
}
Java とは異なる。
まとめ
ひとくちに「クラス変数」というけれど、言語によって意味が違うので要注意。
Python はたぶん異端で、明示されているレシーバがそのクラス変数の持ち主。基底クラス内の cls.class_var
は、 cls
が異なれば異なる変数。cls
指すクラスの別のスーパークラスで同名のクラス変数を使うと同じ変数となる。
ruby, Java, C++, C# は、クラス変数が定義されたクラスがクラス変数の持ち主。
Generics / template を使う場合。
型引数が違っても同じクラス、というのが Java の立場。
template 引数 / 型引数が違うクラスは別のクラス、というのが C++ と C# の立場。
他に試すべき言語あるかなぁ。
余談
今は亡き J# でジェネリクス使ったらどうなるんだろ。
そもそも J# でジェネリクス使えるかどうかも知らないけど。
以下、投稿の翌日に追加。
Python と ruby のクラス変数の気持ち悪いところ
コメントいただいたり他に試したりして、何が気持ち悪いのかわかってきた。
Python の場合
シンプルな例
まずは普通っぽい例。
class B:v="B0"
class C(B):...
class D(B):...
# [1]
print(B.v, C.v, D.v) #=> "B0 B0 B0"
# [2]
C.v = "C0"
print(B.v, C.v, D.v) #=> "B0 C0 B0"
[1] の print
内の C.v
は、 B
の v
のことで、
[2] の C.v="C0"
の C.v
は、 C
の v
のことなので、C.v="C0"
は D.v
で得られる値には影響を与えない。
同じ C.v
という式だけど、参照するときと代入の左辺になるときで意味が違うのがわかりにくい。とはいえ、オブジェクト指向とはそのようなものだとも思う。
わかりにくい現象
このわかりにくさと Python の +=
なんかのわかりにくさが合体して、こんなわかりにくいことが起こる。
class B:
a=["a"]
b=["b"]
c = "c"
d = "d"
class C(B):...
print(repr([B.a, B.b, B.c, B.d])) #=> [['a'], ['b'], 'c', 'd']
C.a += ["+="]
C.b = C.b + ["+"]
C.c += "+="
C.d = C.d + "+"
print(repr([B.a, B.b, B.c, B.d])) #> [['a', '+='], ['b'], 'c', 'd']
リストの +=
は新たなオブジェクトは作られないので基底クラスの B.a
が書き換わり。
文字列の +=
は新たなオブジェクトが作られるので代入と同じことになるから B.c
は書き換わらない。
かなり気持ち悪い動作だと思うけど、なんでそうなるのかは説明できる感じ。
ruby の場合
シンプルな例
一方 ruby の普通っぽい例を実行すると。
class B
def set_vb(x); @@v=x; end
def vb(); @@v; end
end
class C < B
def set_vc(x); @@v=x; end
def vc(); @@v; end
end
class D < B
def vd(); @@v; end
end
B.new.set_vb("B0")
p [B.new.vb, C.new.vc, D.new.vd] #=> ["B0", "B0", "B0"]
C.new.set_vc("C0")
p [B.new.vb, C.new.vc, D.new.vd] #=> ["C0", "C0", "C0"]
上記の通り Python とは違う。
class C
内の @@v
を書き換えると class B
内で定義された @@v
が書き換わる。なので、C
の派生クラスでも基底スラスでもない D
から見える @@v
にも影響を与える。
ここだけ見るとそれはそれでありかなと思うけれど。
わかりにくい現象
ならば、派生で @@v
を作ってから基底で @@v
に代入すると
class B
def set_vb(x); @@v=x; end
def vb(); (defined? @@v) ? @@v : :undefined; end
end
class C < B
def set_vc(x); @@v=x; end
def vc(); (defined? @@v) ? @@v : :undefined; end
end
# [1]
C.new.set_vc("C0")
p [B.new.vb, C.new.vc] #=> [:undefined, "C0"]
# [2]
B.new.set_vb("B0")
p [B.new.vb, C.new.vc]
# ruby 2.7.5 では、["B0", "B0"]
# ruby 3.1.0 では例外。class variable @@v of C is overtaken by B
なんと、 ruby 3.1 では例外。
2.7.5 では、基底クラスの @@v
を書き換えると派生クラスの @@v
が書き換わる。難しい。難しすぎるので例外にしたんだろうと思う。
あるいは。
以下を実行すると。
module B
@@x = :bx
def self.b()
[ (defined? @@x) ? @@x : :undef,
(defined? @@y) ? @@y : :undef,
(defined? @@z) ? @@z : :undef]
end
end
module C
@@y = :cy
def self.c()
[ (defined? @@x) ? @@x : :undef,
(defined? @@y) ? @@y : :undef,
(defined? @@z) ? @@z : :undef]
end
end
class D
include B, C
@@x = :dx
@@y = :dy
@@z = :dz
def self.d();
[ (defined? @@x) ? @@x : :undef,
(defined? @@y) ? @@y : :undef,
(defined? @@z) ? @@z : :undef]
end
end
p [B.b, C.c, D.d]
#=> [[:dx, :undef, :undef], [:undef, :dy, :undef], [:dx, :dy, :dz]]
module B
@@x = :bx2
@@y = :by2
@@z = :bz2
end
p [B.b, C.c, D.d] # 3.0.3 だとここで例外
#=> 3.1.0: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :dz]]
#=> 2.7.5: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :bz2]]
module C
@@x = :cx2
@@y = :cy2
@@z = :cz2
end
p [B.b, C.c, D.d]
#=> 3.1.0: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:bx2, :cy2, :dz]]
#=> 2.7.5: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:cx2, :cy2, :cz2]]
なななんと、2.7.5 でも 3.1.0 でも例外にならずに完走するが、結果が違う。
びっくりした。
3.1.0 だと完走するのに、3.0.3 だと例外。さらにびっくりした。
びっくりしたし、それぞれどんな理路でそのような出力になったり例外になったりするのかは私には説明できない。
2.7.5 と 3.1.0 で結果が違うことに気づいた時点で真剣に考えるのを放棄したということでもあるけれど。
再度まとめ
Python と ruby のクラス変数は、両方ともわりと思いがけないことが起こる。
どちらも気持ち悪いけど、どちらかというとというかわりと圧倒的に ruby の方が気持ち悪い。
ruby のクラス変数はめちゃくちゃ気持ち悪いし、中の人も悩んでいるんだと思うので、継承が絡む局面での利用は避けたほうがいいと思う。