2012-01-24 4 views
8

Python 3では、(1回のパスしか許されないイテレータではなく)オブジェクトがコンテナであるかどうかをどのように確認できますか?ここでイテラブルが複数のパスを許可するかどうかをチェックする方法?

は例です:

明らか
def renormalize(cont): 
    ''' 
    each value from the original container is scaled by the same factor 
    such that their total becomes 1.0 
    ''' 
    total = sum(cont) 
    for v in cont: 
     yield v/total 

list(renormalize(range(5))) # [0.0, 0.1, 0.2, 0.3, 0.4] 
list(renormalize(k for k in range(5))) # [] - a bug! 

renormalize関数はジェネレータ式を受信したときに意図したとおり、それは動作しません。コンテナを複数回反復することができますが、ジェネレータは1回だけ通過することができます。

理想的には、私はこれを行うにはしたいと思います:

def renormalize(cont): 
    if not is_container(cont): 
     raise ContainerExpectedException 
    # ... 

どのように実装することができis_container

私は、引数が空であるかどうかをチェックすることができます。私たちは2番目のパスを開始するようになっています。しかし、このアプローチは、2回目のパスが正確に始まるときにはっきりしない、より複雑な関数に対しては機能しません。さらに、私はむしろ、機能の深い部分ではなく、関数の入口にバリデーションを置くことにします(関数が変更されたときはいつでもそれをシフトします)。

もちろん、renormalize関数を1パスのイテレータで正しく動作するように書き換えることができます。しかし、それは入力データをコンテナにコピーする必要があります。何百万もの大規模なリストを「リストではない場合に備えて」コピーすることによるパフォーマンスへの影響はばかげています。

EDIT:私のオリジナルの例では、weighted_average機能使用:

def weighted_average(c): 
    ''' 
    returns weighted average of a container c 
    c contains values and weights in tuples 
    weights don't need to sum up 1 (automatically renormalized) 
    ''' 
    return sum((v * w for v, w in c))/sum((w for v, w in c)) 

weighted_average([(0,1), (1,1)]) #0.5 
weighted_average([(k, 1) for k in range(2)]) #0.5 
weighted_average((k, 1) for k in range(2)) #mistake 

をしかし、単一のパスを使用するようにリライトweighted_averageのバージョンがとにかく間違いなく優れているので、それが最良の例ではありませんでした。

def weighted_average(it): 
    ''' 
    returns weighted average of an iterator it 
    it yields values and weights in tuples 
    weights don't need to sum up 1 (automatically renormalized) 
    ''' 
    total_value = 0 
    total_weight = 0 
    for v, w in it: 
     total_value += v 
     total_weight += w 
    return total_value/total_weight 
+0

一般的なバージョンの問題は表示されません。プロファイルを作成しましたか?視覚的な複雑さはどういう意味ですか? – LBarret

+0

「正確に2回目のパスが始まるのは明白ではありませんか?これはどういう意味ですか? 'itertoolsを使うことができます。tee() 'を使用して、必要な回数だけ反復できることを無条件に保証します。アルゴリズムを設計しているとき、どうしてそれが明らかでないのでしょうか? –

+0

@LionelBarret:一般的な 'weighted_average'を使わない理由はないと思います。私は別の例を与えるために質問を更新しました。 – max

答えて

4

すべてのイテレート可能オブジェクトが、残念ながら、ないそれらのすべてを行い、collections.Iterableをサブクラス化する必要がありますが。ここでは、オブジェクトが実装するインターフェースに基づいて、「宣言する」ものではなく、答えを示します。

短い答え:

A「コンテナ」あなたはそれを呼び出すように、排出される発電機であることとは対照的に、複数回にわたり反復することができ、リスト/タプルすなわち、通常の両方を実装します__iter__および__getitem__。したがって、あなたはこれを行うことができます。

>>> def is_container_iterable(o): 
...  return hasattr(o, '__iter__') and hasattr(o, '__getitem__') 
... 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter([])) 
False 

長い答え:

しかし、あなたが排出されず、GetItem関数をサポートしていないのiterableを作ることができます。たとえば、素数を生成する関数。必要に応じて何回も世代を繰り返すことができますが、1065番目のプライムを取得する関数を持つと計算量が膨大になるため、それをサポートしたくないかもしれません。 :-)

これ以上「信頼できる」方法はありますか?

すべてのiterablesは、イテレータを返す__iter__関数を実装します。イテレータには__next__関数があります。これを反復処理するときに使用されるものです。 __next__を繰り返し呼び出すと、最後にイテレータが使い果たされます。

したがって、__next__関数がある場合は反復子であり、使い果たされます。まだイテレータではありません

>>> def foo(): 
... for x in range(5): 
...  yield x 
... 
>>> f = foo() 
>>> f.__next__ 
<method-wrapper '__next__' of generator object at 0xb73c02d4> 

反復可能オブジェクトは、反復可能に返すよう、__next__機能を持っていませんが、__iter__機能を実装します:

>>> r = range(5) 
>>> r.__next__ 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
AttributeError: 'range' object has no attribute '__next__' 
>>> ri = iter(r) 
>>> ri.__next__ 
<method-wrapper '__next__' of range_iterator object at 0xb73bef80> 

だから、オブジェクトが__iter__を持っていることを確認することができますが、それは__next__を持っていません。

>>> def is_container_iterable(o): 
...  return hasattr(o, '__iter__') and not hasattr(o, '__next__') 
... 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter(range(5))) 
False 

イテレータも自己を返します__iter__機能を有しています。あなたが壊れたイテレータを返すオブジェクト、お電話ないリターン自己を行うものを実装する場合

>>> def is_container_iterable(o): 
...  return iter(o) is not o 
... 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter([])) 
False 

失敗します:

>>> iter(f) is f 
True 
>>> iter(r) is r 
False 
>>> iter(ri) is ri 
True 

したがって、あなたはチェックのこれらの変動を行うことができますiter()をもう一度実行します。しかし、あなた(またはサードパーティのモジュール)のコードは、実際には間違ったことをしています。

イテレータの作成に依存するため、理論上は副作用があるオブジェクト__iter__を呼び出すことになりますが、上記のhasattrコールには副作用があってはいけません。 OK、それはgetattributeを持っている可能性があります。しかし、あなたは以下のようなものをこれを修正することができます

>>> def is_container_iterable(o): 
...  try: 
...   object.__getattribute__(o, '__iter__') 
...  except AttributeError: 
...   return False 
...  try: 
...   object.__getattribute__(o, '__next__') 
...  except AttributeError: 
...   return True 
...  return False 
... 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter(range(5))) 
False 

をこの1つは、合理的に安全であり、かつ物体が__getattribute__の呼び出しで動的に__next__または__iter__を生成した場合を除き、すべてのケースで動作するはずですが、あなたは非常識であることを行う場合。 :-)

本質的に私の好みのバージョンはiter(o) is oですが、経験に基づいているわけではありません。

+0

+1:クラスが 'collections.Iterable'からサブクラス化しなければならない場合、これを行う方法があるとは思いませんでした。 (IterableとIteratorの両方から有意義なクラスが得られますか?) – max

+0

IterableはIteratorから派生しています。 –

3

collectionsモジュールで定義されている抽象基本クラスを使用して、itがcollections.Iteratorのインスタンスであるかどうかを確認して確認できます。私は複数のリスト内包/和バージョンより読みはるかに簡単加重平均のあなたのイテレータ優しいバージョンを見つけるのに個人的に

if isinstance(it, collections.Iterator): 
    # handle the iterator case 

。 :-)

+0

はい、同意します。私は1つのパスを使用することが実現可能ではないように見える例を示すために質問を更新しました。 – max

+0

ニース! 3.xに標準的な方法があるように見えます。 –

+0

"virtual container"のようなオブジェクトであっても、range(5) 'のように動作しているようです。素晴らしく見える! – max

1

最良の方法は、抽象基底クラスのインフラストラクチャを使用することです:

def weighted_average(c): 
    if not isinstance(c, collections.Sequence): 
     raise ContainerExpectedException 
関連する問題