书名:Python极客项目编程(第2版)
ISBN:978-7-115-64236-3
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美]马赫什•文基塔查拉姆(Mahesh Venkitachalam)
译 袁国忠
责任编辑 龚昕岳
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
Python是一种解释型、面向对象、动态数据类型的高级程序设计语言,通过Python编程能够解决现实生活中的很多问题。本书并不介绍Python语言的基础知识,而是通过一系列有趣的项目,展示如何用Python解决各种实际问题,以及如何使用一些流行的Python库。
本书共15章,每章讲解一个有趣的Python项目,这些项目可以分成5个部分:第一部分是热身,包括科赫雪花、繁花曲线;第二部分是模拟生命,包括康威生命游戏、使用Karplus-Strong算法生成泛音、群体行为模拟;第三部分是好玩的图形,包括文本图形、照片马赛克、裸眼立体画;第四部分是走进三维,包括理解OpenGL、圆环面上的康威生命游戏、体渲染;第五部分是玩转硬件,包括在树莓派Pico 上实现Karplus-Strong算法、树莓派激光音乐秀、物联网花园、树莓派音频机器学习。此外,本书还通过附录介绍如何安装Python和设置树莓派。
本书适合已经了解了基本的Python语法和编程知识、想要尝试和探索通过Python编程解决实际问题的读者阅读,也可作为Python初学者练习项目开发的参考用书。
如果你想熟练地运用Python做些聪明的事情,很难找到比此书中的项目更好或更有用的资料来帮助你理解这门语言的工作方式。
——Network World News Magazine
每个Python程序员的书架上都应有这本书。
——Full Circle Magazine
此书中充满有趣的项目。
——iProgrammer网站
本书适合想进一步提升编程水平和扩展Python语言知识的程序员阅读。本书很好地诠释了Python的相关细节,并确保读者能够清楚地了解Python程序中正在发生的事情。
——InfoQ网站
本书为具有科学思维的程序员和对编程感兴趣的科学爱好者提供了优秀的项目,适合各种人群阅读。
——《Python编程快速上手:让繁琐工作自动化》作者Al Sweigart
这是一本难得一见的编程图书,读起来很有趣,不仅适合中高级Python程序员参考,即便是新手也应该读一读。阅读本书的过程始终都很愉快。
——Goodreads网站评论
此书充满有趣且娱乐性十足的想法,是一部非典型的编程著作,适合编程爱好者用来寻找乐趣,千万不要错过!
——亚马逊网站评论
马赫什·文基塔查拉姆(Mahesh Venkitachalam)是一名计算机图形学和嵌入式系统顾问,拥有二十余年从业经验。他是Electronut Labs的创始人,该公司以开发充满创意的开源硬件著称。他经常撰写有关编程和电子技术的博客。
埃里克·莫滕松(Eric Mortenson)拥有威斯康星大学麦迪逊分校数学学士和博士学位,曾在宾夕法尼亚州立大学、昆士兰大学和马克斯·普朗克数学研究所担任研究和教学职务,目前为圣彼得堡国立大学数学和计算机科学副教授。
赞德·索尔达特(Xander Soldaat)是乐高MINDSTORMS社区前合作伙伴,曾从事IT基础架构师和工程师工作18年,做过全职软件开发人员,最近重返老本行——Linux领域,担任Red Hat公司的OpenShift云架构师。他在闲暇之余喜欢捣鼓机器人、3D打印和自制复古计算机。
有人说,写书就像跑马拉松。本书的编写工作确实考验了我耐力的极限,如果没有亲朋好友和家人的支持和鼓励,这项工作就不可能完成。
首先,感谢我的妻子Hema,在我写作本书的两年时间里,她始终对我充满了爱、鼓励和耐心。感谢我的朋友Raviprakash Jayaraman,每当我面临困境时,他都与我共进退,并对本书进行审阅,我们一起吃过午餐、看过电影,还一起逛过动物园。感谢Seby Kallarakkal,他敦促我完成本书的写作,并同我做过有趣的讨论。万分感激Santosh Hemachandra博士,他同我讨论快速傅里叶变换,给我提供了极大的帮助。感谢Karthikeyan Chellappa帮助我测试Python模块的安装情况,还多次与我一起绕着Kaikondrahalli湖跑步。还要感谢Matthew Denham,他让我明白了万花尺的数学原理。
感谢No Starch出版社的Tyler Ortman和Bill Pollock接受我编写本书的提案,感谢Serena Yang对本书第1版所做的专业编辑工作,还要感谢Nicholas Kramer对本书第1版进行技术审稿。
感谢Nathan Heidelberger对本书第2版所做的细致认真的编辑工作。正是因为他的严谨,第1版的很多地方才能得到极大的改进。感谢Eric Mortenson和Xander Soldaat,他们分别对本书的软件部分和硬件部分做了技术审稿。再次感谢Raviprakash Jayaraman,他在多个操作系统中对第2版的代码进行了测试,提供了极大的帮助。还要感谢我的儿子兼办公室同事Aryan Mahesh,感谢他在我写作本书期间给我讲傻傻的笑话、向我推荐音乐,并同我深入探讨科幻题材的图书和电影。
感谢我的父母A.V. Venkitachalam和N. Saraswathy,他们为我提供了超出其经济能力的教育。最后,感谢所有激励过我的老师,真希望一生都能做一名学生。
欢迎阅读本书!本书提供15个令人兴奋的项目,旨在鼓励读者探索Python编程世界。这些项目涵盖各种主题,如绘制繁花曲线图案、进行三维渲染、让激光图案随音乐起舞,以及使用机器学习识别语音等。除本身具有的趣味性外,这些项目还提供了很大的扩展空间,为读者探索自己的创意提供跳板。
本书是为想通过编程来理解和探索创意的人编写的,阅读本书需要了解基本的Python语法和编程概念,并熟悉高中数学。在每个项目中,我将竭尽全力诠释所需的数学知识。
本书并非Python入门教程,不介绍基础知识,而是通过一系列重要项目演示如何使用Python解决各种实际问题。在完成这些项目的过程中,你将探索Python编程语言的玄妙之处,并学习如何使用一些深受欢迎的Python库。更重要的是,你将学习如何将问题化整为零、设计出解决问题的算法并使用Python从零开始实现解决方案。
一些实际问题解决起来可能很难,因为它们通常是开放性的,要求你具备众多领域的专业知识,但Python提供了帮助你解决问题的工具。在成为专家级程序员的路途中,克服困难、找到实际问题的解决方案是最重要的。
下面快速浏览一下本书的内容。
第一部分包含几个帮助你热身的项目。
第1章“科赫雪花”,介绍使用递归函数和海龟绘图法绘制有趣的分形图案。
第2章“繁花曲线”,介绍使用参数方程和海龟绘图法绘制类似于万花尺生成的曲线。
第二部分包含多个使用数学模型模拟真实现象的项目。
第3章“康威生命游戏”,介绍使用NumPy和Matplotlib实现著名的“元胞自动机”模型,根据几个简单规则生成不断进化的模拟生命系统。
第4章“使用Karplus-Strong算法生成泛音”,介绍如何模拟弹拨乐器的声音,并使用PyAudio播放这些声音。
第5章“群体行为模拟”,介绍使用NumPy和Matplotlib实现Boids算法,并模拟鸟群的行为。
第三部分的项目介绍如何使用Python读取和操作二维图像。
第6章“文本图形”,介绍Python图像库(Python Imaging Library,PIL)的模块Pillow,演示如何将图像转换为文本图形。
第7章“照片马赛克”,介绍将一组较小的图像拼接在一起,创建较大的可识别图像。
第8章“裸眼立体画”,介绍利用深度贴图和像素操作赋予二维图像立体效果。
第四部分介绍如何使用着色器和OpenGL库,基于图形处理单元(Graphics Processing Unit,GPU)快速而高效地渲染三维图形。
第9章“理解OpenGL”,介绍有关如何使用OpenGL创建简单三维图形的基础知识。
第10章“圆环面上的康威生命游戏”,介绍如何在三维环面上实现模拟生命系统。
第11章“体渲染”,介绍用于渲染体数据的体光线投射算法—— 一种常用于医学成像领域(如MRI和CT)的技术。
最后,第五部分利用树莓派和其他电子元件来介绍如何在嵌入式系统中使用Python进行编程。
第12章“在树莓派Pico上实现Karplus-Strong算法”,介绍如何组装可演奏的电子乐器,并使用MicroPython在微控制器树莓派Pico上实现Karplus-Strong算法。
第13章“树莓派激光音乐秀”,介绍如何在树莓派中使用Python控制两个旋转镜片和一束激光,从而生成随音乐起舞的激光秀。
第14章“物联网花园”,介绍使用低功耗蓝牙将树莓派与运行CircuitPython的Adafruit硬件连接起来,搭建一个对花园温度和湿度进行监控的物联网系统。
第15章“树莓派音频机器学习”,介绍如何在树莓派中实现语音识别系统,带你进入激动人心的TensorFlow机器学习领域。
每章都有“实验”一节,提供如何扩展该章项目或进一步探索相关主题的建议。
本版包含5个新项目,其中包括第1章“科赫雪花”和第10章“圆环面上的康威生命游戏”。此外,最重要的修订在硬件部分,本版专注于基于树莓派的系统,不再涉及Arduino。因此,第五部分的每个项目要么是全新的(第12章、第14章和第15章),要么做了全面修订(第13章)。通过使用树莓派,本书简化了硬件项目的组装过程,确保专注于Python编程,而不再需要在Python和Arduino编程语言(一种C++版本)之间切换。通过阅读修订后的第五部分,读者还将体验MicroPython和CircuitPython编程——两个针对资源有限的嵌入式系统做了优化的Python版本。
本版的其他重要修订如下。
❏ 第4章播放WAV文件时,使用PyAudio替代Pygame。
❏ 第7章为照片马赛克查找最佳图像匹配时,对线性查找算法和k-d树数据结构的性能做了比较。
❏ 第8章新增介绍如何创建用于生成裸眼立体画的自定义深度贴图。
❏ 附录A新增介绍如何使用Anaconda简化Python安装。
除这些具体修订外,还对全书进行了审校和修正,并基于第1版出版后Python发生的变化对代码做了必要的修订。
Python是一种非常适合用来探索编程的语言。作为一种多范式语言,它在程序编写方式方面具有很大的灵活性。可将Python作为脚本语言用于执行代码,可将其作为过程型语言用于将程序组织成一组相互调用的函数,还可将其作为面向对象语言,从而使用类、继承和模块来打造层次结构。这种灵活性让用户能够根据项目的需求选择最合适的编程风格。
使用C或C++等更传统的语言进行开发时,必须在运行前编译并链接代码,但使用Python时,编写好代码后就可直接运行(在幕后,Python将代码编译为中间字节码,再由Python解释器运行,但这些过程对用户来说是透明的)。在使用Python进行实践时,反复修改并运行代码的过程非常简便。
Python提供了为数不多的几个简单而强大的数据结构,因此只要熟悉字符串、列表、元组、字典、列表推导式及基本控制结构(如for和while循环),便在学习Python的道路上迈出了巨大的一步。Python语法简洁而富有表现力,只需编写几行代码就能执行复杂的操作。熟悉Python内置模块和第三方模块后,便掌握了一整套解决实际问题(如本书中介绍的项目)的工具。可采用标准方式在Python中调用C/C++代码(或者反过来),并且无论要实现什么功能,几乎都能找到相应的Python库,这让用户能够在较大的项目中轻松地将Python和其他语言模块结合起来使用。正因为如此,Python被认为是一种绝佳的“胶水语言”,让用户能够轻松地将各种软件组件组合在一起。第四部分的三维图形项目表明,可将Python同类似C语言的OpenGL着色语言结合起来使用。此外,第 14 章将 HTML(超文本标记语言)、CSS(串联样式表)和JavaScript结合起来使用,为物联网花园监控器创建Web界面。在开发实际软件项目时,通常需要结合使用多种软件技术,Python非常适合用于开发这样的分层架构。
Python还提供了一个方便的工具——Python解释器,让用户能够轻松地检查代码语法、执行快速计算,乃至对正在开发的代码进行测试。编写Python代码时,我会同时打开3个窗口:文本编辑器、Shell和Python解释器。在编辑器中开发代码时,我将函数或类导入解释器,并在开发的同时进行测试。
在代码中使用新模块前,我还会先使用解释器来熟悉它们。例如,开发第14章的物联网花园项目时,我要测试数据库模块sqlite3。为此,我打开Python解释器并尝试执行如下代码,确保自己知道如何创建和添加数据库记录。
>>> import sqlite3
>>> con = sqlite3.connect('test.db')
>>> cur = con.cursor()
>>> cur.execute("CREATE TABLE sensor_data (TS datetime, ID text, VAL numeric)")
>>> for i in range(10):
... cur.execute("INSERT into sensor_data VALUES (datetime('now'),'ABC', ?)", (i, ))
>>> con.commit()
>>> con.close()
>>> exit()
为确认上述做法可行,我执行如下代码,以检索前面添加的部分数据:
>>> con = sqlite3.connect('test.db')
>>> cur = con.cursor()
>>> cur.execute("SELECT * FROM sensor_data WHERE VAL > 5")
>>> print(cur.fetchall())
[('2021- 10- 16 13:01:22', 'ABC', 6), ('2021- 10- 16 13:01:22', 'ABC', 7),
('2021- 10- 16 13:01:22', 'ABC', 8), ('2021- 10- 16 13:01:22', 'ABC', 9)]
这个示例说明了Python解释器这个强大工具在开发中的实际用途:要快速进行实验,无须编写完整的程序,打开解释器就可开始。这只是我喜爱Python(同时认为你也会喜爱它)的众多原因之一。
对于本书中每个项目的代码,我都竭尽所能、力图条分缕析地做出详尽的剖析。你可手动输入代码,也可按“资源与支持”页所述方式获取本书所有程序的完整代码。
接下来,我将带领你完成众多令人兴奋的项目,但愿你玩得和我开发时一样开心。别忘了探索每个项目中的实验。祝你在阅读本书的过程中拥有愉快的编程时光!
本书提供如下资源:
❏ 本书源代码;
❏ Python排障手册电子书;
❏ 程序员面试手册电子书;
❏ 异步社区30天VIP会员。
要获得以上资源,您可以扫描下方二维码,根据指引领取。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区(https://www.epubit.com),按书名搜索,进入本书页面,单击“发表勘误”按钮,输入错误信息,然后单击“提交勘误”按钮即可(见下图)。本书的作者和编辑会对您提交的错误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作者与读者的在线交流互动,以及传统出版与数字出版的融合发展。
“异步图书”是异步社区策划出版的精品IT图书的品牌,依托于人民邮电出版社在计算机图书领域30余年的发展与积淀。异步图书面向IT行业以及各行业使用IT的用户。
下面来开启我们的Python探险之旅,研究如何绘制一种有趣的形状——科赫雪花。科赫雪花是瑞典数学家黑尔格·冯·科赫(Helge von Koch)于1904 年发明的,它是一种分形(fractal),一种不断放大时会不断重复自身的图形。
分形的重复特征源自递归,而递归是一种使用自身定义自身的技巧。具体地说,当使用递归算法绘制分形时,便开启了一个重复过程,并将每次重复的输出作为下一次重复的输入。
本章主要介绍如下内容。
❏ 有关递归算法和函数的基本知识。
❏ 使用模块turtle绘制图形的方法。
❏ 绘制科赫雪花的递归算法。
❏ 一些有关线性代数的知识。
图 1.1 展示了科赫雪花是什么样的。其左、右两部分的形状与中间那部分完全相同,只是规模更小。同理,中间那部分本身是由形状与其相同但规模更小的部分组成的。这就是分形的自相似重复特征。
图1.1 科赫雪花
只要知道如何计算构成科赫雪花的基本形状中的各个点,便可开发一种算法来递归地执行相同的计算,从而绘制出越来越小的基本形状,构建出科赫雪花这种分形。本节将首先概述递归的工作原理,然后研究如何利用递归、一些线性代数知识和Python模块turtle来绘制科赫雪花。
为对递归的工作原理有所认识,下面来看一个简单的递归算法:计算阶乘。阶乘可使用下面的函数定义:
f(N) = 1 × 2 × 3× …× (N − 1) × N
换而言之,N的阶乘就是整数1~N的乘积。对于上面的函数,可改写成下面这样:
f(N) = N × (N − 1)× …× 3 × 2 × 1
并进一步改写为下面这样:
f(N) = N × f(N − 1)
上面使用f本身定义了f的阶乘,这就是递归。调用f(N)将导致调用f(N − 1),进而导致调用f(N − 2),以此类推。然而,在什么情况下停止递归呢?为此,需要将f(1)定义为1,为递归指定终点。
下面演示如何使用Python来实现递归的阶乘函数:
def factorial(N):
❶ if N == 1:
return 1
else:
❷ return N * factorial(N- 1)
在❶处,处理了N为1的情形——直接返回1;在❷处,实现了递归调用:调用函数factorial(),并传入参数N − 1。这个函数将不断调用自身,直到N为1。这样做的效果是,当这个函数返回时,就计算出了整数1~N的乘积。
一般而言,实现使用递归的算法时应采取如下步骤。
1.定义一个基线条件(base case)。满足基线条件时,递归将终止。阶乘的基线条件是factorial(1)=1。
2.定义递归步骤。为此,需要考虑如何将算法表示为递归过程。在有些算法中,可能需要执行多次递归调用(稍后将介绍)。
解决可分解为其小型版本的问题时,递归是一个很有用的工具。可用于阶乘算法,也可用于科赫雪花的绘制。然而,递归并非总是效率最高的问题解决方式,在有些情况下,合理的选择是以循环的方式重新实现递归算法。但相比于循环实现,递归算法通常更紧凑、更优雅。
下面来看看如何构建科赫雪花。图 1.2 展示了用于绘制科赫雪花的基本图案,以下称之为片段(flake)。这个片段基于长度为d的线段AB,该线段被分为等长的3部分(AP1、P1P3和P3B),其中每部分的长度都为r。P1并非直接连接到P3,而是经由P2连接到P3。P2是这样确定的:P1、P2和P3构成一个边长为r、高为h的等边三角形。点C为P1和P3之间的中点,它位于P2的正下方,因此AB和CP2是垂直的。
明白如何确定图 1.2 所示的各点后,就可递归地绘制越来越小的片段,从而构建出科赫雪花。大致而言,目标如下:给定点A和点B,需要计算点P1、P2和P3的坐标,并像图1.2那样将它们连接起来。要计算这些坐标,需要使用一些线性代数知识,线性代数是数学的一个分支,能够根据向量计算距离以及确定点的坐标。所谓向量,指的是有长度和方向的量。
图1.2 用于绘制科赫雪花的基本图案
下面介绍一个简单的线性代数公式,后面将用到它。给定三维空间中的点A和单位向量n(单位向量是长度为1的向量),要计算从点A出发沿这个单位向量移动距离d到达的点B的坐标,可使用下面的公式:
B = A + d × n
可通过示例轻松地验证这一点。假设点A的坐标为(5, 0, 0),单位向量n为(0, 1, 0),从点A出发沿这个单位向量移动10个单位到达点B,那么点B的坐标是多少呢?使用刚才的公式,可得到如下结果:
B = (5, 0, 0) + 10 × (0, 1, 0) = (5, 10, 0)
换而言之,要从点A到点B,需要沿y轴的正方向移动10个单位。
下面介绍另一个技巧,即垂直向量计算技巧(perpendicular vector trick)。假设有向量A= (a, b),如果有另一个向量B,它与向量A垂直,那么可将它表示为向量B= (−b, a)。要验证这个关系,可计算A和B的点积。要计算两个二维向量的点积,可将两个向量的第一个分量相乘,并将第二个分量相乘,再将这两个乘积相加。在这里,A和B的点积如下:
两个向量垂直时,它们的点积为0,因此B确实垂直于A。
掌握这些线性代数知识后,回过头来看图1.2所示的片段。给定点A和点B的坐标,如何计算点P2的坐标呢?要到达点P2,可从点C出发,沿单位向量n移动距离h。根据前面的第一个线性代数公式可知:
下面来将变量代入公式。点C为线段AB的中点,因此C = (A + B) / 2。其次,h为边长为r的等边三角形的高。根据勾股定理可知:
在这里,r为点A到点B的距离的三分之一。如果点A的坐标为(x1, y1),点B的坐标为(x2, y2),可使用下面的公式计算它们之间的距离:
只需将这个公式的结果除以3,就可得到r的值。
最后,需要将向量 n 表示出来。假设 n 与向量垂直,而向量可表示为点B的坐标减去点A的坐标:
向量的长度。现在利用前面的垂直向量计算技巧,使用A和B来表示n:
接下来需要计算P1和P3的坐标,为此需要用到另一个线性代数公式。假设有线段AB,而点C位于这条线段上;同时假设a为A到C的距离,而b为B到C的距离。可使用下面的公式来计算点C的坐标:
要明白这个公式,可假设点C为线段AB的中点,此时a和b相等。在这种情况下,凭直觉就能知道C的坐标应该为(A + B) / 2。在上面的公式中,将所有的b都替换为a,结果如下:
有了这个新公式后,就可计算P1和P3的坐标了。这两个点将线段AB三等分,这意味着P1到B的距离为P1到A的距离的两倍(即b1 = 2a1),P3到A的距离为P3到B的距离的两倍(即a3 = 2b3)。将这些值代入前面的公式,可得到如下计算P1和P3坐标的公式:
至此,就获得了绘制科赫雪花分形的第1层级所需的一切:给定点A和点B,然后通过计算确定点P1、P2和P3。在这个分形的第2层级,将第1层级中片段内的每条线段(如图1.2所示)替换为更小的片段,结果如图1.3所示。
图1.3 构建科赫雪花的第2步
注意,对于图1.2所示的4条线段(AP1、P1P2、P2P3和P3B),以每条线段为基础构建了一个新片段。在绘制科赫雪花的程序中,将把每条线段的端点(如A和P1)作为新片段的A和B的值,并递归地执行生成图1.2所示点的计算。
在分形的每个层级,都将再次对片段进行替换,从而绘制出越来越小的自相似图形。这就是科赫雪花绘制算法中的递归步骤,此后不断地重复这个步骤,直到满足基线条件。基线条件应该为线段AB的长度小于特定的阈值,如10像素。到达这个阈值后,将只绘制线段,而不再递归。
为让最终绘制出的科赫雪花更漂亮,可在分形的第1步绘制3个相连的片段,这样结果将更像雪花,呈六角对称,如图1.4所示。
图1.4 合并3个片段
知道如何计算绘制科赫雪花所需的坐标后,下面来看看如何在Python中使用这些坐标来绘制图形。
为绘制科赫雪花,本章将使用Python模块turtle,这是一款简单的绘图程序,以海龟在沙滩上拖着尾巴前行为模型,绘制出各种图案。模块 turtle 包含用于设置画笔(相当于海龟的尾巴)位置和颜色的方法,还有很多有助于绘图的函数。
只需使用几个绘图函数就可绘制出科赫雪花。实际上,从turtle的角度看,绘制科赫雪花几乎与绘制三角形一样简单。为证明这一点,并初步讲解turtle是如何工作的,下面的程序使用turtle绘制了一个三角形。请输入这些代码,将其保存为test_turtle.py文件,再在Python中运行它。
❶ import turtle
def draw_triangle(x1, y1, x2, y2, x3, y3, t):
#尝试绘制一个三角形
❷ t.up()
❸ t.setpos(x1, y1)
❹ t.down()
t.setpos(x2, y2)
t.setpos(x3, y3)
t.setpos(x1, y1)
t.up()
def main():
print('testing turtle graphics...')
❺ t = turtle.Turtle()
❻ t.hideturtle()
❼ draw_triangle(-100, 0, 0, -173.2, 100, 0, t)
❽ turtle.mainloop()
# 调用main()函数
if __name__ == '__main__':
main()
首先,导入了模块turtle❶。接下来,定义了方法draw_triangle(),其参数为 3 对坐标(三角形的3个顶点),以及turtle对象t。在这个方法中,先调用了up()❷,让Python抬起画笔,换而言之,就是让画笔离开虚拟纸张,以免在移动海龟时进行绘画。开始绘画前,需要指定海龟的位置。调用函数setpos()❸将海龟的位置设置为第1对坐标对应的点。调用函数down()❹将画笔放下,因此每次调用setpos()时,都相当于将海龟移到了下一组坐标处,进而绘制出一条线段。最终的结果为一个三角形。
接下来,声明了函数main(),实际的绘画工作是由它来完成的。在这个函数中,创建了用于绘画的turtle对象❺,并将其隐藏起来❻。如果没有隐藏turtle对象,将在绘制的线段开头看到一个海龟的图案。接下来,为绘制三角形,调用了draw_triangle(),并将所需的坐标作为参数传递给它❼。调用函数mainloop()❽确保绘制三角形后不会关闭tkinter窗口(tkinter是Python默认使用的GUI库)。
图1.5显示了这个简单程序的输出。
图1.5 使用海龟绘图法绘制三角形
有了完成本章项目所需的一切后,下面来绘制科赫雪花。
在本章项目中,将使用Python模块turtle来绘制科赫雪花。
为绘制科赫雪花,需要定义一个递归函数drawKochSF()。这个函数根据点A和点B的坐标计算点P1、P2和P3(见图1.2)的坐标;再递归地调用自己,为越来越短的线段执行同样的计算,直到满足基线条件;最后使用模块turtle绘制片段。要查看完整的项目代码,请参阅1.7节“完整代码”,也可见本书配套源代码中的“/koch/koch.py”。
在函数drawKochSF()中,首先计算为绘制图1.2所示基本片段图案所需的所有点的坐标。
def drawKochSF(x1, y1, x2, y2, t):
d = math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2))
r = d/3.0
h = r*math.sqrt(3)/2.0
p3 = ((x1 + 2*x2)/3.0, (y1 + 2*y2)/3.0)
p1 = ((2*x1 + x2)/3.0, (2*y1 + y2)/3.0)
c = (0.5*(x1+x2), 0.5*(y1+y2))
n = ((y1-y2)/d, (x2-x1)/d)
p2 = (c[0]+h*n[0], c[1]+h*n[1])
函数drawKochSF()被定义为将线段AB的端点的x和y坐标作为参数,该线段定义了图1.4所示雪花中的一个片段。该函数还将turtle对象t作为参数,该参数用于完成实际的绘图工作。接下来,这个函数将计算图1.2所示的所有参数,这些参数在1.1.2小节“构建科赫雪花”中讨论过。首先计算的是d——A到B的距离。将这个距离除以3得到r,这是构成片段的各条线段(总共4条)的长度。然后根据r计算h,这是片段中间的三角形的高。
接下来计算其他参数,它们都是元组,包含x坐标和y坐标。元组p3和p1定义了片段中间的三角形底边的两个端点,元组c表示p1和p3的中点,而n是与线段AB垂直的单位向量。通过结合使用c、h和n,计算出了p2——片段中间的三角形的顶点坐标。
在函数drawKochSF()的下一部分中,使用递归将第1个片段分解成越来越小的片段。
❶ if d > 10:
# 第1个片段
❷ drawKochSF(x1, y1, p1[0], p1[1], t)
# 第2个片段
drawKochSF(p1[0], p1[1], p2[0], p2[1], t)
# 第3个片段
drawKochSF(p2[0], p2[1], p3[0], p3[1], t)
# 第4个片段
drawKochSF(p3[0], p3[1], x2, y2, t)
首先确定了递归停止条件❶。如果d(线段AB的长度)大于10像素,就继续递归,这是通过调用函数drawKochSF() 4次实现的。每次调用drawKochSF()时,都传入了一组不同的参数,这些参数是根据构成片段的4条线段的端点坐标确定的,而这些端点坐标已在函数开头计算得到。例如,在❷处,为线段AP1调用了函数drawKochSF(),而此后几次函数调用分别针对的是线段P1P2、P2P3和P3B。在这些递归调用中,都将根据新的点A和点B坐标重复之前的计算,并判断d是否依然大于10像素,如果是就再次递归调用drawKochSF() 4次,以此类推。
下面来看看线段AB小于10像素时的情况,这是此递归算法的基线条件。小于这个阈值后将不再递归,而是将构成单个片段的4条线段返回,为此使用了模块turtle中的方法up()、down()和setpos(),这些方法在1.1.3小节“使用海龟绘图法绘图”中介绍过。
else:
# 绘制中间的角
t.up()
❶ t.setpos(p1[0], p1[1])
t.down()
t.setpos(p2[0], p2[1])
t.setpos(p3[0], p3[1])
# 绘制两侧的边
t.up()
❷ t.setpos(x1, y1)
t.down()
t.setpos(p1[0], p1[1])
t.up()
❸ t.setpos(p3[0], p3[1])
t.down()
t.setpos(x2, y2)
首先绘制了由点P1、P2和P3构成的角❶,然后绘制了线段AP1❷和P3B❸。由于在函数drawKochSF()的开头完成了所有必要的计算,因此实际绘图工作很简单,将合适的坐标传递给方法setpos()即可。
函数main()创建并设置一个turtle对象,再调用drawKochSF()。
def main():
print('Drawing the Koch Snowflake...')
t = turtle.Turtle()
t.hideturtle()
# 绘制科赫雪花
try:
❶ drawKochSF(-100, 0, 100, 0, t)
❷ drawKochSF(0, -173.2, -100, 0, t)
❸ drawKochSF(100, 0, 0, -173.2, t)
❹ except:
print("Exception, exiting.")
exit(0)
# 等用户在屏幕上单击后退出
❺ turtle.Screen().exitonclick()
从图 1.4 可知,要绘制3个片段,确保最终输出为六角对称的雪花图形。为此,调用了drawKochSF() 3次:对于第1个片段,点A和点B的坐标分别为(−100, 0)和(100, 0)❶;对于第2个片段,坐标为(0, −173.2)和(−100, 0)❷;对于第3个片段,坐标为(100, 0)和(0, −173.2)❸。请注意,这些坐标与前面在程序test_turtle.py中绘制三角形时使用的坐标相同。请尝试确定这些坐标是如何计算出来的。(提示:)
为捕获绘图期间可能发生的异常,将对函数drawKochSF()的调用放在一个Python try块中。例如,用户在绘图期间关闭了窗口,将引发异常,可在except块中捕获此异常❹,然后输出一条消息并退出程序。如果用户没有终止绘图过程,将执行代码turtle.Screen().exitonclick()❺,等待用户单击将窗口关闭。
在终端使用下面的命令运行代码,其输出如图1.6所示。
$ python koch.py
至此,就绘制出了一片漂亮的科赫雪花!
图1.6 绘制的科赫雪花
本章介绍了有关递归函数和递归算法的基本知识,以及如何使用Python模块turtle绘制简单图形,并介绍了如何结合使用这些概念绘制一种漂亮的图形——被称为科赫雪花的有趣分形。
学会如何绘制科赫雪花后,来看看另一个有趣的分形——谢尔平斯基三角形,它是以波兰数学家瓦茨瓦夫·谢尔平斯基(Wacław Sierpiński)的名字命名的,其形状如图1.7所示。
图1.7 谢尔平斯基三角形
请尝试使用海龟绘图法绘制谢尔平斯基三角形。可像绘制科赫雪花时那样使用一种递归算法。如果仔细观察图1.7,将发现大三角形可被分成3个小三角形,并在中央形成一个倒三角形孔洞;而每个小三角形本身又能被分成3个更小的三角形,同样在中央形成一个倒三角形孔洞,以此类推。这提供了该如何拆分要使用的递归算法的线索。
这个问题的解决方案可见本书配套源代码中的“/koch/koch.py”。
下面是绘制科赫雪花的完整代码:
"""
koch.py
一个绘制科赫雪花的程序。
开发者:Mahesh Venkitachalam
"""
import turtle
import math
#以递归的方式绘制科赫雪花
def drawKochSF(x1, y1, x2, y2, t):
d = math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2))
r = d/3.0
h = r*math.sqrt(3)/2.0
p3 = ((x1 + 2*x2)/3.0, (y1 + 2*y2)/3.0)
p1 = ((2*x1 + x2)/3.0, (2*y1 + y2)/3.0)
c = (0.5*(x1+x2), 0.5*(y1+y2))
n = ((y1-y2)/d, (x2-x1)/d)
p2 = (c[0]+h*n[0], c[1]+h*n[1])
if d > 10:
# 第1个片段
drawKochSF(x1, y1, p1[0], p1[1], t)
# 第2个片段
drawKochSF(p1[0], p1[1], p2[0], p2[1], t)
# #第3个片段
drawKochSF(p2[0], p2[1], p3[0], p3[1], t)
# 第4个片段
drawKochSF(p3[0], p3[1], x2, y2, t)
else:
# 绘制中间的角
t.up()
t.setpos(p1[0], p1[1])
t.down()
t.setpos(p2[0], p2[1])
t.setpos(p3[0], p3[1])
# 绘制两侧的边
t.up()
t.setpos(x1, y1)
t.down()
t.setpos(p1[0], p1[1])
t.up()
t.setpos(p3[0], p3[1])
t.down()
t.setpos(x2, y2)
# 函数main()
def main():
print('Drawing the Koch Snowflake...')
t = turtle.Turtle()
t.hideturtle()
# 绘制科赫雪花
try:
drawKochSF(-100, 0, 100, 0, t)
drawKochSF(0, -173.2, -100, 0, t)
drawKochSF(100, 0, 0, -173.2, t)
except:
print("Exception, exiting.")
exit(0)
# 等用户在屏幕上单击后退出
turtle.Screen().exitonclick()
# 调用函数main()
if __name__ == '__main__':
main()