目录
如今数学界中很多分析方法都被科学界广泛使用,比如图像和解析式。他们用于数学本身,比如研究发展微积分,线性代数,非欧几何,以及统计学等,而这些数学领域的知识又广泛应用于自然科学的各类研究。从图像到解析式本身是一项壮举,因为他把几何与代数联系了起来。这让研究自然科学更容易了,因为你无法在摆钟上看到一个他高度和动能的关系式,但是你可以通过随着时间变化,摆钟的高度变化作一个图像,再同理做一个动能的变化图像,通过图像研究他的代数解析式和图像关系,然后再用数学的法则运算得到最终结果后,总结出规律。
但现在的问题是,大多图像都是一条或几条平滑的曲线,不是我们能用一把尺子就能画的出来的直线。这个时候,我们就需要计算机的帮助,我们希望我们输入一个解析式,他就能返回对应的图像,或者...没有图像。好,我们现在开始了解他是怎么实现图像绘制的吧
#画坐标轴,标箭头,写坐标点
import turtle
def draw_line(x,y,heading,length,color):
#画坐标轴
turtle.pensize(4)
turtle.shape("circle")
turtle.up()
turtle.goto(x,y)
turtle.setheading(heading)
turtle.down()
turtle.pencolor(color)
turtle.fd(length)
#标x,y轴箭头
turtle.left(135)
turtle.fd(15)
turtle.back(15)
turtle.setheading(heading)
turtle.right(135)
turtle.fd(15)
turtle.back(15)
#坐标轴信息
turtle.setheading(heading)
turtle.up()
turtle.fd(20)
turtle.down()
if heading==0:
turtle.write("x",font=("arial",24,"bold"))
if heading==90:
turtle.write("y", font=("arial", 24, "bold"))
这段画x,y轴的代码要分三部分。1.画x,y坐标轴 2.给轴标上箭头表示正x和正y的方向 3.写坐标轴信息---给坐标轴写上x,y的标签
画坐标轴说白了就是画两条互相垂直的线段,就是美化线段的代码比较多。我们先设置线段粗细,设置海龟画笔形状,然后抬笔并落到开始绘制的位置,然后turtle.setheading()设置绘制初始角度(默认0°是海龟画笔朝右,水平于屏幕。比如,如果是画y轴,就设置turtle.setheading(90))。接下来选择绘制线段的颜色,落笔,然后画笔就向前走一段距离,抬笔。这样我们就完成了一个轴的绘制。
在原来位置的基础上向左旋转135°,向前画十五格,再退回到原来位置和绘制前角度,然后向右边旋转相同度数,向前,向后,哎,完成箭头绘制!
首先写之前方向要清楚!然后才到抬笔,海龟画笔按方向移动20格距离,然后落笔开始写信息。如果heading==0,即此时绘制的是x轴,就写上x,第二个参数负责字体样式(这里是"arial")字体大小(这里大小为24),和其他装饰(这里选了"bold"即加粗字体)。y轴类似
import turtle
import os
class Scale:
@classmethod
def mark(cls, list_num, heading, convert): # list_num存储了坐标轴的所有刻度值
turtle.pensize(1)
turtle.setheading(heading)
turtle.up()
for i in list_num:
if heading == 90: # x轴刻度垂直于屏幕
turtle.goto(i, 0)
turtle.color("black")
elif heading == 0:
turtle.goto(0, i) # i 代表高度
turtle.color("blue")
turtle.down()
turtle.fd(15) # 轴刻度长15格
turtle.up()
turtle.back(30) # 后退30格来标注刻度值
turtle.write(cls.isdiv(i, convert), align="center", font=("arial", 6))
turtle.up()
turtle.home() # 回到起始位置准备下一步的绘制
@classmethod
def isdiv(cls, value, cvt): # value参数表示的是当前刻度值
if value%cvt!=0:
return round(value/cvt, 1) # 若不整除则保留一位小数
else:
return int(value/cvt)
这段代码主要分两个部分,第一个是绘制轴刻度和轴刻度值,一个是判断生成的轴刻度值列表中的所有数是否整除转换。那什么是转换呢?因为海龟画函数图像的原理是:根据解析式,得到一定范围内整数x和y值,每个整数x和其对应的y组成一个点。每个整数非常小,所以一个正常大小的图像需要的刻度值就很大。为了让轴刻度值尽可能地按照数学界的大小走,我们将实际刻度值除以一个数让他变小一些。
在类的mark()函数中第一行设置画笔大小为一,然后就是设置绘制方向,如果是画x轴的坐标轴刻度那画的方向就是90°,垂直于屏幕,y轴的相反。接下来就是一个循环。list_num是什么东西?他在主程序中会有详细信息。他大概记载了轴刻度要画的位置。抬笔之后就是一个判断,通过判断setheading()内的heading参数确定要去的位置,从而使其能够在正确的位置画出相应的x,y坐标刻度。接下来落笔,按heading方向向前画15个单位,并在画笔退三十个单位长度后使用write()函数写坐标刻度值。
想完全理解这一段要先阅读下面主程序的“检测环节”内容,我现在做一个简单的介绍,为什么需要isdiv()方法。我们一般会在画刻度后标上相应的刻度值,这样便于我们涉足非欧几何领域。但是如果这个刻度值是以海龟库的单位长度为基准的话,每个刻度值将会很大。而这并不是我们想看到的,原因是这样不便于我们观察研究函数图像,也不方便我们后续用代数方法去计算得到该函数的其他性质。所以我们需要一个参数cvt,用他来除这个刻度值,让其变小一点。
考虑到还有除不尽的问题,我又在isdiv()方法中设立了if ...else条件判断语句。如果不取余不为零,就保留商的一位小数,整除则另起一行直接返回商。毕竟18.0,20.0这样的数字并不好看,同理,18.04738,20.89208也丑陋无比。需要注意的是,我们调用isdiv()到另一个类方法mark()中使用的格式是cls.isdiv()而不是isdiv()。这个语法不是无缘无故出现的,他告诉编译器isdiv()是Scale类中的另一个方法,而不是随便从哪里蹦出来的一个函数。
import turtle, os, math
import xy_axis#导入存放绘制x,y轴的程序的源文件
from mark_label import Scale#导入存放绘制标坐标轴刻度的程序的源文件
def draw_func(a,b,func,cvt,color):#参数b,a是分别是函数定义域的上界和下界
turtle.speed(0)
turtle.pensize(2)
turtle.color(color)
turtle.penup()
for x in range(a,b,5):
y=func(x)
turtle.goto(x,y)
turtle.pendown()
if x%100==0:#标记能整除一百的坐标
turtle.dot(6,"blue")
turtle.up()
turtle.goto(x+13,y)
turtle.write(f"({int(x/cvt)},{int(y/cvt)})")
turtle.goto(x,y)
turtle.down()
def line(x):
y=2*x+100
return y
def curve2(x):
y=1/20*x*x-1/40*x
return y
def curve3(x):
y=1/2000*x*x*x-1/50*x
return y
def sin(x):
y=math.sin(x*20)*40
return y
def curve4(x):
y=math.sqrt(x)*10
return y
#检测正/负轴长能否被间隔长整除,这保证了所有间隔都相等
def isvalid_len(length,interval):
if (length/2)%interval==0:
return length
else:
print("invalid length or step value")
os._exit(0)
#检测比例因子是否为整数
def dec(num):
if len(str(num).split("."))>1:
return True
else:
return False
#用户输入和检测环节
step = 30
length = 720
cvt1=6
obj=Scale()
R1=isvalid_len(length,step)#R1表示刻度间隔长
if cvt1<0 or cvt1>step or dec(cvt1):
print("invalid convertion")
os._exit(0)
list_num=[-int(R1/2)+i*step for i in range(int(R1/step+1))]#list comprehension
print(list_num)
#绘制坐标轴,这里把坐标轴画在了海龟画布正中央。
turtle.setup(900, 900)
xy_axis.draw_line(-int(R1/2), 0, 0, R1, "black")
xy_axis.draw_line(0, -int(R1/2), 90, R1, "blue")
#绘制轴刻度
obj.mark(list_num, 0, cvt1)
obj.mark(list_num,90,cvt1)
#绘制函数图像
colors=["red","darkblue","orange","lightblue","darkgreen","darkgoldenrod"]
lst=[line,curve2,curve3,sin]
for i in range(len(lst)):
draw_func(-85, 90, lst[i],cvt1, colors[i%len(colors)])
draw_func(0,200,curve4,cvt1,"darkgreen")#该函数图像绘制需要单独列出,因为定义域是[0,∞)
turtle.done()
这段代码列举了绘制一次,二次和三次函数,三角函数和幂函数的功能。三次函数要注意,因为函数很陡峭,所以我们尽量把x控制在[-90,90]的范围内,不然到时候你等一分钟都等不到他画完。
该代码分几部分,分别是:1.函数从解析式到图像的绘制方法 2.函数解析式 3.检测用户输入的进率和轴长是否符合规范 4.主程序,绘制坐标轴和函数
这个绘制方法其实是很简单易懂的,原理和我们上学时学的什么5点法画曲线相像,只是说他会在函数(在程序中是参数func)的定义域中每个整数x上都取一个点,所以画的满精准。注意,这里 if x%100==0 条件语句内的代码块会把x值整除100的点标上,大家可以根据需要,指定程序去标一些特殊点,比如y==0的点(可以用来求高次方程的近似解),或者和其他函数的交点。
这里用到了math库,第一个绘制一次函数f(x)=kx+b(k!=0),然后依次是二次f(x)=ax^2+bx+c(a!=0)、三次函数f(x)=ax^3+bx^2+cx+d(a!=0)、正弦函数f(x)=sin(x)和幂函数f(x)=x^α,其中α等于1/2,可以推出定义域为[0,∞)。
另外,这里一定要注意把函数解析式的系数写小一些,因为之前提过,海龟库的单位1比数学中的长度要小很多,所以如果按正常系数大小来标,你得到的将是一个缩小很多倍的函数曲线——这对于做研究很不方便。
“用户输入”代码块中找到变量length了么?他表示的是x/y坐标轴的长度(注:x,y轴长度相等,且正轴和负轴长度相等)。那么给他除以二就是把轴拆成了两段,一段是负数轴,一段是正数轴。我们目前想要解决的问题是,到底mark()函数要在轴上画多少个刻度。因为轴刻度之间间隔相等,那不难得出,要画的刻度数量=轴总长(length)/刻度间隔长(step)+1。
有了思路,我们看代码。坐标轴上所有刻度的绘制可以用一个循环实现,该循环从负半轴第一个刻度小突起开始,每次循环都给他加上一个间隔,并将该结果储存至刻度坐标列表list_num中。那要进行多少次循环呢?在程序中是循环总次数可以写成:int(R1/step+1)。这里加一是因为总间隔数量总比总刻度数量要少一。你说list_num列表生成式看不懂?没关系,我们把它拆开来看。
list_num=[-int(R1/2)+i*step for i in range(int(R1/step+1))]
#等价于以下代码:
startPos=-int(R1/2)#坐标起始位置
number=int(R1/step)
list_num=[]
for i in range(number+1):
list_num.append(startPos+i*step)#每一个元素都是坐标刻度的位置
注:我们为了让坐标系处于海龟画布的中心,才把坐标起始位置设成了轴总长的一半的相反数-int(R1/2)。懂了list_num这个记录轴刻度位置的列表,我们审视一下其他环节。
isvalid_len()函数
前面我们提到list_num。大家有没有想过,如果正负半轴长(length/2)无法被刻度间隔长step整除会怎么样(比如length是个奇数)?那么就会出现正负半轴长短不一致的情况。而且因为除不尽,轴刻度的实际间隔并不完全相等。我们想要的是一个好看又对称的坐标轴,所以我们要避免这个情况。isvalid_len()函数华丽登场!他可以检测用户输入的轴长的一半是否能被间隔整除,一旦不能被整除,就打印“无效输入”并用os._exit(0)停止程序。
怪了,那为什么不是轴长,而是他的一半呢?举个例子,间隔大小20,用户输入轴长180。虽然180确实可以被20整除,但是90/20=4.5......每个半轴有4.5个间隔?那这样原点在哪里呢?所以要用半轴长90取余间隔20,如果余数不为零,就表示他无法被整除,就说明length或step的值有问题,反之亦然。
dec()函数
另外还有一个检测cvt1大小的代码块。cvt1是一个调整比例的数,所以他既不能大于间隔,也不能小于0,还不能是小数。判断大小的在dec()函数下面的“用户输入和检测环节”中有写,这里就不再赘述。而判断cvt1是否为小数的函数是dec()。他把整型转为字符串,并调用split()函数,以"."为标识符,将该字符串的小数部分和整数部分分开并存储进列表中,返回该列表。随后我们再用len()判断列表中有两个还是一个元素。两个说明是小数,反之是整数。
接下来就是主程序了,分为三个部分,绘制坐标轴,绘制轴刻度,绘制函数。(可能内容会有与之前重复的部分)
turtle.setup()是设置海龟画布的大小,有他没他都无所谓,全凭个人喜好。draw_line()第一第二个参数是x,y值,他代表轴开始画的具体坐标,反正在负半轴。第三个参数是海龟画笔的起始绘制方向。输入0代表横着画,水平于屏幕;90代表竖着画,垂直于屏幕。绘制轴刻度没什么好说的
绘制函数直线曲线这里使用了个for循环遍历列表lst。lst里面每一个元素都是一个对应关系,这样一来,下次再新添加函数解析式时,只要把他门的自定义函数名写到列表lst中就可以生成它对应的图像了,多方便!当然,像y=x^(1/2)这种x定义域不能为负的函数就要单独拎出来处理,不过除了他这种以外,其他函数都可以在循环中,参数基本都是一样的。颜色用的是老方法:取余循环取出列表colors中的元素当作直线/曲线的颜色。
补充
在这里补充一个有趣的数学现象。我们注意到,如果刻度间隔长step能被cvt整除的话,所有坐标值都能被cvt整除。在初等数论中,我们有:若a|b, b|c 则a|c(a, b, c均为整数)。在列表生成式的分解中我们看到,每一个坐标刻度值都可以用startPos+i*step表示。我们已经用isvalid_len()函数测过,length/2是可以被step整除的,且step整除他自己。根据另外一条定理:若a|b, a|c 则a|bx+cy (x,y均是不为零的整数),因此我们知道step|startPos+i*step,那么cvt|startPos+i*step(cvt为整数,step为整数),证毕。
海龟库只能实现一些基本图像的绘制,且这个绘制方法对于绘制三角函数,指数对数类函数等还是有一定限制的——毕竟我们从他的绘制原理不难看出他的绘制精度还有待提高。他实际上不是专门设计出来绘制图像的库。随着大家学习的深入,会接触到像matplotlib,numpy这种强大的第三方库,使用它绘制各类函数图像会更加的简单快捷,因为他的方法都是包装好了的,你不需要写多少代码他就能帮你生成图像。
不过不论是海龟还是numpy,他们之间绘制图像的底层逻辑都是十分相似的,因此这篇文章足以让你了解部分底层逻辑,拓展知识。