タプル:Dev Basics/Keyword
タプルは「複数のデータをひとまとめにして扱う」ためのデータ構造だが、その位置付けや用途、使われ方は言語ごとに異なるものとなる。
タプル(tuple)とは「複数のデータをひとまとめにして扱う」ためのデータ構造。「n倍」を表す「n-tuple」が語源となっている。
タプルの特徴
single(1倍)、double(2倍)、triple(3倍)、quadruple(4倍)、quintple(5倍)……など、「n倍」を表す言葉を一般化した「n-tuple」がタプルの語源であり、「n個の要素」で構成されるデータを簡便に取り扱う仕組みとしてさまざまな言語でタプルが実装されている。
ただし、タプルは「複数のデータをひとまとめにして扱える」ことを共通の特徴とするが、実際の取り扱いについては言語ごとに異なる部分も多い。以下ではC# 7で追加されたタプル型とPythonのタプルを見ていこう。
C# 7のタプル
まずは簡単なタプルの使用例を以下に示す(ここでは、.NET Frameworkに以前から存在するSystem.Tupleクラスではなく、C# 7で追加されたタプル型に焦点を当てる)。なお、Visual Studio 2017 RTMにおいても.NET Framework 4.6.2など、既存の.NET Frameworkをターゲットとしてプロジェクトを作成した場合、タプルを利用するにはNuGet経由でSystem.ValueTupleパッケージをインストールする必要があることに注意されたい(tuple issueのVisual Studio Teamの返答を見ると、.NET Framework 4.7など、新しいバージョンの.NET Framework/.NET Coreで組み込まれるようだ)。
static void Main(string[] args)
{
int x = 10;
int y = 20;
(x, y) = (y, x);
Console.WriteLine($"x: {x}, y: {y}");
Console.ReadKey();
}
この例では変数x/yをかっこで囲み「(x, y)」「(y, x)」のようにしてタプルとして扱っている。このようにタプルはかっこで囲んで表現する。以下のような記述も可能だ。
(string, string) name = ("shinji", "kawasaki");
Console.WriteLine(name.Item1);
(string first, string last) name2 = ("shinji", "kawasaki");
Console.WriteLine(name2.first + " " + name2.last);
変数nameは「(string, string)」型となる。つまり、これは2つのstring型フィールドで構成される変数であり、その要素には「name.Item1」「name.Item2」のようにしてアクセスする。次の変数name2の宣言では型名にfirst/lastというフィールド名まで含まれている。この場合には「name2.first」のように、フィールド名を利用してその要素にアクセスが可能だ。なお、「Item1」「Item2」のようなフィールド名は、タプル型にフィールド名を含めている場合でも利用可能だ(つまり、「name2.Item1」のようにして要素にアクセス可能)。
C#と後述するPythonのタプルの大きな違いは、C#ではタプルの要素は変更可能であり、Pythonでは変更不可能という点だ。例えば、C#では次のようなコードで、タプルの要素を変更できる(ただし、C#は静的型付け言語であり、タプルを作成した時点でその型が決まることから、ある要素に異なる型の値を代入することはできない。また、タプルに要素を追加することも許されていない)。
var t = (num: 100, str: "200");
t.num = 200;
これに相当する処理がPythonでは許されない(ただし、変更可能なオブジェクトをタプルの要素に含めることは可能。例えば、リストをタプルの要素として、そのリストの内容を変更することは可能。後述)。なお、上のコードのようにリテラルとしてタプルを記述することも可能であり、これを「タプルリテラル」という(上のコードではさらにそのフィールド名を指定している)。
タプルを引数に取ったり、タプルを戻り値とするメソッド/プロパティも作成可能だ。そうしたメソッドの例を以下に示す。
static (int newx, int newy) swap((int x, int y) num) => (num.y, num.x);
static (int newx, int newy) swap(int x, int y) => (y, x);
static void Main(string[] args)
{
int x = 10;
int y = 100;
var newvalue = swap((x, y));
Console.WriteLine($"new x: {newvalue.newx}, new y: {newvalue.newy}");
(var newx, var newy) = swap(x, y);
Console.WriteLine($"new x: {newx}");
Console.ReadKey();
}
ここでは2つのswapメソッド(オーバーロード)を作成している。前者は「(int x, int y)」型のパラメーターnumを持ち、後者は2つのパラメーターxとyを持つ。両者ともに、受け取った2つの値を交換したものをタプルとして返送している。
前者のメソッドを呼び出すには「swap((x, y))」のように引数にもタプルを指定する。後者のメソッドはこれまでと同様に「swap(x, y)」のようにして呼び出せる(強調書体で示した2つのメソッド呼び出しを参照)。
また、戻り値がタプルとなっている場合には、「var newvalue = ……」のように単一の変数で受け取ることもできるし、「(var newx, var newy) = ……」のように戻り値を受け取る時点でその値を「分解」(deconstruct)することもできる。前者のように単一の変数で受け取ったときには、メソッドの戻り値型に指定したフィールド名を利用して、それぞれの要素にアクセス可能だ(例:newvalue.newx)。なお、タプルを分解して受け取る場合、不要な要素については以下のように「_」を指定すれば、その値を読み捨てることができる。
int yy;
(_, yy) = swap(x, y);
Console.WriteLine($"new y: {yy}");
C# 7においては、タプルを利用することで「メソッドから複数の値を一括して返送する」ことがとても簡単に行えるようになる。
Pythonのタプル
Pythonでもタプルはかっこで囲んで記述する(必須ではない)。C# 7版の最初のサンプルをPythonで書き直したものを以下に示す。
x = 10
y = 20
(x, y) = (y, x)
print(f"x: {x}, y: {y}")
なお、タプルの表現にC#とPythonでかっこを使うからといって、他の言語でもそうだとは考えない方がよい。例えば、TypeScriptではタプルは角かっこを使用して記述する。また、Pythonではかっこを使わずとも、タプルは記述可能だ。以下に例を示す。
x = 1, 2, "3", 4
print(type(x)) # 出力結果: <class 'tuple'>
C# 7で追加されたタプルは最低でも2つの要素を必要とするが、Pythonでは無要素、あるいは1要素のタプルも作成可能だ。ただし、1要素のタプルを作成するには、末尾に「,」を指定する必要がある(以下のサンプルからカンマを削除してみると分かるが、カンマがないと「t2 = (1)」「t3 = 1」のように、それがタプルであることを明示できなくなってしまうので、1要素のタプルではカンマが必須となる)。
t1 = () # 空のタプル
t2 = (1,) # 1要素からなるタプル
t3 = 1, # 1要素からなるタプル
print(type(t1)) # 出力結果: <class 'tuple'>
print(type(t2)) # 出力結果: <class 'tuple'>
print(type(t3)) # 出力結果: <class 'tuple'>
Pythonでは「タプルの要素は変更不可能」なことは、C# 7のタプルとは大きく異なる点なので注意が必要だ。
t = (123, "456")
t[0] = 1234 # TypeError: 'tuple' object does not support item assignment
print(t[0])
Pythonにおいては「変更可能な要素を格納するコレクションがリスト」「変更不可能な要素を格納するコレクションがタプル」という位置付けになっていることに注意しよう。ただし、上でも述べたように、タプルの要素に変更可能なものを含めることは可能だ。以下に例を示す。
l = list(range(3))
t = (1, l)
print(t) # 出力結果: (1, [0, 1, 2])
t[1].append(3)
print(t[1][3]) # 出力結果: 3
print(l[3]) # 出力結果: 3
t[1] = list(range(5)) # TypeError
また、上のコードを見ると分かる通り、Pythonに組み込みのタプルでは要素へのアクセスにはインデックスを利用する。C# 7のタプルとは異なり、要素に名前を付けることはできない(ただし、collectionsモジュールのnamedtuple関数を利用すれば、名前付きフィールドを持つタプルを作成可能。詳細はPythonのドキュメント「8.3. collections − コンテナデータ型」を参照)。
なお、Pythonの関数ではパラメーターリストとしてタプルを記述することはできない(が、タプルを受け取ることは可能だ)。以下に例を示す。これは受け取った値(その型)をそのままコンソールに出力するだけの関数だ。
def echo(x):
print(type(x))
print(x)
def echo2((x, y)): # このような記述はSyntax Error
pass
x = 10
y = 20
t = (x, y)
echo(t) # 出力結果: 「<class 'tuple'>」「(10, 20)」の2行
パラメーターリストにはあくまでも、その関数が受け取る値を並べて記述していく。タプルを渡せば、それが対応するパラメーターに渡されることになる。タプルを返す関数は次のようになる。これは上で見たC#版のswapメソッドと同等な処理を行う。
def swap(x, y):
return (y, x)
x = 10
y = 20
t = (x, y)
print(swap(*t))
#print(swap(t)) # TypeError
関数の内容は特に説明の必要はないだろう。値を入れ替えて返しているだけだ。注目してほしいのは、その呼び出しで「*t」としてタプルを渡している点。これにより、タプルの内容が展開されて、xとyの2つのパラメーターに渡されるようになる。逆にコメントアウトされている行のようにそのままタプルを渡すと、この場合は引数の数とパラメーターの数が一致しなくなってしまう。
C# 7のタプルで行ったようなタプルの分解も可能だ。また、前述した通り、組み込みのタプルでは要素に名前を付けられないので、返送されたタプルの要素にはインデックスを介してアクセスをすることになる。
newvalue = sap(x, y)
print(f"new x: {newvalue[0]}, new y: {newvalue[1]}")
(newx, newy) = swap(x, y)
print(f"new x: {newx}, new y: {newy}")
Pythonにおけるタプルは、C# 7で追加されたタプルとは異なり、あくまでも「複数の変更不可能なオブジェクトを格納するコレクション」であり、その用途はC#におけるコレクション的な利用法を含む、より広範なものとなるだろう。
本稿ではタプルの概要と、C# 7/Pythonにおけるタプルの特徴を見てみた。どちらの言語においてもタプルは「複数の値をひとまとめにして扱う」ための機構であるが、その位置付けや使われ方は言語ごとに異なっている。なお、C# 7におけるタプル/Pythonのタプルの詳細については以下の資料を参考にされたい。
参考資料
- タプル: Wikipediaでのタプルの解説
- Quickstart guide for tuples: Visual Studio 2017でタプルを使うためのクイックガイド
- タプル: マイクロソフトMVPの岩永氏によるC# 7のタプルの解説記事
- タプル(Tuples): 兄弟サイト「Build Insider」におけるC# 7のタプルの解説
- シーケンス型 − list, tuple, range: Pythonにおけるタプルを含むシーケンス型のデータ構造についての解説
Copyright© Digital Advantage Corp. All Rights Reserved.