First off I think your use case is a nifty way of getting into Python, and it looks like aside from the bugs that others have already pointed out you'll likely soon be unstoppable.
However, instead of simplifying the code I'd suggest modularizing as well as making use of __doc__
strings. It'll make adding features much easier in the future, and if you so choose, allow for making a full application with Kivy
, Blender
, or one of the other many GUI frameworks for Python development. Plus modularizing or abstraction allows for simplifying the intentions/usage.
Some notes before diving-in...
it's probably a good idea to get a snack and drink; I'm a bit verbose and am about to compress some years of knowledge
__bar__
when spoken is "dunder bar" , and the phylum that they're classified under are "magic methods"
what I share is not gospel as such, but a collection of tricks I wish someone had shown me when I was getting into Python
... okay back on track.
Here's some example code inspired by yours that shows some of what I was going on about in your question's comments...
#!/usr/bin/env python
import time
import random
print_separator = "".join(['_' for _ in range(9)])
__author__ = "S0AndS0"
#
# Functions
#
def question(message):
""" Returns response to `message` from user """
return input("{message}? ".format(message = message))
#
# Classes
#
class Gone_Fishing(dict):
"""
Gone_Fishing is a simple simulation inspired by
[Python - Fishing Simulator](https://codereview.stackexchange.com/q/217357/197446)
## Arguments
- `fishes`, `dict`ionary such as `{'cod': {'amount': 0, 'chances': [1, 2]}}`
- `min_chance`, `int`eger of min number that `random.randint` may generate
- `max_chance`, `int`eger of max number that `random.randint` may generate
"""
def __init__(self, fishes, min_chance = 1, max_chance = 10, **kwargs):
super(Gone_Fishing, self).__init__(**kwargs)
self.update(fishes = fishes,
chances = {'min': min_chance, 'max': max_chance})
@staticmethod
def keep_fishing(message, expected):
""" Return `bool`ean of if `response` to `message` matches `expected` """
response = question(message)
if not response or not isinstance(response, str):
return False
return response.lower() == expected
@property
def dump_cooler(self):
"""
Returns `score`, a `dict`ionary similar to `{'cod': 5, 'tire': 2}`,
after printing and reseting _`amount`s_ caught
"""
score = {}
for fish, data in self['fishes'].items():
if data['amount'] > 0:
score.update({fish: data['amount']})
if data['amount'] > 1 and data.get('plural'):
fish = data['plural']
print("{amount} {fish}".format(**{
'fish': fish,
'amount': data['amount']}))
data['amount'] = 0
return score
def catch(self, chance):
""" Returns `None` or name of `fish` caught based on `chance` """
caught = []
for fish, data in self['fishes'].items():
if chance in data['chances']:
caught.append(fish)
return caught
def main_loop(self):
"""
Asks questions, adds to _cooler_ anything caught, and prints score when finished
"""
first = True
message = 'Go fishing'
expected = 'yes'
while self.keep_fishing(message, expected):
time.sleep(1)
if first:
first = False
message = "Keep fishing"
chances = random.randint(self['chances']['min'], self['chances']['max'])
caught = self.catch(chances)
if caught:
for fish in caught:
self['fishes'][fish]['amount'] += 1
fancy_fish = ' '.join(fish.split('_')).title()
print("You caught a {fish}".format(fish = fancy_fish))
else:
print("Nothing was caught this time.")
print("{0}\nThanks for playing".format(print_separator))
if True in [x['amount'] > 0 for x in self['fishes'].values()]:
print("You caught")
self.dump_cooler
print(print_separator)
if __name__ == '__main__':
"""
This block of code is not executed during import
and instead is usually run when a file is executed,
eg. `python gone_fishing.py`, making it a good
place for simple unit tests and example usage.
"""
gone_fishing = Gone_Fishing(
fishes = {
'cod': {'amount': 0, 'chances': [1]},
'salmon': {'amount': 0, 'chances': [5]},
'shark': {'amount': 0, 'chances': [9, 10], 'plural': 'sharks'},
'wild_fish': {'amount': 0, 'chances': [7], 'plural': 'wild_fishes'},
'old_shoe': {'amount': 0, 'chances': [10, 15], 'plural': 'old_shoes'},
'tire': {'amount': 0, 'chances': [2, 19], 'plural': 'tires'},
},
min_chances = 0,
max_chances = 20,
)
gone_fishing.main_loop()
... okay there's a bit going on up there, so feel free to dissect it's operation by adding breakpoints
or print(something)
lines.
Here's what output of running the above script may look like
# python gone_fishing.py
Go fishing? 'yes'
You caught a Wild Fish
Keep fishing? 'yes'
Nothing was caught this time.
Keep fishing? 'yes'
You caught a Shark
You caught a Old Shoe
Keep fishing? 'yes'
Nothing was caught this time.
# ... trimmed for brevity
Keep fishing? 'no'
_________
Thanks for playing
You caught
2 sharks
1 tire
2 wild_fishes
1 cod
_________
Taking it from the top print_separator = "".join(['_' for _ in range(9)])
is what I like to use when generating strings of repeating characters because it's easy to make something that outputs _-_-_
via "-".join(['_' for _ in range(3)])
.
Note from the future; check the comments of this answer for some swell suggestions from @Izaak van Dongen.
By defining a class that inherits from the built in dict
ionary class
(that's what the class Gone_Fishing(dict):
line did), I'm being a bit lazy as this allows for dumping all saved states via...
print(gone_fishing)
# -> {'cod': {'amount': 2, 'chances': [1]}, ...}
... and while I'm on the tangent of getting info back out...
print(gone_fishing.main_loop.__doc__)
# Or
# help(gone_fishing.main_loop)
... will print the previously mentioned __doc__
strings.
... and figuring out where you too can avoid re-inventing the wheel is just something that'll get picked up over time. Personally I choose to view it as expanding one's vocabulary, when I discover some built-in that's been waiting to solve some edge-case.
The __init__
method
absorbs three arguments and re-assigns'em with self.update()
so that other methods that use the self
argument are able to get and/or modify class
saved states; more on that latter.
Side note; the __init__
method is one of many that are called implicitly by preforming some action with an object, eg. __add__
is called implicitly by using +
between two Objects
with a __add__
method (side-side note, I'll get into why that was an a
and not an an
in a bit), which is why the following works with lists...
list_one = [3, 2, 1]
list_two = [0, -1, -2]
list_one + list_two
# -> [3, 2, 1, 0, -1, -2]
That bit with **kwargs
stands for key word arguments
which passes things as a bare dict
ionary, the other syntax you may run across is *args
, which passes things as a bare list
of arguments; there be some fanciness that can be done with this syntax that I'll not get into at this point other than saying that context matters. However, you'll find some examples of passing an unwrapped dictionary, such as to format
via print("{amount} {fish}".format(**{...}))
, which hint hint, is a great way of passing variable parameter names.
This is one of those idiomatic things that you can pick-up with some experimentation (and grokking-out others' code bases); it's super powerful so use it often but be kind to your future self too.
The bit with super(Gone_Fishing, self).__init__(**kwargs)
is what allows the Gone_Fishing
class
to call dict
's __init__
from within it's own __init__
method
... indeed that was a little convoluted so taking a sec to unpack that...
class SomeThing(dict):
def __init__(self, an_argument = None, **kwargs):
super(SomeThing, self).__init__(**kwargs)
self.update({'an_argument': an_argument})
... it's possible to call self.update()
from within SomeThing.___init__
without causing confusion of intent, where as to have SomeThing
still operate as a dict
ionary, eg. assigning something = SomeThing(spam = 'Spam')
without causing errors, one should use super(SomeThing, self).__init__(**kwargs)
to allow Python to preform it's voodoo with figuring out which inheriting class
'll take responsibility for those arguments.
That does mean that one could do class SomeThing(dict, Iterator)
, and have that mean something but I'll not get into that here; kinda already covered that specifically on math stack in regards to graph modeling and prioritization.
The @staticmethod
and other decorators
are ways of denoting a special use method
. In the case of property
s they operate similarly to Object
properties, eg...
class Test_Obj:
pass
o = Test_Obj()
o.foo = 'Foo'
print(o.foo)
# -> Foo
... but can only be gotten not set, which makes'em a great place to stash dynamic or semiprivate properties about an Object
.
In the case of staticmethod
s, they're not passed a reference to self
so cannot easily access or modify saved states, but they can be more easily used without initializing so operate similarly to regular functions, eg...
responses = []
responses.append(question("Where to"))
print("I heard -> {response}".format(response = responses[-1]))
for _ in range(7):
responses.append(question("... are you sure"))
print("I heard -> {response}".format(response = responses[-1]))
print("Okay... though...")
Note also the various .format()
usages are to show ways of future prepping (for perhaps using f strings
in the future), as well as making strings somewhat more explicit.
Generally I use'em to make the intended usage more explicit but that's not to say that you couldn't get lost in the amount of options available just for decorating a method
.
Note from the future; as pointed out by @Maarten Fabré I indeed slipped in some superfluous use of the staticmethod
decorator, good catch there, and this'll now serve as an example of getting carried away when decorat
ing.
Generally I use staticmethod
s when I've a class that isn't concerned with it's internal state but isn't large enough to warrant it's own file, very edge case kinda thing, and usually it means that I should probably split'em out into a file that organizes similar functions. Hopefully recent edits now look closer to proper for future readers.
That bit within the main_loop
method
with while self.keep_fishing(message, expected)
, when unwrapped I think you'll really like, it's returning True
or False
at the top of every iteration based on asking the user a question and comparing their response with what's expected.
And the bit with if True in [x['amount'] > 0 for x in self['fishes'].values()]
is something that masks data using list comprehensions
, I'll advise against getting too fancy with'em, and instead try to utilize'em whenever it doesn't make code less readable. Also don't get to attached to such cleverness because numpy
, pandas
, or one of the many other libraries, will preform similar tasks far faster.
The things happening bellow the if __name__ == '__main__':
, aside from the doc string ...
Side note for those new to Python; sure you could call'em "dunder docs" and those in the know would know what you where saying, but they'd also likely smize at ya too, and saying "dundar doc string" if timed when a listener is drinking could have messy consequences... so "pro-tip", callem "doc strings" to be super
class
y when talking about Python code ;-)
gone_fishing = Gone_Fishing(fishes = {
'cod': {'amount': 0, 'chances': [1]},
'salmon': {'amount': 0, 'chances': [2]},
'shark': {'amount': 0, 'chances': [3], 'plural': 'sharks'},
'wild_fish': {'amount': 0, 'chances': [4], 'plural': 'wild_fishes'},
'old_shoe': {'amount': 0, 'chances': [5, 6], 'plural': 'old_shoes'},
'tire': {'amount': 0, 'chances': [7, 8], 'plural': 'tires'},
})
... and how the above is parsed could take some words to do a full stack trace, but the gist is that chances
is a list
that you could even have overlapping integers, eg. a shark
who had an old_shoe
inside could be...
gone_fishing['fishes']['shark']['chances'].append(5)
... though without adjustments to other values that would make for a very large shoal of soul hungry sharks.
Note from the future; I've made adjustments to the code to enable overlapping values and returning of more than one result; there probably be better ways of doing it but this is also an example of iterative development now.
When you've figured out how plural
is an optional key value pair within a nested dictionary you'll start seeing similar things in other code (at least it's one of those things I've not been unable to unsee), try not to get messy with that trick though, otherwise I think it's self-explanatory as to the intentions of it's usage.
The arguments that I didn't assign, min_chance
and max_chance
, much like the chances
with sharks
could be updated similarly, eg...
gone_fishing['chances']['max'] = 20
... though initializing a new trip would look like...
another_fishing_trip = Gone_Fishing(
fishes = {
'cod': {'amount': 0, 'chances': [1]},
'salmon': {'amount': 0, 'chances': [5]},
'shark': {'amount': 0, 'chances': [9, 10], 'plural': 'sharks'},
'wild_fish': {'amount': 0, 'chances': [7], 'plural': 'wild_fishes'},
'old_shoe': {'amount': 0, 'chances': [10, 15], 'plural': 'old_shoes'},
'tire': {'amount': 0, 'chances': [2, 19], 'plural': 'tires'},
},
min_chances = 0,
max_chances = 20,
)
... which serves as an example of something you'd be wise to avoid doing to your own code, swapping words especially isn't going to win any points from a future self or other developers.
There's certainly more room for improvement, eg. having gone_fishing['fishes'][fish_name]['amount']
subtracted from, while adding to gone_fishing['cooler']
or similar structure; just for a start. But this was all just to expose quick-n-dirty methods of organizing the problem space with Object Oriented Programing.
Hopefully having code with a bit more abstraction shows ya that going with something that looks a bit more complex can allow for simplifying the usage and future feature creep
. Please keep us posted if ya make something more out of your learning project.