• 使用__slots__

为了达到限制属性添加的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性,如:

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
s = Student() # 创建新的实例
s.name = 'Michael' # 绑定属性'name'
s.age = 25 # 绑定属性'age'
s.score = 99 # 绑定属性'score'
'''Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score''''

实例可以绑定__slots__中的属性,如上例中的name、age;但不能绑定__slots__中没有的属性,不然将得到AttributeError的错误,如上例中的score。

使用__slots__要注意的是,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

  • 使用@property

为了避免类中的属性直接暴露和赋值检查,一般可采用将属性设置为private,然后定义getter和setter函数来获取和设置属性值的方法。@property是一种装饰器,负责把一个方法变成属性调用,简化getter和setter的调用。

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

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

以上就是一个使用@property的例子。把一个getter方法变成属性,只需要加上@property就可以了,把一个setter方法变成属性赋值则加上@score.setter。若只定义了getter方法而没有定义setter方法,那么这个属性就变成了一个只读属性。

  • 关于MixIn

在设计类的继承关系时,通常,主线都是单一继承下来的,如果需要“混入”额外的功能,通过多重继承就可以实现,这种方式通常称之为MixIn。为了更好地看出继承关系,多重继承的“功能”父类通常以“xxxMixIn”的形式命名。

  • 定制类

前面说到类中有很多形似__xxx__的函数,这种函数一般都有着特殊的用途,可以帮助我们定制类。如上面说到的__slots__可以限制实例能够绑定的属性,以下简单介绍几种特殊函数:

__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)

__repr__:定制变量调用时实例打印类容,如:

class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__

s=Student('Michael')
print(s)  #Student object (name: Michael)

__iter__:使类可用于for…in循环,此时需要同时定义__next__,使用户可以获取循环的下一个值:

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
...'''

__getitem__:使类可以像list一样按下标取出元素,如:

class Fib(object):
    def __getitem__(self, n):
        if isinstance(n, int): # n是索引
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a
        if isinstance(n, slice): # n是切片
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L

f=Fib()
print(f[1])  #1
print(f[10])  #89
print(f[0:5])  #[1,1,2,3,5]

以上实现了通过索引和切片方式取值,但是没有对切片的step参数做处理,也没有对切片参数为负数做处理,所以要正确实现一个__getitem__还有很多事情要做,以上仅为简单介绍。

此外,如果把对象看成dict,__getitem__的参数也可能是一个可以作key的object,例如str。与之对应的是__setitem__方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__方法,用于删除某个元素。

__getattr__:定制属性返回值,如:

class Student(object):

    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):
        if attr=='score':
            return 99

s=Student()
print(s.name)  #'Michael'
print(s.score)  #99

只有在没有找到属性的情况下,Python解释器才调用__getattr__,如上述例子,类中没有“score”属性,因此调用__getattr__返回99;对于已有的属性,比如上述例子中的“name”,不会在__getattr__中查找。另外__getattr__也可以返回函数。若定义了__getattr__,那么定义中没有写出的属性将默认返回None,要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误。

__call__:使实例可直接被调用,如:

class Student(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('My name is %s.' % self.name)

s=Student('Michael')
print(s)  #My name is Michael.

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

如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__的类实例。

Python的class允许定义许多定制方法,可以让我们非常方便地生成特定的类。还有很多可定制的方法,请参考Python的官方文档

  • 枚举类

当我们需要定义常量时,一个办法是用大写变量通过整数来定义,这样做的好处是简单,缺点是仍然是变量。更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能,如定义月份常量:

from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from enum import Enum, unique

@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

其中@unique装饰器可以帮助我们检查保证没有重复值。

  • 元类

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:

def fn(self, name='world'): # 先定义函数
    print('Hello, %s.' % name)

Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
h = Hello()
print(h.hello())  #Hello, world.
print(type(Hello))  #<class 'type'>
print(type(h))  #<class '__main__.Hello'>

要创建一个class对象,type()函数依次传入3个参数:

  1. class的名称;
  2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  3. class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同。

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass

metaclass,直译为元类,简单的解释就是:

当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。连接起来就是:先定义metaclass,就可以创建类,最后创建实例。所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,很少会碰到需要使用metaclass的情况,metaclass我目前还不太懂,先记下来。