Python turtle库实现基本剖析

时间:2022-07-28
本文章向大家介绍Python turtle库实现基本剖析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

有关turtle的相关使用请参考《python图形绘制库turtle中文开发文档及示例大全》

本篇文为turtle库的实现剖析,但不涉及 python 的 TK库。

开始

入口探寻

在turtle中,直走是使用 forward 或者 fd 函数;在本机安装好了 turtle 库后,在以下的目录下找到了 turtle.py 文件:

我们先从常规的方式从入口开始探究turtle库的基本实现;新建一个turtle对象:

tt=Turtle()

在文件中找到 class Turtle:

class Turtle(RawTurtle):
    """RawTurtle auto-creating (scrolled) canvas.

    When a Turtle object is created or a function derived from some
    Turtle method is called a TurtleScreen object is automatically created.
    """
    _pen = None
    _screen = None

    def __init__(self,
                 shape=_CFG["shape"],
                 undobuffersize=_CFG["undobuffersize"],
                 visible=_CFG["visible"]):
        if Turtle._screen is None:
            Turtle._screen = Screen()
        RawTurtle.__init__(self, Turtle._screen,
                           shape=shape,
                           undobuffersize=undobuffersize,
                           visible=visible)

从注释中可以的到此类将会自动创建 TurtleScreen 对象以及 canvas,这一点在 __init__ 方法中有代码过程;之后调用了 RawTurtle__init__ 创建 turtle的动画部分,实现如下:

	screens = []

    def __init__(self, canvas=None,
                 shape=_CFG["shape"],
                 undobuffersize=_CFG["undobuffersize"],
                 visible=_CFG["visible"]):
        if isinstance(canvas, _Screen):
            self.screen = canvas
        elif isinstance(canvas, TurtleScreen):
            if canvas not in RawTurtle.screens:
                RawTurtle.screens.append(canvas)
            self.screen = canvas
        elif isinstance(canvas, (ScrolledCanvas, Canvas)):
            for screen in RawTurtle.screens:
                if screen.cv == canvas:
                    self.screen = screen
                    break
            else:
                self.screen = TurtleScreen(canvas)
                RawTurtle.screens.append(self.screen)
        else:
            raise TurtleGraphicsError("bad canvas argument %s" % canvas)

        screen = self.screen
        TNavigator.__init__(self, screen.mode())
        TPen.__init__(self)
        screen._turtles.append(self)
        self.drawingLineItem = screen._createline()
        self.turtle = _TurtleImage(screen, shape)
        self._poly = None
        self._creatingPoly = False
        self._fillitem = self._fillpath = None
        self._shown = visible
        self._hidden_from_screen = False
        self.currentLineItem = screen._createline()
        self.currentLine = [self._position]
        self.items = [self.currentLineItem]
        self.stampItems = []
        self._undobuffersize = undobuffersize
        self.undobuffer = Tbuffer(undobuffersize)
        self._update()

创建完一个turtle对象后,调用一下 forward 函数画一根线段。

我们打开 turtle 文件,按照一般形式的函数定义,查询 forward 函数的定义:

从注释中了解到,调用函数可以使用 forward | fd ,参数为传入一个距离;具体使用方法请参考文章头标注的文章,在这里并不做太多解释。

在 forward 函数底部,发现调用了 _go 方法:self._go(distance) 。查看 _go 方法:

def _go(self, distance):
        """move turtle forward by specified distance"""
        ende = self._position + self._orient * distance
        self._goto(ende)

在 _go 方法中,传入了 距离,并且 ende 赋值为 self._position + self._orient * distance ,先搞懂 _position 、_orient 、distance 这几个成员是什么东西。

_go 方法位于 TNavigator 类中,在 TNavigator 的 init 方法中,使用了 reset 方法,reset方法中有 _position 、_orient 的初始化:

def reset(self):
        """reset turtle to its initial values

        Will be overwritten by parent class
        """
        self._position = Vec2D(0.0, 0.0)
        self._orient =  TNavigator.START_ORIENTATION[self._mode]

我们再查看 Vec2D :

class Vec2D(tuple):
    """A 2 dimensional vector class, used as a helper class
    for implementing turtle graphics.
    May be useful for turtle graphics programs also.
    Derived from tuple, so a vector is a tuple!

    Provides (for a, b vectors, k number):
       a+b vector addition
       a-b vector subtraction
       a*b inner product
       k*a and a*k multiplication with scalar
       |a| absolute value of a
       a.rotate(angle) rotation
    """
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    def __add__(self, other):
        return Vec2D(self[0]+other[0], self[1]+other[1])
    def __mul__(self, other):
        if isinstance(other, Vec2D):
            return self[0]*other[0]+self[1]*other[1]
        return Vec2D(self[0]*other, self[1]*other)
    def __rmul__(self, other):
        if isinstance(other, int) or isinstance(other, float):
            return Vec2D(self[0]*other, self[1]*other)
    def __sub__(self, other):
        return Vec2D(self[0]-other[0], self[1]-other[1])
    def __neg__(self):
        return Vec2D(-self[0], -self[1])
    def __abs__(self):
        return (self[0]**2 + self[1]**2)**0.5
    def rotate(self, angle):
        """rotate self counterclockwise by angle
        """
        perp = Vec2D(-self[1], self[0])
        angle = angle * math.pi / 180.0
        c, s = math.cos(angle), math.sin(angle)
        return Vec2D(self[0]*c+perp[0]*s, self[1]*c+perp[1]*s)
    def __getnewargs__(self):
        return (self[0], self[1])
    def __repr__(self):
        return "(%.2f,%.2f)" % self

查看 Vec2D 后得知,其实也就是一个元组;在查看 TNavigator 类:

class TNavigator(object):
    """Navigation part of the RawTurtle.
    Implements methods for turtle movement.
    """
    START_ORIENTATION = {
        "standard": Vec2D(1.0, 0.0),
        "world"   : Vec2D(1.0, 0.0),
        "logo"    : Vec2D(0.0, 1.0)  }
    DEFAULT_MODE = "standard"
    DEFAULT_ANGLEOFFSET = 0
    DEFAULT_ANGLEORIENT = 1

随后查看 TNavigator.START_ORIENTATION[self._mode],在 TNavigator 类中得知 _modestandard。此时 TNavigator.START_ORIENTATION[self._mode]Vec2D(1.0, 0.0)

接下来查看 _goto 方法:

def _goto(self, end):
        """Move the pen to the point end, thereby drawing a line
        if pen is down. All other methods for turtle movement depend
        on this one.
        """
        ## Version with undo-stuff
        go_modes = ( self._drawing,
                     self._pencolor,
                     self._pensize,
                     isinstance(self._fillpath, list))
        screen = self.screen
        undo_entry = ("go", self._position, end, go_modes,
                      (self.currentLineItem,
                      self.currentLine[:],
                      screen._pointlist(self.currentLineItem),
                      self.items[:])
                      )
        if self.undobuffer:
            self.undobuffer.push(undo_entry)
        start = self._position
        if self._speed and screen._tracing == 1:
            diff = (end-start)
            diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2
            nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed))
            delta = diff * (1.0/nhops)
            for n in range(1, nhops):
                if n == 1:
                    top = True
                else:
                    top = False
                self._position = start + delta * n
                if self._drawing:
                    screen._drawline(self.drawingLineItem,
                                     (start, self._position),
                                     self._pencolor, self._pensize, top)
                self._update()
            if self._drawing:
                screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)),
                                               fill="", width=self._pensize)
        # Turtle now at end,
        if self._drawing: # now update currentLine
            self.currentLine.append(end)
        if isinstance(self._fillpath, list):
            self._fillpath.append(end)
        ######    vererbung!!!!!!!!!!!!!!!!!!!!!!
        self._position = end
        if self._creatingPoly:
            self._poly.append(end)
        if len(self.currentLine) > 42: # 42! answer to the ultimate question
                                       # of life, the universe and everything
            self._newLine()
        self._update() #count=True)

在 goto_方法中,最开头的注释说明了该方法的作用“从当前的位置移动到传入的end参数坐标点,在移动的过程中,绘制出线段,并且所有的 turtle 绘制方法都基于这个 goto_方法”。goto_方法中,开始定义了一个元组 go_modes :

go_modes = ( self._drawing,
                     self._pencolor,
                     self._pensize,
                     isinstance(self._fillpath, list))

在go_modes 元组中,传入了 _drawing、_pencolor、_pensize,并且调用了 isinstance 方法判断 _fillpath 是否为 list;并且接下来构造了一个 undo_entry元组。判断 if self.undobuffer: 后,为空或者Null 则 self.undobuffer.push(undo_entry)。之后为默认状态下的绘制方法:

if self._speed and screen._tracing == 1:
            diff = (end-start)
            diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2
            nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed))
            delta = diff * (1.0/nhops)
            for n in range(1, nhops):
                if n == 1:
                    top = True
                else:
                    top = False
                self._position = start + delta * n
                if self._drawing:
                    screen._drawline(self.drawingLineItem,
                                     (start, self._position),
                                     self._pencolor, self._pensize, top)
                self._update()
            if self._drawing:
                screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)),
                                               fill="", width=self._pensize)

其中 start为当前位置的坐标点,end是目标位置的坐标点,以上最主要的方法中最重要是:_drawline,使用_drawline传入了配置参数、坐标序列、笔颜色、绘制线的宽度以及 是否指定 polyitem;(具体坐标序列的算法我没搞清楚,希望有知道的同学可以告诉我这是咋算的,是什么公式,谢谢!)。

查看 _drawline 的实现:

def _drawline(self, lineitem, coordlist=None,
              fill=None, width=None, top=False):
    """Configure lineitem according to provided arguments:
    coordlist is sequence of coordinates
    fill is drawing color
    width is width of drawn line.
    top is a boolean value, which specifies if polyitem
    will be put on top of the canvas' displaylist so it
    will not be covered by other items.
    """
    if coordlist is not None:
        cl = []
        for x, y in coordlist:
            cl.append(x * self.xscale)
            cl.append(-y * self.yscale)
        self.cv.coords(lineitem, *cl)
    if fill is not None:
        self.cv.itemconfigure(lineitem, fill=fill)
    if width is not None:
        self.cv.itemconfigure(lineitem, width=width)
    if top:
        self.cv.tag_raise(lineitem)

以上文章暂未全部剖析实现,之后将会更新。