共计 5956 个字符,预计需要花费 15 分钟才能阅读完成。
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
你所须要做的就是实现__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
下面的代码咱们向基类增加关键字参数,该参数能够在定义子类时设置。在理论用例中可能会在想要解决提供的参数而不仅仅是赋值给属性的状况下应用此办法。
看起来十分艰涩并且很少会用到,但其实你可能曾经遇到过很屡次了,因为它个别都是在构建 API 时应用的,例如在 SQLAlchemy 或 Flask Views 中都应用到了。
另一个元类的神奇办法是__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
对于只有静态方法的类,不须要创立类的实例就用到了这个办法。
另一个相似的场景是单例模式——一个类最多只能有一个实例:
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")
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")
在某些状况下,咱们可能须要绕过创立实例的通常过程,下面的代码演示了如何做到这一点。咱们不调用 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
这里定义 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) 定义 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'
装璜器函数 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 解释器中,这就与你的代码不兼容了。所以对于这些办法,咱们只有记住和应用就好了。
https://avoid.overfit.cn/post/6a5057b4833b4f188d8c850385cfcbca
作者:Martin Heinz