一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第16天,点击查看活动详情。
面向对象编程的基本概念
一个完善的程序是由数据和指令组成的。过程式编程利用“分而治之”的思想,使用函数对数据进行处理,数据与函数之前的关系是松散的,即同样的数据可以被程序中的所有函数访问,而一个函数也可以访问程序中的不同数据。这导致了,如果出现异常,需要在整个系统中查找错误代码。
为了解决这一问题,面向对象编程 (Object Oriented Programming, OOP) 将系统划分为不同对象,每个对象包含自身的信息数据以及操作这些数据的方法。例如,每个字符串对象具有字符数据,同时还具有改变大小写、查找等方法。
面向对象编程使用类描述其所包含的所有对象的共同特性(属性),即数据属性(也称数据成员或成员变量)和功能属性(也称成员函数或方法)。
一个类的对象也称为这个类的一个实例。例如,32 就是一个整数类 int 的对象,可以使用函数 type() 来获取对象所属类:
>>> type(32)
<class 'int'>
可以利用内置函数 dir() 查询类的属性:
>>> dir(32)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
在继续讲解之前,我们首先来快速介绍下面向对象的三大特性——多态、封装和继承。
多态
多态:可对不同类型的对象执行相同的操作。
多态其实很常见,例如列表对象和元组对象都具有 count 方法,使用变量调用 count 方法时,我们无需知道它究竟是列表还是元组,这就是多态的作用。这不仅仅适用于方法,许多内置运算符和函数也使用了多态:
>>> [1,3,3,3].count(3)
3
>>> (1,3,3,3).count(3)
3
>>> [1,3]+[1,3]
[1, 3, 1, 3]
>>> 'hello' + ' world!'
'hello world!'
封装
封装:对外部隐藏有关对象具体操作的细节。
封装与多态类似,都属于抽象原则,都用于处理程序的组成部分而无需关心不必要的细节,但不同的是,多态使我们无需知道对象所属的类就能调用其方法,而封装使我们无需知道对象的内部构造就能使用它。例如我们将虚数的实部和虚部作为对象的数据属性,就是将对象的属性“封装”在对象中。
继承
继承:用于建立类的层次结构,基于上层的类创建出新类。
如果我们有了一些类,再创建新的类时发现与已存在的类十分相似,只需要添加一些新方法,那么我们可能不想复制旧类的代码至新类中,这时我们就要用到继承了。例如,我们有了一个 Fruit 类,具有描述外观的方法 show_shape,如果想要新建一个 Apple 类,除了描述外观外,我们还想知道如何计算总价,那么我们就可以让 Apple 类继承 Fruit 的方法,使得对 Apple 对象调用方法 show_shape 时,将自动调用 Fruit 类的这个方法。
自定义类
我们已经知道抽象数据类型就是一个由对象以及对象上的操作组成的集合,对象和操作被捆绑为一个整体,不但可以使用对象的数据属性,还可以使用对象上的操作。操作(在类中称为方法)定义了抽象数据类型和程序其他部分之间的接口。接口定义了操作要做什么,但没有说明如何做,因此我们可以说抽象的根本目标是隐藏操作的细节。而类就是为了实现数据抽象类型。
Python 用关键字 class 定义一个类,格式如下,其中方法定义与函数定义语法类似:
class 类名:
方法定义
接下来构建实现抽象数据类型 Imaginary (虚数)的类,用于展示如何实现自定义类。
定义类时首先需要提供构造方法,构造方法定义了数据对象的创建方式。要创建一个 Imaginary 对象,需要提供实部和虚部两部分数据,Python 中,__init__() 作为构造方法名:
class Imaginary:
def __init__(self, real, imag):
self.real = real
self.imag = imag
形式参数列表的第一项是一个指向对象本身的特殊参数(习惯上通常使用 self ),在调用时不需要提供相应的实际参数,而构造方法中的剩余参数必须提供相应的实参,使得新创建的对象能够知道其初始值,与函数定义一样,可以通过默认值为形参提供默认实参。如在 Imaginary 类中,self.real 定义了 Imaginary 对象有一个名为 real 的内部数据对象作为其实部数据属性,而self.imag 则定义了虚部。
创建 Imaginary 类的实例时,会调用类中定义的构造方法,使用类名并且传入数据属性的实际值完成调用:
>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a
<__main__.Imaginary object at 0x0000020CF1B80160>
以上代码创建了一个对象,名为 imaginary_a,值为 6+6i,这就是封装的示例,将数据属性和操作数据属性的方法打包在对象中。
除了实例化外,类还支持另一操作:属性引用(包括数据属性和功能属性),通过点标记法访问与类关联的属性:
>>> imaginary_a.real
6
>>> imaginary_a.imag
6
除了数据属性外,还需要实现抽象数据类型所需要的方法(功能属性),需要牢记的是,方法的第一个参数 self 是必不可少的,例如要实现打印实例化的虚数对象,编写类方法 display():
class Imaginary:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def display(self):
print('{}{:+}i'.format(self.real, self.imag))
调用类方法打印实例化的虚数对象:
>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a.display()
6+6i
>>> print(imaginary_a)
<__main__.Imaginary object at 0x0000020CF1B72D90>
可以看到,如果使用 print() 函数只能打印存储在变量中的地址,这是由于将对象转换成字符串的方法 __str__() 的默认实现是返回实例的地址字符串,如果想要使用 print 函数打印对象,需要重写默认的 __str__() 方法,或者说重载该方法:
class Imaginary:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def display(self):
print('{}{:+}i'.format(self.real, self.imag))
def __str__(self):
print('{}{:+}i'.format(self.real, self.imag))
此时如果再次使用 print() 函数,就可以直接打印对象了:
>>> imaginary_a = Imaginary(6, 6)
>>> print(imaginary_a)
6+6i
可以重载类中的很多方法,最常见的是重载运算符,这是由于人们习惯使用熟悉的运算符对数据进行运算,这要比使用函数对数据进行运算更加直观且易于理解,如表达式:8 + 6 / 3,如果用函数则为:add(8, div(6, 3)),显然前者比后者更加符合习惯。 如果某种类型的对象要使用常见运算符,就必须对这种类型重新定义相应的运算符函数,例如,Python 对于 int 整型、float 浮点型、str 字符串类型等都重新定义了乘法运算符函数,对一个类型重新定义运算符函数的也称“运算符重载”。我们可以编写 Imaginary 类的 __mul__() 方法重载乘法运算:
class Imaginary:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def display(self):
print('{}{:+}i'.format(self.real, self.imag))
def __str__(self):
print('{}{:+}i'.format(self.real, self.imag))
def __mul__(self, other):
new_real = self.real * other.real - self.imag * other.imag
new_imag = self.real * other.imag + self.imag * other.real
return Imaginary(new_real, new_imag)
还可以重载其他运算符,如比较运算符 ==,即 __eq__() 方法,重载 Imaginary 类的 __eq__() 方法允许两个虚数进行比较,查看它们的值是否相等,这也称为深相等;而根据引用进行判断的浅相等,只有两个变量是同一个对象的引用时才相等:
# shallow_and_deep_equal.py
class ImaginaryFirst:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def display(self):
print('{}{:+}i'.format(self.real, self.imag))
def __str__(self):
print('{}{:+}i'.format(self.real, self.imag))
class Imaginary:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def display(self):
print('{}{:+}i'.format(self.real, self.imag))
def __str__(self):
print('{}{:+}i'.format(self.real, self.imag))
def __eq__(self, other):
return self.real == other.real and self.imag == self.imag
print('浅相等:只有两个变量是同一个对象的引用时才相等。')
imag_1 = imag_2 = ImaginaryFirst(6, 6)
print('imag_1 == imag_2 ', imag_1 == imag_2)
imag_1 = ImaginaryFirst(6, 6)
imag_2 = ImaginaryFirst(6, 6)
print('imag_1 == imag_2 ', imag_1 == imag_2)
print('深相等:两个变量的值相等即表示对象相等。')
imag_1 = imag_2 = Imaginary(6, 6)
print('imag_1 == imag_2 ', imag_1 == imag_2)
imag_1 = Imaginary(6, 6)
imag_2 = Imaginary(6, 6)
print('imag_1 == imag_2 ', imag_1 == imag_2)
程序运行结果如下所示:
浅相等:只有两个变量是同一个对象的引用时才相等。
imag_1 == imag_2 True
imag_1 == imag_2 False
深相等:两个变量的值相等即表示对象相等。
imag_1 == imag_2 True
imag_1 == imag_2 True
还有其他一些常见的运算符对应的运算符方法,如下表所示,在定义类时,可以根据需要重载这些运算符方法。
| 运算符 | 运算符方法 | 含义 |
|---|---|---|
| + | __add__(self, other) | 加法 |
| - | __sub__(self, other) | 减法 |
| * | __mul__(self, other) | 乘法 |
| / | __truediv__(self, other) | 浮点除法 |
| // | __floordiv__(self, other) | 取整除法 |
| % | __mod__(self, other) | 求余 |
| ** | __pow__(self, other) | 幂运算 |
| < | __lt__(self, other) | 小于 |
| <= | __le__(self, other) | 小于等于 |
| __gt__(self, other) | 大于 | |
| >= | __ge__(self, other) | 大于等于 |
| == | __eq__(self, other) | 相等 |
| != | __ne__(self, other) | 不相等 |
| & | __and__(self, other) | 按位与 |
| | | __or__(self, other) | 按位或 |
| __xor__(self, other) | 按位异或 | |
| ~ | __invert__(self) | 按位取反 |
| << | __lshift__(self, other) | 左移 |
| >> | __rshift__(self, other) | 右移 |
| [] | __getitem__(self, index) | 下标运算 |
| in | __contains__(self, value) | 成员运算 |