linuxBean14.04(170)Pythonのデスクリプタの学習を少し

ラベル: ,
Strategyパターンで関数の入れ替えをいじっているうちに、デスクリプタの解説に行き当たりました。

前の関連記事:linuxBean14.04(169)Pythonでデザインパターン:Strategy


Pythonの「属性」と「プロパティ」


HTMLの要素では「属性」、JavaScriptでは「プロパティ」、というものがPythonでは「プロパティ」(property)と「属性」(attribute)があって、「プロパティ」は「属性」の一種らしいです(What's the difference between a Python "property" and "attribute"? - Stack Overflow)。
(2017.8.26追記Python Cookbookの8.6. Creating Managed Attributesにはプロパティは遅いので、必然性があるとき、、、値を実行時に生成する必要があるとき、のみの使用に限定するように書いてあります。)

2. 組み込み関数 — Python 3.5.3 ドキュメント

組み込み関数にpropertyクラスがありました。

ということでPythonについてはjavaScriptの「プロパティ」に相当するものは「属性」と呼ぶことにします。
(2017.8.26追記。LibreOfficeではインターフェイスがもつ値がアトリビュート、サービス(old-serviceのみ)がもつ値がプロパティとなっています。アトリビュート(つまりインターフェイスがもつ値)は属性名でのアクセスができるのに対して、プロパティ(つまりサービスがもつ値)はgetPropertyValue()/setPropertyValue()といったメソッドを使ってアクセスします(Objects, Interfaces, and Services - Apache OpenOffice Wiki)。だけどAPIリファレンスをみるとプロパティもAttributeと書いてあるます。)

属性に代入した関数

linuxBean14.04(169)Pythonでデザインパターン:StrategyのModelDuckのインスタンス化部分だけ抽出します。
In [1]:
def flyNoWay():
    print("I can't fly")
def flyRocketPowered():
    print("I'm flying with a rocket")      
class Duck:    
    def swim(self):
        print("All ducks float, even decoys!") 
class ModelDuck(Duck):
    def __init__(self):
        self.performFly = flyNoWay 
ModelDuckクラスをインスタンス化してpeformFly属性に代入した関数を実行してみます。
In [2]:
model = ModelDuck()
model.performFly()  
I can't fly
In [3]:
model.performFly
Out[3]:
<function __main__.flyNoWay>
これはただの関数です。
modelインスタンスのpeformFly属性の関数を入れ替えます。
In [4]:
model.performFly = flyRocketPowered 
model.performFly()
I'm flying with a rocket
ここまでは前回までにやったことです。

このpeformFly()は属性に代入した関数を実行しているだけなので、ModelDuckクラスのメソッドが持つはずの引数selfは持っていません。

属性に代入した関数にメソッドと同様にselfを渡すにはtypes.MethodType()を使う

In [5]:
def flyNoWayWithName(self):
    print(self.name + " can't fly") 
class ModelDuck2(Duck):
    def __init__(self,name):
        self.performFly = flyNoWayWithName 
        self.name = name  
属性に代入する関数に、メソッドと同様にselfを引数にして代入しました。
In [6]:
carue = ModelDuck2("Carue")
carue.performFly()  
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-adfc9da75012> in <module>()
      1 carue = ModelDuck2("Carue")
----> 2 carue.performFly()

TypeError: flyNoWayWithName() missing 1 required positional argument: 'self'
これはpeformFly()の引数が与えられていないと言われて実行できません。

属性に代入した関数にはメソッドと違ってselfが暗黙的には渡されません。
In [7]:
carue.performFly
Out[7]:
<function __main__.flyNoWayWithName>
これは標準の関数です。
In [8]:
import types
class ModelDuck2(Duck):
    def __init__(self,name):
        self.performFly = types.MethodType(flyNoWayWithName,self) 
        self.name = name  
そこでtypes.MethodType()の登場です。

performFly属性に代入するときにtypes.MethodType()を使って関数flyNoWayWithNameの引数にselfを与えます。
In [9]:
carue = ModelDuck2("Carue")
carue.performFly()  
Carue can't fly
これはうまくいきました。

ちゃんとselfがflyNoWayWithNameの引数に渡されています。
In [10]:
def flyRocketPoweredWithName(self):
    print(self.name + " is flying with a rocket")    
最初の例と同様にpeformFly属性の関数をこのflyRocketPoweredWithNameに入れ替えることにします。

flyRocketPoweredWithNameもselfを引数に持っています。
In [11]:
carue.performFly = types.MethodType(flyRocketPoweredWithName,carue) 
carue.performFly()
Carue is flying with a rocket
これもtypes.MethodType()を使うとうまくいきましたが、第2引数に与えるのはselfではなく、インスタンスそのものをいれます。

selfに入れてもスコープにないものなのでエラーがでます。

引数に渡したのはインスタンスなので、performFly属性が変更になるのはこのインスタンスに対してだけです。

これも属性に普通に関数を代入した時と同じです。
In [12]:
carue2 = ModelDuck2("Carue2")
carue2.performFly()  
Carue2 can't fly
確かに他のインスタンスのperformFly属性は変更されていません。

クラスに対してメソッドを追加する

他のインスタンスの属性にも追加したいときはクラスの属性に対して関数を代入します。

クラスの属性に関数を代入するときはselfが暗黙的に引数に与えられるのでtypes.MethodType()を使う必要がありません。
In [13]:
ModelDuck2.performFly2 = flyRocketPoweredWithName
In [14]:
carue2.performFly2() 
Carue2 is flying with a rocket
ところがこのように新しい関数をクラスに追加できるのに、すでに存在する属性の入れ替えはうまくいきません。
In [15]:
ModelDuck2.performFly = flyRocketPoweredWithName
carue2.performFly() 
Carue2 can't fly
なぜでしょう、、、新たにインスタンスを作成しても結果は同じです。

setattr()を使っても同じでした。
In [16]:
carue3 = ModelDuck2("Carue3")
carue3.performFly()
Carue3 can't fly
よく考えたらこれができるのなら、クラスの継承しなくてもよくなってしまいますね。

標準の関数、メソッド(method)、静的メソッド(staticmethod)、クラスメソッド(classmethod)

Pythonでの関数は使われ方によって、標準の関数(regular function)、メソッド(method)、静的メソッド(staticmethod)、クラスメソッド(classmethod)に分類されることがわかりました。

標準の関数(regular function)とはオブジェクトと紐付けられていない単独の関数です。

関数がオブジェクトと紐付けられて、属性に入れられると、それはメソッド(method)か静的メソッド(staticmethod)かクラスメソッド(classmethod)になります。

で、これらの違いが何なのか調べてみると、その呼び出され方が違うことがわかりました。

静的メソッドとクラスメソッドに引数の変換方法が解説されています。

属性はデスクリプタプロトコルで呼び出され方が定義されています。
あるオブジェクトが __get__() と __set__() の両方を定義していたら、それはデータデスクリプタとみなされます。 __get__() だけを定義しているデスクリプタは、非データデスクリプタと呼ばれます
デスクリプタ HowTo ガイド — Python 3.6.1 ドキュメント
要は、呼び出しと設定の両方をしているときはデータデスクリプタ、呼び出しだけで設定をしていないときは、非データデスクリプタということです。

データデスクリプタ
インスタンスの辞書
非データデスクリプタ

属性の呼び出しはこの順に優先されるので、 get()とset() の両方を定義している属性(データデスクリプタ)のあとにget()だけを定義している属性(非データデスクリプタ)を代入しても、あとから代入した属性は無視されます(python data and non-data descriptors - Stack Overflow)。

propertyはクラスでこのQ&Aではデコレーターにして使っています。

クラスをデコレーターにする例はDecorators and Functional Pythonにありました。
上記のIn[15]でperformFly属性への代入が無視されたのは、データデスクリプタに非データデスクリプタを代入しようとしたから、と納得しましたが確証はありません。
In [17]:
carue2.performFly
Out[17]:
<bound method flyNoWayWithName of <__main__.ModelDuck2 object at 0xaeae0ccc>>
In [18]:
ModelDuck2.performFly ="flyRocketPoweredWithName"
carue2.performFly
Out[18]:
<bound method flyNoWayWithName of <__main__.ModelDuck2 object at 0xaeae0ccc>>
文字列も代入できませんね。

boundしているから?

staticmethod()、@staticmethodを使う

staticmethod()classmethod()は関数を引数にして関数を返す組み込み関数が用意されているのでそれを使ってみます。

まずstaticmethod()を使ってみます。

staticmethod()は第一引数にインスタンスを持たない関数を引数にします。
In [19]:
class ModelDuck3(Duck):
    def __init__(self,name):
        self.name = name 
    performFly = staticmethod(flyNoWay)
In [20]:
carue3 = ModelDuck3("Carue3")
carue3.performFly()
I can't fly
In [21]:
carue3.performFly
Out[21]:
<function __main__.flyNoWay>
でも、別にこれはstaticmethod()を使わなくても同じ結果です。
In [22]:
class ModelDuck4(Duck):
    def __init__(self,name):
        self.name = name 
    performFly = flyNoWay
In [23]:
ModelDuck4.performFly()
I can't fly
いまのところ私が思いつくstaticmethod()使う目的は、標準の関数の記載をやめて、Javaのようにすべてをクラスの中に書く、ということぐらいしかありません。
In [24]:
class Fly:
    @staticmethod
    def flyNoWay():
        print("I can't fly")
    @staticmethod
    def flyRocketPowered():
        print("I'm flying with a rocket")      
class Duck5:    
    def swim(self):
        print("All ducks float, even decoys!") 
class ModelDuck5(Duck):
    def __init__(self):
        self.performFly = Fly.flyNoWay 
In [25]:
model5 = ModelDuck5()
model5.performFly()
I can't fly
FlyクラスのflyNoWay()メソッドはself(=インスタンス)を引数に必要としないので、インスタンス化しなくてもFlyクラスの属性として呼び出しています。

@staticmethodは書かなくても動くので、これも単にJavaのアノテーションのイミテーションなのかと思ってしまいます。

あとで気が付きましたが、PyDevでは@staticmethodを使わないとselfがないと言われます。

アノテーションは文法チェックに有効ですね。

classmethod()、@classmethodを使う

classmethod()はインスタンスではなくクラスを引数とする関数を引数にする関数です。
Python's Class Development Toolkit - YouTube(これのスライドPython's Class Development Toolkit by Raymond Hettinger // Speaker Deck)では、classmethod()をalternative constructorとしての使用例が挙げられています。
In [26]:
import types
def flyNoWayWithName(self):
    print(self.name + " can't fly") 
def flyRocketPoweredWithName(self):
    print(self.name + " is flying with a rocket")      
class Duck:    
    def swim(self):
        print("All ducks float, even decoys!") 
class ModelDuck6(Duck):
    def __init__(self,name):
        self.performFly = types.MethodType(flyNoWayWithName,self)
        self.name = name 
    @classmethod
    def altInit(cls,name):  # alternative constructor
        name = "Duck " + name
        return cls(name)
In [27]:
carue6 = ModelDuck6.altInit("Carue6")
carue6.performFly()
Duck Carue6 can't fly
classmethod()は使わないと第一引数にクラスが与えられないのでエラーになります。

クラスの多重継承を使ってtypes.MethodType()を使わない方法

In [28]:
class Fly2:
    def flyNoWayWithName(self):
        print(self.name + " can't fly") 
    def flyRocketPoweredWithName(self):
        print(self.name + " is flying with a rocket")      
class Duck:    
    def swim(self):
        print("All ducks float, even decoys!") 
class ModelDuck7(Duck,Fly2):
    def __init__(self,name):
        self.performFly = self.flyNoWayWithName
        self.name = name 
In [29]:
carue7 = ModelDuck7("Carue7")
carue7.performFly()
Carue7 can't fly
標準の関数をクラスにいれてそれを継承すれば、うまくいきました。

8.9. types — 動的な型生成と組み込み型に対する名前 — Python 3.6.1 ドキュメントにはtypes.MethodType()の使い方例が解説されていないことを考えると、Javaのインターフェイスを置換する正攻法はクラスに置換してそのクラスを継承する方法だと思いました。

参考にしたサイト


What's the difference between a Python "property" and "attribute"? - Stack Overflow
Pythonではproperty(プロパティ)はattribute(属性)の一種のようです。

2. 組み込み関数 — Python 3.5.3 ドキュメント
property()、staticmethod()、classmethod()について学習しました。

デスクリプタ HowTo ガイド — Python 3.6.1 ドキュメント
属性が作動する仕組み。

python data and non-data descriptors - Stack Overflow
データデスクリプタと非データデスクリプタでは優先順位が異なります。

Decorators and Functional Python
デコレーターの使い方例がたくさんあります。クラスのデコレーターの例もあります。

(1) Python's Class Development Toolkit - YouTube
classmethod()とかproperty()使い方の実践例が解説されています。

Python's Class Development Toolkit by Raymond Hettinger // Speaker Deck
上のビデオのスライド。

8.9. types — 動的な型生成と組み込み型に対する名前 — Python 3.6.1 ドキュメント
Pythonのドキュメントを読んでもtypes.MethodType()の使い方はわかりませんでした。

次の関連記事:linuxBean14.04(171)デザインパターン:コンポジションでstrategy_headfirst.pyを書き直す

PR

0 件のコメント:

コメントを投稿