Python のマジックメソッド

WBOY
WBOY転載
2023-04-13 10:25:061423ブラウズ

Python のマジック メソッドは、クラスに「魔法」を追加できる特別なメソッドで、多くの場合、2 つのアンダースコアで囲まれた名前が付けられます。

Python のマジックメソッド

Python の魔法のメソッド。ダンダー (二重下線) メソッドとも呼ばれます。ほとんどの場合、コンストラクター (init)、文字列表現 (str、repr)、算術演算子 (add/mul) などの単純なものに使用します。実は、あまり聞いたことのない便利なメソッドがたくさんあります。この記事では、それらの魔法のメソッドを整理していきます!

#Iterator size

誰もが知っている __len__ メソッドは、コンテナ クラスに len() 関数を実装するために使用できます。しかし、反復子を実装するクラス オブジェクトの長さを取得したい場合はどうすればよいでしょうか?

it = iter(range(100))
 print(it.__length_hint__())
 # 100
 next(it)
 print(it.__length_hint__())
 # 99
 
 a = [1, 2, 3, 4, 5]
 it = iter(a)
 print(it.__length_hint__())
 # 5
 next(it)
 print(it.__length_hint__())
 # 4
 a.append(6)
 print(it.__length_hint__())
 # 5

必要なのは、反復子 (ジェネレーターではなく) の組み込みメソッドである __length_hint__ メソッドを実装することだけです。上でわかるように、動的な長さの変更もサポートしています。ただし、名前のとおり、これは単なるヒントであり、完全に正確であることは保証されていません。リスト反復子の場合は正確な結果が得られますが、他の反復子の場合は正確ではありません。しかし、たとえ正確でなくても、PEP 424 で説明されているように、必要な情報を入手するのに役立ちます。

length_hint は、整数 (TypeError が発生する) または NotImplemented を返す必要があり、正確である必要はありません。コンテナの実際のサイズ。NotImplemented の戻り値は、有限の長さの推定がないことを示します。負の値を返すことはできません (そうでない場合は、ValueError が発生します)。

##メタプログラミング

めったに見られない魔法のメソッドのほとんどはメタプログラミングに関連しています。メタプログラミングは毎日使用する必要があるものではないかもしれませんが、それを使用できる便利なトリックがいくつかあります。

そのようなトリックの 1 つは、メタクラスを処理せずに基本クラスの機能を拡張するショートカットとして __init_subclass__ を使用することです:

 class Pet:
def __init_subclass__(cls, /, default_breed, **kwargs):
super().__init_subclass__(**kwargs)
cls.default_breed = default_breed
 
 class Dog(Pet, default_name="German Shepherd"):
pass

上記のコードでは、キーワード引数を基本クラスに追加しています。 、サブクラスを定義するときにパラメータを設定できます。実際の使用例は、単にプロパティに割り当てるのではなく、指定されたパラメータを処理する場合にこのメソッドを使用することです。

これは非常に曖昧でめったに使用されないようですが、SQLAlchemy や Flask Views などの API を構築するときに一般的に使用されるため、実際には何度も遭遇したことがあるかもしれません。

もう 1 つのメタクラス マジック メソッドは __call__ です。このメソッドを使用すると、クラス インスタンスが呼び出されたときに何が起こるかをカスタマイズできます:

 class CallableClass:
def __call__(self, *args, **kwargs):
print("I was called!")
 
 instance = CallableClass()
 
 instance()
 # I was called!

これを使用して、呼び出すことができないクラスを作成できます:

 class NoInstances(type):
def __call__(cls, *args, **kwargs):
raise TypeError("Can't create instance of this class")
 
 class SomeClass(metaclass=NoInstances):
@staticmethod
def func(x):
print('A static method')
 
 instance = SomeClass()
 # TypeError: Can't create instance of this class

静的メソッドのみを持つクラスの場合は、クラスを作成する必要はありません。このメソッドは例で使用されます。

もう 1 つの同様のシナリオは、シングルトン パターンです。クラスは最大でも 1 つのインスタンスしか持てません。

 class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
 
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
else:
return cls.__instance
 
 class Logger(metaclass=Singleton):
def __init__(self):
print("Creating global Logger instance")

シングルトン クラスにはプライベート __インスタンスがあります。そうでない場合は、作成されて割り当てられます。すでに存在する場合にのみ返されます。

クラスがあり、__init__ を呼び出さずにそのインスタンスを作成したいとします。 __new__ メソッドは、この問題の解決に役立ちます。

 class Document:
def __init__(self, text):
self.text = text
 
 bare_document = Document.__new__(Document)
 print(bare_document.text)
 # AttributeError: 'Document' object has no attribute 'text'
 
 setattr(bare_document, "text", "Text of the document")

場合によっては、インスタンスを作成する通常のプロセスをバイパスする必要がある場合があります。上記のコードは、これを行う方法を示しています。 Document(…) を呼び出す代わりに、Document.__new__(Document) を呼び出します。これにより、__init__ を呼び出さずにベア インスタンスが作成されます。したがって、インスタンスの属性 (この場合はテキスト) は初期化されないため、さらに setattr 関数を使用して値を割り当てる必要があります (これもマジック メソッド __setattr__ です)。 ######どうしてそれをするの。

 class Document:
def __init__(self, text):
self.text = text
 
@classmethod
def from_file(cls, file): # Alternative constructor
d = cls.__new__(cls)
# Do stuff...
return d

ここでは from_file メソッドが定義されており、コンストラクターとして機能します。最初に __new__ を使用してインスタンスを作成し、次に __init__ を呼び出さずに構成します。

メタプログラミングに関連した次の魔法のメソッドは __getattr__ です。このメソッドは、通常のプロパティ アクセスが失敗したときに呼び出されます。これは、欠落しているメソッドへのアクセス/呼び出しを別のクラスに委任するために使用できます。

 class String:
def __init__(self, value):
self._value = str(value)
 
def custom_operation(self):
pass
 
def __getattr__(self, name):
return getattr(self._value, name)
 
 s = String("some text")
 s.custom_operation() # Calls String.custom_operation()
 print(s.split()) # Calls String.__getattr__("split") and delegates to str.split
 # ['some', 'text']
 
 print("some text" + "more text")
 # ... works
 print(s + "more text")
 # TypeError: unsupported operand type(s) for +: 'String' and 'str'

クラスに追加の関数 (上記のcustom_operation など) を追加して、文字列のカスタム実装を定義したいと考えています。ただし、split、join、capitalize などのすべての文字列メソッドを再実装する必要はありません。ここで、__getattr__ を使用して、これらの既存の文字列メソッドを呼び出すことができます。

これは通常のメソッドでは機能しますが、上記の例では、マジック メソッド __add__ (接続などの操作を提供する) はデリゲートを取得しないことに注意してください。したがって、それらを同様に機能させたい場合は、再実装する必要があります。

自省(introspection)

最后一个与元编程相关的方法是__getattribute__。它一个看起来非常类似于前面的__getattr__,但是他们有一个细微的区别,__getattr__只在属性查找失败时被调用,而__getattribute__是在尝试属性查找之前被调用。

所以可以使用__getattribute__来控制对属性的访问,或者你可以创建一个装饰器来记录每次访问实例属性的尝试:

 def logger(cls):
original_getattribute = cls.__getattribute__
 
def getattribute(self, name):
print(f"Getting: '{name}'")
return original_getattribute(self, name)
 
cls.__getattribute__ = getattribute
return cls
 
 @logger
 class SomeClass:
def __init__(self, attr):
self.attr = attr
 
def func(self):
...
 
 instance = SomeClass("value")
 instance.attr
 # Getting: 'attr'
 instance.func()
 # Getting: 'func'

装饰器函数logger 首先记录它所装饰的类的原始__getattribute__方法。然后将其替换为自定义方法,该方法在调用原始的__getattribute__方法之前记录了被访问属性的名称。

魔法属性

到目前为止,我们只讨论了魔法方法,但在Python中也有相当多的魔法变量/属性。其中一个是__all__:

 # some_module/__init__.py
 __all__ = ["func", "some_var"]
 
 some_var = "data"
 some_other_var = "more data"
 
 def func():
return "hello"
 
 # -----------
 
 from some_module import *
 
 print(some_var)
 # "data"
 print(func())
 # "hello"
 
 print(some_other_var)
 # Exception, "some_other_var" is not exported by the module

这个属性可用于定义从模块导出哪些变量和函数。我们创建了一个Python模块…/some_module/单独文件(__init__.py)。在这个文件中定义了2个变量和一个函数,只导出其中的2个(func和some_var)。如果我们尝试在其他Python程序中导入some_module的内容,我们只能得到2个内容。

但是要注意,__all__变量只影响上面所示的* import,我们仍然可以使用显式的名称导入函数和变量,比如import some_other_var from some_module。

另一个常见的双下划线变量(模块属性)是__file__。这个变量标识了访问它的文件的路径:

 from pathlib import Path
 
 print(__file__)
 print(Path(__file__).resolve())
 # /home/.../directory/examples.py
 
 # Or the old way:
 import os
 print(os.path.dirname(os.path.abspath(__file__)))
 # /home/.../directory/

这样我们就可以结合__all__和__file__,可以在一个文件夹中加载所有模块:

# Directory structure:
 # .
 # |____some_dir
 # |____module_three.py
 # |____module_two.py
 # |____module_one.py
 
 from pathlib import Path, PurePath
 modules = list(Path(__file__).parent.glob("*.py"))
 print([PurePath(f).stem for f in modules if f.is_file() and not f.name == "__init__.py"])
 # ['module_one', 'module_two', 'module_three']

最后一个我重要的属性是的是__debug__。它可以用于调试,但更具体地说,它可以用于更好地控制断言:

 # example.py
 def func():
if __debug__:
print("debugging logs")
 
# Do stuff...
 
 func()

如果我们使用python example.py正常运行这段代码,我们将看到打印出“调试日志”,但是如果我们使用python -O example.py,优化标志(-O)将把__debug__设置为false并删除调试消息。因此,如果在生产环境中使用-O运行代码,就不必担心调试过程中被遗忘的打印调用,因为它们都不会显示。

创建自己魔法方法?

我们可以创建自己的方法和属性吗?是的,你可以,但你不应该这么做。

双下划线名称是为Python语言的未来扩展保留的,不应该用于自己的代码。如果你决定在你的代码中使用这样的名称,那么将来如果它们被添加到Python解释器中,这就与你的代码不兼容了。所以对于这些方法,我们只要记住和使用就好了。

以上がPython のマジックメソッドの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事は51cto.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。