python魔术方法模拟篇

发布时间 2023-07-16 18:54:15作者: 天才九少

6,模拟篇

  • __call__
  • __len__
  • __length_hint__
  • __getitem__
  • __setitem__
  • __delitem__
  • __reversed__
  • __contains__
  • __iter__
  • __missing__
  • __enter__和__exit__

__call__方法

所谓的callable就是可以以函数调用的形式来使用的对象,那想让一个类的对象成为callable,我们需要给它定义这个__call__ method,下面我们建立一个Multiplier的object o 并且让它的乘法倍数是3,然后我们可以像使用函数一样使用这个对象o,那么它实际上调用的就是这个__call__ method ,我们给进的参数4,就被传到了这个arg里,所以我们理论上该返回一个3乘以4就是12

class Multiplier:
    def __init__(self, mul):
        self.mul = mul

    def __call__(self, arg):
        return self.mul * arg


o = Multiplier(3)
print(o(4))
# 12

 

__len__方法

接下来我们看emulate container type,所谓container就是容器,在python中我们最常见的容器是list跟Dictionary,它们呢也分别对应了两个类别,一个叫sequence一个叫mapping。下面我们写了一个套皮list,我们要看的魔术方法是__len__,它要返回你这个container的长度,这里我直接借用list实现,返回我这个self.data的长度,我们建立了一个MyList object然后打印它的长度就是2,这个__len__最主要的作用呢就是被这个方法builtin方法len调用。

class MyList:
    def __init__(self, data):
        self._data = data

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


x = MyList([1, 2])
print(len(x))
# 2

如果你这个object没有被定义__boo__这个魔术方法,当你把它当作一个条件去判断的时候,它就会尝试去找这个__len__魔术方法,如果他的长度是0认为是False,否则认为是True,这个特性和我们所有builtin的container都是一样的,我们平常也会写if lst   if dct,我们运行下面的代码是有输出的,那如果我们把这个MyList初始化为空呢它就没有了

class MyList:
    def __init__(self, data):
        self._data = data

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


x = MyList([1, 2])
if x:
    print("Yeah")
# Yeah

 

__length_hint__方法

说完__len__我们稍微提一下这个__length_hint__,__length_hint__是返回一个估计的长度,它是被这个operator.length_hint调用的,我们运行一下,结果是2,但是这个magic method实际上是很少用到的

import operator


class MyList:
    def __init__(self, data):
        self._data = data

    def __length_hint__(self):
        return len(self._data)


x = MyList([1, 2])
print(operator.length_hint(x))
# 2

 

__getitem__方法

__getitem__就是你用方括号的时候会调用的这么一个魔术方法,比如让你打印x[0]的时候,这个0就会被放到key里传进来,这里的结果就是返回它的index为0的那个值,运行结果是1

class MyList:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, item):
        return self._data[item]


x = MyList([1, 2])
print(x[0])
# 1

我们做一个小小的改变,有的时候我们拿到的数据这个0是一个string,如果我们不小心把这个string作为index传进去了,那list是没有办法认出来这种index的,所以它就会报错

class MyList:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, item):
        return self._data[item]


x = MyList([1, 2])
print(x["0"])
TypeError: list indices must be integers or slices, not str

这个时候我们可以在我们这个类的__getitem__里面,把这个key先转化成int,那这样一来就算传进来的key是一个string,它也能正确的返回这个值了

class MyList:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, item):
        return self._data[int(item)]


x = MyList([1, 2])
print(x["0"])
# 1

 

__setitem__方法

当我们用方括号去读取一个值的时候使用的是__getitem__,但是当我们用方括号去赋值的时候调用的就是__setitem__,例如给x[0]赋值为3,然后打印x[0],那么运行结果就是3。

class MyList:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, item):
        return self._data[int(item)]

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


x = MyList([1, 2])
x[0] = 3
print(x["0"])
# 3

x[3] = 4
print(x["0"])
IndexError: list assignment index out of range

那比如说有时候我们给这个list赋值它超出了这个index,比如x[3] = 4,它就会报错,如果我希望这个list,可以在这个Index错误的时候直接无视它不要管它,我们就可以把这个__setitem__稍微改一下,如果出现IndexError直接pass算了,这个时候当我给x[3]赋值它就当无事情发生了

class MyList:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, item):
        return self._data[int(item)]

    def __setitem__(self, key, value):
        try:
            self._data[key] = value
        except IndexError:
            pass


x = MyList([1, 2])
x[3] = 4
print(x["0"])
# 1

 

__delitem__方法

__delitem__就是删除一个key,就是被del这个关键字触发的,x本来是[1, 2, 3]然后把x[1]删除掉,再打印x[1],由于原来的x[1]这个2被删除掉了,现在的x[1]就是3了

class MyList:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, item):
        return self._data[int(item)]

    def __setitem__(self, key, value):
        try:
            self._data[key] = value
        except IndexError:
            pass

    def __delitem__(self, key):
        self._data = self._data[0: key] + self._data[key+1:]


x = MyList([1, 2, 3])
del x[1]
print(x[1])
# 3

 

__reversed__方法

__reversed__会被python的builtin function reversed调用,reverse平时就是把这个东西反过来

class MyList:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, item):
        return self._data[int(item)]

    def __setitem__(self, key, value):
        try:
            self._data[key] = value
        except IndexError:
            pass

    def __delitem__(self, key):
        self._data = self._data[0: key] + self._data[key+1:]

    def __reversed__(self):
        return MyList(self._data[::-1])


x = MyList([1, 2, 3])
print(reversed(x)._data)
# [3, 2, 1]

 

__contains__方法

__contains__方法它是当你使用in这个操作的时候被调用的

...
    def __contains__(self, item):
        return item in self._data


x = MyList([1, 2, 3])
print(2 in x)
print(5 in x)
# True
# False

 

__iter__方法

__iter__是返回这个container的一个iterator,这里我们就直接把保存的这个_data的iterator给返回回去,这样我们在做for i in x的时候,我们实际上迭代的就是里面的那个list,可以看到打印出来的就是1,2,3

...
    def __iter__(self):
        return iter(self._data)


x = MyList([1, 2, 3])
for i in x:
    print(i)
# 1
# 2
# 3

 

__missing__方法

container里面有一个稍微特殊点的的magic method叫做__missing__,这个__missing__只有在dict的subclass里面才有用,也就是你新建的这个class,必须要继承它builtin的这个dict,它的作用是当你想在这字典里找一个key,但找不着的时候它应该干嘛,比如下面我们新建了一个字典之后我们想找d[0],但是我们知道这个字典里面现在一个key都没有,那当这个字典找不到这个key的时候,它就会把这个key仍到__missing__里面来问应该怎么办,这里我们统一返回0,可以看到那这种情况下d[0]就是0了

class MyDict(dict):
    def __missing__(self, key):
        return 0


d = MyDict()
print(d[0])

 

__enter__和__exit__方法

说完了container,我们介绍一下这个context type,我们平常在写python的时候经常会用到with什么东西,这个with后面的东西就叫做context,想自定义一个context,我们需要定义它的__enter__和__exit__ magic method,下面我们做了一个最简单的Timer,也就是在当进入这个context的时候,记录一下时间,再出去这个context的时候打印一下这个context里面花了多少时间

import time


class Timer:
    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"T: {time.time()- self.start}")


with Timer():
    _ = 1000 * 1000
# T: 0.0

 有的时候我们会遇见这种with Timer() as t的用法,这个as t 就是把__enter__这个的函数的返回值给保存到 t 里面,在这个部分最常见的操作就是直接把self给返回回去,比如我们就这样做然后在里面打印一下t.start,可以看到把这个context的起始时间就给打印出来了

import time


class Timer:
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"T: {time.time()- self.start}")


with Timer() as t:
    print(t.start)
    _ = 1000 * 1000
1689503740.810218
T: 0.0

那你可能会发现这个__exit__函数里面还有一些其他的argument,它们分别是exc_type、exc_value还有traceback,这几个东西呢主要是在出现exception的时候使用到,比如我们把刚才那个操作变成1000 / 0 ,然后我们把这几个argument都打印出来,可以看到exception type是zero division error,exception value是division by zero,然后这个traceback是一个object,注意这里我们依然把这个程序跑的时间给记录下来了,然后python才报了错,也就是说即便在这个context里面它raise了一个exception,我timer里__exit__函数的全部内容依然被执行了,所以这个context非常适合拿来释放资源,比如我们常常鼓励大家在打开文件的时候使用with,它就可以防止你忘记了把这个文件关上,同时就算程序报错了你依然可以把这个文件成功的关上

import time


class Timer:
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(exc_type, exc_val, exc_tb)
        print(f"T: {time.time()- self.start}")


with Timer() as t:
    _ = 1000 / 0