对于Python中的类而言,从最底层来看,就是命名空间。但是这个命名空间和模块不一样,类支持实例化多个对象,类支持命名空间继承,类支持运算符重载。模块的命名空间不支持这些功能。
类对象提供默认行为,是实例对象的工厂。类对象由class语句创建。
实例对象是程序实际处理的对象,每个实例对象都有自己的命名空间,并且拥有创建该实例对象的类的属性和方法。实例对象由类调用创建。
python中的类和def以及模块是类似的,这可能也是python一直以来的一致性设计导致的结果。python中的类和其它编程语言中的类是大不相同的。
当执行class语句的时候,就会得到类对象。
当调用类对象的时候,会创建实例对象。
最终的结果是,类定义了公用的属性并生成实例,实例反映了具体应用程序中的实体,并记录了自己的数据。
下面是一个类定义的简单例子,借此来再来解释一下前文的定义。
class C1: # 创建一个类对象,并赋值给C1
def setvalue(self, value): # 创建一个函数并赋值给setvalue
self.value = value
def output(self): # 创建一个函数并赋值给output
print(self.value)
定义类对象就和定义函数类似,class语句定义类的名称。之后的两个def都缩进了,成为了C1类的属性。(因为def实际上是赋值运算,而类内顶层赋值的名称都会称为类属性)习惯上,我们称类内的函数为类的方法。
接下来,定义实例对象。如下所示:
obj = C1() # 调用类,产生实例化对象
一开始obj应该是一个空的命名空间。接下来,我们使用类的属性,python就会通过继承搜索来访问类中的名称。
obj.setvalue(123) # 继承类C1的属性setvalue
obj.output() # 继承类C1的属性output
obj本身是没有setvalue以及output属性的,为了寻找这个属性,python会从obj开始,然后是该对象之上的所有类,自下至上,由左到右进行搜索,从而在C1这个类中找到setvalue和output属性。这就是Python的继承。
在C1的setvalue函数中,传入的值会被赋给self.value(python会自动传递对象obj到setvalue函数的第一个参数self),所以赋值语句会把值存储在实例的命名空间,而不是类的命名空间。
因为类可以产生多个实例,所以方法必须通过self参数才能获取当前处理的实例。另外,如果在调用setvalue之前,调用了output,那么将会触发没有属性value的错误,这是因为value属性在setvalue之前是不存在。
在类外部通过对实例对象进行赋值运算来修改实例属性,例如:
obj.value = "Hello" # 在类外部通过对实例对象进行赋值运算来修改实例属性
obj.output()
这次,我们给value赋值了一个字符串,不在是数字。当调用output的时候,会打印出Hello。甚至,我们还可以在类外给实例命名空间中添加新的属性,例如:
obj.name = 'obj'
这样,我们就向obj中添加了属性name,但是这种添加的数据类是无法使用的,通常而言是没有意义的。
在python中,实例继承自类,而类继承自父类。下面列出的是属性继承的核心观点。
class C2(C1): # C2类继承自C1
def output(self): #
print("value:",self.value)
obj = C2()
obj.setvalue(1)
obj.output()
C2继承自C1,并且覆盖了父类的output属性,完成了属于C2类的定制方法output,根据继承搜索的顺序,setvalue方法是在C1类中找到的,而output方法是在C2类中找到的。
python的运算符重载是让编写的类对象可以截获并响应在内置类型上的运算,例如:加法,切片,打印等。实际上这只是一种自动分发机制:“表达式和其他内置运算被路由到了类内部的实现”。运算符重载能让自定义的类对象拥有内置对象那样的行为,这样就可以让对象接口更为一致。
下面是运算符重载的主要概念:
运算符重载是可选的功能,不是必须的。不能因为运算符重载看起来很nice,就随意重载运算符。一条好的建议是“除非类真的需要模仿内置类型,否则不应该使用运算符重载”。
除了__init__
方法,其它的运算符重载方法必须慎重使用。
下面的例子中的C3类依旧继承自C1类。我们重载了一些运算符。
class C3(C1):
def __init__(self, value):
"""构造函数"""
self.value = value
def __add__(self, other):
"""重载+运算符"""
return C3(self.value + other.value)
def __str__(self):
"""将对象转换为字符串的时候自动调用"""
return str(self.value)
# 调用构造函数初始化obj1和obj2
obj1 = C3(258)
obj2 = C3(111)
obj3 = obj1 + obj2 # 调用__add__
obj3.output()
print(obj1) # 调用__str__
s = str(obj2) # 调用__str__
print(s)
需要注意的是,我们在实现__add__
的时候,other应该是C3的实例对象,并且返回值也是C3实例对象。这一点我们遵从了python3中不同类型无法混合运算的设计。
当然,这些特殊命名的方法和普通方法一样,都是可以手动调用的,但是在类外是不建议直接调用这些方法的。通常只是在类内调用父类的特殊命名方法的时候可以直接调用。
obj4 = obj1.__add__(obj2) # 手动调用
运算符重载在实现具有数学本质的对象的时候,可能会大量使用,而其它的类可能根本不会使用运算符重载(不包括__init__
)。
尝试好玩的语言工具是无可厚非的,但是它们并不总是能转化为产品代码。在合适的地方使用合适的工具是需要时间去积累经验的。
python的类模型是相当动态的,类和实例只是命名空间对象。它们所携带的属性是通过赋值语句动态创建的。下面定义一个空类(实际上是空的命名空间)
class C:
...
我们可以在类外通过赋值,给类增加属性。例如:
C.name = '类' # 在类外通过给变量名赋值增加名为name的类属性
def output(obj):
print(obj.name)
C.output = output # 在类外通过给变量名赋值增加名为output的方法
obj = C()
obj.output()
这就是我们反复强调的Python中的OOP其实就只是在已连接命名空间(类树)对象内寻找属性而已。
类和对象实际上是命名空间对象