类,和模块的三个不同之处:类支持多个对象产生、命名空间继承以及运算符重载
类产生多个实例对象
类对象提供默认行为,是实例对象的工厂。实例对象是程序处理的实际对象:各自都有独立的命名空间。类对象来自于语句,而实例来自于调用。
类对象提供默认行为
Python类主要特性的要点:
class语句创建类对象并将其赋值给变量名。
class语句内的赋值语句会创建类的属性。
类属性提供对象的状态和行为
实例对象是具体的元素
像函数那样调用类对象会创建新的实例对象。每次类调用时,都会建立并返回新的实例对象。实例代表了程序领域中的具体元素。
每个实例对象继承类的属性并获得了自己的命名空间。由类所创建的实例对象是新的命名空间。一开始是空的,但是会继承创建该实例的类对象内的属性。
在方法内对self属性做赋值运算会产生每个实例自己的属性。在类方法函数内,第一个参数(按惯例称为self)会引用正处理的实例对象。对self的属性做赋值运算,会创建或修改实例内的数据,而不是类的数据。
第一个例子
>>> class FirstClass:
def setdata(self, value):
self.data = value
def display(self):
print(self.data)
位于类中的函数通常称为方法。方法是普通的def。在方法函数中,调用时,第一个参数自动接收隐含的实例对象:调用的主体。
建立实例:
>>> x = FirstClass()
>>> y = FirstClass()
上述产生两个实例和一个类,结构如下:
如果对实例以及类对象内的属性名称进行点号运算,Python会通过继承搜索从类取得变量名(除非该变量名位于实例内):
>>> x.setdata("King Arthur")
>>> y.setdata(3.14159)
x或y本身都没有setdata属性,为了寻找这个属性,Python会顺着实例到类的连接搜索。而这就是所谓的Python的继承:继承是在属性点号运算时发生的,而且只与查找连接对象的变量名有关。
>>> x.display()
King Arthur
>>> y.display()
3.14159
在类内时,通过方法内对self进行赋值运算;而在类外时,则可以通过对实例对象进行赋值运算:
>>> x.data = "new value"
>>> x.display()
new value
>>>
类通过继承进行定制
Python可以让类继承其他类,因而开启了编写类层次结构的大门,在阶层较低的地方覆盖现有的属性,让行为特定化。在这里和模块不一致:模块的属性存在于一个单一、平坦的命名空间之内(这个命名空间不接受定制化)。
在Python中,实例从类中继承,而类继承于超类。以下是属性继承机制的核心观点:
超类列在了类开头的括号中。要继承另一个类的属性,把该类列在class语句开头的括号中就可以了。含有继承的类称为子类,而子类所继承的类就是其超类。
类从其超类中继承属性。就像实例继承其类中所定义的属性名一样,类也会继承其超类中定义的所有属性名称。当读取属性时,如果它不存在于子类中,Python会自动搜索这个属性。
实例会继承所有可读取类的属性。每个实例会从创建它的类中获取变量名,此外,还有该类的超类。寻找变量名时,Python会检查实例,然后是它的类,最后是所有的超类。
每个object.attribute都会开启新的独立搜索。包括在class语句外对实例和类的引用(例如X.attr),以及在类方法函数内对self实例参数属性的引用。
逻辑的修改是通过创建子类,而不是修改超类。在树种层次较低的子类中重新定义超类的变量名,子类就可取代并制定所继承的行为。
第二个例子
>>> class SecondClass(FirstClass):
def display(self):
print('Current value = "%s" ' % self.data)
定义一个新的类SecondClass,继承FirstClass所有变量名,并提供其自己的一个变量名。
由于继承搜索会从实例往上进行,之后到子类,然后到超类,直到所找的属性名称首次出现为止。这样,我们SecondClass覆盖了FirstClass中的display。我们把这种在树种较低处发生的重新定义的、取代属性的动作称为重载。
另外,SecondClass(以及其任何实例)依然会继承FirstClass的setclass方法:
>>> z = SecondClass()
>>> z.setdata(42)
>>> z.display()
Current value = "42"
实例模型如下:
类是模块内的属性
当class语句执行时,这只是赋值给对象的变量,而对象可以用任何普通的表达式引用。
Python中的通用惯例指出,类名应该以一个大写字母开头,以使得它们更为清晰:
例如person.py下有Person类
import person
x = person.Person()
此外,虽然类和模块都是附加属性的命名空间,它们是非常不同的源代码结构:模块反应了整个文件,而类只是文件内的语句。
类可以截获Python运算符
运算符重载:就是让用类写成的对象,可截获并响应用在内置类型上的运算:加法、切片、打印和点号运算等。
以下是重载运算符主要概念的概要:
以双下划线命名的方法(__X__)是特殊钩子。Python运算符重载的实现是提供特殊命名的方式来拦截运算。
当实例出现在内置运算时,这类方法会自动调用。例如,如果实例对象继承了__add__方法,当对象出现在+表达式内时,该方法就会调用。该方法的返回值会变成相应表达式的结果。
类可覆盖多数内置类型运算。
运算符覆盖方法没有默认值,而且也不需要。如果没有定义或继承运算符重载方法。就是说相应的运算在类实例中并不支持。例如,如果没有__add__,+表达式就会引发异常。
运算符可让类与Python的对象模型相集成。重载类型运算时,以类实现的用户定义对象的行为就会像内置对象一样,因此,提供了一致性,以及与预期接口的兼容性。
第三个例子
>>> class ThridClass(SecondClass):
def __init__(self, value):
self.data = value
def __add__(self, other):
return ThridClass(self.data + other)
def __str__(self):
return '[ThridClass: %s]' % self.data
def mul(self, other):
self.data *= other
函数调用:
>>> a = ThridClass('abc') # __init__ called
>>> a.display()
Current value = "abc"
>>> print(a) # __str__: return display
[ThridClass: abc]
>>>
>>> b = a+'xyz' # __add__:makes a new instance
>>> b.display() # b has all ThridClass methods
Current value = "abcxyz"
>>> print(b)
[ThridClass: abcxyz]
>>>
>>> a.mul(3) # mul: changes instance in-place
>>> print(a)
[ThridClass: abcabcabc]
>>>
从上述可以看出:
ThridClass生成的调用现在会传递一个参数(例如上述“abc”),这是传给__init__构造函数内的参数value的,并将其赋值给self.data。直接效果是,ThridClass计划在构建时自动设置data属性,而不是在构建之后请求setdata调用。
ThridClass对象现在可以出现在+表达式和print调用中。对于+,Python把左侧的实例对象传给__add__中的self参数,而把右边的对象传给other,__add__返回的内容成为+表达式的结果。对于print,Python把要打印的对象传递给__str__中的self;该方法返回的字符串看做是对象的打印字符串。使用__str__,我们可以用一个常规的print来显示该类的对象,而不是调用特殊的display方法。
运算符重载方法的名称并不是内置变量或保留字,只是当对象出现在不同的环境时Python会去搜索的属性。Python通常会自动进行调用,但偶尔也能由程序代码调用。
世界上最简单的类
>>> class rec:pass
建立这个类后,就可以完全在最初的class语句外,通过赋值变量名给这个类增加属性:
>>> rec.name = 'Bob'
>>> rec.age = 40
通过赋值语句创建该属性后,就可以使用一般的语法将它们取出。
>>> print(rec.name)
Bob
注意:其实该类还没有实例,也能这样用。类本身也是对象,也是没有实例。事实上,类只是独立完备的命名空间,只要有类的引用值,就可以在任何时刻设定或修改其属性。
建立两个实例:
>>> x = rec()
>>> y = rec()
>>> x.name,y.name
('Bob', 'Bob')
其实,这些实例本身没有属性,它们只是从类对象那里取出name属性。
事实上,命名空间对象的属性通常都是以字典的形式实现的,而类继承树(一般而言)只是连接至其他字典的字典而已。
>>> rec.__dict__.keys()
dict_keys(['__module__', '__dict__', '__weakref__', '__doc__', 'name', 'age'])
在这里,类的字典显示出我们进行赋值了的name和age属性。
Python的类模型相当动态。类和实例只是命名空间对象,属性是通过赋值语句动态建立。即使是方法也可以完全独立地在任意类对象的外部创建。
比如我们在任意类之外定义一个简单的函数:
>>> def upperName(self):
return self.name.upper()
>>> upperName(x)
'BOB'
>>>
如果我们把这个简单函数的赋值成类的属性,就会变成方法,可以由任何实例调用:
>>> rec.method = upperName
>>> x.method()
'BOB'
>>>
在通常情况下,类是由class语句填充的,而实例的属性则是通过在方法函数内对self属性进行赋值运算而创建的。不过,重点在于并不是如此。Python中的OOP其实就是在已连接命名空间对象内寻找属性而已。