8

私のプログラミング経験の大部分はC++でした。ここでのBjarneStroustrupの講演に触発されて、私のお気に入りのプログラミング手法の1つは「タイプリッチ」プログラミングです。機能を型にラップすることによって記述しなければならないコードの量を減らすだけでなく、新しい堅牢なデータ型の開発(たとえば、newVec.x = vec1.x + vec2.x;newVec.yの代わりにベクトルの追加) = ...など、newVec = vec1 + vec2)を使用できますが、強力な型システムを使用したコンパイル時にコードの問題も明らかになります。

私がPython2.7で行った最近のプロジェクトでは、上限と下限を持つ整数値が必要です。私の最初の本能は、Pythonの通常の数値とすべて同じ動作をするが、常にその(動的)境界値内にある新しいデータ型(クラス)を作成することです。

class BoundInt:
    def __init__(self, target = 0, low = 0, high = 1):
        self.lowerLimit = low
        self.upperLimit = high
        self._value = target
        self._balance()

    def _balance(self):
        if (self._value > self.upperLimit):
            self._value = self.upperLimit
        elif (self._value < self.lowerLimit):
            self._value = self.lowerLimit
        self._value = int(round(self._value))

    def value(self):
        self._balance()
        return self._value

    def set(self, target):
        self._value = target
        self._balance()

    def __str__(self):
        return str(self._value)

これは良いスタートですが、そのようにこれらのBoundIntタイプの肉にアクセスする必要があります

x = BoundInt()
y = 4
x.set(y)           #it would be nicer to do something like x = y
print y            #prints "4"
print x            #prints "1"
z = 2 + x.value()  #again, it would be nicer to do z = 2 + x
print z            #prints "3" 

多数のPythonの「マジックメソッド」定義をクラスに追加して、さらにいくつかの機能を追加できます。

def __add__(self, other):
    return self._value + other

def __sub__(self, other):
    return self._value - other

def __mul__(self, other):
    return self._value * other

def __div__(self, other):
    return self._value / other

def __pow__(self, power):
    return self._value**power

def __radd__(self, other):
    return self._value + other

#etc etc

現在、コードのサイズは急速に拡大しており、記述されている内容が何度も繰り返されていますが、ほとんど返されませんが、これはまったくPythonのようには見えません。

通常のPython番号(整数?)や他のBoundIntオブジェクトからBoundIntオブジェクトを作成したい場合、事態はさらに複雑になります。

x = BoundInt()
y = BoundInt(x)
z = BoundInt(4)

私の知る限り、Pythonは(cスタイル)オーバーロードをサポートしていないため、BoundInt()コンストラクター内でかなり大きい/醜いif/else型チェックステートメントを使用する必要があります。

これらはすべて、Pythonでc ++コードを書き込もうとしているように感じます。これは、私のお気に入りの本の1つであるCodeComplete2が真剣に受け止められた場合の重大な罪です。動的型付けの流れに逆らって泳いでいるような気がします。

私はPythonの「pythonic-ally」のコーディングを学びたいと思っています。この種の問題ドメインにアプローチするための最良の方法は何ですか?適切なpythonicスタイルを学ぶための良いリソースは何ですか?

4

4 に答える 4

4

標準ライブラリ、一般的な PyPI モジュール、および ActiveState レシピには、この種のことを行うコードがたくさんあるため、最初の原則から理解しようとするよりも、例を読んだほうがよいでしょう。また、これはlist-like またはdict-like クラスの作成と非常によく似ていることに注意してください。これにはさらに多くの例があります。

ただし、やりたいことに対する答えはいくつかあります。最も深刻なものから始めて、逆方向に作業します。

通常の python 数値 (整数?) から BoundInt オブジェクトを構築したい場合や、他の BoundInt オブジェクトを構築したい場合、事態はさらに複雑になります。 Python は (c スタイルの) オーバーロードをサポートしていないため、BoundInt() コンストラクター内のステートメント。

ああ、でも自分が何をしているのか考えてみてください:BoundInt整数のように振る舞えるものからintaを構築していBoundIntます。では、なぜでしょうか。

def __init__(self, target, low, high):
    self.target, self.low, self.high = int(target), int(low), int(high)

__int__もちろん、すでにメソッドをに追加していると仮定していますBoundInt( C++ に相当explicit operator int() const)。

また、コピーを作成するための「コピー コンストラクター」がないため、オーバーロードの欠如は、C++ から来ると考えているほど深刻ではないことに注意してください。オブジェクトを渡すだけで、すべてがカバーの下で処理されます。

たとえば、次の C++ コードを想像してください。

BoundInt foo(BoundInt param) { BoundInt local = param; return local; }
BoundInt bar;
BoundInt baz = foo(bar);

これは、 、 、および名前のない「戻り値」変数にコピーされbar、それが にparamコピーされます。これらのいくつかは最適化され、他のもの (C++11) はコピーの代わりに移動を使用しますが、それでも、コピー/移動コンストラクター/代入演算子の 4 つの概念的な呼び出しがあります。paramlocallocalbaz

次に、同等の Python を見てください。

def foo(param): local = param; return local
bar = BoundInt();
baz = foo(bar)

ここでは、BoundInt明示的に作成されたインスタンスが 1 つだけあり、新しい名前をそれにバインドしているだけです。bazのスコープを超えてコピーを作成しない新しいオブジェクトのメンバーとしてbar割り当てbazても。コピーを作成する唯一のことは、明示的にBoundInt(baz)再度呼び出すことです。(これは 100% 真実というわけではありません。なぜなら、誰かがいつでもあなたのオブジェクトを調べて外部からクローンを作成しようとする可能性がありpickledeepcopyなどは実際にそうするかもしれません…しかし、その場合、彼らはまだ「コピー」を呼び出していないからです。あなたまたはコンパイラーが書いたコンストラクター」)。

では、これらすべての演算子を値に転送するのはどうでしょうか?

1 つの可能性は、動的に行うことです。詳細は、Python 3 と 2 のどちらを使用しているかによって異なります (また、2 の場合は、サポートする必要がある期間はどれくらいか)。しかし、アイデアは、名前のリストを持っているだけで、それぞれに対して、値オブジェクトで同じ名前のメソッドを呼び出すその名前のメソッドを定義するということです。このスケッチが必要な場合は、追加情報を提供して質問してください。ただし、動的メソッド作成の例を探す方がよいでしょう。

それで、それはPythonicですか?まあ、それは依存します。

数十の「整数のような」クラスを作成している場合は、コードをコピーして貼り付けたり、「コンパイル時」の生成ステップを追加したりするよりも確かに優れており、そうでなければ不要な基本クラスを追加するよりもおそらく優れています。

また、Python の多くのバージョンで作業しようとしていて、「どのバージョンの提供を停止すれば、再び__cmp__同じように振る舞うことができるのか?」ということを思い出したくない場合は、int質問を入力すると、さらに進んで、メソッドのリストをintそれ自体から取得する可能性があります (dir(int())いくつかの名前を取り出してブラックリストに載せます)。

しかし、たとえば、Python 2.6-2.7 または 3.3+ だけでこの 1 つのクラスを実行している場合、それはトスアップだと思います。

読むのに適したfractions.Fractionクラスは、標準ライブラリのクラスです。これは明確に記述された純粋な Python コードです。また、動的メカニズムと明示的メカニズムの両方を部分的に示しています (一般的な動的転送関数の観点から各特別なメッセージを明示的に定義しているため)。2.x と 3.x の両方を使用している場合は、2 つを比較対照できます。

その間、あなたのクラスは指定されていないようです。xが で が であるBoundInt場合y、(コードでそうであるように)本当に を返すint必要がありますか? そうでない場合は、バインドする必要がありますか? どうですか?どうすればいいですか?等々。x+yinty+xx+=y

最後に、Python では、直観的な C++ の等価物が可変であっても、このような「値クラス」を不変にする価値があることがよくあります。たとえば、次のように考えてください。

>>> i = BoundInt(3, 0, 10)
>>> j = i
>>> i.set(5)
>>> j
5

私はあなたがこれを期待しているとは思わない。これはj = i、新しいコピーを作成するため、C++ (典型的な値クラス) では発生しませんが、Python では、新しい名前を同じコピーにバインドするだけです。BoundInt &j = i(ではなくと同等BoundInt j = iです。)

不変になりたい場合はBoundInt、 のような明白なものを排除するsetだけでなく、実装しないようにして__iadd__ください。を省略する__iadd__と、i += 2は : に変わりi = i.__add__(2)ます。つまり、新しいインスタンスが作成され、その新しいインスタンスに再バインドiされ、古いインスタンスはそのままになります。

于 2012-11-06T01:05:47.787 に答える
2

これについてはおそらく多くの意見があります。しかし、特別な方法の急増に関しては、それを完了するためにそれをしなければならないでしょう。しかし、少なくともあなたはそれを一度だけ、一箇所で行います。また、組み込みの数値タイプをサブクラス化することもできます。それは私が同様の実装のためにしたことであり、あなたはそれを見ることができます

于 2012-11-06T01:10:07.680 に答える
1

あなたのset方法は忌まわしいものです。デフォルト値がゼロの数値を作成してから、その数値を別の数値に変更することはありません。これは、Python で C++ をプログラミングしようとする試みであり、これらを実際に数値と同じように扱いたい場合は、無限の頭痛の種になります。関数に渡すたびに、それらは参照によって渡されるためです(パイソン)。したがって、数値のように扱うことができると思われるものに大量のエイリアシングが発生することになり、エイリアシングされていることに気付いていない、または取得できることを期待していない数値の値を変更することにより、ほぼ確実にバグに遭遇します。BoundIntをキーとしてディクショナリに格納された値は、別BoundIntの値に同じ値を提供します。

私にとって、特定の値に関連付けられたデータ値highではなく、型パラメーターです。1 から 10 の間に制限されている数値ではなく、typeの数値が必要です。これらはすべて type の値です。lowBoundInt7BoundInt(1, 10)7BoundInt

私が本当にこのようなことをしたいのであれば、私がとるアプローチは、サブクラス化することであり、クラス ファクトリとしてint扱うことです。BoundInt範囲を指定すると、その範囲内に制限される整数の型が得られます。その型を「int のような」オブジェクトに適用すると、その範囲に固定された値が得られます。何かのようなもの:

_bound_int_cache = {}
def BoundInt(low, low):
    try:
        return _bound_int_cache[(low, high)]
    except KeyError:
        class Tmp(int):
            low = low
            high = high
            def __new__(cls, value):
                value = max(value, cls.low)
                value = min(value, cls.max)
                return int.__new__(cls, value)

        Tmp.__name__ = 'BoundInt({}, {})'.format(low, high)
        _bound_int_cache[(low, high)] = Tmp
        return _bound_int_cache[(low, high)]

(キャッシュはBoundInt、同じ低い/高い値の型を取得しようとする 2 つの異なる試みが、同じように動作する 2 つの異なるクラスではなく、まったく同じクラスを与えることを保証するためのものです。ほとんどの場合、実際には問題にならないでしょう。 、しかしそれはより良いようです。)

これを次のように使用します。

B = BoundInt(1, 10)
x = B(7)

「クラス ファクトリ」アプローチとは、整数をバインドする意味のある範囲が少数ある場合、それらの範囲のクラスをグローバルに (意味のある名前で) 作成し、通常のクラスとまったく同じように使用できることを意味します。

サブクラス化intにより、これらのオブジェクトは不変になり (これが で初期化を行う必要があった理由__new__です)、エイリアシングのバグから解放されます (数値のような単純な値の型でプログラミングしている場合、人々は心配する必要はないと考えています)。正当な理由)。また、すべての整数メソッドを無料で提供するため、これらの型は とまったく同じようにBoundInt動作しますが、作成時に値が型によってクランプされる点が異なります。残念ながら、これは、これらの型に対するすべての操作が、オブジェクトではなくオブジェクトを返すことを意味します。intintBoundInt

たとえば に含まれる 2 つの異なる値の低い値と高い値を調整する方法を思いついた場合はx + y、特別なメソッドをオーバーライドしてBoundInt値を返すようにすることができます。頭に浮かぶアプローチは次のとおりです。

  1. x + y左のオペランドの境界を取り、右のオペランドを無視します (乱雑で非対称に見えます; =という仮定に違反していますy + x)
  2. 最大low値と最小high値を取ります。これは見事に対称的であり、and 値を持たない数値をlowandhigh値であるかのようsys.minintに扱うことができますsys.maxint(つまり、他の値の境界を使用するだけです)。範囲がまったく重ならない場合は、空の範囲になってしまうため、あまり意味がありませんが、そのような数値を一緒に操作しても、おそらくあまり意味がありません.
  3. 最小low値と最大high値を取ります。BoundIntこれも対称ですが、ここでは、通常の数値が整数範囲全体に及ぶ可能性のある値であると偽るのではなく、通常の数値を明示的に無視することをお勧めします。

上記のいずれもうまくいく可能性があり、おそらくある時点で驚かれることでしょう (たとえば、正の範囲に制限されている数値を否定すると、常に範囲内の最小の正の数値が得られますが、これは私には奇妙に思えます)。

このアプローチを取る場合、おそらくサブクラス化したくないintでしょう。がある場合、コードを尊重せずに追加を処理するためnormalInt + boundedIntですnormalInt。代わりにboundedIntint値として認識しないようにしたいので、それは機能せず、クラスに を試す機会を与えintます。しかし、私はあなたのクラスを「不変」として扱い、新しい数値を作成するすべての操作で新しいオブジェクトを作成します数値をその場で変更すると、いつかバグが発生することが実質的に保証されます。__add____radd__

したがって、私はそのアプローチを次のように処理します。

class BoundIntBase(object):
    # Don't use this class directly; use a subclass that specifies low and high as
    # class attributes.
    def __init__(self, value):
        self.value = min(self.high, max(self.low, int(value)))

    def __int__(self):
        return self.value

# add binary operations to BoundInt
for method in ['__add__', '__radd__', ...]:
    def tmp(self, other):
        try:
            low = min(self.low, other.low)
            high = max(self.high, other.high)
        except AttributError:
            cls = type(self)
        else:
            cls = BountInd(low, high)
        v = getattr(int(self), method)(int(other))
        return cls(v)
    tmp.__name__ = method
    setattr(BountIntBase, method, tmp)


_bound_int_cache = {}
def BoundInt(low, low):
    try:
        return _bound_int_cache[(low, high)]
    except KeyError:
        class Tmp(int):
            low = low
            high = high
            def __new__(cls, value):
                value = max(value, cls.low)
                value = min(value, cls.max)
                return int.__new__(cls, value)

        Tmp.__name__ = 'BoundInt({}, {})'.format(low, high)
        _bound_int_cache[(low, high)] = Tmp
        return _bound_int_cache[(low, high)]

まだ必要以上のコードのように見えますが、実際にやろうとしていることは、思っているよりも複雑です。

于 2012-11-06T02:54:07.590 に答える
0

Python では豊富な構文がサポートされているため、すべての状況で数値とまったく同じように動作する型には多くの特別なメソッドが必要です (Python では、リストのように動作する型を定義する方がはるかに簡単です。たとえば、Python の dict:いくつかのメソッドがあり、 Sequence があります)。コードの繰り返しを少なくするには、いくつかの方法があります。

などの ABC クラス numbers.Integral は、一部のメソッドのデフォルト実装を提供します。たとえば、 がサブクラスで実装されている場合、は__add__自動的 に使用可能になります。__radd____sub____rsub__

fractions.Fraction を使用 して、他の数値型を処理するためのフォールバック演算子を_operator_fallbacks 定義および提供します。__r*__

__op__, __rop__ = _operator_fallbacks(monomorphic_operator, operator.op)

Python では、ファクトリ関数/メタクラスでクラスを動的に生成/変更 できます。たとえば、この Python コードを要約するのを手伝ってくれる人はいますか? . (非常に)まれなケースでも exec使用できます namedtuple()

数値は Python では不変であるため、__new__の代わりに使用する必要があり__init__ます。

でカバーされないまれなケースは、クラス メソッド__new__で定義できます 。from_sometype(cls, d: sometype) -> your_type逆に、特別な方法でカバーされていない場合は、方法を使用できます as_sometype(self) -> sometype

あなたの場合のより簡単な解決策は、アプリケーション ドメインに固有の上位レベルの型を定義することです。数値の抽象化が低レベルすぎる可能性があります。たとえば、 decimal.Decimal6 KLOC を超えています。

于 2012-11-06T10:54:36.900 に答える