PythonでJavaScriptの分割代入をどこまで再現できるかやってみた

はじめに

JavaScript には分割代入(Destructuring assignment)と呼ばれる代入の方法があります。下記はMDN のサンプルの一部です。

let a, b, rest;
[a, b] = [10, 20];
console.log(a);  // => 10
console.log(b);  // => 20

ところで、Python でも似たような構文が使えます。実際、下記の Python コードは上記の JavaScript のコードと同じ意味です。

>>> [a, b] = [10, 20]
>>> print(a)
10
>>> print(b)
20

そこで JavaScript の分割代入の知識は、Python ではどこまで活かせるのかが気になったので調査してみます。具体的には、下記の MDN のページのサンプルコードを Python で再現できるかどうか試していきます。以下に書く JavaScript のコードは下記ページからの引用です。

分割代入 - JavaScript | MDN

以下、見出しは上記の MDN のページに合わせています。

配列の分割代入

簡単な例#

var foo = ['one', 'two', 'three'];

var [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"

これは Python でもほぼそっくりそのまま同じことができます。

foo = ['one', 'two', 'three']
[one, two, three] = foo
print(one)
print(two)
print(three)

見た目もほとんど同じですね。

宣言後の割り当て#

Python には変数を宣言する構文が存在しないのでパスします。

既定値の設定#

var a, b;

[a=5, b=7] = [1];
console.log(a); // 1
console.log(b); // 7

存在しない部分の値(上のコードでは b )にたいして代入するとき、初期値を決める例ですが、これは Python にはできません。少し考えてみましたが、スマートにやる方法も思いつきません(情報求む)。

変数の入れ替え#

var a = 1;
var b = 3;

[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

これはできます。

a = 1
b = 3
[a, b] = [b, a]
print(a)  # 3
print(b)  # 1

別に a, b = b, a とリストを使わずともできます1

関数から返された配列の解析#

function f() {
  return [1, 2];
}

var a, b;
[a, b] = f();
console.log(a); // 1
console.log(b); // 2

これもできます。やっぱり見た目もほぼそっくりそのままです。

def f():
    return [1, 2]

a, b = f()
print(a)  # 1
print(b)  # 2

例えば scikit-learn の train_test_split を使うときとかはよく使うんじゃないでしょうか。

from sklearn.model_selection import train_test_split
# X, y があらかじめ定義されているとして
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42)

返り値の無視#

function f() {
  return [1, 2, 3];
}

var [a, , b] = f();
console.log(a); // 1
console.log(b); // 3

Python の場合は JavaScript のように完全に無視することはできませんが、慣習として不要な変数は _ で受けるのが一般的だと思います。

def f():
  return [1, 2, 3]

[a, _, b] = f()
print(a)  # 1
print(b)  # 3

配列の残余部分を変数に代入する#

var [a, ...b] = [1, 2, 3];
console.log(a); // 1
console.log(b); // [2, 3]

* で展開すれば受け取れます。

[a, *b] = [1, 2, 3]
print(a)  # 1
print(b)  # [2, 3]

正規表現のマッチからの値取得#

function parseProtocol(url) {
  var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
  if (!parsedURL) {
    return false;
  }
  console.log(parsedURL); // ["https://developer.mozilla.org/ja/Web/JavaScript", "https", "developer.mozilla.org", "en-US/Web/JavaScript"]

  var [, protocol, fullhost, fullpath] = parsedURL;
  return protocol;
}

console.log(parseProtocol('https://developer.mozilla.org/ja/Web/JavaScript')); // "https"

サンプルコードが長いですが、ポイントは parsedURL が配列になっていることなので、Python の正規表現モジュール(re)を使ってマッチした結果が Python のリストで返ってくるかだけが争点です。で、結論から言うと exec() メソッドと(ほぼ同じ)返り値を持つ re.findall() メソッドがあるので、この例も Python で再現できます。。

import re

regex = r'^(\w+)\:\/\/([^\/]+)\/(.*)$'
text = 'https://developer.mozilla.org/ja/Web/JavaScript'
# 返り値がリストになっている: [('https', 'developer.mozilla.org', 'ja/Web/JavaScript')]
# 分割代入を2度使う
[(protocol, fullhost, fullpath)] = re.findall(regex, text)
print(protocol)  # https

オブジェクトの分割代入

簡単な例#

var o = {p: 42, q: true};
var {p, q} = o;

console.log(p); // 42
console.log(q); // true

この簡単な例でさえ直接にはできません。Python のリストは分割代入をサポートしていますが、辞書型はサポートしていないためです。

下記の質問に様々な分割代入(のようなこと)を実現する例がありますが、多くは右辺をリストにしてリストの分割代入に帰着させています。

python - Destructuring-bind dictionary contents - Stack Overflow

上のリンクの中で際立っているのは、 operator.itemgetter() を利用する方法でしょうか。

from operator import itemgetter

o = {'p': 42, 'q': True}
p, q = itemgetter('p', 'q')(o)
print(p)  # 42
print(q)  # True

Python の公式ドキュメント によれば、itemgetter は下記の関数と等価です。

def itemgetter(*items):
    if len(items) == 1:
        item = items[0]
        def g(obj):
            return obj[item]
    else:
        def g(obj):
            return tuple(obj[item] for item in items)
    return g

そんなわけで、以降の例はほとんど(タプルやリストへの変換なしには)実現不可能です

宣言のない割り当て#

こちらも Python には変数宣言の構文が存在しないので省略します。

異なる名前を持つ変数への代入#

var o = {p: 42, q: true};
var {p: foo, q: bar} = o;

console.log(foo); // 42
console.log(bar); // true

あえていえば、上記で解説した operator.itemgetter() を使えばよいでしょう。

from operator import itemgetter
o = {'p': 42, 'q': True}
foo, bar = itemgetter('p', 'q')(o)
print(foo)  # 42
print(bar)  # True

既定値の設定#

var {a = 10, b = 5} = {a: 3};

console.log(a); // 3
console.log(b); // 5

配列のときと同様できません。

異なる名前の変数に代入して既定値を設定する#

var {a: aa = 10, b: bb = 5} = {a: 3};

console.log(aa); // 3
console.log(bb); // 5

こちらもできません、異なる変数名に代入できますが、既定値の設定ができないためです。

関数の引数に対する規定値の設定#

MDN のサイトの ES2015 バージョン のみ引用します。

function drawES2015Chart({size = 'big', coords = {x: 0, y: 0}, radius = 25} = {}) {
  console.log(size, coords, radius);
  // do some chart drawing
}

drawES2015Chart({
  coords: { x: 18, y: 30 },
  radius: 30
});

関数であれば Python でもキーワード引数と辞書の展開(** 演算子)が使えるので、規定値の設定をしつつ、辞書型を渡せば似たようなことが実現できます。


def draw_chart(size='big', coords=None, radius=25):
    print(size, coords, radius)

dic = {
    'coords': {'x': 18, 'y': 30},
    'radius': 30
}
draw_chart(**dic)

入れ子になったオブジェクトと配列の分割代入#

const metadata = {
    title: 'Scratchpad',
    translations: [
       {
        locale: 'de',
        localization_tags: [],
        last_edit: '2014-04-14T08:43:37',
        url: '/de/docs/Tools/Scratchpad',
        title: 'JavaScript-Umgebung'
       }
    ],
    url: '/en-US/docs/Tools/Scratchpad'
};

let {
  title: englishTitle, // rename
  translations: [
    {
        title: localeTitle // rename
    },
  ],
} = metadata;

console.log(englishTitle); // "Scratchpad"
console.log(localeTitle);  // "JavaScript-Umgebung"

入れ子にする前からできないので、入れ子になったところでできません。

イテレーターでの分割代入の利用#

var people = [
  {
    name: 'Mike Smith',
    family: {
      mother: 'Jane Smith',
      father: 'Harry Smith',
      sister: 'Samantha Smith'
    },
    age: 35
  },
  {
    name: 'Tom Jones',
    family: {
      mother: 'Norah Jones',
      father: 'Richard Jones',
      brother: 'Howard Jones'
    },
    age: 25
  }
];

for (var {name: n, family: {father: f}} of people) {
  console.log('Name: ' + n + ', Father: ' + f);
}
// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

上のサンプルコード自体は、オブジェクトの分割代入なのでできません。しかしリストであれば、Python でもイテレータと合わせて使うこともできます。簡単な例では次のようなものです:

L = [['a'], ['b'], ['c']]
for [item] in L:
    print(item)

# a
# b
# c

ささやかな抵抗です。

引数に指定されたオブジェクトの属性への参照#

function userId({id}) {
  return id;
}

function whois({displayName, fullName: {firstName: name}}) {
  console.log(displayName + ' is ' + name);
}

var user = {
  id: 42,
  displayName: 'jdoe',
  fullName: {
      firstName: 'John',
      lastName: 'Doe'
  }
};

console.log('userId: ' + userId(user)); // "userId: 42"
whois(user); // "jdoe is John"

キーワード引数名と、辞書のキー名が同じであれば ** による展開で同じことができます。しかし、不要なキーがあっても無視することが Python の場合はできないので、 **kwargs を使わなくても引数に定義しておく必要があります。また、辞書が入れ子になっているので ** の展開では firstName のみ取り出すことができません。

def user_id(id, **kwargs):
    return id

def whois(display_name, full_name, **kwargs):  # 入れ子の辞書のためここでは分割代入できない
    first_name = full_name['first_name']
    print(display_name, 'is', first_name)

user = {
  'id': 42,
  'display_name': 'jdoe',
  'full_name': {
      'first_name': 'John',
      'last_name': 'Doe'
  }
}

print('userId:', user_id(**user))  # "userId: 42"
whois(**user)  # "jdoe is John"

計算されたオブジェクトのプロパティの名前と分割代入#

let key = 'z';
let {[key]: foo} = {z: 'bar'};

console.log(foo); // "bar"

Python は辞書のキー用のリテラルが特にないので省略します。

オブジェクトの分割代入の残余#

let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40}
a; // 10
b; // 20
rest; // { c: 30, d: 40 }

これもできないと思います。強いて言うなら、 abpop() すれば残りが自動的に現れますが、もはや分割代入ではありません。

無効な JavaScript 識別子をプロパティ名として使用する#

Python の識別子と JavaScript の識別子で無効なものは違うので省略します。

配列とオブジェクトの分割代入を組み合わせる#

const props = [
  { id: 1, name: 'Fizz'},
  { id: 2, name: 'Buzz'},
  { id: 3, name: 'FizzBuzz'}
];

const [,, { name }] = props;

console.log(name); // "FizzBuzz"

3番目の ‘name’ だけ必要なので、 次のようになるでしょう。あくまでリストの分割代入をし、オブジェクトについては普通に取得します。

props = [
  { 'id': 1, 'name': 'Fizz'},
  { 'id': 2, 'name': 'Buzz'},
  { 'id': 3, 'name': 'FizzBuzz'}
]

_, _ , prop = props
print(prop['name'])

まとめ

  • JavaScript の配列に対する分割代入と同じことは Python でもそこそこできる
  • JavaScript のオブジェクトに対する分割代入は Python では基本的にはできない
    • ただし、関数を使うときは、キーワード引数と **演算子による展開を組み合わせれば、一部機能は再現できる

所感

もともと JavaScript の分割代入について調べていて、自分の知っている言語と比較しようとこの記事を書いてみたのですが、思っていたよりも JavaScript の分割代入が強力でした。特にオブジェクトのキーと変数名が一致していたら、 value をそのまま代入できるのがとても強力ですね。

とはいえ、リストについては Python もほぼ同じコードで分割代入の実現ができたので、決して非力ではないといったところでしょうか。

Ruby 2.7.0 からは(実験的なものとはいえ)パターンマッチの構文が入っているので、おそらくこの内容については Ruby のほうが簡潔に書けるんじゃないかなとか思います(未調査、無責任)。


  1. 実際には a, b はタプルに評価されているので、リストは使っていないけどタプルは使っていますが、見た目の上では現れないということで。 ↩︎