Objects & Graphics

Part 2

We reviewed some stuff from the first lecture on this topic, and began by demonstrating that we could create multiple windows if we so desired! Let's create two windows, win and win2. Realize the power of Classes as we use GraphWin to create multiple instances from itself.

In [1]:
from graphics import GraphWin, Point, Circle
win = GraphWin()
win2 = GraphWin()

With two windows on your screen, you can now draw graphics objects on both; however note that single graphics objects can only be drawn to one window at a time.

In [2]:
p = Point(30, 40)
In [3]:
p.draw(win)
Out[3]:
Point(30.0, 40.0)
In [4]:
p.draw(win2)
---------------------------------------------------------------------------
GraphicsError                             Traceback (most recent call last)
<ipython-input-4-21bc946d1a56> in <module>()
----> 1 p.draw(win2)

~/Library/Python/3.6/lib/python/site-packages/graphics.py in draw(self, graphwin)
    479         is already visible."""
    480 
--> 481         if self.canvas and not self.canvas.isClosed(): raise GraphicsError(OBJ_ALREADY_DRAWN)
    482         if graphwin.isClosed(): raise GraphicsError("Can't draw to closed window")
    483         self.canvas = graphwin

GraphicsError: Object currently drawn

We see that our point p cannot be drawn twice! It was already drawn on win, and therefore it makes no sense to draw it again on win2. No problem, if we wished it to be drawn on win2, we simply "undraw" it and then draw it on the correct window.

In [ ]:
p.undraw()
p.draw(win2)

Name Binding and Aliasing

Here we explore some basic ideas of how objects names work and when what we think is object duplication isn't really duplication!

Before we get into it, let's make a new window, called win, but let's close our original one first:

In [ ]:
win.close()

Then, let's make a new window and assign it to win.

In [18]:
win = GraphWin("Face Demo", 600, 600)

You should be asking yourself where did the extra information in the constructor call to GraphWin come from!? Well, first, it's in the slides and in the documentation (try: help(GraphWin)). Secondly, we are seeing optional function parameters at play here (whether we are talking about class methods, a class constructor call or a plain-ol' function, they are all just functions in the end.)

How does this work? Well, in this first time introduction, we'll keep it light until we hit the 'Functions' topic in fully in a week or so.

Effectively, GraphWin's constructor call allows us to use default values for the title and size of the window. The order of these parameters are defined in the graphics.py file, and can be understood through that call to help I mention above. Once you know the order you can simply call it as:

win = GraphWin("Face Demo", 600, 600)

That is, the title, width and height have to be provided in that order.

Now, the fun thing what if you just want to modify the title, but not the size?

win = GraphWin("Face Demo")

or,

win = GraphWin(title="Face Demo")

What if you don't care about the title, or the width? You just want to give the window a custom height?

win = GraphWin(height=400)

The beauty of optional parameters is they are named! You can leave off the ones you don't care about and provide only the ones you do, so long as you name them explicitly.

Our first example of creating win didn't use names. These optional parameters, when given unnamed suddenly become required, even if you intend on only changing one default value! We'll talk more about this later.

Next… let create an eye!

In our class example, we discussed the start of making a face using the graphics library. So, naturally we start with the eyes. We begin by making a left eye from the Circle class, called left_eye. We set the fill color to red, the outline to yellow and then draw it to the window.

In [19]:
left_eye = Circle(Point(80, 50), 20)
left_eye.setFill('yellow')
left_eye.setOutline('red')
left_eye.draw(win)
Out[19]:
Circle(Point(80.0, 50.0), 20)

Let's move the eye to place it in a better location. We do so using .move() on the Circle instance we called left_eye. The movement is given as a difference of pixel positions, not absolute position. So we are moving the eye 200 pixels right on the X axis from it's current position of X = 80, and 150 pixels down on the Y axis from it's current position of Y = 50.

In [20]:
left_eye.move(200, 150)

Ah! Not quite right, so we adjust once more. This time we move our eye left by -50 pixels on the X axis, but we move 0 pixels in the Y axis.

In [21]:
left_eye.move(-50, 0)

Now, we need a right eye. Let's call it…right_eye! Fine, so you may instictively try this the following assignment, thinking "Hey, I'll just assign another variable the same 'value' as left_eye". Sound logic, right? Will it work? Let's investigate.

In [22]:
right_eye = left_eye
right_eye.move(20, 0)
right_eye.draw(win)
---------------------------------------------------------------------------
GraphicsError                             Traceback (most recent call last)
<ipython-input-22-d3e765be12c2> in <module>()
      1 right_eye = left_eye
      2 right_eye.move(20, 0)
----> 3 right_eye.draw(win)

~/Library/Python/3.6/lib/python/site-packages/graphics.py in draw(self, graphwin)
    479         is already visible."""
    480 
--> 481         if self.canvas and not self.canvas.isClosed(): raise GraphicsError(OBJ_ALREADY_DRAWN)
    482         if graphwin.isClosed(): raise GraphicsError("Can't draw to closed window")
    483         self.canvas = graphwin

GraphicsError: Object currently drawn

We simply assigned right_eye to left_eye. Then we moved it (before drawing it, or so we thought) and then tried to draw it. You should have noticed:

  • The left_eye moved before we even drew the right_eye. Huh? Why?
  • Then, when we pushed forth anyways to draw the right_eye, it claims it was already drawn. Well, that's frustrating.

What you inadvertantely did was 'alias' an object. You created a Circle instance that you named left_eye. Then, the assignment operation of right_eye = left_eye did not create a new object, but simply gave your existing Circle instance a second name, right_eye.

You can verify object ID numbers using the id() function. Each object has a unique identifier given to it. If you give anobject two, or more names, then id() will return the same ID number for all those names, since they refer to the same object in memory.

In [23]:
print(id(left_eye), id(right_eye))
4624114632 4624114632

Yup, our eyes have the same ID. So, how do we resolve this? The graphics library endows its classes with a helpful method called .clone().

In [24]:
# First, move back our left eye which erroneously moved!
left_eye.move(-20, 0)

# Now, clone the eye!
right_eye = left_eye.clone()

Great! Let's confirm they are two different objects:

In [25]:
print(id(left_eye), id(right_eye))
4624114632 4624115304

So, they are distinct objects now, perfect. Let's move the right eye to its final spot.

In [26]:
right_eye.move(100, 0)
right_eye.draw(win)
Out[26]:
Circle(Point(330.0, 200.0), 20)

This demonstrates the basics of Pythons name binding rules! If you like the nitty-gritty details, feel free to read the official documentation at: https://docs.python.org/3/reference/executionmodel.html