前言#
最近、Python があまりにも「簡単すぎる」と感じたので、師匠の川爷の前で大胆に言ってしまった:「私は Python が世界で最も簡単な言語だと思います!」。すると川爷の口元に軽蔑の微笑みが浮かびました(内心 OS:Naive!Python 開発者として、あなたに少し人生経験を教えなければなりません。さもなければ、あなたは天高く地厚いことを知らないでしょう!)。それで川爷は私に満点 100 点の問題を出しました。そしてこの文章は、その問題を解く過程での失敗を記録したものです。
1. リスト生成器#
説明#
以下のコードはエラーになります。なぜでしょう?
class A(object):
x = 1
gen = (x for _ in xrange(10)) # gen=(x for _ in range(10))
if __name__ == "__main__":
print(list(A.gen))
答え#
この問題は変数のスコープの問題です。gen=(x for _ in xrange(10))
の中で、gen
はgenerator
であり、generator
内の変数は独自のスコープを持ち、他のスコープとは隔離されています。したがって、NameError: name 'x' is not defined
という問題が発生します。では、解決策は何でしょうか?答えは:lambda を使うことです。
class A(object):
x = 1
gen = (lambda x: (x for _ in xrange(10)))(x) # gen=(x for _ in range(10))
if __name__ == "__main__":
print(list(A.gen))
またはこうします。
class A(object):
x = 1
gen = (A.x for _ in xrange(10)) # gen=(x for _ in range(10))
if __name__ == "__main__":
print(list(A.gen))
補足#
コメント欄でいくつかの意見をいただき、ここで公式文書の説明を示します:
クラスブロック内で定義された名前のスコープはクラスブロックに限定されており、メソッドのコードブロックには拡張されません。これには、関数スコープを使用して実装されている内包表現や生成器式も含まれます。つまり、以下は失敗します:
class A:
a = 42
b = list(a + i for i in range(10))
参考リンク Python2 Execution-Model 、 Python3 Execution-Model。これは PEP 227 に追加された提案だそうです。後でさらに詳しく調べます。コメント欄の @没头脑很着急 @涂伟忠 @Cholerae の 3 人に感謝します。
2. デコレーター#
説明#
関数 / メソッドの実行時間を測定するためのクラスデコレーターを書きたいです。
import time
class Timeit(object):
def __init__(self, func):
self._wrapped = func
def __call__(self, *args, **kws):
start_time = time.time()
result = self._wrapped(*args, **kws)
print("elapsed time is %s " % (time.time() - start_time))
return result
このデコレーターは通常の関数で動作します:
@Timeit
def func():
time.sleep(1)
return "invoking function func"
if __name__ == '__main__':
func() # output: elapsed time is 1.00044410133
しかし、メソッドで実行するとエラーになります。なぜでしょう?
class A(object):
@Timeit
def func(self):
time.sleep(1)
return 'invoking method func'
if __name__ == '__main__':
a = A()
a.func() # Boom!
もし私がクラスデコレーターを使い続けるなら、どのように修正すればよいでしょうか?
答え#
クラスデコレーターを使用すると、func
関数を呼び出す過程で、その対応するインスタンスは__call__
メソッドに渡されず、method unbound
が発生します。では、解決策は何でしょうか?デスクリプタが最高です。
class Timeit(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print('invoking Timer')
def __get__(self, instance, owner):
return lambda *args, **kwargs: self.func(instance, *args, **kwargs)
3.Python の呼び出しメカニズム#
説明#
私たちは__call__
メソッドが丸括弧の呼び出しをオーバーロードできることを知っています。さて、問題はそれほど簡単だと思いますか?Naive!
class A(object):
def __call__(self):
print("invoking __call__ from A!")
if __name__ == "__main__":
a = A()
a() # output: invoking __call__ from A
今、私たちはa()
がa.__call__()
と等しいように見えることがわかります。簡単そうですね。さて、私はまた無謀なことをして、次のようなコードを書きました。
a.__call__ = lambda: "invoking __call__ from lambda"
a.__call__()
# output:invoking __call__ from lambda
a()
# output:invoking __call__ from A!
皆さん、なぜa()
がa.__call__()
を呼び出さなかったのか説明してください(この問題は USTC の王子博先輩が提起しました)。
答え#
理由は、Python では新しいスタイルのクラス(new class)の組み込み特殊メソッドとインスタンスの属性辞書が相互に隔離されているためです。具体的には、Python の公式文書でこの状況について説明されています。
新しいスタイルのクラスの場合、特殊メソッドの暗黙の呼び出しは、オブジェクトの型で定義されている場合にのみ正しく動作することが保証されており、オブジェクトのインスタンス辞書では保証されていません。この動作が、以下のコードが例外を発生させる理由です(古いスタイルのクラスの同等の例とは異なります)。
同様に、公式も次の例を示しています。
class C(object):
pass
c = C()
c.__len__ = lambda: 5
len(c)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: object of type 'C' has no len()
私たちの例に戻ると、a.__call__=lambda:"invoking __call__ from lambda"
を実行すると、確かにa.__dict__
に__call__
というキーを持つアイテムが新たに追加されましたが、a()
を実行すると、特殊メソッドの呼び出しが関与するため、呼び出しプロセスはa.__dict__
から属性を探すのではなく、type(a).__dict__
から属性を探します。したがって、上記のような状況が発生します。
4. デスクリプタ#
説明#
私は、属性 math が [0,100] の整数であり、範囲外の値を割り当てると例外を投げる Exam クラスを書きたいと思います。この要件をデスクリプタを使って実現することにしました。
class Grade(object):
def __init__(self):
self._score = 0
def __get__(self, instance, owner):
return self._score
def __set__(self, instance, value):
if 0 <= value <= 100:
self._score = value
else:
raise ValueError('grade must be between 0 and 100')
class Exam(object):
math = Grade()
def __init__(self, math):
self.math = math
if __name__ == '__main__':
niche = Exam(math=90)
print(niche.math)
# output : 90
snake = Exam(math=75)
print(snake.math)
# output : 75
snake.math = 120
# output: ValueError:grade must be between 0 and 100!
すべてが正常に見えます。しかし、ここには巨大な問題があります。問題は何でしょうか?
この問題を解決するために、Grade デスクリプタを次のように書き直しました。
class Grad(object):
def __init__(self):
self._grade_pool = {}
def __get__(self, instance, owner):
return self._grade_pool.get(instance, None)
def __set__(self, instance, value):
if 0 <= value <= 100:
_grade_pool = self.__dict__.setdefault('_grade_pool', {})
_grade_pool[instance] = value
else:
raise ValueError("fuck")
しかし、これによりさらに大きな問題が発生します。どうすればこの問題を解決できますか?
答え#
1. 最初の問題は非常に簡単です。print(niche.math)
をもう一度実行すると、出力値が75
であることがわかります。なぜでしょうか?これは Python の呼び出しメカニズムから説明できます。属性を呼び出す場合、その順序は最初にインスタンスの__dict__
を検索し、見つからなければ、クラス辞書、親クラス辞書を順に検索し、最終的に見つからなくなるまで続きます。さて、私たちの問題に戻ると、self.math
の呼び出しプロセスは、最初にインスタンス化されたインスタンスの__dict__
で検索し、見つからなければ、次にクラスExam
で検索します。見つかりましたので、返されます。つまり、self.math
に対するすべての操作はクラス変数math
に対する操作であるため、変数汚染の問題が発生します。では、どうすれば解決できるでしょうか?多くの人が、__set__
関数で値を具体的なインスタンス辞書に設定すれば良いと言うかもしれません。
これで可能でしょうか?答えは、明らかに不可能です。その理由は、Python のデスクリプタのメカニズムに関係しています。デスクリプタとは、デスクリプタプロトコルを実装した特別なクラスであり、3 つのデスクリプタプロトコルは__get__
、__set__
、__delete__
であり、Python 3.6 では新たに__set_name__
メソッドが追加されました。__get__
と__set__
/__delete__
/__set_name__
を実装したものがデータデスクリプタであり、__get__
のみを実装したものが非データデスクリプタ
です。では、違いは何でしょうか?前述のように、** 属性を呼び出す場合、その順序は最初にインスタンスの__dict__
を検索し、見つからなければ、クラス辞書、親クラス辞書を順に検索し、最終的に見つからなくなるまで続きます。このとき、クラスインスタンス辞書にその属性がデータデスクリプタ
である場合、インスタンス辞書にその属性が存在するかどうかにかかわらず、無条件にデスクリプタプロトコルを呼び出します。クラスインスタンス辞書にその属性が非データデスクリプタ
である場合、最初にインスタンス辞書の属性値を呼び出し、デスクリプタプロトコルをトリガーしません。インスタンス辞書にその属性値が存在しない場合、非データデスクリプタ
のデスクリプタプロトコルをトリガーします。** 以前の問題に戻ると、__set__
で具体的な属性をインスタンス辞書に書き込んでも、クラス辞書にデータデスクリプタ
が存在するため、math
属性を呼び出すと、依然としてデスクリプタプロトコルがトリガーされます。
2. 改良されたアプローチでは、dict
のキーの一意性を利用して、具体的な値をインスタンスにバインドしますが、同時にメモリリークの問題を引き起こします。なぜメモリリークが発生するのでしょうか?まず、dict
の特性を復習しましょう。dict
の最も重要な特性は、ハッシュ可能なオブジェクトがすべてキーになれることです。dict
はハッシュ値の一意性(厳密には一意ではなく、ハッシュ値の衝突の確率が非常に低いため、ほぼ一意と見なされます)を利用してキーの重複を防ぎます。同時に(黒板を叩いて、重要なポイントです)、dict
のキーは強い参照タイプであり、対応するオブジェクトの参照カウントを増加させる可能性があるため、オブジェクトがガーベジコレクションされず、メモリリークを引き起こす可能性があります。では、これをどう解決すればよいでしょうか?2 つの方法があります。
最初の方法:
class Grad(object):
def __init__(self):
import weakref
self._grade_pool = weakref.WeakKeyDictionary()
def __get__(self, instance, owner):
return self._grade_pool.get(instance, None)
def __set__(self, instance, value):
if 0 <= value <= 100:
_grade_pool = self.__dict__.setdefault('_grade_pool', {})
_grade_pool[instance] = value
else:
raise ValueError("fuck")
weakref ライブラリのWeakKeyDictionary
によって生成された辞書のキーはオブジェクトへの弱い参照タイプであり、メモリ参照カウントの増加を引き起こさないため、メモリリークを引き起こしません。同様に、値がオブジェクトへの強い参照を引き起こさないようにするために、WeakValueDictionary
を使用することもできます。
2 つ目の方法:Python 3.6 では、PEP 487 により、デスクリプタに新しいプロトコルが追加され、対応するオブジェクトをバインドするために使用できます。
class Grad(object):
def __get__(self, instance, owner):
return instance.__dict__[self.key]
def __set__(self, instance, value):
if 0 <= value <= 100:
instance.__dict__[self.key] = value
else:
raise ValueError("fuck")
def __set_name__(self, owner, name):
self.key = name
この問題は多くのことを含んでいます。ここでいくつかの参考リンクを示します。invoking-descriptors 、 Descriptor HowTo Guide 、 PEP 487 、 Python 3.6 の新機能 。
5.Python の継承メカニズム#
説明#
以下のコードの出力結果を求めてください。
class Init(object):
def __init__(self, value):
self.val = value
class Add2(Init):
def __init__(self, val):
super(Add2, self).__init__(val)
self.val += 2
class Mul5(Init):
def __init__(self, val):
super(Mul5, self).__init__(val)
self.val *= 5
class Pro(Mul5, Add2):
pass
class Incr(Pro):
csup = super(Pro)
def __init__(self, val):
self.csup.__init__(val)
self.val += 1
p = Incr(5)
print(p.val)
答え#
出力は 36 です。具体的にはNew-style Classes 、 multiple-inheritanceを参照してください。
6. Python の特殊メソッド#
説明#
私は__new__
メソッドをオーバーロードしてシングルトンパターンを実現するクラスを書きました。
class Singleton(object):
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance:
return cls._instance
cls._isntance = cv = object.__new__(cls, *args, **kwargs)
return cv
sin1 = Singleton()
sin2 = Singleton()
print(sin1 is sin2)
# output: True
今、私はシングルトンパターンを実現するために多くのクラスを作りたいので、メタクラスを使ってコードを再利用しようと考えています。
class SingleMeta(type):
def __init__(cls, name, bases, dict):
cls._instance = None
__new__o = cls.__new__
def __new__(cls, *args, **kwargs):
if cls._instance:
return cls._instance
cls._instance = cv = __new__o(cls, *args, **kwargs)
return cv
cls.__new__ = __new__
class A(object):
__metaclass__ = SingleMeta
a1 = A() # これはどういうことですか?
ああ、イライラします。なぜこれがエラーになるのか、私は以前この方法で__getattribute__
をパッチして成功したのに、以下のコードはすべての属性呼び出しをキャッチしてパラメータを印刷できます。
class TraceAttribute(type):
def __init__(cls, name, bases, dict):
__getattribute__o = cls.__getattribute__
def __getattribute__(self, *args, **kwargs):
print('__getattribute__:', args, kwargs)
return __getattribute__o(self, *args, **kwargs)
cls.__getattribute__ = __getattribute__
class A(object): # Python 3ではclass A(object,metaclass=TraceAttribute):
__metaclass__ = TraceAttribute
a = 1
b = 2
a = A()
a.a
# output: __getattribute__:('a',){}
a.b
なぜ__getattribute__
をパッチするのが成功したのに、__new__
をパッチするのが失敗したのか説明してください。もし私がメタクラスを使って__new__
をパッチしてシングルトンパターンを実現したい場合、どのように修正すればよいでしょうか?
答え#
実際、これは最もイライラする点です。クラス内の__new__
はstaticmethod
であるため、置き換える際にはstaticmethod
として置き換える必要があります。答えは以下の通りです:
class SingleMeta(type):
def __init__(cls, name, bases, dict):
cls._instance = None
__new__o = cls.__new__
@staticmethod
def __new__(cls, *args, **kwargs):
if cls._instance:
return cls._instance
cls._instance = cv = __new__o(cls, *args, **kwargs)
return cv
cls.__new__ = __new__
class A(object):
__metaclass__ = SingleMeta
print(A() is A()) # output: True
结语#
師匠の一連の問題に感謝します。新しい世界の扉を開いてくれました。ええ、ブログではエイリアスできないので、気持ちを伝えるしかありません。正直なところ、Python の動的特性は、さまざまなblack magic
を使用して非常に快適な機能を実現できますが、同時に言語の特性や落とし穴の理解も厳しくなります。皆さんが暇なときに公式文書を読んで、早く装逼如風、常伴吾身の境地に達することを願っています。