Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
12 rats tied together
Sep 7, 2006

e: adding quote since this is a new page

Falcon2001 posted:

So I have a bit of a best practices question. In the event that you have a function that mutates a data structure like a list/etc, is it best practice to return the mutated version, or to just make it clear in the docstring that the function mutates the object? For the sake of argument, let's assume that this isn't a library or other externally facing function and is pretty tightly bound business logic specific to a situation, where you're separating code out into functions to improve readability/etc.

code:
big_dict = retrieve_data()
remove_butts(big_dict)
vs
code:
big_dict = retrieve_data()
big_dict = remove_butts(big_dict)

IMHO you should return a copy of the original that is mutated. I wouldn't muck about in caller state without it being an explicit requirement, since otherwise it seems more fragile for no benefit.

Ideally "remove_butts" can be expressed directly in the caller state as a dict comprehension, or similar, and it does not need to be a function though.

Adbot
ADBOT LOVES YOU

QuarkJets
Sep 8, 2008

Protocol7 posted:

I think that's kind of up to your preference. Though if you are not returning anything then you should be very clear that the function does not return anything and thus mutates an argument as a side effect. It shouldn't do both; either return a new object or mutate the existing one.

I like to do both, personally: mutating the input and also returning it. So long as the behavior is consistent I think that's what really matters (if you have a bunch of list mutators, they should all have the same behavior)

Armitag3
Mar 15, 2020

Forget it Jake, it's cybertown.


Falcon2001 posted:

So I have a bit of a best practices question. In the event that you have a function that mutates a data structure like a list/etc, is it best practice to return the mutated version, or to just make it clear in the docstring that the function mutates the object? For the sake of argument, let's assume that this isn't a library or other externally facing function and is pretty tightly bound business logic specific to a situation, where you're separating code out into functions to improve readability/etc.

code:
big_dict = retrieve_data()
remove_butts(big_dict)
vs
code:
big_dict = retrieve_data()
big_dict = remove_butts(big_dict)

My opinion is that the first is better over the second example. All things being the same (documented mutation behavior, etc) returning a value from a function immediately makes me think away from mutation. In your second example its fine since you're reassigning to the same variable, but if you weren't (or your peer didn't read the docstring and assumed the return was a copy) and assigned it to buttless_big_dict, you now have two references to the same dict.

The first example is less ambiguous in that it doesn't return anything (or if it does, you don't care because you're not capturing it in a variable), and looks closer to a function that just does something with or to the arguments you passed.

As a bonus, here's a third option I'd prefer:
Python code:
class BigDict:
    def __init__(self):
        self.data = retrieve_data()

    def remove_butts(self):
        """This motherfucker mutates `data`."""
        # remove butts ...

big_dict = BigDict()
big_dict.remove_butts()  # mutation, clear as day

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug
Thanks for the feedback! I think the biggest takeaway is that it should be clear, and I properly type hint all my functions/etc, so it's clear that this doesn't return anything (-> None), which means I think I'm going in the right direction.

I also agree with the nicest option being the 'make it a class and then have a method that does the thing to itself'. This is a four year old codebase that I just came into, so there's a few different places we're just passing dictionaries around instead of using dataclasses/etc. Some of those I'll be trying to clean up as I go along and generally docstring or otherwise fix up some of the older bits of this program.

Armitag3
Mar 15, 2020

Forget it Jake, it's cybertown.


Falcon2001 posted:

Thanks for the feedback! I think the biggest takeaway is that it should be clear, and I properly type hint all my functions/etc, so it's clear that this doesn't return anything (-> None), which means I think I'm going in the right direction.

I also agree with the nicest option being the 'make it a class and then have a method that does the thing to itself'. This is a four year old codebase that I just came into, so there's a few different places we're just passing dictionaries around instead of using dataclasses/etc. Some of those I'll be trying to clean up as I go along and generally docstring or otherwise fix up some of the older bits of this program.

Godspeed goon

Macichne Leainig
Jul 26, 2012

by VG

QuarkJets posted:

I like to do both, personally: mutating the input and also returning it. So long as the behavior is consistent I think that's what really matters (if you have a bunch of list mutators, they should all have the same behavior)

I was curious so I looked it up. The docs say that they try to avoid doing both in the standard library.

quote:

Some operations (for example y.append(10) and y.sort()) mutate the object, whereas superficially similar operations (for example y = y + [10] and sorted(y)) create a new object. In general in Python (and in all cases in the standard library) a method that mutates an object will return None to help avoid getting the two types of operations confused. So if you mistakenly write y.sort() thinking it will give you a sorted copy of y, you’ll instead end up with None, which will likely cause your program to generate an easily diagnosed error.

https://docs.python.org/3/faq/programming.html#why-did-changing-list-y-also-change-list-x

Not really much of an official style guideline on it that I could find though. And of course it's a bit different when calling an instance method like y.sort() IMO.

Macichne Leainig fucked around with this message at 22:19 on Mar 22, 2022

ExcessBLarg!
Sep 1, 2001
When mutating I personally prefer to return self than return None, since the former enables the opportunity for method chaining. But that's a much more prominent thing in Ruby than Python, and the Python style used in the stdlib favors returning None for mutations. Even if you return self, you definitely don't need to reassign it in the caller though.

Returning a mutated copy is a pattern you'll find frequently in the stdlib, especially among built-in functions that take itetables as inputs. That makes a lot of sense when working with generic data structures but any methods that operate on specific types (i.e., your business logic) probably wouldn't make sense to return copies outside of an explicit copy() operation.

Oh, if you have a top-level function that mutates a dict because your legacy code base doesn't do classes I wouldn't return the dict parameter, just let it fall off with None.

ExcessBLarg! fucked around with this message at 00:33 on Mar 23, 2022

punk rebel ecks
Dec 11, 2010

A shitty post? This calls for a dance of deduction.

QuarkJets posted:

There's a lot of extraneous stuff going on here and I don't really understand what you want to do, sorry. Here's a simple example that flips a QLabel between two images, is that kind of what you're looking for?

Let me be more descriptive.

This is the code snippet that includes what happens when my pokedex app can't find a pokemon in the api:

code:
def start_button_click(self):
        try:
            Data.extract(self)
        except JSONDecodeError:
            self.error_screen.setHidden(False)
            self.error_screen.setText("No Pokémon in Database")
            Data.error_light_on(self)
            QTimer.singleShot(4500, lambda: self.error_screen.setHidden(True))
            threading.Thread(target=error_sound).start()
            x = 0
            for i in range(5):
                x+=500
                QTimer.singleShot(x, lambda: Data.error_light_on(self))
                x+=500
                QTimer.singleShot(x, lambda: Data.lights_off(self))
This is what it looks like in action:



Notice the red blinking light? I want the yellow light to do the same thing when the function I posted earlier runs.

My goal is to have the yellow light keep blinking until the "extract" function is finished. I plan on doing this by creating a "while loop" for the function that has the light blinking function run until the extract code gets to "self.loading = False". At the end.

However, when running the application, this is what occurs:



As you can see it goes straight to Data.complete_light_on(self) at the end.

I decided to experiment and instead of having the loading light load a few times in a range and putting it in the code as such:

code:
#The "Mother Code" Run        
    def extract(self):
        self.loading = True
        load_flash = self.button_lights.setPixmap(QPixmap(f"{start}/assets/passets/button_loading_loading.png"))
        self.pokemon = self.line.text().lower()
        Data.run_api(self,"pokemon", self.pokemon)

        while self.loading:
            Data.blinking_load_light(self)
            #A Bunch of Code Here
            self.loading = False
     
    def blinking_load_light(self):
        x=0
        for i in range(5):
            x+=500
            QTimer.singleShot(x, lambda: Data.load_light_on(self))
            x+=500
            QTimer.singleShot(x, lambda: Data.lights_off(self))
As you can see I also took out the light complete function at the bottom so it can't override the yellow light.

But the result I get is the yellow light blinks only after all the other code in the function has ran:



That's my issue. I want the light to blink as long as the extract function is running and stop when it isn't in order to tell the user the data is loading.

QuarkJets
Sep 8, 2008

Protocol7 posted:

I was curious so I looked it up. The docs say that they try to avoid doing both in the standard library.

https://docs.python.org/3/faq/programming.html#why-did-changing-list-y-also-change-list-x

Not really much of an official style guideline on it that I could find though. And of course it's a bit different when calling an instance method like y.sort() IMO.

Seems reasonable to me

In numpy, functions always (?) return a new array, and then some functions will also let you provide the output array, and this may be the input array for in-place mutation. That's a weird set of cases where a mutation occurs on the input only when you ask for that, but an output is always returned

ExcessBLarg!
Sep 1, 2001

punk rebel ecks posted:

My goal is to have the yellow light keep blinking until the "extract" function is finished.
If the extract function takes "a long time" to run, and you're running it on the same thread as your Qt event loop, then those QTimers won't fire until the function is done and the event loop runs again.

See Use PyQt's QThread to Prevent Freezing GUIs.

ExcessBLarg! fucked around with this message at 01:45 on Mar 23, 2022

punk rebel ecks
Dec 11, 2010

A shitty post? This calls for a dance of deduction.

ExcessBLarg! posted:

If the extract function takes "a long time" to run, and you're running it on the same thread as your Qt event loop, then those QTimers won't fire until the function is done and the event loop runs again.

See Use PyQt's QThread to Prevent Freezing GUIs.

While this does seem to be what I'm looking for, it's a bit too advanced for me.

Not QThread as a concept, but the fact that I'm doing so many things in the button click at once it will be difficult to know how to separate it all into various functions for the worker threads.

I may just edit the yellow light out and use the loading idea for a future project.

QuarkJets
Sep 8, 2008

punk rebel ecks posted:

While this does seem to be what I'm looking for, it's a bit too advanced for me.

Not QThread as a concept, but the fact that I'm doing so many things in the button click at once it will be difficult to know how to separate it all into various functions for the worker threads.

I may just edit the yellow light out and use the loading idea for a future project.

Yeah the trick is that Qt runs an event loop in the background and it sounds like you're combining IO into the same event that handles the GUI alterations

QuarkJets
Sep 8, 2008


It's still very difficult to diagnose what's going on here. What kind of object is Data? Why is self being passed to it? What does Data.run_api do? What invokes extract()? Is extract() part of a QDialog class or something else? There could be a lot of reasons that are causing you to see what you're seeing, basically. Places like stackoverflow have a rule that sums up as "post complete examples" for this reason, it's probably impossible to say exactly what's going on here using only what's shown.

You could try working from the example that I wrote earlier, where a QTimer just flips an image between off and on. Try doing that as the very first step of your method, to ensure that it's in Qt's event loop. Disable anything else that actually does IO and just get the light blinking. Then add in more until it breaks, and you'll learn more about what the code is really doing. That may help guide you to a solution.

code:
     
    def __init__(self, ...):
        ...
        self.loading_lights = (QPixmap(f"loading_light_on.png"),
                                              QPixmap(f"loading_light_off.png"))
        self.loading_light_index = 0
        ...

    def extract(self):
        self.loading_timer = QTimer()
        self.loading_timer.timeout.connect(self.blink_loading_light)
        self.loading_timer.start(500)
        
        # load stuff here

        self.loading_timer.stop()
        
    def blinking_load_light(self):
        self.button_lights.setPixmap(self.loading_lights[self.loading_light_index])
        self.loading_light_index = 1 - self.loading_light_index 
QTimer.stop() is also a slot, you could connect a signal to it. For instance, a signal that you have your class emit after it finishes loading data.

Hed
Mar 31, 2004

Fun Shoe
There was a video on type hints in the past couple of years where some popular library went through their rollout of it with MyPy and other type checkers. One of their big takeaways was that it not only made the code base easier to run through but it caught some subtle bugs that they didn't otherwise cover in unit tests.

Does anyone know what this talk was? I thought it was about the requests library or something, but I can't find it.

Macichne Leainig
Jul 26, 2012

by VG
Thought someone would get a kick out of this terrible fstring I wrote today.



Remind me how I get paid to write Python again? :stonklol:

QuarkJets
Sep 8, 2008

Protocol7 posted:

Thought someone would get a kick out of this terrible fstring I wrote today.



Remind me how I get paid to write Python again? :stonklol:

What the gently caress

Armitag3
Mar 15, 2020

Forget it Jake, it's cybertown.


Protocol7 posted:

Thought someone would get a kick out of this terrible fstring I wrote today.



Remind me how I get paid to write Python again? :stonklol:

Apparently this works
Python code:
guid = "1234-123123-12123-1234"
print(f"{{{guid}}}")
# > {1234-123123-12123-1234}
lol

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

Armitag3 posted:

Apparently this works
Python code:
guid = "1234-123123-12123-1234"
print(f"{{{guid}}}")
# > {1234-123123-12123-1234}
lol

https://peps.python.org/pep-0498/#specification it's in the spec, yeah. I think I had to use this recently.

ExcessBLarg!
Sep 1, 2001
Today I learned that Python 3.10 supports match statements (i.e., switch, case, etc.), finally.

Unrelated, but is there a function that slices a sequence given the starting index and length of the slice? I can write a function to convert (start, length) to (start, end) easy enough, but it seems like something that should be in itertools or elsewhere in the stdlib but I don't see it.

ExcessBLarg! fucked around with this message at 02:00 on Mar 24, 2022

QuarkJets
Sep 8, 2008

I'm not aware of one, it's too easy to just slice as sequence[i:i+length]

punk rebel ecks
Dec 11, 2010

A shitty post? This calls for a dance of deduction.
So the last issue I'm having with my project is that I'm trying to have the bottom label scrollable .

Here is a snippet of my code that includes the "QScrollArea" and the label I want to apply it to "self.summary_screen".

code:
    def scrolling(self):
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidget(self.summary_screen)
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setFixedHeight(120)
        self.scroll_area.setFixedWidth(440)
        self.scroll_area.setGeometry(45, 463, 440, 180)
        
        
    def stats(self):
   	#Code
        self.summary_screen = QLabel (placeholder_text, self)
        self.summary_screen.setGeometry(45, 463, 440, 180)
        self.summary_screen.setStyleSheet("color: black")
        self.summary_screen.setFont(QFont('Arial',13))
        self.summary_screen.setWordWrap(True)
     	#Code
This results in nothing displaying in the bottom at all, let alone a scrollbar if say there was too much text.



So curious, I assigned the scroll area to a layout:

code:
    def scrolling(self):
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidget(self.summary_screen)
        self.scroll_area.setFixedHeight(120)
        self.scroll_area.setFixedWidth(440)
        self.scroll_area.setGeometry(45, 463, 440, 180)
        self.layout = QVBoxLayout(self)
        self.layout.addWidget(self.scroll_area)
Now the QScrollArea (which can scroll) shows up but it's in the center of the main window.:



I just simply want the scroll window down at the bottom like self.summary.screen is. But it won't move no matter what I .setGeometry is set to.


For the whole context, here is the full code: https://paste.pythondiscord.com/ocixuxamiv

QuarkJets posted:

It's still very difficult to diagnose what's going on here. What kind of object is Data? Why is self being passed to it? What does Data.run_api do? What invokes extract()? Is extract() part of a QDialog class or something else? There could be a lot of reasons that are causing you to see what you're seeing, basically. Places like stackoverflow have a rule that sums up as "post complete examples" for this reason, it's probably impossible to say exactly what's going on here using only what's shown.

I tried messing around with this but I have so much code (since my project is all but done) that it would be easier implementing this for my next project. I'll keep tabs of this.

Data is just a class btw. I posted my full code right above to get full context.

QuarkJets
Sep 8, 2008

punk rebel ecks posted:

So the last issue I'm having with my project is that I'm trying to have the bottom label scrollable .


This looks like a geometry issue. See here: https://doc.qt.io/qt-5/application-windows.html#window-geometry and the description of geometry here: https://doc.qt.io/qt-5/qwidget.html#geometry-prop, which is what's being manipulated when you use setGeometry(int, int, int, int)

Basically, your other widgets are all given a parent widget:
Python code:
self.summary_screen = QLabel(placeholder_text, self)
This creates a QLabel, assigns it some text, and then assigns a parent widget (in this case: self, which is an instance of your MainWindow class, which inherits from QWidget). But the QScrollArea you create has no parent assigned. Try giving it the same parent as your other objects, like this:

Python code:
self.scroll_area = QScrollArea(self)
That's probably enough.

Returning to your earlier question, it seems like you should shove nearly all of the code in your extract() method into a new method that gets handled by a QThread or QThreadPool. But also, you're using a lot of repeated calls to a website to fetch the same data over and over:

Python code:
    def run_api(self,directory, subdirectory):
        self.directory = directory
        self.subdirectory = subdirectory
        contact = {'User-Agent': '("Accept: application/json", "Cache-Control: max-age=360",)'}
        response = requests.get (f'https://pokeapi.co/api/v2/{self.directory}/{self.subdirectory}', headers = contact)
        self.data = response.json()
This is nearly always being invoked in the same way: Data.run_api(self,"pokemon", self.pokemon) (by the way, this should be rewritten as self.run_api("pokemon", self.pokemon)). It'd be better to cache this information somewhere and only request it again if self.pokemon changes, that'd probably make your application more responsive.

ExcessBLarg!
Sep 1, 2001

QuarkJets posted:

I'm not aware of one, it's too easy to just slice as sequence[i:i+length]
It is easy for a fixed i, but I have a situation where I need to slice as part of a comprehension and "i" is a larger expression involving the iteration variable.

Armitag3
Mar 15, 2020

Forget it Jake, it's cybertown.


Today I learned that the Ellipsis ... in python is an actual object that can be used to mean I don't care

ExcessBLarg!
Sep 1, 2001
"..." is a singleton object that I guess doesn't have much use in standard Python. So it's like None, but less meaningful.

"pass" is probably a more idiomatic to indicate a "don't care" statement such as when dropping exceptions.

D34THROW
Jan 29, 2012

RETAIL RETAIL LISTEN TO ME BITCH ABOUT RETAIL
:rant:
:psyduck: I just refactored an entire module and it works correctly, 100% as expected the first time, what the gently caress? I don't think this has ever happened before. Ever.

PierreTheMime
Dec 9, 2004

Hero of hormagaunts everywhere!
Buglord
Anyone have any experience with Paramiko throwing an error during a rename? I can connect to the SFTP server and download a file from that remote host, but asking it to move the remote file (into remote archive path) results in an "[Errno 2] File not found". I can use the same client to lstat both the original filepath and the destination directory filepath and both return the appropriate information. I have full admin rights with the user on the SFTP-side.

This is using an SFTP client object that's created using the .from_transport() method. The other commands I'm issuing with it (list_dir, file) work fine. There's not really much to it:
Python code:
sftp.rename(orig_path, archive_path)
I tried using the .posix_rename() method instead, but I get a "not supported" error which tracks since it's a Windows host. I've dug around on a number of sites and I can't seem to find too many posts about it. Any help would be appreciated.

:confused:

ExcessBLarg!
Sep 1, 2001
Does it work if you issue a rename from a sftp command-line client?

SFTP rename is weird. It's supposed to error out if the destination file already exists, which isn't how rename(2) works. Instead OpenSSH SFTP makes a new hardlink for the target and the unlinks the source. This requires that the underlying file system supports hardlinks, and specifically cross-directory ones if you're moving between directories. Not sure what SFTP server you're using but that it's on Windows makes this even more complicated.

PierreTheMime
Dec 9, 2004

Hero of hormagaunts everywhere!
Buglord
It’s Globalscape EFT, a relatively robust but persnickety hosting tool.

I think it’s probably just going to be a no-go and I’ll just redo it from the SFTP-end and manipulate things at the OS. I’m genuinely curious is there’s anything that could be done to get it working but I’m not interested enough not to take the easy fix.

punk rebel ecks
Dec 11, 2010

A shitty post? This calls for a dance of deduction.

QuarkJets posted:

This looks like a geometry issue. See here: https://doc.qt.io/qt-5/application-windows.html#window-geometry and the description of geometry here: https://doc.qt.io/qt-5/qwidget.html#geometry-prop, which is what's being manipulated when you use setGeometry(int, int, int, int)

Basically, your other widgets are all given a parent widget:
Python code:
self.summary_screen = QLabel(placeholder_text, self)
This creates a QLabel, assigns it some text, and then assigns a parent widget (in this case: self, which is an instance of your MainWindow class, which inherits from QWidget). But the QScrollArea you create has no parent assigned. Try giving it the same parent as your other objects, like this:

Python code:
self.scroll_area = QScrollArea(self)
That's probably enough.

You are correct.

It works perfectly now.

Thank you.

QuarkJets posted:

Returning to your earlier question, it seems like you should shove nearly all of the code in your extract() method into a new method that gets handled by a QThread or QThreadPool. But also, you're using a lot of repeated calls to a website to fetch the same data over and over:

Python code:
    def run_api(self,directory, subdirectory):
        self.directory = directory
        self.subdirectory = subdirectory
        contact = {'User-Agent': '("Accept: application/json", "Cache-Control: max-age=360",)'}
        response = requests.get (f'https://pokeapi.co/api/v2/{self.directory}/{self.subdirectory}', headers = contact)
        self.data = response.json()
This is nearly always being invoked in the same way: Data.run_api(self,"pokemon", self.pokemon) (by the way, this should be rewritten as self.run_api("pokemon", self.pokemon)). It'd be better to cache this information somewhere and only request it again if self.pokemon changes, that'd probably make your application more responsive.

What do you mean by "cache this information somewhere"?

D34THROW
Jan 29, 2012

RETAIL RETAIL LISTEN TO ME BITCH ABOUT RETAIL
:rant:

punk rebel ecks posted:

What do you mean by "cache this information somewhere"?

Retain it in local memory somewhere, rather than fetching it each and every time. Your browser doesn't fetch the little bookmark star, or the images on the quote/report/reply buttons each time you need them. Rather, it caches them locally for faster lookup.


EDIT: QuarkJets, thank you for the mapping trick. I use that extensively and it saves me a ton of if-elif-else statements. Cases in point:

Python code:
# In my windoor.py module for calculating material for different windows/doors...
CALCULATORS = {
    "SH": self._single_hung,
    "DH": self._double_hung,
    "HR": self._side_slider,
    "AW": self._awning,
    "CA": self._casement,
    "PW": self._fixed,
    "AR": self._fixed,
}
# In my cli.py module for running various tests based on a command-line argument...
FUNCTIONS = {
    "pgt-csv": cli.tests.pgt_csv_parser,
    "pgt-xml": cli.tests.pgt_xml_parser,
    "pgt-all": cli.tests.pgt_all_parsers,
    "pgt-multi": cli.tests.pgt_mull_multipart,
    "purge": cli.db_functions.delete_all_data,
    "clear-lookups": cli.db_functions.delete_lookup_data,
    "fix-lookups": cli.db_functions.fix_lookups,
    "fill-lookups": cli.db_functions.fill_lookup_tables,
    "table-loop-test": cli.db_functions.table_loop_test,
    "storm-panels": cli.tests.storm_panels,
    "pan-roof": cli.tests.pan_roofs,
    "poly-roof": cli.tests.poly_roofs,
    "windoor": cli.tests.windoor_order,
}
So on and so forth. So much loving better.


EDIT EDIT: It's a really weird and good feeling to have your bugs being more dumb typos than just completely misunderstanding a concept. It's even better when those typos aren't in the method you're testing, but rather the method to call the test. Python is sticking with me like no other language has :unsmith:

D34THROW fucked around with this message at 21:40 on Mar 24, 2022

punk rebel ecks
Dec 11, 2010

A shitty post? This calls for a dance of deduction.

D34THROW posted:

Retain it in local memory somewhere, rather than fetching it each and every time. Your browser doesn't fetch the little bookmark star, or the images on the quote/report/reply buttons each time you need them. Rather, it caches them locally for faster lookup.

So extract everything from self.data and add it to a variable that I can sort though and pull from things that way?

D34THROW
Jan 29, 2012

RETAIL RETAIL LISTEN TO ME BITCH ABOUT RETAIL
:rant:
Phoneposting so no code, but if self.data is overwritten for each new loaded Pokemon, what I would do is store the self.data json (I'm assuming, or dict) in a dict cached_data, keyed by, say, the Pokemon number. Then I would add a clause to whatever function pulls the data to check first if that pokemons data is in cached_data, then pull from the API if it isn't.

punk rebel ecks
Dec 11, 2010

A shitty post? This calls for a dance of deduction.

D34THROW posted:

Phoneposting so no code, but if self.data is overwritten for each new loaded Pokemon, what I would do is store the self.data json (I'm assuming, or dict) in a dict cached_data, keyed by, say, the Pokemon number. Then I would add a clause to whatever function pulls the data to check first if that pokemons data is in cached_data, then pull from the API if it isn't.

I’m not sure how to make a dict for cached-data.

Defeatist Elitist
Jun 17, 2012

I've got a carbon fixation.

punk rebel ecks posted:

I’m not sure how to make a dict for cached-data.

I believe they're just using cached_data as a stand in for whatever you would want to name the dict. They just mean you should store it in a dict. You would make it similarly to how you would normally use a dict for caching, etc.

punk rebel ecks
Dec 11, 2010

A shitty post? This calls for a dance of deduction.
Okay so I essentially just created this dictionary:

code:
class Data():
    
    pokemon_dict = {}
And then added all of self.data to it:
code:
#The "Mother Code" Run        
    def extract(self):
        self.loading = True
        load_flash = self.button_lights.setPixmap(QPixmap(f"{start}/assets/passets/button_loading_loading.png"))
        self.pokemon = self.line.text().lower()
        Data.run_api(self,"pokemon", self.pokemon)
	Data.pokemon_dict.update(self.data)
	#Rest of code
And do this when retrieving code:
code:
#Height & Weight of Pokémon   
    def height_weight(self):
    
        #Height
        self.height_cm = (int(Data.pokemon_dict["height"]) * 10)
        self.height_ft = self.height_cm * 0.0328084
Is this what you all had in mind?

nullfunction
Jan 24, 2005

Nap Ghost
I've never worked with Qt so I can't speak to the offload of work in QThreads or whatever, hopefully this gets the caching idea across, and may get you thinking about how you can modify your program's structure to make your life a little easier in the future:

Python code:
# pokemon.py
import requests

# The dict where we'll store the results locally from your API calls
local_pokedex = {}

class PokemonApi():
    @staticmethod
    def get_pokemon(name: str) -> dict:
        # Probably not a bad idea to add some error handling around this! APIs fail sometimes!
        contact = {'User-Agent': '("Accept: application/json", "Cache-Control: max-age=360",)'}
        response = requests.get(f'https://pokeapi.co/api/v2/pokemon/{name}', headers=contact)
        return response.json()

class Pokemon():
    def __init__(self, data: dict):
        self.name = data['name']
        self.height = data['height']
        self.height_cm = self.height * 10
        self.height_ft = self.height_cm * 0.0328084
        # Other interesting attributes could be processed here,  or if you're not sure what 
        # you'll use, you might store the entire JSON
        # As the final step of initializing, update the cache
        local_pokedex[self.name] = self
    
    @classmethod
    def from_api(cls, name: str):
        return cls(data=PokemonApi.get_pokemon(name))
    
    @classmethod
    def from_cache(cls, name: str):
        if name not in local_pokedex.keys():
            # We don't have this pokemon in our local cache, so we have to go to the API
            return cls.from_api(name)
        # If we're here, we know we have a cached version of the pokemon and don't need to call the API
        return local_pokedex[name]
Normally you'd use the from_cache classmethod when you need to get a new Pokemon due to user input, since it will handle cache hit/miss, but you could always call the from_api classmethod if you wanted to force a cache refresh, or you could call the bare constructor if you loaded the data dictionary from a file or something.

The first time I call for Pikachu using from_cache, it's 100-200ms to retrieve from the API. The next time I ask for a Pikachu, the data is already local so I'm just spending the time to access the dictionary of Pokemon objects (absurdly fast in comparison, at the cost of a little bit of RAM).

Python code:
# poketest.py
from datetime import datetime
from pokemon import Pokemon

first_call_start = datetime.now()
first_pika = Pokemon.from_cache('pikachu')
first_call_finish = datetime.now()

second_call_start = datetime.now()
second_pika = Pokemon.from_cache('pikachu')
second_call_finish = datetime.now()

print(f'{(first_call_finish - first_call_start).microseconds}usec to get my first Pikachu')
print(f'{(second_call_finish - second_call_start).microseconds}usec to get my next Pikachu')
code:
$ python3 poketest.py
112527usec to get my first Pikachu
3usec to get my next Pikachu

D34THROW
Jan 29, 2012

RETAIL RETAIL LISTEN TO ME BITCH ABOUT RETAIL
:rant:

nullfunction posted:

Python code:
# pokemon.py
import requests

# The dict where we'll store the results locally from your API calls
local_pokedex = {}

class PokemonApi():
    @staticmethod
    def get_pokemon(name: str) -> dict:
        # Probably not a bad idea to add some error handling around this! APIs fail sometimes!
        contact = {'User-Agent': '("Accept: application/json", "Cache-Control: max-age=360",)'}
        response = requests.get(f'https://pokeapi.co/api/v2/pokemon/{name}', headers=contact)
        return response.json()

class Pokemon():
    def __init__(self, data: dict):
        self.name = data['name']
        self.height = data['height']
        self.height_cm = self.height * 10
        self.height_ft = self.height_cm * 0.0328084
        # Other interesting attributes could be processed here,  or if you're not sure what 
        # you'll use, you might store the entire JSON
        # As the final step of initializing, update the cache
        local_pokedex[self.name] = self
    
    @classmethod
    def from_api(cls, name: str):
        return cls(data=PokemonApi.get_pokemon(name))
    
    @classmethod
    def from_cache(cls, name: str):
        if name not in local_pokedex.keys():
            # We don't have this pokemon in our local cache, so we have to go to the API
            return cls.from_api(name)
        # If we're here, we know we have a cached version of the pokemon and don't need to call the API
        return local_pokedex[name]

This is pretty much exactly what I had in mind. However, I am self taught and fairly new so classmethods are still fairly new to me.

For my own edification, let me see if I understand here:

Pokemon.from_cache returns a new instance of Pokemon (i.e. a constructor) if the Pokemon is not in the API, or the copy of the locally cached data. In other words, from_cache is a Pokemon factory?

I had a pretty decent grasp of factory patterns and interfaces in VBA but for some reason, in Python they break my brain.

nullfunction
Jan 24, 2005

Nap Ghost

D34THROW posted:

Pokemon.from_cache returns a new instance of Pokemon (i.e. a constructor) if the Pokemon is not in the API, or the copy of the locally cached data. In other words, from_cache is a Pokemon factory?

I had a pretty decent grasp of factory patterns and interfaces in VBA but for some reason, in Python they break my brain.

A classmethod is effectively a factory, really all it's doing is receiving the class as an argument, along with potentially other arguments, and then calling the constructor (or another classmethod that eventually calls the constructor). It's worth noting that it has access to class attributes, but not instance attributes:

Python code:
class Butt():
    crack = True
    def __init__(self):
        self.fart_noise = "toot"
    
    @classmethod
    def alternate_butt(cls):
        print(cls.crack) # True
        print(cls.fart_noise) # AttributeError: type object 'Butt' has no attribute 'fart_noise'
Sort of a contrived example, but let's say you want to create a consistent object from one of two inconsistent sources. Maybe the XML input expresses a timespan as a start and end date, and the JSON equivalent stores a start date and a duration, but in your object, all you need is the duration. Your from_xml classmethod could handle subtracting the two dates and substituting a datetime.timedelta while the from_json classmethod might convert the duration over to a datetime.timedelta and ignore the start date completely. It's a great way to hook in some format- or source-specific logic without polluting the other methods in the class, making the whole of the class a lot easier to read, reason about, and perhaps most importantly, test.


E: since I was mocking this up pretty late at night, I didn't go into error handling, but in the case of a Pokemon not existing in the API, the code I posted above blows up. Were this code I was actually using, I'd be checking the status code of the response at a minimum, and raising a better exception (along the lines of a PokemonNotFoundError) and handling that error with something in the UI. In fact, it looks like that API only supports lowercase names, so doing a .lower() and .strip() on the name in the classmethod is probably a good idea too!

nullfunction fucked around with this message at 19:13 on Mar 25, 2022

Adbot
ADBOT LOVES YOU

D34THROW
Jan 29, 2012

RETAIL RETAIL LISTEN TO ME BITCH ABOUT RETAIL
:rant:
I can already think of an example (that involves major refactoring and thus is way beyond what I feel like loving with right now). I have a PGTExportData object that takes a path as an argument to its __init__ function. That path will be either to a CSV or an XML file, and I have a parse_xml function and a parse_csv function. Ideally, I'd make those classmethods for a PGTImport factory class and have them return a PGTExportData object rather than directly setting instance variables from within those two functions. Something like that?

It blows my mind that the API goes by name and not number. Is it because of bullshit like Galarian Whateverthefuck and Kalosian Hoosiewhatsit?

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply