虽然 Python 可以写函数式编程,但是本质上是一门面对对象编程语言 (object-oriented programming language),简称 oop。面对对象编程是把代码包装成一个对象 Object, 然后做对象与对象之间的交互。这么做的好处是可以把复杂的代码逻辑嵌入对象内部 (Abstraction),而调用对象的时候仅需要了解其界面 (Interface)。
这篇教程有一定的难度,所以需要先看完之前的三篇教程:多多教Python:Python 基本功: 3. 数据类型zhuanlan.zhihu.com多多教Python:Python 基本功: 7. 介绍函数zhuanlan.zhihu.com多多教Python:Python 基本功: 9. 是非逻辑zhuanlan.zhihu.com
教程需求:
类 Class
Python 中通过类(Class)来定义一对象,Object。因为对象是用来包装代码的,所以类是一个外部看上去简单,但是内部很复杂的结构(就像教程标图中的金字塔)。一个类有自己的语境(Context),属于自己的属性(Property), 属于自己的成员(Member), 属于自己的方法(Methods),和属于自己的界面(Interface)。下面我们写代码来创建一个简单的类:
import numpy as np
class Sample:
max_sample = 100 # 类属性
def __init__(self, name='weight sample'):
"""类的初始化,一般用来初始化类成员, 不一定需要重写。self 关键字是指被创建后的类自己,每一个 self 代表一个被创建的类。"""
self.name = name # 创建类成员
self.samples = [] # 创建类成员
return
def __repr__(self):
"""重写类的表达式,不一定需要重写。"""
return "My name is:" + str(self.name) + ", and I have " + str(len(self.samples)) + " samples"
def __getitem__(self, index):
"""重写并且添加原本不支持的类索引,不一定需要重写。"""
try:
return self.samples[index]
except IndexError as error:
print("No sample with index: " + str(index) + ", err: " + str(error))
return None
@property
def sample_size(self):
"""类的方法变成员,通过 @property变成员的类方法不能传除了 self 的参数"""
return len(self.samples)
@classmethod
def calculate_average(cls, samples):
"""静态类方法,不需要创建类就可以调用这里注意不是 self 参数,而是 cls 参数,cls 参数直接通过类名字 Sample 就可以调用比如: Sample.calculate_average()"""
return np.mean(samples)
def collect_samples(self, samples):
"""动态类方法"""
for sample in samples:
if self.sample_size >= self.max_sample:
print("Sample size larger than: " + str(self.max_sample))
break
self.samples.append(sample)
return
def _analyze(self):
"""隐藏动态类方法不应该从外部调用的类方法,下划线开头的名称表示专门用于类的内部调用但是 Python 不阻止你从外部调用"""
average_weight = self.calculate_average(self.samples)
std_weight = np.std(self.samples)
return self.sample_size, average_weight, std_weight
def get_analytics_report(self):
"""动态类方法"""
results = self._analyze()
print("Number of samples: " + str(results[0]))
print("Average weight: " + "{0:.2f}".format(results[1]))
print("Standard deviation of weight: " + "{0:.2f}".format(results[2]))
return
def __del__(self):
"""释放类, 不一定要重写"""
print(self.name + " with sample size " + str(self.sample_size) + " is being released")
读完代码,注意其中的注释,然后我们来介绍一下这个类里面的几个重要元素:类名 Class Name: 这里 Class 是创建类的关键字,后面跟随着类名,然后冒号之后就进入类的语境。
类属性 Class Property: 这里进入类之后先定义了类属性,max sample, 这里类属性可以被初始化,并且可以在类不被创建的时候直接调用如:Sample.max_sample。
类成员 Class Member: 类成员通常在类的初始化中定义,并且赋予一个默认数值。当然也有在其他地方新建,定义类成员的,但是不建议这么做,而且 IDE 像 PyCharm 会警告。这里的类成员包括了一个字符串 name,和一个空列表 samples。
类方法 Class Methods: 类方法就是在类中定义并且可以被调用的函数。这里定义类方法的关键字就是 def ,当然在代码的注释中我们看到了不同的类方法种类,包括了:重写的类方法:在创建类的时候,Python 已经为你创建的类自动设置了一些类的方法,这些类方法是以 “__” 开头和 "__" 结尾的名字。这里重写的时候就覆盖了默认的类方法,加入了新建类的内容,比如上面例子的重写初始化,表达式,索引和释放。
动态类方法:动态类方法是完全新设计的函数,需要在类创建之后再调用,可以从外部或者内部调用。
静态类方法:静态类方法也是新设计的函数,但是可以在类创建之前使用,使用方法将在后文提到。
隐藏的类方法:Python 中不会强制性的隐藏类方法,但是以下划线开头的一般不直接从外部调用。这么设计你的类方法名字也可以帮助你避免一些错误。
变成员的类方法:@property这个关键字是把类方法转换成类成员,这样在外部调用某一个类成员的时候,实际上是在调用一个方法,但是这个方法不能加入其他参数。类界面 Class Interface: 这里的类界面包括了所有可以从外部调用的类方法,类成员的界面,并且通过重写 "__getitem__" 添加了索引界面。
这是一个针对于 专业性/挑战性学习的教程,虽然这是在基本功系列里写的第一个类,但是结构已经比较复杂了。下面我们来和这个类做一些互动,来认识一下这个新建的类可以通过其界面做哪些事情:
In [2]:Sample.max_sample
Out[2]:100
In [3]:sample = Sample(name='weight sample')
In [4]:sample.sample_size
Out[4]:0
In [5]:sample.collect_samples([120, 110, 140, 130, 200, 170, 166])
In [6]:print(sample)
Out [6]:My name is:weight sample, and I have 7 samples
In [7]:sample[1]
Out[7]:110
In [8]:sample.get_analytics_report()
Out [8]:Number of samples: 7
Average weight: 148.00
Standard deviation of weight: 29.59
In [9]:sample.calculate_average([100, 200, 300])
Out[9]:200.0
In [10]:Sample.calculate_average([100, 200, 300])
Out[10]:200.0
In [11]:sample._analyze()
Out[11]:(7, 148.0, 29.587642207999128)
In [12]:del sample
Out [12]:weight sample with sample size 7 is being released
同样的,我们来一行行解释:在 Jupyter 笔记本的第一个单元格定义类 Sample,如何定义参见上一段代码。
在未创建类的情况下,直接访问其属性 max sample,得到100。
创建一个类 Sample, 名字叫 'weight sample',然后赋予一个新变量 sample。
查看变成员 sample size 的类方法,得到 0 个sample。
调用动态类方法 collect samples,传入一个列表,7个 samples。
打印类,这里类会调用我们重写的 "__repr__",返回我们定义的字符串,确认了名字和样本数量。
通过索引,直接在类中查找第二个(第一个是0) sample 样本,返回 110。
调用动态类方法获得样本分析报告。
调用静态类方法计算一个列表的平均数,注意这里是通过已经创建的类 sample 调用。
这里通过定义但是未创建的类 Sample 来直接调用静态方法,返回结果一样。
这里调用了类的隐藏方法,仍然可以获得结果,但是不建议这么做,因为内部使用的方法一般返回的数据结构不明确,容易出错,就像这里返回的是一个 Tuple 类, 参考: 多多教Python:Python 基本功: 3. 数据类型。
内存释放创建的 sample 类,Python 调用了 重写的 "__del__" 方法,并且打印出了效果。
继承 Inheritance
一个类不仅可以从头开始写,也可以从另外一个类直接进行拓展或者重写,这就是继承。继承避免你去写重复代码,而且在一些 Python 的模块例如 多线程 (Threading),你必须通过继承的方法来实现模块的运用。子类继承父类的所有,并且进行拓展或者重写覆盖
上图就表达了 Python 中比较简单的继承关系,你可以无限的往下继承,或者扩展子类,但是除非是必要的我不建议这么做,因为这样会导致继承关系过于复杂,限制了子类的延展性。下面我们来继续上文写一个继承类的例子:
# In [13]:
class RemoveLastSample(Sample):
def _analyze(self):
"""重写内部调用类方法,在分析的时候去除最后一个样本元素。"""
samples_to_analyze = self.samples[0:-1]
average_weight = self.calculate_average(samples_to_analyze)
std_weight = np.std(samples_to_analyze)
return max(0, self.sample_size -1), average_weight, std_weight
这个 RemoveLastSample 类继承了 Sample 类,通过第一行语法,然后其内部只是重写了一个类方法 _analyze。注意既然是重写,那父类 (Sample Class) 的 _analyze 界面要保持一致。随后我们进行内部功能的实现,就像注释里所说的,我们在分析前去除了最后一个样本。
# In [14]:
class PlusOneSample(Sample):
def _analyze(self):
"""重写内部调用类方法,在分析的时候对每个样本的数值+1"""
samples_to_analyze = [sample + 1 for sample in self.samples]
average_weight = self.calculate_average(samples_to_analyze)
std_weight = np.std(samples_to_analyze)
return self.sample_size, average_weight, std_weight
这里 PlusOneSample 类是另一个继承了 Sample 的子类。内部的实现方法是分析时对每一个样本的数值+1,注意这里 "[sample + 1 for sample in self.samples]" 用到了 Python 的生成器语法,我们在之后的教程会讲到。现在我们对两个子类进行互动:
In [15]:remove_l_sample = RemoveLastSample(name='remove last weight sample')
In [16]:remove_l_sample.collect_samples([120, 110, 140, 130, 200, 170, 166])
In [17]:remove_l_sample.get_analytics_report()
Out [17]:Number of samples: 6
Average weight: 145.00
Standard deviation of weight: 30.96
In [18]:plus_1_sample = PlusOneSample(name='plus one weight sample')
In [19]:plus_1_sample.collect_samples([120, 110, 140, 130, 200, 170, 166])
In [20]:plus_1_sample.get_analytics_report()
Out [20]:Number of samples: 7
Average weight: 149.00
Standard deviation of weight: 29.59
我们分别创建了两个子类,并且传入了一样的样本。在调用了 "get_analytics_report()" 之后,我们发现了两个不同的分析结果,我们发现在实现相似的功能时候,利用继承可以省去许多多余的代码,使得代码结构更加简洁明了。
小结:
在 Python 中,绝大部分功能不需要定义类来实现,通过函数式编程或者脚本式就够了。但是当做到大型项目,或者调用模块 (多线程 Threading 模块)时,你不得不通过类来实现。类可以帮助你的代码更加合理有效,但是过分的使用类来封装,继承来延展就会让结构变得过于冗长复杂。所以这里有一个权衡 Tradeoff:不创建类,不用继承 --> 代码没有结构,可随意修改容易出错。
创建太多类,使用太多继承 --> 代码结构复杂,界面刻板缺乏延展性。
下面来两个教程外部的链接,有兴趣的小伙伴可以继续深入,当然后面的教程都会学到:Python中的多态与虚函数 - Tony_Wong的专栏 - CSDN博客blog.csdn.net
Python 多线程模块,利用到类的继承:python 多线程编程之threading模块(Thread类)创建线程的三种方法 - 风雨一肩挑 - 博客园www.cnblogs.com