web-dev-qa-db-ja.com

Pythonでオブジェクトプロパティのタイプと値を明確に制限するにはどうすればよいですか?

私は昔からpythonを学んでいます。しかし、ほぼ最初に、簡単な質問に出くわしました。難しいクラスの説明を作成せずに、オブジェクトの値またはオブジェクトのプロパティに制限(制限)を設定する方法です。 Pythonの厳密でない型付けには問題はありません。変数のタイプだけでなく、変数の値のダイアパソンも制御するメソッドを検索します。たとえば、古典的な教育クラス「Circle」を作成してみましょう。 Circle-オブジェクトは3つのプロパティを持つ必要があります。2つの原点座標(一部のx、y)と半径(人間の目に見える世界では)xとyは実際の数値であり、半径は実際の正の数値である必要があります。私はこのタスクを解決しました、以下の「Circle」クラスの対応するメソッド:

def __setattr__(self,name,value):
    def setting_atr(name,value):
        if isinstance(value,(float,int)):
            if name=='radius' and value<0:
                raise ValueError("Attribute 'radius' should be no negative")
            self.__dict__[name]=value
        else:
            raise TypeError("Attribute should be numerical")
    if len(self.__dict__)<3: #number of object attribute
        return setting_atr(name,value)
    Elif name not in self.__dict__:
        raise AttributeError("'Circle' object has no such attribute")
    else:
        return setting_atr(name,value)

それにもかかわらず、私はそれを行った方法に全く不満を感じました。コードは非常に小さな概念のために巨大であり、ほとんど再利用できません(記述子について知っています-ほぼ同じ結果になります)。私は宣言的なスタイルを好むでしょう。私が代替案の検索を始めていたとき、私の最初の考えは自分のオペレーターを追加することでした。しかし、ハードググリングの後、python自体でそれを作成することは不可能であることに気付きました(私がまだ間違っている場合-どうかご安心ください)。私は初心者だけで、python開発者チームに参加したり、フォークしたpython(:))をしたりすることはできません。私はこの考えをあきらめました。宣言型スタイルのもう1つのパスは、次のようなものを書くことです。

radius=radius if isinstance(radius,(float,int)) else raise TypeError("Attribute should be numerical")
radius=radius if radius>=0 else raise ValueError("Attribute 'radius' should be no negative")

もちろん、それによって「レイズ」を使用する能力がないため、機能しません。エラーオブジェクトを関数として呼び出して、それを実現します。これを行う最も簡単な方法は、次のようなエラー発生関数を記述したことです。

def SomeError(exception=Exception, message="Something don't well"):
if isinstance(exception.args,Tuple):
    raise exception
else:
    raise exception(message)

だからそれは書くことができます:

radius=radius if radius>=0 else SomeError(ValueError("Attribute 'radius' should be no negative"))

私はフォローされているフォームについても考えました:

@dfValueError(['isinstance(radius,(float,int))'],"Attribute should be numerical")
@dfTypeError(['radius>=0'],"Attribute 'radius' should be no negative")
def fun_radius(x)
    return x

このアプローチの小さな問題は、印刷が複雑で情報量が少ないエラー出力印刷にあります。最初のエラーメッセージは、 "エラー"が実際に2行目でのみ発生した場所である、偽のエラー発生関数につながります。

私の目標は、上記で示したようなラムダ関数とコード行から例外を発生させ、コードをより明確にすることです。これを行うには、callメソッドを実装した独自の例外クラスを作成する必要があります。問題:継承されたクラスは自分自身の名前空間に表示されなかったため、メソッド

__call__(self):
    raise self #yes ha-ha

結果を与える:

TypeError: __call__() missing 1 required positional argument: 'self'

だから私の質問は簡単です:エラーオブジェクトを呼び出し可能にすることは可能ですか?はいの場合、それを行うために、私は何を読むべきか。そして、もし誰かがこの小さな問題を解決してくれれば幸いです。追伸上記のサンプル(半径変数宣言について)は、この「サークル」クラスではそのまま実装できないことを知っています。私は自分の思考の軌跡を示すための例としてのみ示します。それを選んではいけません。

あとがき(Maratによる)このトピックに書き込んで投票してくれた方に感謝します。回答を読んだ後、私は非常に長く考え、最終的にこのトピックにいくつかの拡張を加えるために自分自身を引き上げました(たとえそれがルールを侵害するリスクを伴うとしても)。最初に。一部の人々は、オブジェクトの作成状態でのエラーを気にする必要はなく、変数の型をまったく制御する必要がないと書いています(デフォルトでpythonに実装されていないため)。私はオブジェクト指向プログラミングに長い間携わっていませんが、それ以前は、技術的なプロセスやデバイスの計算に携わっていました(そして今もそうです)。したがって、私はこの単語について非常に自信があります。「考えられるエラーはできるだけ早くローカライズする必要があります(改善されたとは言いませんでした)」。実際の物理プロセスを計算するための多くのアルゴリズムは反復と再帰を使用するため、(後で処理するために)抜けた場合、発生した間違いが最終結果に検出可能なエラーをもたらす可能性があります。したがって、この観点からはpythonがあります。 Pythonの「ダックタイピング」は、迅速なプロトタイピングと応用プログラミングの簡略化に最適です。しかし、「アヒルのタイピング」は宗教的な要求ではありません。それにもかかわらず、Pythonの厳密でない型付けは、必要に応じて型コントロールを追加することは奇妙なことではありません。しかし、それを実行するためにPythonが課すスタイルは、C++、Javaなどの静的分類言語のスタイルとは異なります。Pythonでは、オブジェクトを制御できます一部の「コントロールポイント」のみ。これは、「システム指向プログラミング」の原則へのガイドです(Thermは私のものですが、プレスに存在するかどうかは確認しなかったため、他の著者と同じ意味である可能性があります。 )。 ((もしあれば))(main)システムの要素の入口と出口に「制御点」を設定する必要があります。システム要素は何ですか?これは、独自の名前空間を持ち、(メソッドによっても)関数として外部と対話できる任意のオブジェクトである必要があります。 function要素を制御するインレットの最も単純なバージョンは次のとおりです。

def func(a,b,c):
    #just like this
    a=int(a)
    b=str(b)
    c=list(c)

ここに示すより複雑で興味深いアプローチ: Pythonの型チェックデコレータ2

別に、私の共著者が提案した方法について。 「プロパティ」デコレータは記述子と同じメソッドを使用するため、object属性をclassメソッド。 pythonでは同じクラスのオブジェクトがクラス名前空間を共有するため、このモードは慎重に使用する必要があります。 @Izkataが書いたのと同じプログラムを__setattr__(もう一度)で書き換えました。

filter_function_for_radius=lambda value: value if value>0 else SomeError(ValueError, 'Radius cannot be negative')
class Circle(object):
    def __init__(self, x=0, y=0, radius=1):
        self.x = x
        self.y = y
        self.radius =radius
    def __setattr__(self,name,value):
        if name=='radius':
            value=filter_function_for_radius(value)
        self.__dict__[name]=value

...そして、ある架空の円に別の(おそらく複雑または負の)半径が必要な場合は、クラスを継承または再定義する必要はなく、「フィルターを変更する」だけです。ただし、関数スタイルのその利点は、__setattr__を使用するか、独自のメタクラスを作成することによってのみ得られます。そして、もう一度お答えいただきありがとうございます。

2
Marat

これがTypeError: __call__() missing 1 required positional argument: 'self'なしの私のコードです。このエラーをどのように作成しますか?

>>> class X(Exception):
    def __call__(self):
        raise self


>>> x = X()
>>> x()

Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    x()
  File "<pyshell#3>", line 3, in __call__
    raise self
X
>>> class Other(X):
    pass

>>> o = Other()
>>> o()

Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    o()
  File "<pyshell#3>", line 3, in __call__
    raise self
Other
0
User

Pythonのpropertyデコレータを再発明しようとしているようです。 StackOverflowでそれらがどのように使用されているかについての良い質問があり、一番上の答えはあなたがここでやろうとしていることを正確に持っています: Pythonでプロパティ機能を使用する方法に関する実際の例?

最初に戻って、代わりにサークルのプロパティを使用してみましょう:

Circle-オブジェクトは3つのプロパティを持つ必要があります。2つの原点座標(一部のx、y)と半径(人間の目に見える世界では)xとyは実際の数値であり、半径は実際の正の数値である必要があります。

最初は、制限のない基本的なCircleは次のようになります。

class Circle(object):
   def __init__(self, x=0, y=0, radius=1):
      self.x = x
      self.y = y
      self.radius = radius

ここで、何かが「数値である」ことを確認すると、Pythonの哲学 ダックタイピング に反することになります。他の回答に示されているように、おそらく間違っているでしょう。さらに、それらを数学の方程式で使用しようとすると、pythonはとにかく TypeError を発生させます。

したがって、これは、propertyを使用して、radiusが負にならないようにするために、__setattr__内の属性名をいじくる必要がない方法です。

class Circle(object):
   def __init__(self, x=0, y=0, radius=1):
      self.x = x
      self.y = y
      self.radius = radius

   @property
   def radius(self):
      return self._radius

   @radius.setter
   def radius(self, radius):
      if radius < 0:
         raise ValueError('Radius cannot be negative')

      self._radius = radius
4
Izkata

Pythonは型チェックされることを意図していないため、一般的にPythonは醜くなり、多くの追加コードが必要になります。 Pythonは(私が見つけた):信頼するが検証する。

つまり、型チェックをほとんど行わずにコードを記述し、ユーザーが必要な値を入力できるようにする必要があります。 "hello world"という値を半径とする円を作成したいのですが、どうしてですか?

ただし、この円を拡大しようとすると、エラーが発生するはずです。あなたのコードが私のstrintまたはfloatとして使用しようとしたため、おそらくタイプエラーでしょう。ただし、これはユーザー上です。

なぜ誰もがこれをするのかと思うかもしれません。これがニースの例です。

Haskellのような静的に型付けされた言語では、次のようにします。

data Point = Point { x :: Int, y :: Int }
data Circle = Circle { c :: Point, r :: Int }

実際、完全なサポートが必要な場合は、IntNum a => aに置き換えることができますが、これには、RankNTypesが必要であり、そのすべてに参加したくありません。

ここで、PointCircleは、必要な情報を正確に保持するデータ型であり、ばかげたことは何もできません。まだrを負にすることはできますが、それを簡単に防ぐことはできません。

Pythonで似たようなものを偽造したい場合は、次のようにすることができます:

NUMBER = (float, int, long)

def check_type(value, types, error):
   if isinstance(value, types):
       return True
   raise TypeError(error)

class Point(object):
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        if check_type(x, NUMBER, "x should be a number."): 
            self.x = x
        if check_type(y, NUMBER, "y should be a number"):
            self.y = y

 class Circle(object):
     __slots__ = ['c', 'r']
     def __init__(self, c, r):
         if check_type(c, Point, "c must be a Point"):
             self.c = c
         if check_type(x, NUMBER, "r must be a number"):
             self.r = r

これは私がそれを行うと考えることができる最もきれいな方法であり、それはまったくあまりきれいではありません。基本的には、型チェックコードを抽象化して関数にし、すべての情報を提供します。安全性を求めているなら、おそらくこれはとにかく行く方法ではないでしょう。なぜなら、今では、適切なタイプをすべて提供するのは開発者の負担であるからです。

たとえば、元のコードでは、intfloatのインスタンスのみをチェックしました。しかしながら:

>>> x = 100000000000000000000000000000000000000000000
>>> isinstance(x, (float, int))
False
>>> type(x)
<type 'long'>

この小さな特異性に対応できなかったため、チェックに間違ったタイプを指定したため、エンドユーザーはコードに修正不可能なバグをもたらす可能性があります。以前のHaskellのようなよりタイプ指向の言語では、Num a => aのような宣言でこれを解決できます。ただし、Pythonは、この種の作業を意図したものではないため、Python.

私のコードにもバグがあります!ユーザーが実際の平面ではなくcomplex平面に円を置きたい場合はどうなりますか? NUMBERタプルにcomplexを追加できなかったため、型チェックに失敗しました。

代替案を検討してください(@ Paddy3118の回答に記載されています)。

Point = namedtuple('Point', 'x', 'y')
Circle = namedtuple('Circle', 'c', 'r')

または、個別のPointタイプを使用しない場合:

Circle = namedtuple('Circle', 'x', 'y', 'r')

このコードは短く、簡潔で、必要な機能を正確に提供します。さらにメソッドを追加する必要がある場合は、次のことを実行できます。

import math

class Circle(namedtuple('Circle', 'x', 'y', 'r'):
   __slots__ = ()
   def scale(scale_factor):
       self._replace(r = r * scale_factor)

   @property
   def area(self):
       return (math.pi ** 2) * (self.r)

   @property
   def circumference(self):
       return math.pi * 2 * self.r

その最後のオプションはおそらくあなたの最善の策です。これにより、Pythonの動的型付けの柔軟性が得られますが、挿入できる内容は少し保守的です。

1
sortfiend

トリックは、防御的にコーディングしすぎないようにすることです。最初に最小値をコーディングしてみてください。

from collections import namedtuple
Circle = namedtuple('Circle', 'x, y, r')
c1 = Circle(0.0, 0.0, 1.0)

妥当な値を提供するようにCircleのユーザーに任せてください-通常は機能します。

上記のCircleクラスのdocstringは修正され、次のようにかなり簡潔になっています。

'Circle(x, y, r)'

たとえば、よりわかりやすい名前を使用することで、物事をより明確にすることができます。

Circle = namedtuple('Circle', 'center_x, center_y, radius')

または、より広範なdocstringをサブクラス化して追加します。

class Circle(namedtuple('Circle', 'x, y, r')):
    '''\
    Circle(x, y, r): Circle centered at point x,y of radius r
    '''

あなたが本当に必要広範な型チェックをするかどうか自分自身に尋ねる必要があります。これは通常、Pythonを作成するときに最初に行われることではなく、他のプログラミング言語に慣れているプログラマーには直観に反するように見えることがあります。

0
Paddy3118