Skip to content

魔术方法

在 Python 中,所有以“__”双下划线包起来的方法,都统称为“Magic Method”,中文称:"魔术方法"

这里只补充内置(类)方法中的魔术方法

字符串相关

改变对象的字符串显示的魔术方法

  • 用于控制台输出显示的__str__
  • 用于程序调试显示的__repr__
  • 自定制格式化字符串__format__

内置函数str或者print 会调用 obj.__str__()
内置函数repr会调用 obj.__repr__()
如果__str__没有被定义,那么就会使用__repr__来代替输出(这个很容易导致递归需要留意)

注意:这俩方法的返回值必须是字符串,否则抛出异常

python
# _*_coding:utf-8_*_
format_dict = {
    "nat": "{obj.name}-{obj.addr}-{obj.type}",  # 学校名-学校地址-学校类型
    "tna": "{obj.type}:{obj.name}:{obj.addr}",  # 学校类型:学校名:学校地址
    "tan": "{obj.type}/{obj.addr}/{obj.name}",  # 学校类型/学校地址/学校名
}


class School:
    def __init__(self, name, addr, type):
        self.name = name
        self.addr = addr
        self.type = type

    def __repr__(self):
        return "School(%s,%s)" % (self.name, self.addr)

    def __str__(self):
        return "(%s,%s)" % (self.name, self.addr)

    def __format__(self, format_spec):
        # if format_spec
        if not format_spec or format_spec not in format_dict:
            format_spec = "nat"
        fmt = format_dict[format_spec]
        return fmt.format(obj=self)


s1 = School("oldboy1", "北京", "私立")
print("from repr: ", repr(s1))
print("from str: ", str(s1))
print(s1)
print(format(s1, "nat"))
print(format(s1, "tna"))
print(format(s1, "tan"))
print(format(s1, "asfdasdffd"))

%s%r

python
class B:
    def __str__(self):
        return "str : class B"

    def __repr__(self):
        return "repr : class B"


b = B()
print("%s" % b)
print("%r" % b)

在之前章节中提到的:

在基础字符串类型中,魔术方法__repr__返回的是原始字符串类型。
而基础的容器类型中__str__对容器内部成员一般是用的__repr__方法。

例如:

shell
>>> '\\'.__str__()
'\\'
>>> '\\'.__repr__()
"'\\\\'"
>>> ['\\'].__str__()
"['\\\\']"
>>>

重写__repr__()方法使容器外访问和容器内偏差

python
from colorama import init, Fore

init(autoreset=False)


class Color_str(str):
    color = "WHITE"

    def set_color(self, color):
        self.color = color
        return self

    def __repr__(self):
        """注意不要递归"""
        return f"{getattr(Fore, self.color)}{self}{Fore.RESET}"


arr = [Color_str("Y").set_color("GREEN"), Color_str("N").set_color("RED")]

print(arr[0], arr[1])
print(arr)

print(arr[0], arr[1])是 msg 本体正常颜色的 Y 和 N
print(arr)打印出来的是绿色的 Y 和红色的 N:[Y, N]

索引相关

如果不实现,使用下标会出现 TypeError: '...' object is not subscriptable

  • __getitem__
  • __setitem__
  • __delitem__

在读取、修改、删除对象成员按索引[0]的时候可以分别定义以上方法。
事实上内置模块operator里就包括这些同名的方法映射了索引的操作。
而在需要切片[0:1]时则可以用这些方法结合内置函数slice(start, stop, step)来自定义。

python
class Foo:
    def __init__(self, name):
        self.name = name

    def __getitem__(self, item):
        print(self.__dict__[item])

    def __setitem__(self, key, value):
        self.__dict__[key] = value

    def __delitem__(self, key):
        print("del obj[key]时,我执行")
        self.__dict__.pop(key)

    def __delattr__(self, item):
        print("del obj.key时,我执行")
        self.__dict__.pop(item)


f1 = Foo("sb")
f1["age"] = 18
f1["age1"] = 19
del f1.age1
del f1["age"]
f1["name"] = "alex"
print(f1.__dict__)

如果一个类没实现__iter__只实现了__getitem__ , 也是可以被iter()执行创建初始化的迭代器的。

除此之外还有__missing__
通过 dict[key] 或者 dict.__getitem__ 访问不存在的 key 时就会返回__missing__()
当然,dict类并没有实现它,但是collections.defaultdict类实现了它

其他类型想要转为 dict 类至少需要实现 keys 方法和__getitem__

实例化相关

除了__init__还有一个__new__(构造函数)。
实例化时触发,重写时必须返回super().__new__(cls)

python
class Singleton:
    """
    实现一个极简的单例模式。
    注意,每次即使已经实例化,`__init__`也会被调用。
    而且它不是线程安全的。
    """

    def __new__(cls, *args, **kw):
        if not hasattr(cls, "_instance"):
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance


one = Singleton()
two = Singleton()
print(one is two)  # True

调用相关

__call__
对象后面加括号,触发执行__call__

python
class Foo:
    def __init__(self):
        pass

    def __call__(self, *args, **kwargs):
        print("__call__")


obj = Foo()  # 执行 __init__和__new__
obj()  # 执行 __call__

内置函数callable可以用来辨别是否有实现它。

它也可以用来写装饰器:

python
import threading
from typing import Final, TypeVar, Generic

T = TypeVar("T")


class Singleton(Generic[T]):
    """
    类装饰器实现单例
    双重检查加锁保证了线程安全
    Final在IDE层面防止A-B-A问题
    """

    _cls: Final[type[T]]
    _instance_lock: Final = threading.Lock()
    _unique_instance: Final[T]

    def __init__(self, cls: type[T]):
        self._cls = cls

    def __call__(self, *args, **kwargs) -> T:
        if not hasattr(self, "_unique_instance"):
            with self._instance_lock:
                if not hasattr(self, "_unique_instance"):
                    self._unique_instance = self._cls(*args, **kwargs)  # type: ignore
        return self._unique_instance


@Singleton
class MyClass:
    def __init__(self, name: str = ""):
        self.name = name


a = MyClass("a")
b = MyClass("b")
print(a is b)  # True
print(b.name)  # 'a'

对象属性相关

在访问对象的属性的时候 依照这样一个优先级 经过 ③ 轮遍历:

  1. ①mro 中各个类的__getattribute__方法
  2. 本类的数据描述符属性
  3. 本实例属性(obj.__dict__
  4. 本类的非数据描述符属性
  5. ②mro 各个类的属性和描述符属性(super().__dict__
  6. ③mro 各个类的__getattr__方法
  7. 抛出AttributeError异常

__getattr__

如果找不到对象的属性时会调用这个方法
这个方法应该返回属性值或者抛出AttributeError异常
注意,如果通过以上优先级机制能找到对象属性的话,不会调用__getattr__方法
另外两个

  • __setattr__设置对象的属性
  • __delattr__删除对象的属性

__getattribute__

当访问某个对象的属性时,会无条件的调用这个方法。这个方法只适用于新式类。
该方法应该返回属性值或者抛出AttributeError异常。
当同时定义__getattribute____getattr__时,
__getattr__方法不会再被调用,除非引发AttributeError异常

注意:

当在__getattribute__代码块中,再次执行属性的获取操作时,
会再次触发__getattribute__方法的调用,代码将会陷入无限递归

运算符相关

内置模块operator里的函数与运算符映射。重写以下方法其实是影响的 operator 中函数的调用。不限于运算符,还有下标,下标赋值,长度等大部分魔术方法可定义的。

逻辑运算

布尔类型转换(如直接if)调用__bool__,需要返回布尔值

python
class A:
    def __init__(self):
        self.a = 1
        self.b = 2

    def __bool__(self):
        return bool(self.a)


a = A()
if a:
    print(not bool(a))

如果不定义__bool__, 会默认以bool(__len__())为结果,如果未定义__len__ 始终为 True

比较运算

__eq__

遇到比较运算符时触发,需要返回布尔值

python
class A:
    def __init__(self):
        self.a = 1
        self.b = 2

    def __eq__(self, obj):
        return self.a == obj.a and self.b == obj.b


a = A()
b = A()
print(a == b)

如果一个类重写了__eq__方法,它的__hash__就会变成 None 而继承不到。

其他运算操作符与之相似:__lt__()__le__()__ne__()__gt__()__ge__()

其他内置函数有关

内置函数len()调用__len__

python
class A:
    def __init__(self):
        self.a = 1
        self.b = 2

    def __len__(self):
        return len(self.__dict__)


a = A()
print(len(a))

内置函数hash()调用__hash__

python
class A:
    def __init__(self):
        self.a = 1
        self.b = 2

    def __hash__(self):
        return hash(str(self.a) + str(self.b))


a = A()
print(hash(a))