查看原文
其他

python 魔术方法 : 让自定义类更像内置类型

(点击上方蓝字,快速关注我们)


来源:charliecharlie

segmentfault.com/a/1190000008013646

如有好文章投稿,请点击 → 这里了解详情


Python的魔术方法是Python中那些预定义的像__XXX__类型的函数。


使用Python的魔术方法的最大优势在于python提供了简单的方法让对象可以表现得像内置类型一样。


__str__函数


__str__函数用于处理打印实例本身的时候的输出内容。如果没有覆写该函数,则默认输出一个对象名称和内存地址。


例如:


>>> class Student(object):

...     def __init__(self,name):

...             self._name = name

...

>>> print Student()


输出:


<__main__.Student object at 0x0000000002A929E8>.


那么我们如何让输出的结果可读性更高一点呢?我们可以覆写__str__函数。例如


>>> class Student(object):

...     def __init__(self, name):

...             self._name = name

...     def __str__(self):

...             return  "I'm a student, named %s" % self._name

...

>>> print Student("Charlie")


输出结果就是:I'm a student, named Charlie.


我们将str()函数作用于该对象的时候,其实是调用了该对象的__str__函数。


__repr__ 函数


__repr__也是将对象序列化,但是__repr__更多的是给python编译器看的。__str__更多的是可读性(readable)。


我们将repr()函数作用于摸某一个对象的时候,调用的其实就是该函数的__repr__函数。


与repr()成对的是eval()函数。eval()函数是将序列化后的对象重新转为对象。前提是该对象实现了__repr__函数。


上面这一段话基于自己的理解,不知道对错。


>>> item = [1,2,3]

>>> repr(item)

'[1, 2, 3]'

>>> other_item = eval(repr(item))

>>> other_item[1]

2


__iter__函数


我们经常对list或者tuple使用for…in…来迭代。那是list继承自Iterable。Iterable实现了__iter__函数。


要想将一个自定义的对象变成一个可迭代的对象,那么必须要实现两个方法:__iter__和next.


__iter__函数返回一个对象。迭代的时候则会不断地调用next函数拿到下一个值,直到捕获到StopIteration停止。


廖雪峰老师教程里写的是__next__方法,不知道为啥。


class Fib(object):

    def __init__(self):

        self.a, self.b = 0, 1

 

    def __iter__(self):

        return self

 

    def next(self):

        self.a, self.b = self.b, self.a + self.b

        if self.a > 10000:

            raise StopIteration

        return self.a

 

 

for i in Fib():

    print i


__getitem__函数


上面通过实现__iter__函数实现对象的迭代。


那么如何实现对象按下标取出元素呢。


这是通过实现对象的__getitem__方法。


我们来举一个🌰子。我们新建了一个类MyList,我们要办它实现普通list的一些功能,比如(1)根据下标获取值;(2)正数顺序单步长切片 (3)任意步长切片


class MyList(object):

    def __init__(self, *args):

        self.numbers = args

 

    def __getitem__(self, item):

        return self.numbers[item]

 

 

my_list = MyList(1, 2, 3, 4, 6, 5, 3)

print my_list[2]


当然,上面实现了根据下标获取值。但是这还不够。我们还需要实现切片功能。例如my_list[1:3].


我们对对象进行切片操作的时候,调用的气势也是__getitem__函数。此时,该函数获取到的并不是int对象,而是slice对象。


例如下面的代码


class MyList(object):

    def __init__(self, *args):

        self.numbers = args

 

    def __getitem__(self, item):

        if isinstance(item, int):

            return self.numbers[item]

        elif isinstance(item, slice):

            # 写习惯了其他语言,差点忘记了三元运算符的格式了,吼吼吼。

            # 下面句三元运算符的意思是,若为空,则为切片从0开始。

            start = item.start if item.start is not None else 0

            # 下面句三元运算符的意思是,若为空,则为切片到最末端结束。

            stop = item.stop if item.stop is not None else len(self.numbers)

            return self.numbers[start:stop]

 

 

my_list = MyList(1, 2, 3, 4, 6, 5, 3)

print my_list[2:5]


上面的代码终于实现了切片功能,但是还没考虑负数呢。那么我们加一把劲再来改一下。代码如下:


class MyList(object):

    def __init__(self, *args):

        self.numbers = args

 

    def __getitem__(self, item):

        if isinstance(item, int):

            return self.numbers[item]

        elif isinstance(item, slice):

            start = item.start if item.start is not None else 0

            stop = item.stop if item.stop is not None else len(self.numbers)

 

            length = len(self.numbers)

            start = length + start + 1 if start < 0 else start

            stop = length + stop + 1 if stop < 0 else stop

            return self.numbers[start:stop]

 

my_list = MyList(1, 2, 3, 4, 6, 5, 3)

print my_list[1:-1]


哇塞,写完了,棒棒棒


__getattar__


在调用某一个对象不存在的属性或者方法的时候,会抛出一个一个AttributeError错误。


但是如果我们实现了类中的魔术方法__getattar__,那么在调用不存在的属性或者方法的时候,就会调用该魔术方法。


class Apple(object):

    def __getattr__(self, item):

        if item == "attar1":

            return "print"

        if item == "method1":

            return lambda x: "hello %s" % x

 

 

apple = Apple()

print apple.attar1

print apple.method1


__getattar__函数一个重要的适用场景就是实现链式调用。例如我们在调用某一个api的时候:


GET users/articles/index


那么我们就希望我们的代码可以实现`Api.users.articles.index这么调用。

思考一下,要实现链式调用,最重要的就是每一个调用都是返回一个实例~~。


# coding=utf-8

class Api(object):

    def __init__(self, path=''):

        self._path = path

 

    def __getattr__(self, name):

        return Api("%s/%s" % (self._path, name))

 

    # 定义一个Post方法来发送请求

    def post(self):

        print self._path

 

 

api = Api()

api.user.articles.index.post()


廖雪峰在他的教程中给我们出了一个题目:


例如调用github的api:users/:user/repos一样,中间的user名需要动态替换。


我们希望能api.users("charlie").repos这么调用。那么代码该如何实现呢?这可能需要用到另一个方法__call__


__call__ 函数


一个对象既有属性,又有方法。我们在调用一个实例的方法的时候,我们可以使用instance.method()的形式调用。


其实也可以将实例本身看成一个函数用来调用,我们需要做的就是实现__call__函数本身。


class Apple(object):

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

        return args

 

 

apple = Apple()

print apple("yes", "no")


此时我们再来看一下上面提到的实现api.users("charlie").repos链式调用的方法。


# coding=utf-8

class Api(object):

    def __init__(self, path=''):

        self._path = path

 

    def __getattr__(self, name):

        return Api("%s/%s" % (self._path, name))

 

    def __call__(self, args):

        self._path = "%s/%s" % (self._path, args)

        return Api(self._path)

 

    # 定义一个Post方法来发送请求

    def post(self):

        print self._path

 

 

api = Api()

api.users("Charlie").index.post()


觉得本文对你有帮助?请分享给更多人

关注「Python开发者」

看更多技术干货

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存