Calling class method affects all Class instances

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
gabowave
Newbie
Posts: 10
Joined: Sat Jul 08, 2023 4:15 pm
Contact:

Calling class method affects all Class instances

#1 Post by gabowave »

Hello, I am having a strange error regarding my class instances. For context: I've been working on a phone interface, my problem specifically targets the messaging capabilities and affects my Contacts class. For some reason, when I call a method [Namely JohnnyC.addMessage(blah blah blah)] It not only adds the message for the instance JohnnyC's messages list, but also for DianaC's messages list despite not having called said class at all. I've tried debugging a lot of it myself and even went as far as using ChatGPT in case I missed something and can't for the life of me figure out what's going on. I will attach code from my game, and some code showcasing how I'm calling the code.

Code: Select all

    class Contact():
        def __init__(self, cID, name, last, timerP, imglist=".webp", imglist2=".webp", img='.webp', messages = [], unreads = 0, waitlist= []):
            self.cID = cID
            self.name = name
            self.last = last
            self.timerP = timerP
            self.imglist = imglist
            self.imglist2 = imglist2
            self.img = img
            self.messages = messages
            self.unreads = unreads
            self.first = False
            self.wait = 0
            self.waitlist = waitlist
            self.lastwait = None
            self.choice1 = None
            self.choice2 = None
            self.choice3 = None
            self.choice1label = None
            self.choice2label = None
            self.choice3label = None
            self.hoveredchoice1 = False
            self.hoveredchoice2 = False
            self.hoveredchoice3 = False
            
        def addMessage(self, sender=None, message=None, waits=0, ch1=None, ch2=None, ch3=None, label1=None, label2=None, label3=None):
            self.wait = waits
            if ismessagesonScreen:
                if self.wait > 0:
                    if sender != None and message != None:
                        self.waitlist.append({"name": sender, "message": message, "waits": waits, "ch1": ch1, "ch2": ch2, "ch3": ch3, "label1": label1, "label2": label2, "label3": label3})
                        self.lastwait = message
                else:
                    if len(self.waitlist) > 0:
                        self.messages.append(self.waitlist[0])
                        self.unreads += 1
                        self.last = self.lastwait
                        self.first = True
                        self.choice1 = self.waitlist[0]['ch1']
                        self.choice2 = self.waitlist[0]['ch2']
                        self.choice3 = self.waitlist[0]['ch3']
                        self.choice1label = self.waitlist[0]['label1']
                        self.choice2label = self.waitlist[0]['label2']
                        self.choice3label = self.waitlist[0]['label3']
                        del self.waitlist[0]
                        if len(self.waitlist) > 0:
                            self.wait = self.waitlist[0]['waits']
                            self.unreads += 1
                            self.last = self.lastwait
                            self.first = True
                    if sender != None and message != None and waits !=None:
                        self.messages.append({"name": sender, "message": message})
                        self.unreads += 1
                        self.last = message
                        self.first = True
                        self.choice1 = ch1
                        self.choice2 = ch2
                        self.choice3 = ch3
                        self.choice1label = label1
                        self.choice2label = label2
                        self.choice3label = label3
            elif isphoneonScreen:
                if sender != None and message != None and waits !=None:
                    self.messages.append({"name": sender, "message": message, "waits": waits, "ch1": ch1, "ch2": ch2, "ch3": ch3, "label1": label1, "label2": label2, "label3": label3})
                    renpy.play('audio/SFX/shitringtone.mp3')
                    self.wait = 0
                    self.unreads += 1
                    self.last = message
                    self.first = True
                    self.choice1 = ch1
                    self.choice2 = ch2
                    self.choice3 = ch3
                    self.choice1label = label1
                    self.choice2label = label2
                    self.choice3label = label3
            else:
                if sender != None and message != None and waits !=None:
                    self.messages.append({"name": sender, "message": message, "waits": waits, "ch1": ch1, "ch2": ch2, "ch3": ch3, "label1": label1, "label2": label2, "label3": label3})
                    self.wait = 0
                    self.unreads += 1
                    self.last = message
                    self.first = True
                    self.choice1 = ch1
                    self.choice2 = ch2
                    self.choice3 = ch3
                    self.choice1label = label1
                    self.choice2label = label2
                    self.choice3label = label3
    
        def delMessageLog(self):
            self.messages = []

    DianaC = Contact( 1,'Diana', 'How are you?', 0, "images/Screens/Phone/MessageLogDiana.webp", "images/Screens/Phone/MessageLogSelectedDiana.webp", 'images/Screens/Phone/DianaMessagespic.webp')
    JohnnyC = Contact( 2,'Johnny', 'How are you?', 0, "images/Screens/Phone/MessageLogJohnny.webp", "images/Screens/Phone/MessageLogSelectedJohnny.webp", 'images/Screens/Phone/Johnnymessagesportrait.png')
This is the contact class which contains the contacts that are added to a phone contact list. ^
at first I declared the contacts using default (not inside an init python block) and tried to do it within an Init python block and that didn't seem to work either.

below is how i'm using the code in question:

Code: Select all

##LABEL##
label devtest:
$ _skipping = False
scene black
show screen phone()
$ phone.addContact(DianaC)
$ phone.addContact(JohnnyC)
$ JohnnyC.addMessage('Johnny', 'testJohnny', 4)
$ DianaC.addMessage('Diana', 'testdiana', 4)
$ renpy.pause(hard=True)
I'm also attaching an image to show what I mean, first image is my diana contact:

Image

this is for Johnny:

Image

here is another image of it on the developer console:
Image

I know you might ask to show how the messages are being displayed so I will just provide it

Code: Select all

screen messaging(contact_id):
    zorder 5
    modal True 
    python:
        # Find the contact object based on the contact name
        contact = None
        for c in phone.contacts:
            if c.cID == contact_id:
                contact = c
                break

        firstMessageMC = 0
        firstMessage = 0
        yadj.value = yadjValue
    
    if contact.choice1 != None or contact.choice2 != None or contact.choice3 != None:
        use messagingchoices(contact)
    text '[contact.choice1]' xalign 1.0
    text '[contact.choice2]' xalign 1.0 yoffset 100
    text '[contact.choice3]' xalign 1.0 yoffset 200
    text '[contact.choice1label]' xalign 1.0 yoffset 300
    text '[contact.choice2label]' xalign 1.0 yoffset 400
    text '[contact.choice3label]' xalign 1.0 yoffset 500
    text '[contact.wait]'
    text '[contact.lastwait]' yalign 1.0
    if contact.wait > 0:    
        timer 0.1 repeat True action SetField(contact, 'wait', contact.wait-0.1)
        
    else:
        timer 0.1 action Function(contact.addMessage)

    # action Hide('messaging')
    imagebutton:
        idle "images/Screens/Phone/messages_interface.webp"
        anchor (0.5,0.5)
        pos (0.5,0.5)
        action NullAction()
        at messaging
        focus_mask True

    imagebutton:
        anchor (0.5, 0.5)
        idle "images/Screens/Phone/backbuttonMessages.webp"
        hover "images/Screens/Phone/backbuttonMessages_selected.webp"
        at backbuttonmessages
        action [Hide("messaging"), ToggleVariable('ismessagesonScreen')]
        focus_mask True

    imagebutton:
        anchor (0.5, 0.5)
        pos (1920, 1080)
        idle contact.img
        action NullAction()
        focus_mask True
        at backbuttonmessages

    frame:
        at messagingtexts
        id "windowMail"
        xsize 522
        ysize 852 # Frame in the center of the screen
        background None

        frame:
            xsize 522 
            ysize 853
            xalign 0.5
            padding(0,0,0,0)
            background None 
            yfill True
            if contact.wait > 0:
                text '[contact.name] is typing...' style "msgContactTyping" yalign 1.0 xoffset 15

            viewport yadjustment yadj id "vp":
                
                draggable True scrollbars None 
                yinitial 1.0
                #xfill True
                yfill True

                frame:
                    padding(0,0,0,0) background None
                    xsize 522


                    vbox:
                        spacing -25
                        if contact is not None:
                            for message in contact.messages:
                                frame:
                                    background None
                                    xsize 522
                                    
                                    if message["name"] == "MC":
                                        if firstMessageMC == 0:
                                            frame:
                                                style "phone_right"
                                                text message['message'] style "msgMC"
                                                                
                                        else:
                                            frame:
                                                style "phone_right_1"
                                                text message['message'] style "msgMC"
                                            
                                        
                                        
                                        $ firstMessageMC = 1
                                        $ firstMessage = 0
                                        

                                    else:                                               
                                        if firstMessage == 0:
                                            frame:
                                                style "phone_left"
                                                text message['message'] style "msgContact"
                                            
                                        else:
                                            frame:
                                                style "phone_left_1"
                                                text message['message'] style "msgContact"
                                                    
                                            
                                        $ firstMessageMC = 0
                                        $ firstMessage = 1
I'm sorry if some of my code is completely redundant, I'm still a beginner and have been coding for only about 6 months. Thank you all for any help you may bring and hopefully we can solve what's happening quickly.

User avatar
m_from_space
Eileen-Class Veteran
Posts: 1056
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Calling class method affects all Class instances

#2 Post by m_from_space »

gabowave wrote: Tue Nov 14, 2023 4:27 amFor some reason, when I call a method [Namely JohnnyC.addMessage(blah blah blah)] It not only adds the message for the instance JohnnyC's messages list, but also for DianaC's messages list despite not having called said class at all.
The first thing that comes to mind when reading this (without looking at the code) is, that in Python when you think you're copying a list, you're actually referencing it.

Example:

Code: Select all

a = [1, 2, 3]

# this feels like it's making a copy, but it's actually a reference
b = a

# correct way to copy lists and dicts
c = a.copy()

# this will clear list a, but since b is a reference, b will also be cleared
a.clear()
Now since I did have a look at your code, I didn't find anything at first. But after some headache I finally had an idea. You're doing weird stuff when creating the __init__ function for your contacts, especially when it comes to the messages argument. Long story short: The empty list inside your __init__ is actually a static object that is outside the scope of the function. Until now I didn't know that either, but I just tested it. So when you create a new contact, you're always referencing the same empty list.

Code: Select all

# wrong way to do it
def __init__(self, messages=[]):
    # this will create a reference to the static empty list inside the argument
    # every contact will then refer to that same list
    self.messages = messages
    
# weird working way (but don't do it like this)
def __init__(self, messages=[]):
    self.messages = messages.copy()

# probably the right way
def __init__(self, messages=None):
    if messages is None:
        self.messages = []
    else:
        self.messages = messages

User avatar
Ocelot
Lemma-Class Veteran
Posts: 2444
Joined: Tue Aug 23, 2016 10:35 am
Github: MiiNiPaa
Discord: MiiNiPaa#4384
Contact:

Re: Calling class method affects all Class instances

#3 Post by Ocelot »

m_from_space wrote: Tue Nov 14, 2023 7:43 am Long story short: The empty list inside your __init__ is actually a static object that is outside the scope of the function.
Yeah, do not modify default arguments in Python, this will not end well. One approach is to use None as "not set" value and handle it within function:

Code: Select all

def foo(bar=None):
    if bar is None:
        bar = []

    # now you can use bar, as if you wrote "def foo(bar=[])" but without any problems
< < insert Rick Cook quote here > >

gabowave
Newbie
Posts: 10
Joined: Sat Jul 08, 2023 4:15 pm
Contact:

Re: Calling class method affects all Class instances

#4 Post by gabowave »

Thank you both, this obviously fixed the issue and also thank you, this is now something new that I know and will keep it in mind as I continue developing.

Post Reply

Who is online

Users browsing this forum: Google [Bot]