Skip to content

魔法方法

参考:https://rszalski.github.io/magicmethods/

什么是魔术方法 ?

它们是面向对象的 Python 中的一切。它们是您可以定义为您的类添加“魔法”的特殊方法。 它们总是被双下划线包围(例如 __init__ or __lt__)。这些特殊方法,它们允许我们的类和 Python 更好地集成。 在标准库参考(Standard Library Reference)中,它们被称为魔法方法(Magic Methods),是与 Python 的其他特性无缝集成的基础。

例如,我们用字符串来表示一个对象的值。object 基类包含了__repr__()__str__() 的默认实现,它们提供了一个对象的字符串描述。遗憾的是,这些默认的实现不够详细。我们几乎总会想重写它们中的一个或两个。

python
>>> class Hero:                                                       
...     def __init__(self, name):
...         self.name = name     
...                              
>>> hero = Hero('正心')          
>>> hero                         
<__main__.Hero object at 0x0000014755393C70>
# 期待输出的内容为 <Hero 正心>

我们还会介绍其他的转换方法,尤其是__hash__()__bool__()__bytes__() 。这些方法可以把一个对象转换成一个数字、一个布尔值或者一串字节。

例如,当我们实现了 __bool__() ,我们就可以像下面这样在 if 语句中使用我们的对象:if someobject:

接下来,我们会介绍实现了比较运算符的几个特殊方法:__lt__()__le__()__eq__()__ne__()__gt__()__ge__()datetime 对象能够与数字进行计算,就是因为重写了这些运算符。

当我们定义一个类时,几乎总是需要使用这些基本的特殊方法。

我们也会在介绍__new__()__del__() ,因为它们的使用更加复杂,而且相比于其他的特殊方法,我们并不会经常使用它们。

我们会详细地介绍如何用这些特殊方法来扩展一个简单类。我们需要了解从 object 继承而来的默认行为,这样,我们才能理解应该在什么时候使用重写,以及如何使用它。

构造和初始化

每个人都知道最基本的魔法方法,__init__ 。这是我们可以定义对象的初始化行为的方式。但是,当调用时 x = SomeClass()__init__ 并不是第一个被调用的。实际上,在这之前会先调用 __new__ 实际创建实例对象,然后在初始化时将参数传递给初始化程序。在对象生命周期结束的时候,__del__ 会在对像被销毁前自动调用。让我们仔细看看这3种神奇的方法:

__init__(self, [...])

类的初始化器。无论调用什么主构造函数,它都会被传递(因此,例如,如果我们调用x = SomeClass(10, 'foo'), __init__ 将被传递 10'foo' 作为参数。__init__ 在 Python 类定义中几乎普遍使用。

所有类的超类 object ,有一个默认包含 pass 的 __init__() 实现,这个函数会在对象初始化的时候调用,我们可以选择实现,也可以选择不实现,一般建议是实现的,不实现对象属性就不会被初始化,虽然我们仍然可以对其进行赋值,但是它已经成了隐式的了,编程时显示远比隐式的更好

python
>>> class SomeClass:
...     def __init__(self):             
...         print('__init__ 方法被调用')
... 
>>> e = SomeClass()
__init__ 方法被调用
>>>

__new__(cls, [...])

__new__是在对象的实例化中调用的第一个方法。它接受类,然后是它将传递给的任何其他参数__init____new__ 很少使用,但它确实有其用途,尤其是在子类化不可变类型(如元组或字符串)时。

python
"""
在面试过程中,可能会问 __new__ 是什么东西?
"""


class Son:
    def __new__(cls):
        print("__new__ 方法被调用")
        inst = object.__new__(cls)
        return inst

    def __init__(self):
        super().__init__()
        """实例化属性"""
        print("__init__ 方法被调用")


# 实例化对象是,调用 __init___ 实例属性(new)
p = Son()
# 调用的顺序,它的作用

# 1 __new__ 创建一个实例对象
# 2 __init__ 给实例对象绑定各种属性
# 魔法方法 内置的特殊功能

提示

__new__ 是实现单例模式的原理,关于单例模式可以查看 单例模式

__del__(self)

如果 __new____init__ 形成了对象的构造函数,__del__ 就是析构函数。它没有实现语句的行为 del x (因此代码不会转换为 x.__del__() )。相反,它定义了对象被垃圾收集时的行为。对于在删除时可能需要额外清理的对象(如套接字或文件对象),它可能非常有用。但是要小心,因为当解释器退出时,如果对象仍然存在,则无法保证 __del__ 会执行,因此 __del__ 不能作为良好编码实践的替代品(例如在完成连接后始终关闭连接)。 事实上,__del__ 几乎不应该使用它,因为它被称为不稳定的环境;谨慎使用它!

提示

通过 __del__ 我们可以轻松看出 Python 的 垃圾回收机制 的原理

总结:构造与初始化方法,说白了就是对象的 我是谁,我从哪里来,到哪里去。

控制属性访问

许多从其他语言转向 Python 的人抱怨它缺乏对类的真正封装。也就是说,无法使用公共 getter 和 setter 定义私有属性。这不可能比事实更进一步:碰巧 Python 通过“魔术”完成了大量的封装,而不是方法或字段的显式修饰符。看一看:

  • __getattr__(self, name)

    您可以定义用户尝试访问不存在的属性(根本不存在或尚不存在)时的行为。这对于捕获和重定向常见的拼写错误、发出有关使用已弃用属性的警告(如果您愿意,您仍然可以选择计算并返回该属性)或巧妙地处理AttributeError. 但是,它仅在访问不存在的属性时才被调用,因此它不是真正的封装解决方案。

    python
    >>> class Person:                  
    ...     def __init__(self, name, attrs):
    ...         self.name = name
    ...         self.attrs = attrs
    ...         
    ...     def __getattr__(self, item):
    ...         return self.attrs[item]
    ... 
    >>> zx = Person('正心', {'age': 18})
    >>> zx.name
    '正心'
    >>> zx.age
    18
    >>> zx.gender
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 7, in __getattr__
    KeyError: 'gender'
  • __setattr__(self, name, value)

    __getattr__,不同__setattr__ 的是封装解决方案。它允许您定义分配给属性的行为,而不管该属性是否存在,这意味着您可以为属性值的任何更改定义自定义规则。但是,您必须小心使用__setattr__ ,如列表末尾的示例所示。

    您很容易在控制属性访问的任何方法的定义中引起问题。考虑这个例子:

    python
    def __setattr__(self, name, value):
        self.name = value
        # 每次调用时都会触发 __setattr__() 从而导致递归。
        # 上面设置属性的方式等价与 self.__setattr__('name', value)
        # 以为该方法会不断调用自身,并且不会停止,从而导致程序崩溃
    
    
    def __setattr__(self, name, value):
        self.__dict__[name] = value # 建议使用类的 __dict__ 属性来自定义行为

    案例演示

    python
    >>> class Person:
    ...     def __init__(self, name, attrs):
    ...         self.name = name
    ...         self.attrs = attrs
    ...     
    ...     def __setattr__(self, name, value):
    ...         print('__setattr__', name, value)
    ...         self.__dict__[name] = value
    ...
    
    >>> zx = Person('正心', {'age': 18})
    __setattr__ name 正心
    __setattr__ attrs {'age': 18}
    >>> zx.gender = 'male'
    __setattr__ gender male
    >>> print(zx.gender)
    male
  • __delattr__(self, name)

    这与完全相同 __setattr__ ,但用于删除属性而不是设置属性。__setattr__ 为了防止无限递归(del self.name 在实现中调用 __delattr__ 将导致无限递归) ,还需要采取与 with 相同的预防措施。

  • __getattribute__(self, name)

    毕竟,__getattribute__它与它的同伴__setattr____delattr__. 但是,我不建议您使用它。__getattribute__ 只能与新式类一起使用(所有类在最新版本的 Python 中都是新式的,在旧版本中,您可以通过子类化来使类成为新式object 。它允许您为属性值何时定义规则访问。它遇到了一些与犯罪伙伴类似的无限递归问题(这次你调用基类的__getattribute__ 方法来防止这种情况)。它还主要消除了对 的需要__getattr__,当__getattribute__ 实现时,只有在它被显式调用或AttributeError 被提出。可以使用此方法(毕竟,这是您的选择),但我不推荐它,因为它的用例很小(我们需要特殊行为来检索值而不是分配给它的情况要少得多)并且因为实现无错误可能真的很困难。

同样,Python 的魔法方法非常强大,强大的力量伴随着巨大的责任。了解使用魔术方法的正确方法很重要,这样您就不会破坏任何代码。

那么,关于 Python 中的自定义属性访问,我们学到了什么?不能轻易使用。事实上,它往往过于强大和违反直觉。但它存在的原因是为了抓住某种痛点:Python 并不寻求让坏事变得不可能,而只是让它们变得困难。自由是至高无上的,所以你真的可以为所欲为。下面是一些特殊属性访问方法的示例(请注意,我们之所以使用super ,是因为并非所有类都有属性__dict__):

python
class Person:
    def __init__(self, name):
        super(Person, self).__setattr__("counter", 0)
        super(Person, self).__setattr__("name", name)

    def __setattr__(self, key, value):
        if key == "name":
            super(Person, self).__setattr__("counter", self.counter + 1)

    def __delattr__(self, name):
        if name == "name":
            super(Person, self).__setattr__("counter", self.counter - 1)
        super(Person, self).__delattr__(name)


zx = Person("正心")
zx.name = "正心1"
print(zx.counter)

反射

您还可以通过定义魔术方法来控制使用内置函数 isinstance()issubclass() 行为的反射方式。神奇的方法是:

  • __instancecheck__(self, instance)

    检查实例是否是您定义的类的实例(例如 isinstance(instance, class).

  • __subclasscheck__(self, subclass)

    检查一个类是否是您定义的类的子类(例如issubclass(subclass, class))。

这些魔术方法的用例可能看起来很小,而且很可能是真的。我不会花太多时间在反射魔法方法上,因为它们不是很重要,但它们反映了 Python 和 Python 中面向对象编程的重要内容:几乎总是有一种简单的方法可以做某事,甚至如果很少需要。这些神奇的方法可能看起来没什么用,但如果你需要它们,你会很高兴它们在那里(并且你阅读了本指南!)。

可调用对象

您可能已经知道,在 Python 中,函数是一等对象。这意味着它们可以传递给函数和方法,就像它们是任何其他类型的对象一样。这是一个非常强大的功能。

Python 中的一种特殊魔法方法允许您的类实例表现得好像它们是函数一样,以便您可以“调用”它们,将它们传递给将函数作为参数的函数,等等。这是另一个强大的便利特性,它使 Python 编程变得更加甜蜜。

  • __call__(self, [args...])

    允许将类的实例作为函数调用。本质上,这意味着x()与 相同x.__call__()。请注意,__call__ 它采用可变数量的参数;这意味着您可以像定义__call__任何其他函数一样定义任何其他函数,无论您想要多少参数。

__call__在具有需要经常更改状态的实例的类中特别有用。“调用”实例是改变对象状态的一种直观而优雅的方式。一个示例可能是表示实体在平面上的位置的类:

python
class Entity:
    """Class to represent an entity. Callable to update the entity's position."""

    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size

    def __call__(self, x, y):
        """Change the position of the entity."""
        self.x, self.y = x, y

    # snip...

上下文管理器

在 Python 2.5 中,Python 中引入了一个新关键字以及一种用于代码重用的新方法:with语句。上下文管理器的概念在 Python 中并不新鲜(它之前作为库的一部分实现),但直到PEP 343 被接受后,它才成为一流的语言结构。您之前可能已经看过with以下声明:

python
with open('foo.txt') as bar:
    ...  # perform some action with bar

with上下文管理器允许在对象的创建被语句包装时对其进行设置和清理操作。上下文管理器的行为由两种神奇的方法决定:

  • __enter__(self)

    with定义上下文管理器在语句创建的块的开头应该做什么。请注意, 的返回值__enter__绑定到语句的目标,或者 . 后面的名称。with``as

  • __exit__(self, exception_type, exception_value, traceback)

    定义上下文管理器在其块被执行(或终止)后应该做什么。它可用于处理异常、执行清理或在块中的操作之后立即执行某些操作。如果块成功执行,exception_type, exception_value,traceback 将是None. 否则,您可以选择处理异常或让用户处理;如果您想处理它,请确保在说完之后__exit__返回。True 如果您不希望上下文管理器处理异常,就让它发生。

__enter__并且__exit__对于具有明确定义和常见设置和清理行为的特定类很有用。您还可以使用这些方法来创建包装其他对象的通用上下文管理器。这是一个例子:

python
class Closer:
    """A context manager to automatically close an object with a close method in a with statement."""

    def __init__(self, obj):
        self.obj = obj

    def __enter__(self):
        return self.obj  # bound to target

    def __exit__(self, exception_type, exception_val, trace):
        try:
            self.obj.close()
        except AttributeError:  # obj isn't closable
            print('Not closable.')
            return True  # exception handled successfully

这是一个实际使用的示例Closer,使用 FTP 连接来演示它(可关闭的套接字):

复制

有时,特别是在处理可变对象时,您希望能够复制对象并进行更改,而不会影响您复制的内容。这就是 Pythoncopy发挥作用的地方。然而(幸运的是)Python 模块没有感知能力,所以我们不必担心基于 Linux 的机器人起义,但我们必须告诉 Python 如何有效地复制事物。

  • __copy__(self)

    定义copy.copy()类实例的行为。copy.copy()返回对象的浅表副本 ——这意味着,虽然实例本身是一个新实例,但它的所有数据都被引用——即,对象本身被复制,但它的数据仍然被引用(因此对数据进行了更改在浅拷贝中可能会导致原件发生变化)。

  • __deepcopy__(self, memodict={})

    定义copy.deepcopy()类实例的行为。copy.deepcopy()返回对象的深层副本——对象及其数据都被复制。memodict 是以前复制的对象的缓存——这优化了复制并在复制递归数据结构时防止了无限递归。当您想要深度复制单个属性时,请copy.deepcopy() 将该属性memodict作为第一个参数调用。

这些魔术方法有哪些用例?与往常一样,在任何情况下,您需要比默认行为提供的更细粒度的控制。例如,如果您尝试复制将缓存存储为字典(可能很大)的对象,那么复制缓存也可能没有意义——如果缓存可以在实例之间在内存中共享,那么它应该是。

附录:其他魔术方法

Python中的一些魔术方法直接映射到内置函数;在这种情况下,如何调用它们是相当明显的。但是,在其他情况下,调用远不那么明显。本附录致力于公开导致调用魔术方法的非显而易见的语法。

魔术方法当它被调用时(示例)解释
__new__(cls [,...])instance = MyClass(arg1, arg2)__new__在实例创建时调用
__init__(self [,...])instance = MyClass(arg1, arg2)__init__在实例创建时调用
__cmp__(self, other)self == other,self > other等。要求进行任何比较
__pos__(self)+self一元加号
__neg__(self)-self一元减号
__invert__(self)~self位反转
__index__(self)x[self]对象作为索引时的转换
__nonzero__(self)bool(self)对象的布尔值
__getattr__(self, name)self.name # name doesn't exist访问不存在的属性
__setattr__(self, name, val)self.name = val分配给属性
__delattr__(self, name)del self.name删除属性
__getattribute__(self, name)self.name访问任何属性
__getitem__(self, key)self[key]使用索引访问项目
__setitem__(self, key, val)self[key] = val使用索引分配给项目
__delitem__(self, key)del self[key]使用索引删除项目
__iter__(self)for x in self迭代
__contains__(self, value)value in self,value not in self成员资格测试使用in
__call__(self [,...])self(args)“call”一个实例
__enter__(self)with self as x:with语句上下文管理器
__exit__(self, exc, val, trace)with self as x:with语句上下文管理器
__getstate__(self)pickle.dump(pkl_file, self)Pickling
__setstate__(self)data = pickle.load(pkl_file)Pickling

希望这张表应该已经解决了您可能对哪种语法调用哪种魔术方法的任何疑问。