Page 1 of 1

classes, de-mystified

#1 DK3250   User is offline

  • Pythonian
  • member icon

Reputation: 486
  • View blog
  • Posts: 1,523
  • Joined: 27-December 13

Posted 09 September 2018 - 07:39 AM

In the Python forum, I see that classes are often not well understood by beginners. I fully know the feeling; not so long ago I was also intimidated by the apparent complexity of classes.
With this tutorial, I hope to de-mystify classes and demonstrate some of the fundamental properties.

The first line of most classes can be frightening:
def __init__(self):

I hope to explain both __init__() and 'self' in this tutorial.

The two major examples are graphical, made using pygame.
Throughout the tutorial, I’ll focus on practical solutions for the beginner.

Many important aspects of classes are not covered by this tutorial. Advances stuff like inheritance, metaclasses etc. are completely absent here.

Class vs. Instance

Despite focusing on practical aspects; one thing, though, must be explained.

The class describes how an object behaves. Attributes and methods (roughly equivalent to variables and functions in main code) are described generically i.e. such that the description fit to all members of the class.

The objects itself (an instance of the class) may then specify the actual quality for itself.
This difference can be hard to understand, but hopefully the example below will explain it.
In code:

RED = (255, 0, 0)

# class description
class Car(color):
    def __init__(self, color):  # define the constructor
        self.color = color  # generic description in the class definition (‘self’ is explained later)

# instance creation
my_car = Car(RED)  # specific attribute in an instance of the class
print(my_car.color)

# Output:

>>> 
(255, 0, 0)
>>>


Variables assigned to an object is called attributes.
In my_car.color, ‘color’ is an attribute. In this case the value of the attribute is…: Not RED but the value of RED namely (255, 0, 0).

What is ‘self’?

The ‘self’ is used as an internal reference to each instance; assigned when the instances are created.

Think of it like this:
Each instance gets an internal tag number that looks like this: <__main__.Car object at 0x02DDE350>.

Sometimes really many instances are created. We don’t want to keep track of hundreds such numbers; the ‘self’ constitutes an automatic reference to this internal instance ID.

A small code example will demonstrate this:
class Car():  
    def __init__(self):
        print(self)  # first print


my_car = Car()
print(my_car)  # second print


# produces this output:
>>> 
<__main__.Car object at 0x02D7BC50>  # from print(self)
<__main__.Car object at 0x02D7BC50>  # from print(my_car)
>>>


So ‘self’ is simply a variable, its value is the instance ID..!

In practice, you don’t have to think about it; just add ‘self’ as prefix inside class descriptions when making the generic code.

Just for the record: You can user other names than ‘self’, all legal variables names will do. DON’T do it. Always use ‘self’ – this is one of the most rigorously implemented conventions of python.


__init__(self) is special

Python has several special functions named with leading and trailing double underscores. It is fully ok to use (and even override) such functions; as long as you know what you are doing. Much of this tutorial is about using this function.

Normal functions require all arguments to be defined upon calling. But as mentioned earlier, the __init__(self) function is special:

The first argument, 'self', is not defined when calling the function; is generated and returned by the function itself, using the argument name: 'self'.


The Christmas Card

We need to see and understand this in real code.
Let’s jump right into the first example and see how a class with many instances can be:
import pygame, sys, random
pygame.init()

X = 600
Y = 600

WHITE = (255, 255, 255)
BG = (126, 126, 255)
screen = pygame.display.set_mode((X, Y))


class Snowflake():
    def __init__(self):
        self.y_pos = 1
        self.x_pos = random.randint(2, X-2)
        self.radius = random.randint(2, 4)

    def fall(self):
        self.y_pos += self.radius-1

    def draw(self):
        pygame.draw.circle(screen, WHITE, (self.x_pos, self.y_pos), self.radius, 0)


all_snow = []

while True:
    screen.fill(BG)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
            
    all_snow.append(Snowflake())  # make a new snowflake
    for snow in all_snow:
        snow.fall()
        snow.draw()
        if snow.y_pos > Y:
            all_snow.remove(snow)  # remove at bottom of screen

    pygame.display.flip()
    pygame.time.wait(25)



A screen dump from this program:

Posted Image

Some readers may have seen this code before as I have used it in an answer in the regular python forum.
The Snowflake class is very simple, but let me add a few comments nevertheless.

By convention a class name (here: Snowflake) is written in PascalCase (similar to camelCase except for Capital starting letter).

Let’s look at the first 5 lines of the class.
class Snowflake():  # defines the class name
    def __init__(self):  # defines the initialization/constructor function and the name, ‘self’, later used as internal reference for the individual instances.
        self.y_pos = 1  # for the individual instances add an attribute, y_pos with value 1 i.e. at the top of the screen
        self.x_pos = random.randint(2, X-2)  # for the individual instances add  a x_pos value
        self.radius = random.randint(2, 4)  # for the individual instances add a radius


Let us make a break here and once again see how the individual instances are generated and stored.

In the main we find something like this (modified here):
all_snow = []  # create an empty list
all_snow.append(Snowflake())  # add an instance of Snowflake to the list


Here we first create an empty list (because we plan to generate a flock of snowflakes and want to keep them in a flexible container).

Next we add one instance, the Snowflake(), to the list. Note, this instance has no name; it is just a member of the list. However, the internal instance ID is easily shown:
print(all_snow)
# outputs:
[<__main__.Snowflake object at 0x0531CDF0>]

In the program we generate instances in a loop. Each instance is created by calling the __init__(self) constructor and each get a unique ID stored in the ‘self’ variable.

Once again:
Instances can be named or un-named; we have already seen examples of both:
my_car = Car()  # a named instance
all_snow.append(Snowflake())  # an un-named instance

Typically named instances are used when only one or few instances are generated and you need to handle them individually.

Un-named instances are typically used when many similar objects are in play; they are normally handled in a container.


Methods

Methods describes the behavior of the objects. Methods are similar to functions in basic python.

Our Snowflake object has two methods: fall(self) and draw(self).
In the fall() method the snowflake moves down the screen; large flakes fall faster than small ones. This creates a small 3D effect and is more exciting to look at than if all flakes fall at same speed.

The draw() method, well.., draws the instances on the screen.

Methods are written using the ‘self’ variable. See line 18-22
Methods are called using dot notation: <instance>.<method()>. See line 36-37


Main Code

Line 25-32 are straightforward; however, if you are new to pygame: All it does is checking for user ending the execution by clicking in the top right corner.

Line 34-39 are interesting; note that this is executed in a never-ending loop.
    all_snow.append(Snowflake())  # in each pass of the loop a new snowflake is made and added to the list
    for snow in all_snow:  # iteration through the list
        snow.fall()  # execution of the fall method
        snow.draw()  # execution of the draw method
        if snow.y_pos > Y:  # check if snowflake has reached the bottom of the screen
            all_snow.remove(snow)  # if at bottom, remove the instance from the list


The code is quite simple, and I have commented it extensively here. One thing, however, is worth noting:

At each pass of the loop we create a new instance. As the loop run every 25 milliseconds, we make 40 instances every second, 2400 every minute. This should not take long to use all memory on the computer resulting in a MemoryError.

We do remove instances from the list, so the list will not grow wild. But what about the Snowflake instances themselves – they are not deleted..?


Garbage Collection

Python has an advanced system for Garbage Collection (GC) i.e. cleaning up memory and freeing memory space no longer needed for program execution.

A detailed description of pythons Garbage Collection is outside the scope of this tutorial, but in short:

If an object is no longer referenced it is considered obsolete and removed. As long as an object is in a list (or other container) or named as a variable, the object is referenced; other part of the code may (is able to) use the object.

In our example, upon deleting the unnamed object from the list there is no chance that we can ever again address this object – nowhere in the code is the object ID known: It is no longer referenced.
This is a sign for the garbage collector to go and free the memory taken up by the object. This is done automatic by python; you don’t have to think about it.

You can delete instances yourself; you need to if a named instance must be deleted. Just use the del keyword:
del my_instance


End of example 1

Despite the diminutive size of the code in example 1, I hope it shed some light on aspect of classes that may be difficult to grasp for the beginner – I certainly was for me; it took years before I became confident in using classes on this fairly simple level.

I have included yet an example, slightly more realistic for a game and introducing a few more basic aspects of classes.


Example 2

Look at this screen dump:

Posted Image

In this demo shooting game all shots are fired from the top-right corner of the white square. The direction and speed of the shot is determined by the mouse position in the square; direction is indicated by the red line and speed by the length of the line.

When a target is hit a new target appears; randomly, some targets are moving, crossing the screen from left to right.
The code is here:
import pygame, sys, random
pygame.init()

X = 600
Y = 600

WHITE = (255, 255, 255)
BG = (126, 126, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GRAVITY = 2

screen = pygame.display.set_mode((X, Y))


class Shot():
    def __init__(self, dx, dy):
        self.x_pos = f_size
        self.y_pos = Y - f_size
        self.dx = dx
        self.dy = dy

    def move(self):
        self.old_x = self.x_pos
        self.old_y = self.y_pos
        self.x_pos += self.dx
        self.y_pos += self.dy
        self.dy += GRAVITY

        self.rect = pygame.Rect(min(self.old_x, self.x_pos),
                                min(self.old_y, self.y_pos),
                                abs(self.old_x - self.x_pos),
                                abs(self.old_y - self.y_pos))

    def draw(self):
        pygame.draw.circle(screen, BLACK, (int(self.x_pos), int(self.y_pos)), 3, 0)


class Target():
    def __init__(self):
        self.reset()
        
    def reset(self):
        self.x_pos = random.randint(f_size, X)
        self.y_pos = random.randint(0, Y)
        self.size = random.randint(5, 15)
        self.rect = pygame.Rect(self.x_pos-self.size, self.y_pos-self.size,
                                self.size*2, self.size*2)
        self.rect.center = (self.x_pos, self.y_pos)
        if random.random() > 0.8:
            self.x_speed = random.randint(5, 10) / 2  # moving target
        else:
            self.x_speed = 0  # fixed target

    def draw(self):
        pygame.draw.circle(screen, RED, (int(self.x_pos), self.y_pos), self.size, 0)

    def move(self):
        self.x_pos += self.x_speed
        self.rect.center = (self.x_pos, self.y_pos)
        if self.x_pos > X:
            self.x_pos = 0

    def hit(self, shot):
        if self.rect.colliderect(shot.rect):
            self.reset()


def draw():
    screen.fill(BG)
    target.move()
    target.draw()
    pygame.draw.rect(screen, WHITE, field, 0)
    pos = pygame.mouse.get_pos()
    if field.collidepoint(pos):
        pygame.draw.line(screen, RED, pos, (f_size, Y-f_size), 2)


def event_handling():
    for event in pygame.event.get():
        
        if event.type == pygame.MOUSEBUTTONDOWN:
            pos = pygame.mouse.get_pos()
            if field.collidepoint(pos):
                dx = (f_size - pos[0]) / 2
                dy = (Y - f_size - pos[1]) / 2
                all_shot.append(Shot(dx, dy))  # create a new shot

        elif event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()


def shot_handling():            
    for shot in all_shot:
        shot.move()
        shot.draw()
        target.hit(shot)
        if shot.y_pos > Y:
            all_shot.remove(shot)  # remove shot at bottom of screen


all_shot = []
f_size = 100
field = pygame.Rect(0, Y-f_size, f_size, f_size)  # defines the shooting field
target = Target()

while True:
    draw()
    event_handling()
    shot_handling()
    pygame.display.flip()
    pygame.time.wait(25)



The Shot() class is in many ways similar to the Snowflake() of last example. The __init__() function, however, is a bit different; it looks like this:
def __init__(self, dx, dy):

Each shot is created with a unique x-speed and y-speed (the variables dx and dy), those values are send to the constructor (the __init__() function) as arguments upon instance creation.

The move() and the draw() methods are similar to the methods in example 1. The self.rect attribute will be explained below.

The Target() class is an example of a named instance. Only one instance is ever made and the reset() method applied at start and when it is hit by a shot.

In the main, the game loop repeatedly calls three functions; draw(), event_handling() and shot_handling().

In the function shot_handling() we see two interesting points:
On line 98
target.hit(shot)
we see how a method of the target object takes an instance of the Shot class as argument.
On line 100 we find:
all_shot.remove(shot)

We see here, again, the structure for removing instances from a list and leaving the rest to the GC.

I only want to additionally explain one small detail about the game loop. As the shots move with quite high speed and the target can be small, it is inadequate to just check for collisions between the objects. Chances are that two sequential shot positions are on either side of the target but ‘magically’ the shot pass through the target without hit.

To counteract this behavior, I create a shot.rect (line 30); it is a small rectangle with the two ‘shot’ positions constituting diagonal corners. Then the collision is checked between this rect and the target rect. As the shot can have both positive and negative y-speed value the math (line 30-33) is slightly complicated.

The idea for example 2 is primarily to demonstrate the interactions in a mix of a single named class object and a group of unnamed objects, all in a game-like environment.

For explanations of the pygame functions please refer to some of my other tutorials e.g. https://www.dreaminc...-part-%231/#1/. This tutorial also uses classes, but do not explain them as thoroughly as here.

Is This A Good Question/Topic? 3
  • +

Replies To: classes, de-mystified

#2 jon.kiparsky   User is offline

  • Beginner
  • member icon


Reputation: 11318
  • View blog
  • Posts: 19,359
  • Joined: 19-March 11

Posted 09 September 2018 - 09:25 PM

Quote

The first argument, 'self', is not defined when calling the function; is generated and returned by the function itself, using the argument name: 'self'.


I have not personally done the spelunking on this, but this seems like an infelicitous description to me. I would say rather that python creates an instance of the object's parent class and calls __init__ with that instance as an argument. Saying that the instance is generated "by the function itself" seems to insert more magic than is really called for. Saying that the function receives and acts on an instance seems much more prosaic and less magical.

This, incidentally, is a helpful way to understand what we can and can't do to self in __init__: __init__ receives a local reference to an instance held by some other function. As usual, it can call methods on that instance and (in most cases) set values on it. However, attempting to reassign the local self or to delete it or otherwise tinker with the instance itself (not just modify its state) will affect only the local reference, not the object held by the other function.

If any novice is reading the above and not entirely sure what to make of it, here is some sample code to ponder.

class Foo(object):
    def __init__(self, x, y):
        x.a = 1     # whatever x was given to us will have a=1
        x = Baz()   # this will affect nothing outside this function
        x.b = 1     # this will affect the Baz instance, 
                    # which will go out of scope when the function exits
                    # bar1 will not be changed. 
        y.a = 17    # again, will affect whatever we were given
        del(x)      # will only affect the local reference
        try:
            x.b = 1     # will fail, since local x is deleted
        except Exception as e:
            print "exception was: " + str(e)
            
class Bar(object):
    # just a simple object for us to abuse
    pass

class Baz(object):
    # just a simple object for us to abuse
    pass


bar1 = Bar()
bar2 = Bar()
foo = Foo(bar1, bar2)


Which produces the following:
exception was: local variable 'x' referenced before assignment 
bar1.a = 1            
type of bar1 is <class '__main__.Bar'>    
bar1 has field b? False                                          
bar2.a = 17                                                                    
type of bar2 is <class '__main__.Bar'>           
bar2 has field b? False 



Once you understand what happens to bar1 and bar2, and what does not happen to them, and why, try to convince yourself that self behaves exactly the same way. Specifically, self is created elsewhere, and the process that created it passes it to __init__ in case you wanted to doll it up in some way (which you don't have to do if you don't want to, but you generally want to). Eventually, the process that created self will exit and return that instance to whatever process wanted an instance in the first place.

At this point it is probably worth considering an error message that all of us have seen:
>>> f = Foo(bar)
Traceback (most recent call last):                                                                                                    
  File "<stdin>", line 1, in <module>                                                                                                 
TypeError: __init__() takes exactly 3 arguments (2 given) 


Wait - "(2 given)"? But I see one argument - what gives??

What gives is that python has created that instance and passed it as the first argument to __init__, appending any other arguments you've passed in after it. This is so perfectly sensible that it's easy to miss that something interesting is going on here. Many beginners, and a surprising number of experienced programmers, equate the instantiation call, Foo(bar) with the __init__ function. This is not correct! Strictly speaking, the instantiation call starts a batch of work which eventually includes a call to __init__ but is not identical to it.
Was This Post Helpful? 1
  • +
  • -

#3 DK3250   User is offline

  • Pythonian
  • member icon

Reputation: 486
  • View blog
  • Posts: 1,523
  • Joined: 27-December 13

Posted 10 September 2018 - 12:26 AM

@jon.kiparsky: Thank you for your valuable input; much appreciated.

I think your description requires more insight than we can expect from beginners and intermediate python users; those are the target group for my tutorial.

Actually, I believe that your description may risk re-mystify the class mechanics for the target group.

Would you agree to this wording:

The first argument, 'self', is not defined when calling the function; is generated by some advanced class mechanics and returned using the argument name: 'self'.
Was This Post Helpful? 0
  • +
  • -

#4 andrewsw   User is offline

  • head thrashing
  • member icon

Reputation: 6644
  • View blog
  • Posts: 27,193
  • Joined: 12-December 12

Posted 10 September 2018 - 02:28 AM

At the risk of adding mud to waters, I prefer to use the terminology that 'self' is an implicit reference to the current instance of the class.

We can be explicit, so that

MyClass.instance_method(my_instance, 4)


is equvalent to

my_instance.instance_method(4)

We can see by comparing the two that, in the second case, my_instance is implicitly referenced, or injected, to the method call.

It also helps when we note that the omission of self creates a method on the class, not on an instance, def class_method(x):.

class MyClass(object):
    def __init__(self):
        print ('init...')
    
    def instance_method(self, x):
        print (x * 2)
    
    def class_method(x):
        print (x * 3)

my_instance = MyClass()

my_instance.instance_method(4)
# is the same as 
MyClass.instance_method(my_instance, 4)
# as opposed to
MyClass.class_method(5)
# does not reference an instance


Also, with def __init__(self): 'init' means 'initialise', not 'instantiate'; the instance-reference 'self' already exists when the __init__ method is called. As described above, it is an (optional) method that is called to initialise/dress up the object after it is created (instantiated).
Was This Post Helpful? 1
  • +
  • -

#5 baavgai   User is offline

  • Dreaming Coder
  • member icon


Reputation: 7318
  • View blog
  • Posts: 15,230
  • Joined: 16-October 07

Posted 10 September 2018 - 04:41 AM

We love you, it's why we abuse you... So, a few critiques in the the second one.

The self.old_x should be old_x. An instance level variable should ONLY maintain the state of the instance. To that end, even though you can, you shouldn't declare any self. vars outside the init. If they get initialized elsewhere, then setting them to None in the init is still good practice.

Global vars: again, just because you can, doesn't mean you should.

Here's an exercise: Take what you have and put all the global stuff in a function main. (Your Python program should always have an entry point, anyway, rather than stuff just laying all over the place. ) This will break everything, of course, so start passing only the values needed by each function and class to it until all is unbroke. Then, see if you might find some way to make an App class, the one that holds your screen instance, to wrangle up all those wondering globals. Indeed, identify the "state" that the application will need to maintain to run.

Full disclosure, I did already do this, though I'm unsure if it's appropriate to throw it in here, as it's your party.

I will note that there does seem to be an opportunity to illustrate a base class with shot and target. e.g.
class Sprite(object):
    def __init__(self, x, y, color):
        self.x, self.y, self.color = x, y, color

    def move(self):
        pass

    def draw(self):
        pass

    # as this always seems to happen
    def move_draw(self):
        self.move()
        self.draw()

    # perhaps
    def bounding_rect(self):
        pass




Again, not trying to piss in the cornflakes here. Just hard not to critique the code, particularly if enthusiastic newbs will use it as a jumping off point.
Was This Post Helpful? 1
  • +
  • -

#6 jon.kiparsky   User is offline

  • Beginner
  • member icon


Reputation: 11318
  • View blog
  • Posts: 19,359
  • Joined: 19-March 11

Posted 10 September 2018 - 08:40 AM

View PostDK3250, on 10 September 2018 - 02:26 AM, said:

Would you agree to this wording:

The first argument, 'self', is not defined when calling the function; is generated by some advanced class mechanics and returned using the argument name: 'self'.


Still not quite right, I think. The key insight is that SomeClass.__init__ is just another function. It takes at least one argument, which is an instance of SomeClass's superclass, plus any other args needed, and its purpose is to alter the state of that superclass instance so that it is an acceptable SomeClass instance. This is done by modifying the instance's state in perfectly ordinary ways, ie, setting fields on the class to either default values or to values derived from the args provided.

The important thing is that the person defining __init__ need not worry about defining self or returning it. They may assume that it is an instance of SomeClass's superclass, and that their role is to specify just the state that SomeClass requires to be a well-made instance.

There are some complexities that I'm eliding here - mostly about chained constructors - but if these insights are communicated, the user will probably be able to work with class init functions without too much difficulty.
Was This Post Helpful? 1
  • +
  • -

#7 DK3250   User is offline

  • Pythonian
  • member icon

Reputation: 486
  • View blog
  • Posts: 1,523
  • Joined: 27-December 13

Posted 10 September 2018 - 09:43 AM

OK, often I am receiving very little feedback on my tutorials; this time it's a bit overwhelming..!

First of all: Thank you to all for the great feedback. Yours insight is astonishing and I try to learn as much as as can.

Secondly: My tutorial is targeted at beginners and intermediate programmers; the aim is to make use of simple classes easier and less mysterious. I still think it does that.

Even when I wrote it I knew that the description of 'self' was not 100% correct, but I still find that a small inaccuracy is a small price for increased access.
Admittedly, some of the details about __init__ and self were not 100% clear to me; thank you, again, for clarifying.
I still feel, however, that your replies are over the limit for the target group.
As mentioned above, the increased exactness has a price of reduced accessibility.

To baavgai: Yes, old_x is sufficient, self.old_x is superfluous. In many other tutorials I use a game object (see for example: https://www.dreaminc...game-part-#2/); not exactly what you recommend, but in the same ballpark.
But, you know what: I'll take your challenge and see if I can wrap all code as described by you.
It is rather out of scope for the tutorial, but as long as there is some decent learning to make....

Do you mean the Sprite class should be parent to the Shot and the Target classes? I don't see the advantage, but it might make an ok demo of inheritance.

Let me give it a try before submitting your solutions.
Was This Post Helpful? 0
  • +
  • -

#8 jon.kiparsky   User is offline

  • Beginner
  • member icon


Reputation: 11318
  • View blog
  • Posts: 19,359
  • Joined: 19-March 11

Posted 10 September 2018 - 10:49 AM

As long as the phrase "just another function" is in there, I think you'll be in good shape. :)

And thanks for the excuse to think into the __init__ issues a little more deeply, I hadn't ever really mulled over what was going on there before.
Was This Post Helpful? 0
  • +
  • -

#9 baavgai   User is offline

  • Dreaming Coder
  • member icon


Reputation: 7318
  • View blog
  • Posts: 15,230
  • Joined: 16-October 07

Posted 10 September 2018 - 10:51 AM

View PostDK3250, on 10 September 2018 - 11:43 AM, said:

Do you mean the Sprite class should be parent to the Shot and the Target classes? I don't see the advantage, but it might make an ok demo of inheritance.


Fair enough. Given the nature of Python, with duct typing, inheritance doesn't often buy you as much as in other languages.

My final version looks like:
# base class for moving things
class Sprite(object):
    def __init__(self, x, y, color):
        self.x, self.y, self.color = x, y, color

    # helpers
    def point(self):
        return (int(self.x), int(self.y))

    def move_draw(self, screen):
        self.move()
        self.draw(screen)

    def collition(self, other):
        return self.bounding_rect().colliderect(other.bounding_rect())

    # need implementing
    def move(self):
        pass

    def draw(self, screen):
        pass

    def bounding_rect(self):
        pass



The collision thing there is fun.

You probably see more advantage with the two targets:
class Target(Sprite):
    def __init__(self, x, y, color):
        Sprite.__init__(self, x, y, color)
        self.size = random.randint(5, 15)

    def draw(self, screen):
        pygame.draw.circle(screen, self.color, self.point(), self.size, 0)

    def bounding_rect(self):
        r = pygame.Rect(self.x-self.size, self.y-self.size, self.size*2, self.size*2)
        r.center = (self.x, self.y)
        return r



class MovingTarget(Target):
    def __init__(self, x, y, color, max_x):
        Target.__init__(self, x, y, color)
        self.x_speed = random.randint(5, 10) / 2  # moving target
        self.max_x = max_x

    def move(self):
        self.x += self.x_speed
        if self.x > self.max_x:
            self.x = 0



And, in the main object:
class Game(object):
...
    def draw(self):
        self.screen.fill(Color.BG)
        self.target.move_draw(self.screen)
        pygame.draw.rect(self.screen, Color.WHITE, self.field, 0)
        self._draw_line(pygame.mouse.get_pos())
        for shot in self.all_shot:
            shot.move_draw(self.screen)
            if self.target.collition(shot):
                self.target = self.create_target()
            if shot.y > self.width:
                self.all_shot.remove(shot)




I'll throw in the main. Because, to my mind, your Game/App instance is responsible for implementing three basic things, a draw, an event handler, and a done state. With those, the game loop becomes almost entirely generic. Indeed, it can be completely generic if you pass it a game instance or make a game base class that implements the loop:
 
def main():
    pygame.init()
    game = Game(600, 600)
    while not game.done:
        game.event_handling()
        if not game.done:
            game.draw()
            pygame.display.flip()
            pygame.time.wait(25)
    pygame.quit()
    # no, never, bad sys.exit()


Was This Post Helpful? 0
  • +
  • -

Page 1 of 1