全文共9402字,预计学习时长19分钟
图片来源:https://unsplash.com/@marcelheil
Python是数据科学中一种十分常用的编程语言。对一些人来讲,它的语言灵活、可读性强,对另一些人来讲,它简单易上手,对大多数人来讲,是由于它的多面性。
我们将Python称为多面语言,因为它允许使用四种不同的编码规范进行编码:功能性、命令性、面向对象和面向过程。这些编码风格被正式称为编程范例(https://en.wikipedia.org/wiki/Programming_paradigm),代表了一种根据语言特性对语言进行分类的方法。
本文将对面向对象编程( OOP )(https://en.wikipedia.org/wiki/Object-oriented_programming)进行研究。
什么是面向对象编程( OOP )?
OOP是一种基于对象概念的编程范式。在计算机科学中,“对象”一词可以指代不同概念,但基本上,它是标识符所引用的内存值(https://en.wikipedia.org/wiki/Object_(computer_science))。
在OOP的执行环境中,对象指状态(变量)和行为(方法)的组合。面向对象方法的目标是创建可重复使用的软件,以下四个特征使其更易维护:封装、抽象、继承和多态。
还可以进一步区分面向对象语言,例如,基于类和基于原型。
在基于类的OOP中,对象是类的实例。类是关于如何定义某些内容的蓝图,但它不会提供内容本身——它只是提供结构。
学习策略
有许多方法可以用来学习和练习OOP,因必须选择其中一种,所以笔者决定采用莎士比亚的《罗密欧与朱丽叶》的故事线,制作一个简单的基于文本的游戏。具体步骤如下:
1. 写下故事
2. 指出问题
3. 确定实体——这些是类别
4. 创建实体层次结构
5. 确定实体的功能
6. 编写测试
7. 检查测试是否成功——因未编写过任何代码,所以最初会出现错误!
8. 编写代码
9. 重复!重构!精炼!
这个过程不是一成不变的,也不是注定要被它牵着鼻子走。这一系列步骤帮助笔者开始了编程之路。面向对象不仅仅是一种编程范式,还是一种解决问题的方法,尽管它并非没有非议,但在构建复杂系统时却是一个很好的选择。
兔子洞(The Rabbit Hole)
请注意步骤6:编写测试。现在,这个不是笔者最初的步骤。笔者正计划给各类别编码。但在研究OOP时,笔者发现了测试驱动开发( TDD )这一概念。
TDD是一种编程实践,开始于程序每个功能所进行的测试的设计和开发。这样,在开始编写代码之前,你就不得不考虑其规范、要求或设计。换句话说,在编写任何代码之前,都要编写代码来测试代码。
是不是感到迷惑不解?笔者也是。但做这个练习是完全值得的。
测试驱动开发和单元测试
TDD的过程非常简单:
图片来自Kanchan Kulkarni的TDD教程(https://www.guru99.com/test-driven-development.html)
1. 写出测试
2. 运行测试
3. 写下代码
4. 运行测试
5. 重构代码
6. 重复步骤
在这个例子中,笔者使用单元测试(http://softwaretestingfundamentals.com/unit-testing/)进行TDD。单元测试是软件测试的第一级,其目的是验证程序中的每个单元是否按设计执行。可以使用不同的框架来执行单元测试。
人们对于TDD持有两种不同观点。就我个人的经历而言,TDD的优点如下:
· 在毫无方向地开始输入代码之前,TDD迫使人们必须考虑要解决的问题。
· 在基于类别的OOP的特殊情况下,它能帮助理解每个类别内容。如职责是什么?必须知道什么?——目标是低耦合和高内聚性时,这变得更加息息相关。
· 尽管一开始它可能会减慢速度,但从长远来看,它通过最小化调试时间来节省时间。
· 它鼓励更好的设计,使代码更容易维护、减少冗余(不写重复代码!),并在需要时安全地重构。
· 它是一个动态文档——只需查看测试,就能理解每个单元应该做什么,如此一来,代码就能自证其明。
罗密欧与朱丽叶——代码与测试
在考虑了游戏的故事性之后,笔者决定采用两个不同的故事线:经典桥段和另类桥段。第一个是人们熟知的罗密欧与朱丽叶,第二个故事对我们来说则较为陌生。
故事安排(创建不同类别的引用)如下:
· 场景:蒙面舞会、阳台、决斗,安排、药剂师、卡普莱特墓和另类结局。该“场景”有两个主要职责,即为玩家描述场景,以及以是非问题提示玩家从而获取信息输入。
· 地图:地图就像一个有限状态机(https://en.wikipedia.org/wiki/Deterministic_finite_automaton)。它具有有限的状态(场景)、转换函数(从一个场景移动到另一个场景)和开始状态(第一个场景)。
· 故事线:定义两个唯一常量值。
从场景的定义中可以看到,所有场景都有相同的职责,只有它们的内容发生了变化(场景的描述和提示)。这就是为什么要利用继承的概念。这个概念允许定义一个类,该类别从另一个类中继承了所有方法和属性;在这种情况下,不重复写代码至关重要。
class Storyline(Enum): CLASSIC = "classic" ALTERNATIVE = "alternative"
对于故事线这一类别,笔者使用了Python’s enumeration type(https://docs.python.org/3/library/enum.html)或enum。在文档中,它们的定义为“一组绑定到唯一常量值的符号名(成员)。在枚举中,成员可以按标识进行比较,枚举本身可以循环访问。”
接下来,我们有Sence类和MockSence类。测试代码中有两个值得注意的特征:1.使用MockMap类;2.创建TestScene类以测试Scene类。在单元测试阶段,将创建一个Test类为每个类编写测试。
Class Scene(object): a_map = None def __init__(self, a_map): self.a_map = a_map def get_message(self): return """ This scene is yet to be initialized """ def get_prompt(self): return """ This scene is yet to be initialized """ def enter(self): self.print_description() self.prompt_user() def print_description(self): print(dedent(self.get_message())) def prompt_user(self): input_from_user = input(self.get_prompt()).lower() if input_from_user == "yes": self.a_map.advance_scene(Storyline.CLASSIC) elif input_from_user == "no": self.a_map.advance_scene(Storyline.ALTERNATIVE) self.a_map.play()
import unittest from unittest.mock import patch, mock_open import sys import io from romeo_and_juliet import * class MockMap(Map): storyline = None play_executed = False def advance_scene(self, a_storyline): self.storyline = a_storyline def play(self): self.play_executed = True class TestScene(unittest.TestCase): def test_print_description(self): a_scene = Scene(Map()) # Capturing the standard output as a test harness. capturedOutput = io.StringIO() sys.stdout = capturedOutput a_scene.print_description() self.assertEqual(capturedOutput.getvalue(), dedent(""" This scene is yet to be initialized\n """)) # Releasing standard output. sys.stdout = sys.__stdout__ def test_prompt_user(self): a_map = MockMap() a_scene = Scene(a_map) with patch("builtins.input", return_value = "yes"): a_scene.prompt_user() self.assertEqual(a_map.storyline, Storyline.CLASSIC) with patch("builtins.input", return_value = "no"): a_scene.prompt_user() self.assertEqual(a_map.storyline, Storyline.ALTERNATIVE) self.assertTrue(a_map.play_executed)
最后,来看看Map类和TestMap类。和往常一样,我们会创建一个模拟测试,但在本例中,是为Scene类创建一个MockScene类。
class Map(object): scenes = None current_scene = None def __init__(self): self.scenes = { "the_masked_ball": TheMaskedBall(self), "the_balcony": TheBalcony(self), "the_duel": TheDuel(self), "the_arrangement": TheArrangement(self), "the_apothecary": TheApothecary(self), "the_capulet_tomb": TheCapuletTomb(self), "the_alternative_ending": TheAlternativeEnding(self) } self.current_scene = self.scenes["the_masked_ball"] def get_current_scene(self): return self.current_scene def play(self): self.current_scene.enter() def advance_scene(self, storyline): if storyline == Storyline.CLASSIC: if self.current_scene == self.scenes["the_masked_ball"]: self.current_scene = self.scenes["the_balcony"] elif self.current_scene == self.scenes["the_balcony"]: self.current_scene = self.scenes["the_duel"] elif self.current_scene == self.scenes["the_duel"]: self.current_scene = self.scenes["the_arrangement"] elif self.current_scene == self.scenes["the_arrangement"]: self.current_scene = self.scenes["the_apothecary"] elif self.current_scene == self.scenes["the_apothecary"]: self.current_scene = self.scenes["the_capulet_tomb"] elif self.current_scene == self.scenes["the_capulet_tomb"]: raise Exception if storyline == Storyline.ALTERNATIVE: if self.current_scene == self.scenes["the_masked_ball"]: self.current_scene = self.scenes["the_alternative_ending"] elif self.current_scene == self.scenes["the_balcony"]: self.current_scene = self.scenes["the_alternative_ending"] elif self.current_scene == self.scenes["the_duel"]: self.current_scene = self.scenes["the_alternative_ending"] elif self.current_scene == self.scenes["the_arrangement"]: self.current_scene = self.scenes["the_alternative_ending"] elif self.current_scene == self.scenes["the_apothecary"]: self.current_scene = self.scenes["the_alternative_ending"] elif self.current_scene == self.scenes["the_alternative_ending"]: raise Exception
class MockScene(Scene): was_entered = False def enter(self): self.was_entered = True class TestMap(unittest.TestCase): def test_play(self): a_map = Map() mock_scene = MockScene(a_map) a_map.current_scene = mock_scene a_map.play() self.assertTrue(mock_scene.was_entered) def test_initial_state(self): a_map = Map() self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall) def test_advance_scene_classic(self): a_map = Map() self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheBalcony) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheDuel) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheArrangement) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheApothecary) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheCapuletTomb) with self.assertRaises(Exception): a_map.advance_scene(Storyline.CLASSIC) def test_advance_scene_alternative_one(self): a_map = Map() self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall) a_map.advance_scene(Storyline.ALTERNATIVE) self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding) with self.assertRaises(Exception): a_map.advance_scene(Storyline.ALTERNATIVE) def test_advance_scene_alternative_two(self): a_map = Map() self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheBalcony) a_map.advance_scene(Storyline.ALTERNATIVE) self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding) with self.assertRaises(Exception): a_map.advance_scene(Storyline.ALTERNATIVE) def test_advance_scene_alternative_three(self): a_map = Map() self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheBalcony) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheDuel) a_map.advance_scene(Storyline.ALTERNATIVE) self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding) with self.assertRaises(Exception): a_map.advance_scene(Storyline.ALTERNATIVE) def test_advance_scene_alternative_four(self): a_map = Map() self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheBalcony) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheDuel) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheArrangement) a_map.advance_scene(Storyline.ALTERNATIVE) self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding) with self.assertRaises(Exception): a_map.advance_scene(Storyline.ALTERNATIVE) def test_advance_scene_alternative_five(self): a_map = Map() self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheBalcony) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheDuel) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheArrangement) a_map.advance_scene(Storyline.CLASSIC) self.assertIsInstance(a_map.get_current_scene(), TheApothecary) a_map.advance_scene(Storyline.ALTERNATIVE) self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding) with self.assertRaises(Exception): a_map.advance_scene(Storyline.ALTERNATIVE)
留言 点赞 关注
我们一起分享AI学习与发展的干货
编译组:余书敏、张璐瑶
相关链接:
https://towardsdatascience.com/object-oriented-programming-and-the-magic-of-test-driven-development-d377acae85fa
长按识别二维码可添加关注
读芯君爱你
转载:https://blog.csdn.net/duxinshuxiaobian/article/details/100835187