4

python魔法方法详解

1. 什么是魔法方法

魔法方式(Magic methods)是python的内置函数,一般以双下划线开头和结尾,比如\_\_add\_\_,\_\_new\_\_等。每个魔法方法都有对应的一个内置函数或者运算符。当我们个对象使用这些方法时,相当于对这个对象的这类方法进行重写(如运算符重载)。魔法方法的存在是对类或函数进行了提炼,供python解释器直接调用。当使用len(obj)时,实际上调用的就是obj.__len__方法,其它同理。

如我们对某个类A定义了\_\_add\_\_方法,那么可以直接通过a1+a2(a1,a2分别为A的实例)来实现自定义的“相加”,python会识别+并调用该对象对应的_\_add\_\_方法来运行,而无需像一般的方法一样通过a1.add(a2)来实现。这就是为什么说魔法方法本质是对一些函数进行了提炼和封装,可以方便的赋予对象更多的方法。

dir()可以查看对象的所有方法和属性,其中双下划线开头和结尾的就是该对象具有的魔法方法。以整数对象为例:

>>>dir(int)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

可以看到,整数对象下具有__add__方法,这也是为什么我们可以直接在python中运算1+2,当python识别到+时,就去调用该对象的__add__方法来完成计算。比如我们想给自己的对象定义+方法:

class A:
    def __init__(self,a):
        self.a=a
    def __add__(self,others):
        print('this is a magic method')
        return [self.a,others.a]
    
>>>a=A(1)
>>>b=A(3)
>>>c=a+b#自定义的__add__方法,实现是将两个对象的a属性合并到一个列表中
this is a magic method#+调用的是__add__方法
>>>c
[1, 3]

>>>a.__add__(b)#等价于直接调用
this is a magic method
[1, 3]

同理,再举一个__len__魔法方法的例子来帮助理解。我们定义一个list对象l,通过dir(l)可以看到该对象中具有__len__方法,即表明在python中可以通过len(l)来返回其列表长度。我们在自己定义的对象中当然也可以自定第该方法,来实现我们想通过len()返回的结果。

>>>l=[1,2,3]
>>>dir(l)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>>len(l)
3

自定义一个对象:

class B:
    def __init__(self,a):
        self.a=a
        
>>>b=B(1)
>>>len(b)#因为对象中未定义__len__,因此调用len(b)会报错,没有该方法
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: object of type 'B' has no len()
    
#添加__len__方法
class B:
    def __init__(self,a):
        self.a=a
    def __len__(self):
        print('this is magic method len')
        return 2
>>>a=B(1)
>>>print(len(a))
this is magic method len
2

可以看到,魔术方法在类或对象的某些事件出发后会自动执行,如果希望根据自己的程序定制特殊功能的类,那么就需要对这些方法进行重写。使用这些「魔法方法」,我们可以非常方便地给类添加特殊的功能。

2. 几类常用的魔法方法

魔法方法大致分为如下几类:

  • 构造与初始化
  • 类的表示
  • 访问控制
  • 比较、运算等操作
  • 容器类操作
  • 可调用对象
  • 序列化

2.1 构造与初始化

对类的初始化一般会涉及三个魔法方法,__init__,__new____del__;

当我们初始化一个类是,a=A(1),首先调用的并不是__init__函数,而是__new__;初始化一个类有两步,分别为:

a. 调用该类的__new__方法,并返回该类的实例对象;

b. 调用该类的__init__方法,对实例对象进行初始化。

其中__new__用法如下:

(1) __new__(cls,*args,**kwargs):至少要有一个参数cls,代表传入的类,此参数在实例化时由 Python 解释器自动提供,若返回该类的对象实例,后面的参数直接传递给__init__

(2) __new__可以决定是否使用__init__方法,但是,执行了__new__,并不一定会进入__init__,只有__new__返回了,当前类cls的实例,当前类的__init__才会进入。即使返回父类的实例也不行,必须是当前类的实例;

(3) object将__new__()方法定义为静态方法,并且至少需要传递一个参数cls,cls表示需要实例化的类,此参数在实例化时由Python解释器自动提供。

(4) __init__()有一个参数self,该self参数就是__new__()返回的实例

举例理解:

class A:
    def __init__(self,a,b):
        print('this is A init')
        print(self)
        self.a=a
        self.b=b
    def __new__(cls, *args, **kwargs):
        print('this is A new')
        print('args:%s'%args)
        print('kwargs:%s'%kwargs)
        print(cls)
        print(object.__new__(cls))#<__main__.A object at 0x000001BCD98FB3D0>,一个A的对象实例
        return object.__new__(cls)#创建实例并返回
>>>a=A(1,b=10)
this is A new#先进入__new__
args:1
kwargs:{'b': 10}
<class '__main__.A'>
<__main__.A object at 0x000001BCD98FB3D0>
this is A init#再进入__init__
<__main__.A object at 0x000001D0BC3EB3D0>#self就是__new__返回的对象实例

其中object是所有类的基类,object.__new__则返回当前传入类cls的实例对象;当前类的__new__返回当前类的实例对象后,再进入__init__对实例进行初始化。

如果 __new__未返回当前类的实例,则不会进入当前类的__init__,举例:

class A:
    def __init__(self,a,b):
        print('this is A init')
        self.a=a
        self.b=b
    def __new__(cls, *args, **kwargs):
        print('this is A new')
        print('args:%s'%args)
        print('kwargs:%s'%kwargs)
        print(cls)
>>>m=A(1,b=10)
this is A new
args:1
kwargs:{'b': 10}
<class '__main__.A'>
>>>print(m.a)#报错,未进入到当前类的__init__进行初始化
AttributeError: 'NoneType' object has no attribute 'a'

进一步,__new__返回的是其它实例,当前实例的__init__也不会调用

class A(object):
    def __init__(self,a):
        print('this is A init')
        self.a=a
    def __new__(cls, *args, **kwargs):
        print('this is A new')
        print(cls)
        print(object.__new__(cls))
        return object.__new__(cls)

class B(A):
    def __init__(self, a):
        super().__init__(a)
        print('this is B init')
    def __new__(cls ,*args, **kwargs):
        print('this is B new')
        print(cls)
        return super().__new__(A, *args, **kwargs)#这里返回的是A的实例对象<__main__.A object at 0x000002A08683B370>,因此不会调用B的__init__;
>>>m=B(1)#初始化过程中,调用的顺序为:子类B的__new__》父类A的__new__,调用Object.__new__返回A的实例对象》返回到子类B的__new__中,返回A对象的实例,不是B的对象实例,因此不再调用B的__init__
this is B new
<class '__main__.B'>
this is A new
<class '__main__.A'>
<__main__.A object at 0x000002A08683B370>


#修改子类的__new__:
class B(A):
    def __init__(self, a):
        super().__init__(a)
        print('this is B init')
    def __new__(cls ,*args, **kwargs):#cls默认代表传入
        print('this is B new')
        print(cls)
        return super().__new__(cls, *args, **kwargs)#即是通过super()调用父类object的初始__new__方法来创建实例。返回B的对象实例
>>>m=B(1)
this is B new
<class '__main__.B'>
this is A new
<class '__main__.B'>
<__main__.B object at 0x00000167C541B370>
this is A init
this is B init

#调用顺序:
# 调用B的__new__》传入B对象,调用A的__new__,调用object.__new__,返回B的实例》返回到B的__new__中》调用B的__init__》调用父类A的__init__,初始化并继承A的属性》继续B的__init__属性重写;

__new__的使用场景如单例模式、工厂模式,以及一些不可变对象的继承上。这类应用非常值得关注并使用,可以大大的让代码看起来优美和简洁;这里不再展开。

__del__方法则是当对象被系统回收的时候调用的魔法方法,在对象生命周期调用结束时调用该方法。Python 采用自动引用计数(ARC)方式来回收对象所占用的空间,当程序中有一个变量引用该 Python 对象时,Python 会自动保证该对象引用计数为 1;当程序中有两个变量引用该 Python 对象时,Python 会自动保证该对象引用计数为 2,依此类推,如果一个对象的引用计数变成了 0,则说明程序中不再有变量引用该对象,表明程序不再需要该对象,因此 Python 就会回收该对象。所以大部分时候,都不需要我们手动去删掉不再使用的对象,python的回收机制会自动帮我们做这件事。

class A(object):
    def __init__(self,a):
        print('this is A init')
        self.a=a

    def __del__(self):
        print('this is magic method del')
>>>m=A(1)
this is A init
>>>n=A(2)
>>>n1=n
this is A init
>>>del m#m被删除,这个对象的引用数为0;自动调用其del方法
this is magic method del
>>>del n
>>>del n1
this is magic method del


#进一步,查看python的自动回收机制,不用del去手动删除实例
def func(a,b):
    x=A(a).a
    y=b
    return x+y
>>>func(1,2)
this is A init
this is magic method del#因为函数调用返回后,x和y自动被销毁;因此会触发__del__
3

2.2 控制属性访问

这类魔法方法主要再对对象的属性进行访问、定义、修改时起作用。主要有:

  • __getattr__(self, name): 定义当用户试图获取一个属性时的行为。
  • __getattribute__(self, name):定义当该类的属性被访问时的行为(先调用该方法,查看是否存在该属性,若不存在,接着去调用__getattr__)。
  • __setattr__(self, name, value):定义当一个属性被设置时的行为。
  • __delattr__(self, name):定义当一个属性被删除时的行为。

当初始化属性时如self.a=a时或修改实例属性如ins.a=1时本质时调用魔法方法self.__setattr__(name,values);当实例访问某个属性如ins.a本质是调用魔法方法a.__getattr__(name);当删除对象某个属性如delattr(ins,'a')本质是调用魔法方法a.__delattr__(name)

class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b

    def __setattr__(self, key, value):
        print(key,value)
        print('this is magic method setattr')
    def __getattr__(self, item):
        print('getattr:%s'%item)
        print('this is magic method getattr')
    def __delattr__(self, item):
        print('delattr:%s'%item)
        print('this is magic method delattr')
        
>>>m=A(1,2)
a 1
this is magic method setattr#初始化self.a=a时调用 __setattr__
b 2
this is magic method setattr#初始化self.b=b时调用 __setattr__

>>>a=m.a
getattr:a
this is magic method getattr#访问属性a时调用__getattr__
>>>m.b=100
b 100
this is magic method setattr#修改属性b时调用__setattr__
>>>delattr(m,'a')
delattr:a
this is magic method delattr#删除属性a时调用__delattr__
>>>print(m.a)
getattr:a
this is magic method getattr
None#属性a被删除,为None

上面的代码有一些隐患,因为我们重载了__setattr__,因此属性初始化时就调用重载后的__setattr__;但是在初始化属性调用__setattr__时需要配合实例的属性管理__dict__来进行,即需要将属性都在self.__dict__中进行注册,否则实例是访问不到这些属性的。如

>>>print(m.a)
getattr:a
this is magic method getattr
None#可以看到,并没有初始化成功为a=1,因为重载的__setattr__方法内部尚未将属性在__dict__中注册

#修改上面的__setattr__:
class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b
    def __setattr__(self, key, value):
        print(key,value)
        print('this is magic method setattr')
        self.__dict__[key] = value#在__dict__注册实例属性
        #super().__setattr__(key,value) 也可以通过继承的方式来实现;

    def __getattr__(self, item):
        print('getattr:%s'%item)
        print('this is magic method getattr')
    def f(self):
        return self.__dict__#查看属性管理字典
>>>m=A(1,2)
>>>m.a
1
>>>m.f()
{'a': 1, 'b': 2}

其中,重载__setattr__时一定要配合__dict__,或者在添加了自定义的功能后,通过继承来完成该方法,不然容易陷入无线递归的陷阱。

控制属性重载的使用场景:如在初始化属性时先对属性的值进行拦截,进行相应的处理或者判断(比如类型判断,或者范围判断);

class A(object):
    def __init__(self,age,sex):
        self.age=age
        self.sex=sex

    def __setattr__(self, key, value):
        if key=='age':
            if not 0<=value<=100:
                raise Exception('age must between 0 and 100')
        elif key=='age':
            if not (value=='male' or value=='female'):
                raise Exception('sex must be male of female')
        else:
            pass
        super().__setattr__(key,value)
>>>m=A(age=102,sex='male')
Exception: age must between 0 and 100
>>>m=A(age=90,sex='hhh')
Exception: sex must be male of female
>>>m=A(age=90,sex='male')
>>>print(m.sex,m.age)
male 90

其它控制属性访问的魔法方法以后遇到了比较有体会后再补充。

2.3 容器类操作

有一些方法可以让我们自己定义自己的容器,就像python内置的list,tuple,dict等等;容器分为可变容器和不可变容器,这里的细节需要去了解相关的协议。如果自定义一个不可变容器的话,只能定义__len____getitem__;定义一个可变容器除了不可变容器的所有魔法方法,还需要定义__setitem____delitem__;如果容器可迭代。还需要定义__iter__

  • __len__(self):返回容器的长度
  • __getitem__(self,key):当需要执行self[key]的方式去调用容器中的对象,调用的时该方法
  • __setitem__(self,key,value):当需要执行self[key] = value时,调用的是该方法。
  • __delitem__(self, key):当需要执行 del self[key]时,需要调用该方法;
  • __iter__(self):当容器可以执行 for x in container: ,或者使用iter(container)时,需要定义该方法
  • __reversed__(self):实现当reversed()被调用时的行为。应该返回序列反转后的版本。仅当序列可以是有序的时候实现它,例如对于列表或者元组。
  • __contains__(self, item):定义了调用in和not in来测试成员是否存在的时候所产生的行为。

下面举一个例子,实现一个容器,该容器有List的一般功能,同时增加一些其它功能如访问第一个元素,最后一个元素,记录每个元素被访问的次数等

class SpecialList(object):
    def __init__(self,values=None):
        if values is None:
            self.values=[]
        else:
            self.values=values
        self.count={}.fromkeys(range(len(self.values)),0)

    def __getitem__(self, key):#通过obj[key]访问容器内的对象
        self.count[key]+=1
        return self.values[key]
    
>>>a=SpecialList()
>>>print(len(a))#未定义魔法方法__len__,因此无法通过len()调用
TypeError: object of type 'SpecialList' has no len()
    
>>>b=SpecialList([1,2,3])
>>>print(b[1])#调用__getitem__方法
2

下面是完整例子:

class SpecialList(object):
    def __init__(self,values=None):
        if values is None:
            self.values=[]
        else:
            self.values=values
        self.count={}.fromkeys(range(len(self.values)),0)
    def __len__(self):#通过len(obj)访问容器长度
        return len(self.values)

    def __getitem__(self, key):#通过obj[key]访问容器内的对象
        self.count[key]+=1
        return self.values[key]

    def __setitem__(self, key, value):#通过obj[key]=value去修改容器内的对象
        self.values[key]=value

    def __delitem__(self, key):#通过del obj[key]来删除容器内的对象
        del self.values[key]

    def __iter__(self):#通过for 循环来遍历容器
        return iter(self.values)
    
    def __next__(self):
        # 迭代的具体细节
        # 如果__iter__返回时self 则必须实现此方法
        if self._index >= len(self.values):
            raise StopIteration()
        value = self.values[self._index]
        self._index += 1
        return value

    def __reversed__(self):#通过reverse(obj)来反转容器内的对象
        return SpecialList(reversed(self.values))

    def __contains__(self, item):#通过 item in obj来判断元素是否在容器内
        return item in self.values

    def append(self, value):
        self.values.append(value)

    def head(self):
        # 获取第一个元素
        return self.values[0]

    def tail(self):
        # 获取第一个元素之后的所有元素
        return self.values[1:]

这类方法的使用场景主要在你需要定义一个满足需求的容器类数据结构时会用到,比如可以尝试自定义实现树结构、链表等数据结构(在collections中均已有),或者项目中需要定制的一些容器类型。

2.4 类的表示

类的表示相关的魔法方法主要有__str____repr____bool__

  • __str__主要是在打印对象print(obj)时,会隐式调用str(obj),即调用类中的__str__方法;定了该方法就可以通过str(obj)来调用;
  • __repr__主要式在直接输出对象时的显示,会调用__repr__方法;定义了该方法就可以通过repr(obj)来调用。

这两个方法都是将类显示为字符串;

举例(以datetime模块中的date对象举例):

import datetime
today=datetime.date.today()
>>>print(today)#调用date类的__str__方法来显示
2021-07-04
>>>print(str(today))
2021-07-04
>>>today#调用date类的__repr__来显示
datetime.date(2021, 7, 4)

回溯到date类的源码,可以清晰的对上面的输出进行解释:

   #date类中的__repr__和__str__方法定义: 
    def __repr__(self):
        """Convert to formal string, for repr().

        >>> dt = datetime(2010, 1, 1)
        >>> repr(dt)
        'datetime.datetime(2010, 1, 1, 0, 0)'

        >>> dt = datetime(2010, 1, 1, tzinfo=timezone.utc)
        >>> repr(dt)
        'datetime.datetime(2010, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)'
        """
        return "%s.%s(%d, %d, %d)" % (self.__class__.__module__,
                                      self.__class__.__qualname__,
                                      self._year,
                                      self._month,
                                      self._day)
    def isoformat(self):
        """Return the date formatted according to ISO.

            This is 'YYYY-MM-DD'.

            References:
            - http://www.w3.org/TR/NOTE-datetime
            - http://www.cl.cam.ac.uk/~mgk25/iso-time.html
            """
        return "%04d-%02d-%02d" % (self._year, self._month, self._day)

    __str__ = isoformat

如果自定义类中不定义这两个魔法方法,则会用父类或者object类的默认表示方法,举例:

class A(object):
    def __init__(self, a):
        self.a = a
>>>a = A(1)
>>>print(a)#调用__str__
<__main__.A object at 0x000001540B1F4DF0>
>>>a#调用__repr__
<__main__.A object at 0x000001540B1F4DF0>
>>>str(a)#调用__str__
'<__main__.A object at 0x000001540B1F4DF0>'
>>>print(str(a))#调用__str__
<__main__.A object at 0x000001540B1F4DF0>

可以看到,如果没有定义__str____repr__,则会调用默认的魔法方法(object的__str____repr__)即obj.__class__ object at XXX;

只定义了__str__:

class A(object):
    def __init__(self,a):
        self.a=a
    def __str__(self):
        return '(%s,%s)'%(self.__class__.__name__,self.a)
>>>a=A(1)
>>>print(a)
(A,1)
>>>a#未定义__repr__,仍旧调用默认的__repr__方法
<__main__.A object at 0x000001540B215790>

只定义了__repr__

class A(object):
    def __init__(self, a):
        self.a = a
    # def __str__(self):
    #     return '(%s,%s)'%(self.__class__.__name__,self.a)
    def __repr__(self):
        return '(repr:%s,%s)' % (self.__class__.__name__, self.a)
>>>a = A(1)
>>>a#调用__repr__
(repr:A,1)
>>>print(a)#未定义__str__;转为调用__repr__;
(repr:A,1)
>>>str(a)#未定义__str__;转为调用__repr__;
'(repr:A,1)'

从上面可以看出,当自定义类中没有定义 __str__() __repr__() 时,在进行对象的输出时,会调用默认的 __str__() __repr__() ;当类中只包含 __str__() 时,调用 print() 或str()函数进行对象的输出,会调用 __str__(),直接输出调用默认的 __repr__();当类中既包含 __str__() 又包含 __repr__() 时,调用 print() 或str()函数进行对象的输出,会调用 __str__(),直接输出会调用 __repr__();当类中只包含 __repr__() 时,调用 print() 或str()函数进行对象的输出和直接输出都会调用 __repr__()

因此,对于自定义的类,建议定义__str____repr__,以更好的进行交互;其中__str__可以考虑设计为想转换输出的字符串,在后续str(obj)将对象转为特定的字符串输出时提供一些便利;__repr__可以考虑设计为输出较为详细的信息,如列名,甚至包括部分关键参数名,这样在开发的时候便于获取对象的准确信息(如sklearn中的机器学习模块就是如此设计)

  • __bool__:当调用 bool(obj) 时,会调用 __bool__() 方法,返回 True 或 False:
class A(object):
    def __init__(self,a):
        self.a=a
    def __bool__(self):
        return self.a>10
>>>a=A(1)
>>>bool(a)
False
>>>b=A(100)
>>>bool(b)
True

如果未定义__bool__,则会调用内置的bool对象。

2.5 可调用对象

在Python中,方法也是一种高等的对象。通过对对象实现__call__就可以实现像调用方法一样去调用类;

class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b
    def __call__(self,a):
        self.a=a
        
>>>m=A(1,2)
>>>m.a
1
>>>m.b
2
>>>id(m)
1460475565152
>>>m(100)#像函数一样直接调用类,本质是调用的__call__方法
>>>m.a
100
>>>m.b
2
>>>id(m)
1460475565152

__call__方法的一些使用场景:

自建一个简单的装饰器(实际例子如 bottle 框架源码的 cached_property):

class A(object):
    def __init__(self,func):
        self.func=func
    def __call__(self):
        print('this is call')
        return self.func()
>>>@A   #语法糖,相当于A(f)
>>>def f():
    print('hhh')
>>>f()
this is call
hhh

定义一个对象,对象中具有各类方法,但是核心功能只有一个,可以通过可调用对象来简单且直接的调用,不用每次都实例化对象。

同样还可以用作抽象窗口函数,抽象出使用规则,通过子类的改写其他方法,在不改变原代码的情况下取得不同的结果,比如:

class CSVDataProcess:
    def __init__(self):

    def example_one(self):
        pass
    
    def __call__(self, exmaple):
        try:
            target_func = getattr(self, exmaple)
            target_func()
        except Exception as e:
            print("execute error", e)

if __name__ == "__main__":
    csv = CSVDataProcess()
    csv("example_one")
#这个例子在子类继承时,子类重新实现example;通过调用子类直接就可以通过__call__来直接调用该子类的example方法;整个代码非常简洁了;

2.6 序列化

python中有一个pickle模块来对实例化对象进行序列化;如pickle.loads(obj),pickle.dumps(obj)等;在序列化的时候也是调用的内置魔法方法:

  • __getstate__():用于Python 对象的序列化,指定在序列化时将哪些信息记录下来
  • __setstate__():用于Python 对象的反序列化,指定在反序列化时指明如何利用信息

举例说明:

#直接通过pickle保存一个对象实例,默认将该实例的属性、方法都保存下来;
class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b
import pickle
>>>a=A(1,2)
>>>a_1=pickle.dumps(a)
>>>print(a_1)
b'\x80\x04\x95&\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01A\x94\x93\x94)\x81\x94}\x94(\x8c\x01a\x94K\x01\x8c\x01b\x94K\x02ub.'
>>>a_2=pickle.loads(a_1)
>>>print(a_2)
<__main__.A object at 0x000001BF5B086670>
>>>print(a_2.a,a_2.b)
1 2

如果重写__getstate__()__setstate__(),如下:

class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b
    def __getstate__(self):
        print('this is magic method __getstate__')
        return {'a':self.a,
                'b':self.b}#序列化时返回的,即为在反序列化时传入的state
    def __setstate__(self, state):
        print('this is magic method __setstate__')
        self.a=state['a']
        self.b=300

import pickle
>>>a=A(1,2)
>>>a_1=pickle.dumps(a)#调用__getstate__
>>>print(a_1)
this is magic method __getstate__
b'\x80\x04\x95&\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01A\x94\x93\x94)\x81\x94}\x94(\x8c\x01a\x94K\x01\x8c\x01b\x94K\x02ub.'
>>>a_2=pickle.loads(a_1)#调用__setstate__
>>>print(a_2)
this is magic method __setstate__
<__main__.A object at 0x000001BF5B086670>
>>>print(a_2.a,a_2.b)
1 300

2.7 反射

我们可以控制怎么使用内置在函数sisinstance()和issubclass()方法 反射定义魔术方法. 这个魔术方法是:

  • __instancecheck__(self, instance)
  • __subclasscheck__(self, subclass)

这两个方法定义在元类中在有意义,暂不展开。

2.8 比较、运算、类型等操作

通过定义各类比较、运算、类型相关的魔法方法,来实现对象之间的各类比较、运算等操作。这类魔法方法非常多,不一一展开,以__eq__ 魔法方法举例说明,如果自定义的对象要实现==的比较功能,则必须在类中实现__eq__

#__eq__ 方法,可以判断两个对象是否相等
class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b
    def __eq__(self, other):#这里并没有重写,只是加了一句Print来监控;还是调用的Objecgt的__eq__
        print('eq')
        return super().__eq__(other)
>>>x=A(1,2)
>>>y=A(1,2)
>>>print(x == y)#如果未定义,则这两个对象就算里面的属性和方法都相同,但仍会返回False
eq
eq#返回两个应该和x与y都调用了有关
False
>>>print(x.__eq__(y))#单向x调用;因为最终仍是调用object的__eq__
NotImplemented

自定义__eq__:

class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b
    def __eq__(self, other):
        print('eq')
        if self.a==other.a:
            return True
        else:
            return False
>>>x=A(1,2)
>>>y=A(1,3)
>>>print(x == y)#只要两个实例的a属性相同即返回True
eq
True
>>>print(x.__eq__(y))
eq
True

再举一个__int__的类型转换的魔法方法

class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b

>>>x=A(1,2)
>>>int(x)#未定义__int__,就只能按默认的int对象来转换,里面必须为string,float这类;
TypeError: int() argument must be a string, a bytes-like object or a number, not 'A'
    
    
class A(object):
    def __init__(self,a,b):
        self.a=a
        self.b=b
    def __int__(self):
        return int(self.a[:3])#必须返回为int数字
>>>x=A('345_hhh',3)
>>>print(int(x))
345

其它的比较、运算、类型等魔法方法如下:

  1. 用于比较的魔法方法

  1. 双目运算符或函数

  1. 增量运算

  1. 类型转换

总结

这篇文章较为详细的阐述了python中的各类魔法方法及相关demo;但这类方法在python的文档中较为分散,最重要的是鲜有实际的工程示例。但各类方法只有有实际的案例中才会真正体会到其“魔法”之处,比如大大简化代码,提高代码可读性、健壮性等等。在python一些第三方库中,查其源码都可以见到非常多的魔法方法的实际应用,因此当前这篇文章仅是抛砖引玉,真正的使用需要在开源的优秀源码中以及自身的工程实践中不断加深理解并合适应用。

部分参考:

https://blog.csdn.net/weixin_...

https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...


千翻娃儿
4 声望6 粉丝