#概要
C#にあるLINQの考え方をMayaPythonでも役立てられないか?
というのを探ってみました。
##モチベーション
- もっと楽にわかりやすくリスト操作をしたい。
- C#と似たような書き方が出来ればスイッチングコストが下がるはず。
##LINQとは
LINQについての説明は他サイトにお任せするとして、(参考サイトをページ最下部にまとめています)
ざっくり言うと「リストなどに対してSQL文のような書き方で値をフィルタしたり加工したりする機能」です。
実際にLINQを使ったコードは下記のようになります。
var hoge = new int[] { 0, 1, 2, 3, 4, 5 };
var hogehoge = hoge.where(n => n > 3).select(n => n * n);
foreach (var x in hogehoge)
Debug.WriteLine(x);
>>> 16
>>> 25
上記コードをPythonのリスト内包表記で書くと下記のようになります。
hoge = [0, 1, 2, 3, 4, 5]
hogehoge = [x * x for x in hoge if x > 3]
for x in hogehoge:
print x
>>> 16
>>> 25
このように書くとリスト内包表記があればいいかな~と思いますね。(流石Python)
さらにPythonではスライスが使えるのでリスト操作で困ることは少ないです。
LINQの特徴としてはメソッドチェーンや遅延評価などがありますが、
単純にコードを短くしつつ、わかりやすく書きやすいというのが大きなメリットだと思います。
Pythonのリスト内包表記やスライスで複雑な操作を書くと著しく可読性が落ちてしまいます。
このような状況を踏まえつつ、LINQの良い部分をなんとか再現できないか考えてみました。
#試作第1弾
まずは雰囲気を確かめるためにインターフェースだけそれっぽくしてみました。
LINQと同じメソッドを用意して、単体効果だけ近づけてみたのが試作第1弾です。
class GeneralIterator(object):
"""LINQメソッド名を使ってリスト操作をラップしたクラス
<usage>
selection = GeneralIterator(cmds.ls(sl=True))
selection.count()
for x in selection.generator(): print x
selection.last()
selection.first()
selection.at(3)
selection.distinct()
selection.skip(3)
selection.take(3)
selection.all(lambda x: x.startswith('mesh_'))
selection.any(lambda x: x.startswith('skel_'))
selection.contains("grp_")
selection.union(["group1", "group2"])
selection.reverse()
selection.select(lambda x: cmds.getAttr(x + '.tx'))
selection.where(lambda x: x.endswith('_offset'))
"""
def __init__(self, list=None):
self.set_list(list)
def set_list(self, list):
self.__list = list
def is_empty(self):
return self.__list is None or len(self.__list) == 0
def print_items(self):
for x in self.generator():
print x
def count(self):
if self.is_empty():
return 0
return len(self.__list)
def generator(self):
for x in self.__list:
yield x
def first(self, default=None):
if self.is_empty():
return default
return self.__list[0]
def last(self, default=None):
if self.is_empty():
return default
return self.__list[-1]
def at(self, index, default=None):
if index <= self.count():
return self.__list[index]
return default
def distinct(self):
return list(set(self.__list))
def skip(self, count):
if count < self.count():
return self.__list[count:]
def take(self, count):
if count <= self.count():
return self.__list[:count]
def all(self, func):
for x in self.generator():
if not func(x):
return False
return True
def any(self, func):
for x in self.generator():
if func(x):
return True
return False
def contains(self, obj):
for x in self.generator():
if x == obj:
return True
return False
def union(self, list):
return self.__list + list
def reverse(self):
return list(reversed(self.__list))
def select(self, func):
return [func(x) for x in self.__list]
def where(self, func):
return [x for x in self.__list if func(x)]
まぁとりあえずという感じですね。
値を返しているのでメソッドチェーンはできませんし、遅延評価という要素もまったくありません。
しかしながら、メソッド名とPythonコードの実装内容がざっくり脳内変換できました。
#試作第2弾
次のステップとしては、メソッドチェーンが欲しいですね。遅延実行もできれば欲しい。
LINQの実装方法を読んでみて、クロージャを使えば似たようなものが作れるかもと思って試したのが第2弾です。
class EnumerableIterator(object):
"""LINQのようにメソッドチェーンと遅延実行的なことをやってみたリスト操作クラス
[usage]
hoge = EnumerableIterator(range(10))
for x in hoge.where(lambda x: x > 7).select(lambda x: x * x): print x
"""
def __init__(self, list=None, func=None):
self._set_list(list)
self.func = func
def _set_list(self, list):
self.__list = list
def __execute_func(self):
if self.func is None:
return self.__list
return self.func(self.__list)
def __iter__(self):
for x in self.__execute_func():
yield x
def to_list(self):
return self.__execute_func()
def count(self):
return len(self.__execute_func())
def __is_empty(self, list):
return list is None or len(list) == 0
def first(self, default=None):
result = self.__execute_func()
if self.__is_empty(result):
return default
return result[0]
def last(self, default=None):
result = self.__execute_func()
if self.__is_empty(result):
return default
return result[-1]
def at(self, index, default=None):
result = self.__execute_func()
if self.__is_empty(result):
return default
if index <= len(result):
return list[index]
return default
def distinct(self):
return list(set(self.__execute_func()))
def skip(self, count):
result = self.__execute_func()
return result[count:]
def take(self, count):
result = self.__execute_func()
return result[:count]
def all(self, func):
for x in self:
if not func(x):
return False
return True
def any(self, func):
for x in self:
if func(x):
return True
return False
def contains(self, obj):
for x in self:
if x == obj:
return True
return False
def union(self, list):
return self.__execute_func() + list
def reverse(self):
return list(reversed(self.__execute_func()))
def where(self, func):
def action(list):
result = list
if self.func is not None:
result = self.func(list)
return [x for x in result if func(x)]
return EnumerableIterator(self.__list, action)
def select(self, func):
def action(list):
result = list
if self.func is not None:
result = self.func(list)
return [func(x) for x in result]
return EnumerableIterator(self.__list, action)
書いていて結構混乱しましたね・・・。selectとwhere以外は値かリストを返していますが、
リストを返すものはEnumerableIteratorを返す実装にするのが良いと思います。
クロージャを使った動的メソッドを蓄積し、任意のタイミングでメソッド実行出来るようになりました。
LINQの遅延評価と同じになったとは言えませんが、クエリ構築と実行のタイミングは分離できたかなと思います。
#試作第2弾を使う
このままではMaya関係ない内容になってしまうのでMayaで使ってみます。
class EnumerableSelection(EnumerableIterator):
"""オブジェクト選択イテレータ
[usage]
selection = Selection()
for x in selection.where(lambda x: x.endswith('_offset')).select(lambda x: cmds.getAttr(x + '.tx')):
print x
print selection \
.where(lambda x: x.endswith('Group')) \
.select(lambda x: cmds.getAttr(x + '.tx')) \
.where(lambda x: x > 0.1) \
.first()
"""
def __init__(self, flat=True):
super(EnumerableSelection, self).__init__()
self.__flat = flat
self.update()
def update(self):
self._set_list(cmds.ls(sl=True, fl=self.__flat))
選択されているものをゴニョゴニョするというはよく使うのでわかりやすいですかね。
残念なのは途中改行にバックスラッシュが必要な点ですね・・・そしてlambda・・・
#感想
itertoolsやmore-itertoolsという手もあるんですが、書き方はやっぱりLINQの方が好みですね。
もっと言語仕様を理解して使いこなせれば、Pythonicな書き方ができるんだろうなと感じます。
多方面からのツッコミやアドバイスも待ちつつ、追加のお勉強をしていきたいと思います。
#参考サイト