Python 面向对象编程
根据教材 《数据结构与算法 Python 实现》 整理,代码和笔记可前往我的 Github 库下载 isKage/dsa-notes 。【建议 star !】
1 类定义
类是面向对象程序设计中抽象的主要方法。下面以创建 CreditCard
类作为例子讲解面向对象如何定义类。
1.1 例:CreditCard 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class CreditCard : """有关一个用户的信用卡""" def __init__ (self, customer, bank, acnt, limit ): """初始化一个信用卡实例 Args: customer (str): 用户名 bank (str): 银行名 acnt (str): 用户账户ID limit (float): 信用卡限额 """ self ._customer = customer self ._bank = bank self ._acnt = acnt self ._limit = limit self ._balance = 0. def get_customer (self ): """返回用户名""" return self ._customer def get_bank (self ): """返回银行名""" return self ._bank def get_acnt (self ): """返回账户ID""" return self ._acnt def get_limit (self ): """返回额度""" return self ._limit def charge (self, price ): """ 返回是否能继续提款,即检查是否超出额度 :param price: 希望提取的额度 :return: True 如果加上目前所用额度没有超出限度,否则 False """ if self ._balance + price > self ._limit: return False else : self ._balance += price return True def make_payment (self, amount ): """用现金抵消了部分信用卡贷款""" self ._balance -= amount
1.1.2 self
标识符
self
代表了一个实例,可以理解为对象自己。同时,self
也确定了调用方法时作用的对象。例如 obj.get_customer()
表示对实例化后的对象 obj
调用方法 get_customer()
。
1.1.3 __init__()
方法
在面向对象编程中,我们称位于类定义 class
之中的各种函数 def
为方法。而 __init__()
称为初始化方法,它是当实例化对象时首先被执行的。在这个例子中,参数除了对象自己 self
,也包含了 customer, bank, acnt, limit
这些都是初始化时需要传入的。
1.1.4 其他方法
类似于初始化方法,其他的函数也是接受参数,返回结果。例如:charge()
方法,使用 obj.charge(100)
表示对于对象 obj
,传入参数 price = 100
,然后返回一个结果。
1.1.5 _
变量名
在数据成员名称中的前加下划线,比如 _balance
,表明它被设计为非公有的(nonpublic)。类的用户不应该直接访问这样的成员。可以提供类似于 get_balance
的访问函数,以提供拥有只读访问特性的类的用户。
1.2 实例化
1 cc = CreditCard(customer="John Doe" , bank="Nation Bank" , acnt="123 456 789" , limit=1000. )
使用 cc = CreditCard()
并传入初始化参数的方式实例化对象。此时就可以对对象 cc
进行操作,调用方法。
1.3 错误检查
CreditCard
类的实现方法不够稳健:
例如:没有明确地检查参数的类型,如果用户创建了一个类似于 visa.charge('candy)
的调用,代码可能会崩溃。所以应该设计一些抛出异常。
例如:逻辑错误的。如果允许用户收取一个类似于 visa.charge(-300)
的负价格,这将导致用户的余额变少,这是不和逻辑的
所以,这个例子只是介绍面向对象编程时,定义类、实例化和调用方法的基础知识。之后需要不断完善。
1.4 对类进行测试
一般对类的定义会单独放在一个 .py
文件中,那么我们需要对类进行检查,测试其是否合理,是否报错。但是,如果通过 from ... import ...
的方法导入主程序测试显然不是我们希望的。所以,我们可以在定义类的 .py
文件下使用 if __name__ = '__main__':
的方法。
if __name__ = '__main__':
:这个判断表示,只有当当前文件以主程序的方式运行时,if
语句后面的内容才运行。这样可以避免在 from ... import ...
代入类时,连带运行 if
语句后的测试语句。
例如:在当前目录下创建文件夹 utils
内部放置文件 cc.py
,在 cc.py
书写类的定义,于是可以代入类
1 2 3 from utils.cc import CreditCardcc_new = CreditCard("John" , "Bank1" , "123 456 789" , 3010. )
而在文件 ./utils/cc.py
中就可以书写 if __name__ = '__main__':
的测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class CreditCard : ...if __name__ == '__main__' : wallet = [] wallet.append(CreditCard("John" , "Bank1" , "123 456 789" , 3010. )) wallet.append(CreditCard("Mike" , "Bank2" , "456 123 789" , 6210. )) wallet.append(CreditCard("Ann" , "Bank3" , "789 456 123" , 4010. )) print (wallet[0 ].charge(1000. )) print (wallet[1 ].charge(2000. )) print (wallet[2 ].charge(5000. )) for account in wallet: print ("Customer: {}" .format (account.get_customer())) print ("Balance: {}" .format (account.get_balance())) if account.get_balance() > 100. : account.make_payment(100. ) print ("New Balance: {}" .format (account.get_balance()))
2 运算符重载
2.1 介绍
Python 的内置类为许多操作提供了自然的语义。比如,a + b
可以是数值相加,也可以是字符串相连。
默认情况下,对于新的类来说,+
操作符是未定义的,可通过操作符重载 来定义它。例如:在类的定义里定义方法 __add__()
即可定义 +
的含义。
以 a + b
为例,其中 a
和 b
均为字符串,则 a + b
能成立是因为 Python 内置类 字符串
定义了方法 __add__()
将其表达为字符串相连,于是 a + b
才会正常使用。类似地,自定义类时也可以去重载这些运算符,下面是常见的重载:
2.2 例:多维向量类
下面通过定义向量,来解释如何自定义重载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class Vector : """多维向量""" def __init__ (self, d ): """d 维度向量""" self ._coords = [0 ] * d def __len__ (self ): """获取维度: 重载 len(a)""" return len (self ._coords) def __getitem__ (self, k ): """返回第 k 个维度的值: 重载 a[k]""" return self ._coords[k] def __setitem__ (self, k, v ): """设置第 k 个维度的值为 v: 重载 a[k] = v""" self ._coords[k] = v def __add__ (self, other ): """定义向量加法: 重载 a + b""" if len (self ) != len (other): raise ValueError("Dimensions must be the same!" ) result = Vector(len (self )) for i in range (len (self )): result[i] = self [i] + other[i] return result def __sub__ (self, other ): """定义向量减法: 重载 a - b""" if len (self ) != len (other): raise ValueError("Dimensions must be the same!" ) result = Vector(len (self )) for i in range (len (self )): result[i] = self [i] - other[i] return result def __eq__ (self, other ): """判断向量坐标是否相等: 重载 a == b""" return self ._coords == other._coords def __ne__ (self, other ): """判断向量坐标是否不相等: 重载 a != b""" return not self == other def __str__ (self ): """以字符串的形式展现这个向量类: 重载 str(a) 或 a 可以以字符串形式展示""" return '<' + str (self ._coords)[1 :-1 ] + '>'
以上的方法各个方法都重载了一些运算符,具体解释见代码注释。
2.3 补:自定义 Python 的包
在上面引入类时,我们使用了 from utils.cc import CreditCard
,这并不规范。我们可以将类定义写入一个 python 文件,然后统一放在文件夹 utils 中,并且再写一个 __init__.py
文件将 utils 文件夹整个变成一个标准的 python 包,目录结构见下:
1 2 3 4 ./utils ├── __init__.py ├── cc.py └── vector.py
然后在 __init__.py
文件中写入:
1 2 3 from .cc import CreditCardfrom .vector import Vector
如此便可在其他文件中直接引用
1 from utils import CreditCard, Vector
3 迭代器
3.1 介绍
集合迭代器(iterator)提供了一个关键功能:它支持一个名为 __next__
的特殊方法,如果集合有下一个元素,该方法返回该元素,否则产生一个 StopIteration
异常来表明没有下一个元素。
Python 为实现了 __len__
和 __getitem__
方法的的类提供了一个自动的迭代器,每次调用 __next__
方法时便会索引递增。例如,下面的 SquenceIterator
类。
3.2 例:SequenceIterator 类
【注意】迭代器的实现必须要求【已经实现了 __len__
和 __getitem__
方法】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class SequenceIterator : """为已经定义了__len__和__getitem__的对象实现迭代器方法""" def __init__ (self, seq ): """迭代器初始化""" self ._seq = seq self ._k = -1 def __next__ (self ): """迭代器""" self ._k += 1 if self ._k < len (self ._seq): return self ._seq[self ._k] else : raise StopIteration() def __iter__ (self ): """一般__iter__方法都要返回自己,一种书写规范""" return self
3.3 练习:实现一个类模拟 Python 的 Range
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Range : """模拟Python的Range类: range(start, stop, step)""" def __init__ (self, start, stop=None , step=1 ): """ 初始化 :param self: 对象自身 :param start: 起始数字 :param stop: 终止数字 :param step: 每步跨度 :return: Range类 """ if step == 0 : raise ValueError("step can not be zero" ) if stop is None : start = 0 stop = start self ._length = max (0 , (stop - start + step - 1 ) // step) self ._start = start self ._step = step def __len__ (self ): """长度""" return self ._length def __getitem__ (self, index ): """取数值""" if index < 0 : index += self ._length if index >= self ._length or index < 0 : raise IndexError("index out of range" ) return self ._start + index * self ._step def __str__ (self ): """只是以列表的形式展示,不重要。因为Range是模仿Python的range功能""" result = "" for i in Range(self ._start, self ._length): result += str (self ._start + i * self ._step) + ", " result = "[" + result[:-2 ] + "]" return result
4 继承
4.1 介绍
继承 技术允许基于一个现有的类作为起点定义新的类。在面向对象的术语中,通常描述现有的类为基类 (base class)、父类 (parent class) 或者超类 (superclass),而称新定义的类为子类 (subclass 或者 child class)
子类可以覆盖父类方法,也可以在父类的基础上扩展方法。
下面以第一节中定义的 CreditCard
类为父类,定义新的子类 PredatoryCreditCard
。
4.2 例:PredatoryCreditCard 子类
我们希望实现的新功能:
当尝试收费由于超过信用卡额度被拒绝时,将会收取 5 美元的费用
将有一个对未清余额按月收取利息的机制,即基于参数年利率 apr
计算利息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class PredatoryCreditCard (CreditCard ): """继承父类,扩展方法""" def __init__ (self, customer, bank, acnt, limit, apr ): """ 继承父类,常见新的一个账户 :param customer: 用户名 :param bank: 银行名 :param acnt: 账户ID :param limit: 额度限制 :param apr: 年利率,用以计算利息 """ super ().__init__(customer, bank, acnt, limit) self ._apr = apr def charge (self, price ): """覆盖父类的charge方法,添加扣除手续费功能""" success = super ().charge(price) if not success: self ._balance += 5 return success def process_month (self ): """收取每月利息""" if self ._balance > 0 : monthly_factor = pow (1 + self ._apr, 1 / 12 ) self ._balance *= monthly_factor
4.2.1 继承语法
定义子类 B 继承父类 A 时,采用下面的格式
4.2.2 初始化语句
在子类中首先需要调用父类的初始化语句和传参
1 2 3 4 5 def __init__ (self, param1, param2, sub_param ): super ().__init__(param1, param2) self .sub_param = sub_param
4.2.3 调用父类方法
在子类中调用父类方法,使用 super()
代替父类对象。例如:在使用父类的方法 charge()
时,直接采用
【注意】在子类中,我们直接调用了父类的受保护数据 _balance
(以 _
开头的数据视为受保护数据),这不是最好的方法。只是在这里,我们需要在子类中修改 balance ,但如果调用 get_balance
方法是无法修改 balance 的,所以只好直接修改 ._balance
。
【改进】不过可以在父类中定义非公有/受保护的方法 _set_balance()
让子类能使用它改变 ._balance
而外界则不使用这个方法。如此可以做到子类使用父类方法改变父类受保护数据,而不是直接在父类数据上改变。
4.3 例:迭代数列的类的层次
Progression 类的分层如图。我们希望之后定义的各种数列均以 Progression 类为父类。
Progression 类产生 0, 1, 2, ...
的无穷数列。
4.3.1 父类:Progression 类
定义 __next__, __iter__
方法,实现 next(obj)
的方法迭代,同时也支持 for i in obj
的方法
定义 _advance
方法,为更新提供了非公有方法,为未来子类不同的更新形式提供方法
定义 print_progression
方法,方便以字符串的方式展示当前值的后 n 位数值,以序列的形式展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Progression : """ 定义普适的数列父类 默认产生 0, 1, 2, ... 的无穷数列 """ def __init__ (self, start=0 ): """初始化记录起始默认值""" self ._current = start def _advance (self ): """ 非公有方法,用于更新 self._current 为后续子类覆盖提供方法,子类不同的数列需要覆写不同的更新方法 父类默认的更新方式为 += 1 """ self ._current += 1 def __next__ (self ): """迭代下一个值,或抛出异常 StopIteration""" if self ._current is None : raise StopIteration else : result = self ._current self ._advance() return result def __iter__ (self ): """ 习惯于 iter 与 next 合并使用 By convention, an iterator must return itself as an iterator. """ return self def print_progression (self, n ): """打印当前值之后的 n 个值""" print (" " .join(str (next (self )) for _ in range (n)))
注意此个例子,使用 for i in obj
会进入无穷序列。
4.3.2 子类:等差数列类
我们希望等差数列 ArithmeticProgression 类产生等差数列,只需要继承父类后,覆写 _advance
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class ArithmeticProgression (Progression ): """继承基础数列类,定义等差数列类""" def __init__ (self, increment=1 , start=0 ): """ 初始化等差数列 :param increment: 公差 :param start: 首项 """ super ().__init__(start) self ._increment = increment def _advance (self ): """覆写新更新规则""" self ._current += self ._increment
1 2 3 4 5 6 7 8 9 10 aprog1 = ArithmeticProgression(4 ) aprog2 = ArithmeticProgression(4 , 1 ) aprog1.print_progression(7 ) aprog2.print_progression(7 ) """ 0 4 8 12 16 20 24 1 5 9 13 17 21 25 """
4.3.3 子类:等比数列类
类似地定义等比数列 GeometricProgression 类,需要注意首选不能为 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class GeometricProgression (Progression ): """等比数列""" def __init__ (self, base=2 , start=1 ): """ 初始化等比数列 :param base: 公比,默认为 2 :param start: 首项,不可为 0 """ super ().__init__(start) self ._base = base def _advance (self ): """覆写更新规则""" self ._current *= self ._base
4.3.4 子类:斐波那契数列类
定义斐波那契数列 FibonacciProgression 类。除了提供父类需要的参数首项和自己的参数第二项,还需要增加一个非公有参数记录相邻两项的差 _prev
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class FibonacciProgression (Progression ): """斐波那契数列""" def __init__ (self, first=0 , second=1 ): """ 初始化,提供第一第二项 :param first: 第一项,作为参数传给父类的 start :param second: 第二项 """ super ().__init__(first) self ._prev = second - first def _advance (self ): """覆写更新规则""" self ._prev, self ._current = self ._current, self ._prev + self ._current
1 2 3 4 5 6 7 8 9 fcprog1 = FibonacciProgression() fcprog2 = FibonacciProgression(1 , 1 ) fcprog1.print_progression(7 ) fcprog2.print_progression(7 ) """ 0 1 1 2 3 5 8 1 1 2 3 5 8 13 """
4.4 抽象基类
抽象基类 :一个类的唯一目的是作为继承的基类。其中被继承的我们称之为抽象基类,而其他的类则为具体的类。
一般而言,抽象类不能直接实例化,而具体的类可以被实例化。
理论上之前的例子中, Progression 类虽然严格来说是具体的类,但我们希望把它设计为一个抽象基类。
4.4.1 abc
模块
Python 的 abc
模块提供了正式的抽象基类的定义,例如我们定义一个抽象基类 Sequence
其一,我们不需要这个抽象基类 Sequence 提供 __len__
和 __getitem__
的具体实现,这两个方法的实现由继承它的子类完成。
其二,我们希望这个基类能完成功能:通过整数查看序列元素,所以要具体实现 __contains__
index
和 count
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from abc import ABC, abstractmethodclass Sequence (ABC ): """抽象基类 Sequence""" @abstractmethod def __len__ (self ): """返回序列长度,由子类实现""" pass @abstractmethod def __getitem__ (self, j ): """返回第 j 位置的值,由子类实现""" pass def __contains__ (self, val ): """查看 val 是否在序列中,返回 True or False""" for j in range (len (self )): if val == self [j]: return True return False def index (self, val ): """返回 val 在序列中的下标""" for j in range (len (self )): if val == self [j]: return j raise ValueError("Value not found" ) def count (self, val ): """计数多少值等于 val""" k = 0 for j in range (len (self )): if val == self [j]: k += 1 return k
这一类与 Python 的 collections 模块的 collections.abc.Sequence
类似。
因为采用抽象定义,故类 Sequence
不能被实例化。
4.4.2 collections
模块
使用 abc
模块定义过于复杂,可以直接使用 Python 的 collections
模块,它已经封装了常见的抽象基类。
例如在第 3.3 节实现的 Range 类,已经实现了 __len__
和 __getitem__
方法,但没有实现抽象基类中的__contains__
index
和 count
方法。
而这与 collections.abc.Sequence
类相匹配,可以让我们定义的 Range
类继承 collections.abc.Sequence
类,从覆写 __len__
和 __getitem__
,同时也依靠父类实现 contains、index 和 count 方法。
1 2 3 4 5 from collections.abc import Sequence class Range (Sequence ): ...
【注意】在 Python3.x
版本后 collections
相关抽象基类被移动到了 collections.abc
中。故使用时应导入 from collections.abc import Sequence
【结果测试】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if __name__ == "__main__" : r = NewRange(0 , 10 ) print (r) print (4 in r) print (r.index(0 )) print (r.count(1 )) """ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] True 0 1 """
其实采用之前我们自定义的 Sequence
抽象基类也可以达到相同目的。
1 2 3 4 5 6 7 from abc import ABC, abstractmethodclass Sequence (ABC ): ... class Range (Sequence ): ...
5 命名空间和面对对象
5.1 实例和类的命名空间
以 CreditCard 类和 PredatoryCreditCard 类为例,展示出其类命名空间和实例命名空间如下:
5.2 类数据成员
类级别的数据成员 :一些值(如常量)被一个类的所有实例共享
在这种情况下,在每个实例的命名空间中存储这个值就会造成不必要的浪费。所以使用如下格式定义这样的类数据成员。
1 2 3 4 5 6 7 8 9 10 11 class PredatoryCreditCard (CreditCard ): OVER_LIMIT_FEE = 5 ... def charge (self, price ): """覆盖父类的charge方法,添加扣除手续费功能""" success = super ().charge(price) if not success: self ._balance += PredatoryCreditCard.OVER_LIMIT_FEE return success
5.3 嵌套类
嵌套类 :在一个类的定义里定义另一个类。需要注意的是,二者只是嵌套关系,没有继承关系。
5.4 字典和 __slots__
声明
Python 提供了一种直接的机制来表示实例命名空间:类定义必须提供一个名为 __slots__
的类级别的成员分配给一个固定的字符串序列。例如,在 CreditCard
类中,声明如下:
1 2 class CreditCard : __slots__ = '_customer' , '_bank' , '_acnt' , '_limit' , '_balance'
当子类继承时,只需要声明子类新增的实例即可
1 2 class PredatoryCreditCard (CreditCard ): __slots__ = '_apr'
6 深拷贝和浅拷贝
6.1 直接赋值复制
当使用 a = b
时,Python 在做的只是拷贝了一个别名,或者说是一个地址。
当遇到不可变类型时还不会出现问题,一旦二者指向的是可变类型,例如列表。则会出现一方变,整体变。
1 2 3 4 5 6 7 """ 1. 简单赋值,复制了地址 """ print ("1. 简单赋值,复制了地址" )a = [1 , 2 ] b = a b.append(3 ) print (a)
6.2 浅拷贝
例如:列表中写入颜色对象。 warmtones
表示现有的颜色列表。希望创建一个新的 palette
列表,复制一份 warmtones
列表。但是对 palette
不希望影响到 warmtones
。
1 2 3 4 5 6 7 8 """ 2. 浅拷贝,简历了新地址,但仍然指向同一不可变数据 """ print ("2. 浅拷贝,简历了新地址,但仍然指向同一不可变数据" )warmtones = ['red' , 'green' , 'blue' ] palette = list (warmtones) palette[0 ] = 'yellow' print (warmtones) print (palette)
这比直接赋值更好,即复制了一份可变数据类型 (list) ,但指向的不可变数据仍然相同。例如:warmtones[0]
和 palette[0]
为实例 _red
的别名,实际指向相同。
6.3 深拷贝
我们希望让 warmtones
和 palette
之间完全没有关联,就可以使用 copy.deepcopy()
方法。
1 2 3 4 5 6 7 8 9 10 import copy""" 3. 深拷贝,完全拷贝一份 """ print ("3. 深拷贝,完全拷贝一份" )warmtones = ['red' , 'green' , 'blue' ] palette = copy.deepcopy(warmtones) palette[0 ] = 'yellow' print (warmtones) print (palette)