Python 高级特性之定制类

Par @Martin dans le
Tags :

在这之前, 请确保所有的类都继承于 object !!!
在这之前, 请确保所有的类都继承于 object !!!
在这之前, 请确保所有的类都继承于 object !!!

__slots__ (限制动态属性)

作为一种动态语言, python 支持 在类实例化后, 为实例动态添加属性/方法的功能.

class Student(object):
    pass

s = Student()
s.name = 'Michael' # 动态给实例绑定一个属性
print s.name

然而在某些情况下, 我们可能并不希望这样用, 此时, 可以通过类的 __slots__ 变量, 来限制上面说到的功能.

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称



s = Student() # 创建新的实例
s.name = 'Michael' # 绑定属性'name'
s.age = 25 # 绑定属性 'age'

s.score = 99 # 绑定未在 **slots** 中指定的属性 'score'


当我们运行 s.score = 99 时, 就会出错:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

由于 ‘score’ 没有被放到 __slots__ 中, 所以不能绑定 score 属性, 试图绑定 score 将得到 AttributeError 的错误

使用 __slots__ 要注意, __slots__ 定义的属性仅对当前类起作用, 对继承的子类是不起作用的, 除非在子类中也定义 __slots__, 这样, 子类允许定义的属性就是自身的 __slots__ 加上父类的 __slots__(注意, 是加上! 加上! 加上! 重要的事说三遍).

@property (神奇的 getter/setter)

在 Java 中, 有一种 JavaBean 规范, 即所有属性对外部那是 private 的, 想要访问/设置, 必须通过 getter 和 setter 方法.

这在 Python 中有更好的实现, 那就是 property, 它是一个装饰器, 负责把一个方法变成属性调用.

需要注意一点的是, 属性要定义成 private 或 protected (如 _score 或 __score).

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value


把一个 getter 方法变成属性, 只需要加上 @property 就可以了, 此时, @property 本身又创建了另一个装饰器 @xxx.setter, 负责把一个 setter 方法变成属性赋值, 于是, 我们就拥有一个可控的属性操作:

>>> s = Student()
>>> s.score = 60 # OK, 实际转化为 s.set_score(60)
>>> s.score # OK, 实际转化为 s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!


不仅如此, 还可以定义只读属性, 只定义 getter 方法, 不定义 setter 方法就是一个只读属性:

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2014 - self._birth

上面的 birth 是可读写属性, 而 age 就是一个只读属性.

__str__ 与 __repr__ (打印类)

当我们需要打指定 print 类或者直接输入类时的输出内容, 就可以定制 __str____repr__, 直接显示变量调用的是 __str__, print 时调用的是是 __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 # 返回下一个值


使用结果如下:

>>> for n in Fib():
...     print n
...
1
1
2
3
5
...
46368
75025

__call__ (实例自调用)

当调用一个类的实例方法时, 可以用 xxx.method() 来调用, 那能不能直接在实例本身上调用呢, 像 xxx() 这样?

任何类, 只需要定义一个 __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() 函数就可以了.

>>> callable(Student())
True

__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__ 中查找.

此外, 注意, 如果我们访问一个类中没有定义, 又没有在 __getattr__ 中定义的属性, 就会返回 None, 这是因为我们定义的 __getattr__ 默认返回就是 None, 所以按照约定, 应该 AttributeError 的错误:

class Student(object):

    def __getattr__(self, attr):
        if attr == 'age':
            return lambda: 25
        raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

问题?
我们完成可以为类直接定义新属性, 使用 __getattr__ 方法到底有什么用处呢?

举个例子:
(参考: 廖雪峰老师教程)

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

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


如果要调用这些 API, 那不是要给每个 URL 对应的 API 都写一个方法? 这还不得累死, 而且, API 一旦改动, 我们写的方法也要改…

这里就可以利用完全动态的 __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
'/status/user/timeline/list'


这样, 无论 API 有多少, 我们都可以通过上面这种链式调用的方法去访问!

__getitem__、__setitem__、__delitem__

通过这三个方法, 可以将自己定义的类表现得和内置的 list、tuple、dict 没什么区别, 但这么做的话, 还有很多工作要做, 以后需要用到的时候再来补充吧.