python释放类对象_Python 基本功: 10. 面对对象-类 Class

虽然 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值