元组 (tuple)

向量按存储的数据类型可以分为元组和强类型向量。强类型向量要求所有元素是具有相同的数据类型的标量,因此强类型向量具有明确的数据类型。而元组的存储对象可以是标量或向量,其类型可以相同或不同,因此元组没有明确的数据类型,DolphinDB 定义元组的数据类型为 ANY。

元组的声明符号是圆括号,如:"(1, 2, 3)"。用方括号声明强类型向量时,若其中包含不同的数据类型或包含向量,系统会自动将其转换为一个元组,如:"[1, 'A']","[[1,2,3], [4,5]]"。在本章后面将会提到,可以使用方括号来构造元组。

创建元组

(1) 圆括号内用逗号分隔定义。使用这种方式,无论元素的类型是否相同,一定会生成一个元组。元组内的元素可以跨行书写。

a=(1 2 3, `IBM`MSFT`GOOG, 2.5);
a;
// output: ([1,2,3],["IBM","MSFT","GOOG"],2.5)

// 也可跨行书写式
a=(1 2 3, 
`IBM`MSFT`GOOG, 2.5);
a;
// output: ([1,2,3],["IBM","MSFT","GOOG"],2.5)

a=(1, 2, 3);
typestr a;
// output: ANY VECTOR

x=(1..10, `GOOG);
x;
// output: ([1,2,3,4,5,6,7,8,9,10],"GOOG")

x=1..10;
y=rand(100,10);
a=(x+y,x-y);
a;
// output: ([73,26,32,96,26,86,11,63,21,49],[-71,-22,-26,-88,-16,-74,3,-47,-3,-29])

可以用()来生成一个空的元组。

x=();
x.append!(1..3);
// output: ([1,2,3])
x.append!(`GS);
// output: ([1,2,3],"GS")

x=(1,2,3);
x;
// output: (1,2,3)
x.append!(`GS);
// output: (1,2,3,"GS")

不可使用 append! 函数将强类型向量转换为元组。

x=1 2 3;
x;
// output: [1,2,3]
x.append!(`GS);
// output: Incompatible type. Expected: INT, Actual: STRING

(2) 用空格分隔不同类型的数据,或者在方括号内使用逗号分隔不同类型的数据。方括号 "[]" 首先会尝试创建一个强类型向量。如果不成功,则会创建一个元组。

a = 3 2012.02.03 `GOOG;
// output: (3,2012.02.03,"GOOG")
typestr a;
// output: ANY VECTOR

a=[[1,2,3,4,5,6,7,8,9,10],2,[`IBM, `MSFT,`GOOG]];
a;
// output: ([1,2,3,4,5,6,7,8,9,10],2,["IBM","MSFT","GOOG"])

x=1..10;
y=rand(100,10);
b=[x+y,x-y];
b;
// output: ([73,26,32,96,26,86,11,63,21,49],[-71,-22,-26,-88,-16,-74,3,-47,-3,-29])

(3) 使用函数 array 将第一个输入变量设为 ANY,然后定义每个元素。

a=array(ANY, 3);
a[0]=1..10;
a[1]=2;
a[2]=`IBM`MSFT`GOOG;
a;
// output: ([1,2,3,4,5,6,7,8,9,10],2,["IBM","MSFT","GOOG"])

访问元组

x=(1 2 3 4 5 6 7 8 9 10,(5 7 8, 11 3 5));
x;
// output: ([1,2,3,4,5,6,7,8,9,10],([5,7,8],[11,3,5]))

x[1];
// output: ([5,7,8],[11,3,5])

x[1,1];
// output: [11,3,5]

x[1,1,1];
// output: 3

x[0 1];
// output: ([1,2,3,4,5,6,7,8,9,10],([5,7,8],[11,3,5]))

x[0 1,1];
// output: (2,[11,3,5])

x[, 1];
// output: (2,[11,3,5])

x[1, ,0 2];
// output: ([5,8],[11,5])

x[,1,1];
// output: (2,3)

x[1 2];
// output: (([5,7,8],[11,3,5]),)

修改元组

我们可以向一个元组中添加新元素,也可将新的对象赋值给元组里的元素。

  1. 向一个元组中添加新元素。

    x=`C 120`BH 100;
    x;
    // output: ("C",120,"BH",100)
    
    x.append!("AAPL");
    // output: ("C",120,"BH",100,"AAPL")
    
    x.append!(300);
    // output: ("C",120,"BH",100,"AAPL",300)
    
    x.append!(`IBM 600);
    // output: ("C",120,"BH",100,"AAPL",300,("IBM",600))
  2. 通过将新的对象赋值给元组里的元素来修改元素值。

    tp = [["aaaa", "ccccccc"], ["dddd"]]
    //将元组 tp 的第一个元素赋值给对象 a 
    a = tp[0]
    //修改对象 a 的第2个元素值
    a[1] = "A"
    a;
    // output: ["aaaa","A"]
    //通过将对象 a 赋值给 tp[0],来修改 tp 里的第一个元素值
    tp[0] = a
    tp
    // output: (["aaaa","A"],["dddd"])

涉及元组的计算

元组是用来容纳不同的数据类型,而不是为了高效的计算。即使元组的元素都是数字,仍可使用元组进行计算。

我们可以使用如下函数对元组进行计算:

a=(2, [3, 5], 10);
max a;
// output: [10,10]

prod a;
// output: [60,100]

cumsum a;
// output: (2,[5,7],[15,17])

我们可以将逻辑或关系运算符应用于元组。

a=(2, [3, 5], 10);
a>3;
// output: (0,[0,1],1)

a==2;
// output: (1,[0,0],0)

isNull a;
// output: (0,[0,0],0)

我们可以对元组使用某些运算符,比如:

a=(2, [3, 5], 10);
a+2;
// output: (4,[5,7],12)

a pow 3;
// output: (8,[27,125],1000)

对多个元组赋值

x,y=(1 2 3, 2:5);
x;
// output: [1,2,3]
y;
// output: 2 : 5

元组用于函数返回值,返回多个值

def foo(a,b){return a+b, a-b};
x = foo(15,10);
x;
// output: (25,5)

typestr x;
// output: ANY VECTOR

特殊使用场景

实质上,元组的每个元素存储的是对象的地址。当在元组上应用 take 函数进行循环取值生成一个新的元组时,新元组实际拷贝的是原元组各个元素的地址(引用),而非实际的元素值,即进行的是浅拷贝(shallow copy)。

在某些特殊场景下,如更新字典:字典的 value 是通过对一个元组应用 take 函数生成的元组,多个元素实际上指向了同一个对象。此时对某个 key 对应的 value 进行追加,可能会导致其他 key 对应的 value 发生改变。

下例对 key=1 追加一个元素 0,可以发现 key=4 的值也发生了改变:

t = [[1,2],[2,3],[3]]
d = dict(take(0..10,5),take(t, 5))
d;
/* output: 
0->[1,2]
1->[2,3]
2->[3]
3->[1,2]
4->[2,3]
*/

d.dictUpdate!(append!,1,0)
d;

/* output: 
0->[1,2]
1->[2,3,0]
2->[3]
3->[1,2]
4->[2,3,0]
*/

t = [[1,2],[2,3],[3]]
d = dict(take(0..10,5),[[1,2],[2,3],[3],[1,2],[2,3]])
d;

/* output: 
0->[1,2]
1->[2,3]
2->[3]
3->[1,2]
4->[2,3]
*/

d.dictUpdate!(append!,1,0)
d;

/* output: 
0->[1,2]
1->[2,3,0]
2->[3]
3->[1,2]
4->[2,3]
*/