定制类

    看到类似 slots 这种形如 xxx 的变量或者函数名就要注意,这些在Python中是有特殊用途的。

    slots 我们已经知道怎么用了, len() 方法我们也知道是为了能让class作用于 len() 函数。

    除此之外,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。

    str

    我们先定义一个 Student 类,打印一个实例:

    >>> class Student(object):

    … def init(self, name):

    … self.name = name



    >>> print Student('Michael')

    <main.Student object at 0x109afb190>

    打印出一堆 &lt;main.Student object at 0x109afb190&gt; ,不好看。

    怎么才能打印得好看呢?只需要定义好 str() 方法,返回一个好看的字符串就可以了:

    >>> class Student(object):

    … def init(self, name):

    … self.name = name

    … def str(self):

    … return 'Student object (name: %s)' % self.name



    >>> print Student('Michael')

    Student object (name: Michael)

    这样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。

    但是细心的朋友会发现直接敲变量不用 print ,打印出来的实例还是不好看:

    >>> s = Student('Michael')

    >>> s

    <main.Student object at 0x109afb310>

    这是因为直接显示变量调用的不是 str() ,而是 repr() ,两者的区别是 str() 返回用户看到的字符串,而 repr() 返回程序开发者看到的字符串,也就是说, repr() 是为调试服务的。

    解决办法是再定义一个 repr() 。但是通常 str() repr() 代码都是一样的,所以,有个偷懒的写法:

    class Student(object):

    def init(self, name):

    self.name = name

    def str(self):

    return 'Student object (name=%s)' % self.name

    repr = str

    iter

    如果一个类想被用于 for … in 循环,类似list或tuple那样,就必须实现一个 iter() 方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的 next() 方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

    我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:

    class Fib(object):

    def init(self):

    self.a, self.b = 0, 1 # 初始化两个计数器a,b



    def iter(self):

    return self # 实例本身就是迭代对象,故返回自己



    def next(self):

    self.a, self.b = self.b, self.a + self.b # 计算下一个值

    if self.a > 100000: # 退出循环的条件

    raise StopIteration();

    return self.a # 返回下一个值

    现在,试试把Fib实例作用于for循环:

    >>> for n in Fib():

    … print n



    1

    1

    2

    3

    5



    46368

    75025

    getitem

    Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:

    >>> Fib()[5]

    Traceback (most recent call last):

    File "<stdin>", line 1, in <module>

    TypeError: 'Fib' object does not support indexing

    要表现得像list那样按照下标取出元素,需要实现 getitem() 方法:

    class Fib(object):

    def getitem(self, n):

    a, b = 1, 1

    for x in range(n):

    a, b = b, a + b

    return a

    现在,就可以按下标访问数列的任意一项了:

    >>> f = Fib()

    >>> f[0]

    1

    >>> f[1]

    1

    >>> f[2]

    2

    >>> f[3]

    3

    >>> f[10]

    89

    >>> f[100]

    573147844013817084101

    但是list有个神奇的切片方法:

    >>> range(100)[5:10]

    [5, 6, 7, 8, 9]

    对于Fib却报错。原因是 getitem() 传入的参数可能是一个int,也可能是一个切片对象 slice ,所以要做判断:

    class Fib(object):

    def getitem(self, n):

    if isinstance(n, int):

    a, b = 1, 1

    for x in range(n):

    a, b = b, a + b

    return a

    if isinstance(n, slice):

    start = n.start

    stop = n.stop

    a, b = 1, 1

    L = []

    for x in range(stop):

    if x >= start:

    L.append(a)

    a, b = b, a + b

    return L

    现在试试Fib的切片:

    >>> f = Fib()

    >>> f[0:5]

    [1, 1, 2, 3, 5]

    >>> f[:10]

    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

    但是没有对step参数作处理:

    >>> f[:10:2]

    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

    也没有对负数作处理,所以,要正确实现一个 getitem() 还是有很多工作要做的。

    此外,如果把对象看成 dict getitem() 的参数也可能是一个可以作key的object,例如 str

    与之对应的是 setitem() 方法,把对象视作list或dict来对集合赋值。最后,还有一个 delitem() 方法,用于删除某个元素。

    总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

    getattr

    正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义 Student 类:

    class Student(object):



    def init(self):

    self.name = 'Michael'

    调用 name 属性,没问题,但是,调用不存在的 score 属性,就有问题了:

    >>> s = Student()

    >>> print s.name

    Michael

    >>> print s.score

    Traceback (most recent call last):



    AttributeError: 'Student' object has no attribute 'score'

    错误信息很清楚地告诉我们,没有找到 score 这个attribute。

    要避免这个错误,除了可以加上一个 score 属性外,Python还有另一个机制,那就是写一个 getattr() 方法,动态返回一个属性。修改如下:

    class Student(object):



    def init(self):

    self.name = 'Michael'



    def getattr(self, attr):

    if attr=='score':

    return 99

    当调用不存在的属性时,比如 score ,Python解释器会试图调用 getattr(self, 'score') 来尝试获得属性,这样,我们就有机会返回 score 的值:

    >>> s = Student()

    >>> s.name

    'Michael'

    >>> s.score

    99

    返回函数也是完全可以的:

    class Student(object):



    def getattr(self, attr):

    if attr=='age':

    return lambda: 25

    只是调用方式要变为:

    >>> s.age()

    25

    注意,只有在没有找到属性的情况下,才调用 getattr ,已有的属性,比如 name ,不会在 getattr 中查找。

    此外,注意到任意调用如 s.abc 都会返回 None ,这是因为我们定义的 getattr 默认返回就是 None 。要让class只响应特定的几个属性,我们就要按照约定,抛出 AttributeError 的错误:

    class Student(object):



    def getattr(self, attr):

    if attr=='age':

    return lambda: 25

    raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

    这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。

    这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。

    举个例子:

    现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:

    http://api.server/user/friends

    http://api.server/user/timeline/list

    如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。

    利用完全动态的 getattr ,我们可以写出一个链式调用:

    class Chain(object):



    def init(self, path=''):

    self.path = path



    def getattr(self, path):

    return Chain('%s/%s' % (self.path, path))



    def __str
    (self):

    return self._path

    试试:

    >>> Chain().status.user.timeline.list

    'statususer/timeline/list'

    这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!

    还有些REST API会把参数放到URL中,比如GitHub的API:

    GET users:user/repos

    调用时,需要把 :user 替换为实际用户名。如果我们能写出这样的链式调用:

    Chain().users('michael').repos

    就可以非常方便地调用API了。有兴趣的童鞋可以试试写出来。

    call

    一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用 instance.method() 来调用。能不能直接在实例本身上调用呢?类似 instance() ?在Python中,答案是肯定的。

    任何类,只需要定义一个 call() 方法,就可以直接对实例进行调用。请看示例:

    class Student(object):

    def init(self, name):

    self.name = name



    def call(self):

    print('My name is %s.' % self.name)

    调用方式如下:

    >>> s = Student('Michael')

    >>> s()

    My name is Michael.

    call() 还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。

    如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。

    那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个 Callable 对象,比如函数和我们上面定义的带有 call() 的类实例:

    >>> callable(Student())

    True

    >>> callable(max)

    True

    >>> callable([1, 2, 3])

    False

    >>> callable(None)

    False

    >>> callable('string')

    False

    通过 callable() 函数,我们就可以判断一个对象是否是“可调用”对象。

    小结

    Python的class允许定义许多定制方法,可以让我们非常方便地生成特定的类。

    本节介绍的是最常用的几个定制方法,还有很多可定制的方法,请参考 Python的官方文档