ホームページ  >  記事  >  バックエンド開発  >  Python の属性と記述子の使用

Python の属性と記述子の使用

高洛峰
高洛峰オリジナル
2017-02-25 10:32:131016ブラウズ

@property デコレーターについて

Python では @property デコレーターを使用して、関数呼び出しをプロパティへのアクセスとして偽装します。

それで、なぜこんなことをするのですか? @property を使用すると、クラスのプロパティにアクセスするための単純なインターフェイスを維持しながら、カスタム コードを変数のアクセス/設定に関連付けることができるためです。

たとえば、映画を表す必要があるクラスがあるとします。

class Movie(object):
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description
 self.score = scroe
 self.ticket = ticket

このクラスをプロジェクト内の他の場所で使い始めましたが、その後、次のことに気づきました。映画は?あなたはこれが間違った動作であると考えており、Movie クラスがこのエラーを防止できることを望んでいます。 最初に思いつくのは、Movie クラスを次のように変更することです:

class Movie(object):
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description
     self.ticket = ticket
 if score < 0:
  raise ValueError("Negative value not allowed:{}".format(score))
 self.score = scroe

しかし、それはうまくいきません。コードの他の部分は Movie.score を通じて直接割り当てられるためです。この新しく変更されたクラスは、__init__ メソッド内の誤ったデータのみをキャプチャしますが、既存のクラス インスタンスについては何もできません。誰かが m.scrore= -100 を実行しようとすると、それを止めることはできません。それで今何をすべきですか?

Python のプロパティはこの問題を解決します。

これを行うことができます

class Movie(object):
 def __init__(self, title, description, score):
 self.title = title
 self.description = description
 self.score = score
     self.ticket = ticket
 
 @property
 def score(self):
 return self.__score
 
 
 @score.setter
 def score(self, score):
 if score < 0:
  raise ValueError("Negative value not allowed:{}".format(score))
 self.__score = score
 
 @score.deleter
 def score(self):
 raise AttributeError("Can not delete score")

。これにより、任意の場所でスコアを変更すると、それが 0 より小さいかどうかが検出されます。

プロパティのデメリット

プロパティの最大のデメリットは、再利用できないことです。たとえば、チケットフィールドにも非マイナスチェックを追加したいとします。

以下は変更された新しいクラスです:

class Movie(object):
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description
 self.score = score
 self.ticket = ticket
 
 @property
 def score(self):
 return self.__score
 
 
 @score.setter
 def score(self, score):
 if score < 0:
  raise ValueError("Negative value not allowed:{}".format(score))
 self.__score = score
 
 @score.deleter
 def score(self):
 raise AttributeError("Can not delete score")
 
 
 @property
 def ticket(self):
 return self.__ticket
 
 @ticket.setter
 def ticket(self, ticket):
 if ticket < 0:
  raise ValueError("Negative value not allowed:{}".format(ticket))
 self.__ticket = ticket
 
 
 @ticket.deleter
 def ticket(self):
 raise AttributeError("Can not delete ticket")

コードが大幅に増加していることがわかりますが、繰り返しロジックもたくさんあります。プロパティはクラスのインターフェイスを外側からはきちんとしていて美しく見せますが、内側からはそれほどきちんとしていて美しくはありません。

記述子はこちらです

記述子とは何ですか?

一般的に言えば、記述子はバインディング動作を持つオブジェクト プロパティであり、そのプロパティへのアクセスは記述子プロトコル メソッドによってオーバーライドされます。これらのメソッドは、__get__()、__set__()、および __delete__() です。オブジェクトにこれら 3 つのメソッドの少なくとも 1 つが含まれている限り、そのオブジェクトは記述子と呼ばれます。

記述子は何をしますか?

属性アクセスのデフォルトの動作は、オブジェクトの辞書から属性を取得、設定、または削除することです。たとえば、a.x には、a.__dict__['x'] で始まり、type(a).__dict__[' で始まる検索チェーンがあります。 x'] を実行し、メタクラスを除く type(a) の基本クラスを続行します。検索された値が記述子メソッドの 1 つを定義するオブジェクトである場合、Python はデフォルトの動作をオーバーライドして、代わりに記述子メソッドを呼び出します。優先順位チェーンで発生するかどうかは、どの記述子メソッドが定義されているかによって異なります。——–公式ドキュメントから抜粋

簡単に言えば、記述子は属性の取得、設定、削除の基本的な方法を変更します。

まず、記述子を使用して上記のプロパティ ロジックが繰り返される問題を解決する方法を見てみましょう。

class Integer(object):
 def __init__(self, name):
 self.name = name
 
 def __get__(self, instance, owner):
 return instance.__dict__[self.name]
 
 def __set__(self, instance, value):
 if value < 0:
  raise ValueError("Negative value not allowed")
 instance.__dict__[self.name] = value
 
class Movie(object):
 score = Integer(&#39;score&#39;)
 ticket = Integer(&#39;ticket&#39;)

記述子は優先度が高く、デフォルトの get および set 動作を変更するため、このように Movie().score にアクセスまたは設定すると、記述子 Integer によって制限されます。

ただし、常に次の方法でインスタンスを作成できるわけではありません。

a = Movie()
a.score = 1
a.ticket = 2
a.title = ‘test&#39;
a.descript = ‘…&#39;

これはあまりにも率直なので、コンストラクターがまだ不足しています。

class Integer(object):
 def __init__(self, name):
 self.name = name
 
 def __get__(self, instance, owner):
 if instance is None:
  return self
 return instance.__dict__[self.name]
 
 def __set__(self, instance, value):
 if value < 0:
  raise ValueError(&#39;Negative value not allowed&#39;)
 instance.__dict__[self.name] = value
 
 
class Movie(object):
 score = Integer(&#39;score&#39;)
 ticket = Integer(&#39;ticket&#39;)
 
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description
 self.score = score
 self.ticket = ticket

このように、スコアやチケットを取得、設定、削除する際に、Integerの__get__と__set__が入るようになり、ロジックの繰り返しが減ります。

問題が解決されたので、この記述子が正確にどのように機能するか知りたいかもしれません。具体的には、__init__関数でアクセスされるのは自身のself.scoreとself.ticketですが、クラス属性のscoreやticketとはどのような関係にあるのでしょうか?

記述子の仕組み

公式の説明を参照してください

オブジェクトが __get__() と __set__() の両方を定義する場合、 __get__() のみを定義する記述子はデータ記述子とみなされます (通常、これらは非データ記述子と呼ばれます)。メソッドに使用されますが、他の用途も可能です)。

データ記述子と非データ記述子は、インスタンスのディクショナリ内のエントリに関してオーバーライドが計算される方法が異なります。インスタンスのディクショナリにデータ記述子と同じ名前のエントリがある場合、データ記述子が優先されます。インスタンスの辞書に非データ記述子と同じ名前のエントリがある場合、辞書エントリが優先されます。

覚えておくべき重要な点は次のとおりです:

記述子は __getattribute__() メソッドによって呼び出されます
__getattribute__() をオーバーライドすると、記述子の自動呼び出しが防止されます
object.__getattribute__() と type.__getattribute__() は、__get__() への異なる呼び出しを行います。
データ記述子は常にインスタンス ディクショナリをオーバーライドします。
非データ記述子はインスタンス ディクショナリによってオーバーライドされる可能性があります。

クラスは __getattribute__() を呼び出します。おそらく次のようになります:

def __getattribute__(self, key):
 "Emulate type_getattro() in Objects/typeobject.c"
 v = object.__getattribute__(self, key)
 if hasattr(v, &#39;__get__&#39;):
 return v.__get__(None, self)
 return v

以下は海外ブログからの抜粋です。

クラス「C」とインスタンス「c」(「c = C(…)」)がある場合、「c.name」を呼び出すことは、次のようにインスタンス「c」の属性「name」を検索することを意味します。

Get the Class from Instance
Call the Class's special method getattribute__. All objects have a default __getattribute
Inside getattribute

Get the Class's mro as ClassParents
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a data descriptor
Return the result from calling the data descriptor's special method __get__()
Break the for each (do not continue searching the same Attribute any further)
If the Attribute is in Instance's dict
Return the value as it is (even if the value is a data descriptor)
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a non-data descriptor
Return the result from calling the non-data descriptor's special method __get__()
If it is NOT a descriptor
Return the value
If Class has the special method getattr
Return the result from calling the Class's special method__getattr__.

我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__dict__里是否有同名的data descriptor如果有,就用这个data descriptor代理该属性,如果没有再寻找该实例自身的__dict__ ,如果有就返回。任然没有再查找它和它父类里的non-data descriptor,最后查找是否有__getattr__

描述符的应用场景

python的property、classmethod修饰器本身也是一个描述符,甚至普通的函数也是描述符(non-data discriptor)

django model和SQLAlchemy里也有描述符的应用

class User(db.Model):
 id = db.Column(db.Integer, primary_key=True)
 username = db.Column(db.String(80), unique=True)
 email = db.Column(db.String(120), unique=True)
 
 def __init__(self, username, email):
 self.username = username
 self.email = email
 
 def __repr__(self):
 return &#39;<User %r>' % self.username

总结

只有当确实需要在访问属性的时候完成一些额外的处理任务时,才应该使用property。不然代码反而会变得更加啰嗦,而且这样会让程序变慢很多。以上就是本文的全部内容,由于个人能力有限,文中如有笔误、逻辑错误甚至概念性错误,还请提出并指正。

更多Python属性和描述符的使用相关文章请关注PHP中文网!

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。