Maison  >  Article  >  développement back-end  >  Méthodes magiques en Python

Méthodes magiques en Python

WBOY
WBOYavant
2023-04-13 10:25:061354parcourir

Les méthodes magiques en Python sont des méthodes spéciales qui permettent d'ajouter de la « magie » à une classe. Elles sont souvent nommées entourées de deux traits de soulignement.

Méthodes magiques en Python

Méthode magique de Python, également connue sous le nom de méthode dunder (double soulignement). La plupart du temps, nous les utilisons pour des choses simples comme les constructeurs (init), les représentations sous forme de chaîne (str, repr) ou les opérateurs arithmétiques (add/mul). En fait, il existe de nombreuses méthodes dont vous n'avez peut-être pas entendu parler mais qui sont très utiles. Dans cet article, nous allons trier ces méthodes magiques

La taille des itérateurs

Nous connaissons tous la méthode __len__, vous pouvez ! utilisez-le pour implémenter la fonction len() sur la classe conteneur. Mais que se passe-t-il si vous souhaitez obtenir la longueur d'un objet de classe qui implémente un itérateur ?

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

Tout ce que vous avez à faire est d'implémenter la méthode __length_hint__, qui est une méthode intégrée aux itérateurs (et non aux générateurs), comme indiqué ci-dessus. vu, et prend également en charge les changements de longueur dynamiques. Cependant, comme son nom l'indique, il ne s'agit que d'un indice dont l'exactitude n'est pas garantie : pour les itérateurs de liste, vous pouvez obtenir des résultats précis, mais pour les autres itérateurs, ce n'est pas sûr. Mais même si ce n'est pas exact, cela peut nous aider à obtenir les informations dont nous avons besoin, comme expliqué dans le PEP 424.

length_hint doit renvoyer un entier (sinon une TypeError est levée) ou NotImplemented, et n'a pas besoin d'être précis. Il peut renvoyer une valeur supérieure ou inférieure à la taille réelle du conteneur. de NotImplemented indique qu'il n'y a pas d'estimation de longueur finie. Il peut ne pas renvoyer une valeur négative (sinon une ValueError est levée). Bien que la métaprogrammation ne soit peut-être pas quelque chose que nous devons utiliser tous les jours, il existe quelques astuces pratiques pour l'utiliser. Une de ces astuces consiste à utiliser __init_subclass__ comme raccourci pour étendre les fonctionnalités d'une classe de base sans avoir à gérer les métaclasses :

 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
Dans le code ci-dessus, nous ajoutons un argument mot-clé à la classe de base qui peut être défini lors de la définition du sous-classe. Un cas d'utilisation réel pourrait consister à utiliser cette méthode lorsque vous souhaitez gérer les paramètres fournis plutôt que de simplement les attribuer à une propriété.

Cela semble très obscur et rarement utilisé, mais en fait vous l'avez peut-être rencontré plusieurs fois, car il est généralement utilisé lors de la création d'API, comme dans SQLAlchemy ou Flask Views.

Une autre méthode magique de métaclasse est __call__. Cette méthode permet de personnaliser ce qui se passe lorsqu'une instance de classe est appelée :

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

Vous pouvez l'utiliser pour créer une classe qui ne peut pas être appelée :

 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

Pour les classes avec uniquement des méthodes statiques, cette méthode est utilisée sans créer d'instance de la classe.

Un autre scénario similaire est le modèle singleton - une classe ne peut avoir qu'une seule instance :

 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")

La classe Singleton a une __instance privée - sinon, elle sera créée et attribuée, si elle existe déjà, elle ne sera renvoyée .

Supposons qu'il existe une classe et que vous souhaitiez en créer une instance sans appeler __init__. La méthode __new__ peut vous aider :

 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")

Dans certains cas, nous devrons peut-être contourner le processus habituel de création d'une instance, le code ci-dessus montre comment procéder. Au lieu d'appeler Document(…), nous appelons Document.__new__(Document), qui crée une instance nue sans appeler __init__. Par conséquent, l'attribut de l'instance (le texte dans ce cas) n'est pas initialisé, nous devons donc utiliser en plus la fonction setattr pour attribuer la valeur (c'est aussi une méthode magique __setattr__).

Pourquoi fais-tu ça ? Parce que nous souhaitons peut-être remplacer le constructeur, tel que :

 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

La méthode from_file est définie ici, qui agit comme un constructeur et crée d'abord l'instance en utilisant __new__, puis la configure sans appeler __init__.

La prochaine méthode magique liée à la métaprogrammation est __getattr__. Cette méthode est appelée lorsque l’accès normal aux propriétés échoue. Cela peut être utilisé pour déléguer l'accès/les appels aux méthodes manquantes à une autre classe :

 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'

Nous souhaitons ajouter des fonctions supplémentaires à la classe (comme custom_operation ci-dessus) pour définir une implémentation personnalisée de string. Mais nous ne voulons pas réimplémenter toutes les méthodes de chaîne, telles que split, join, majuscule, etc. Ici, nous pouvons utiliser __getattr__ pour appeler ces méthodes de chaîne existantes.

Bien que cela fonctionne pour les méthodes normales, notez que dans l'exemple ci-dessus, la méthode magique __add__ (qui fournit des opérations telles que les connexions) n'est pas déléguée. Donc, si nous voulons qu’ils fonctionnent également, nous devons les réimplémenter.

自省(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解释器中,这就与你的代码不兼容了。所以对于这些方法,我们只要记住和使用就好了。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer