深圳幻海软件技术有限公司 欢迎您!

Python中的魔法方法

2023-02-28

python中的魔法方法是一些可以让你对类添加“魔法”的特殊方法,它们经常是两个下划线包围来命名的。Python的魔法方法,也称为dunder(双下划线)方法。大多数的时候,我们将它们用于简单的事情,例如构造函数(init)、字符串表示(str,repr)或算术运算符(add/mul)。其实还有许多

python中的魔法方法是一些可以让你对类添加“魔法”的特殊方法,它们经常是两个下划线包围来命名的。

Python的魔法方法,也称为dunder(双下划线)方法。大多数的时候,我们将它们用于简单的事情,例如构造函数(init)、字符串表示(str, repr)或算术运算符(add/mul)。其实还有许多你可能没有听说过的但是却很好用的方法,在这篇文章中,我们将整理这些魔法方法!

迭代器的大小

我们都知道__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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

你所需要做的就是实现__length_hint__方法,这个方法是迭代器上的内置方法(不是生成器),正如你上面看到的那样,并且还支持动态长度更改。但是,正如他的名字那样,这只是一个提示(hint),并不能保证完全准确:对于列表迭代器,可以得到准确的结果,但是对于其他迭代器则不确定。但是即使它不准确,它也可以帮我们获得需要的信息,正如PEP 424中解释的那样。

length_hint must return an integer (else a TypeError is raised) or NotImplemented, and is not required to be accurate.  It may return a value that is either larger or smaller than the actual size of the container.  A return value of NotImplemented indicates that there is no finite length estimate.  It may not return a negative value (else a ValueError is raised).

元编程

大部分很少看到的神奇方法都与元编程有关,虽然元编程可能不是我们每天都需要使用的东西,但有一些方便的技巧可以使用它。

一个这样的技巧是使用__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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

上面的代码我们向基类添加关键字参数,该参数可以在定义子类时设置。在实际用例中可能会在想要处理提供的参数而不仅仅是赋值给属性的情况下使用此方法。

看起来非常晦涩并且很少会用到,但其实你可能已经遇到过很多次了,因为它一般都是在构建API时使用的,例如在SQLAlchemy或Flask Views中都使用到了。

另一个元类的神奇方法是__call__。这个方法允许自定义调用类实例时发生的事情:

 class CallableClass:
    def __call__(self, *args, **kwargs):
        print("I was called!")
 
 instance = CallableClass()
 
 instance()
 # I was called!
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

可以用它来创建一个不能被调用的类:

 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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

对于只有静态方法的类,不需要创建类的实例就用到了这个方法。

另一个类似的场景是单例模式——一个类最多只能有一个实例:

 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")
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

Singleton类拥有一个私有__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")
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

在某些情况下,我们可能需要绕过创建实例的通常过程,上面的代码演示了如何做到这一点。我们不调用Document(…),而是调用Document.__new__(Document),它创建一个裸实例,而不调用__init__。因此,实例的属性(在本例中为text)没有初始化,所欲我们需要额外使用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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这里定义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'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

我们想为类添加一些额外的函数(如上面的custom_operation)定义string的自定义实现。但是我们并不想重新实现每一个字符串方法,比如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'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

装饰器函数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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

这个属性可用于定义从模块导出哪些变量和函数。我们创建了一个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/
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这样我们就可以结合__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']
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

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

 # example.py
 def func():
    if __debug__:
        print("debugging logs")
 
    # Do stuff...
 
 func()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

创建自己魔法方法?

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

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