Find the cheapest shipping option based on item weightAssigning sentiment to each tweet - Twitter trendShipping charge based on weightDynamic class instancing (with conditional parameters and methods) based on a dictionaryFind csv item indexREST API Communication script | ArgparsingCompute a students grade based on weight of assignmentsPiling Up with PythonFind the longest sequencePhone call routing cost checkerLongest palindromes in a string
Why increasing of the temperature of the objects like wood, paper etc. doesn't fire them?
How would you say "You forget wearing what you're wearing"?
Changing stroke width vertically but not horizontally in Inkscape
Can a good but unremarkable PhD student become an accomplished professor?
HSA - Continue to Invest?
What does the coin flipping before dying mean?
Why are condenser mics so much more expensive than dynamics?
Transistor gain, what if there is not enough current?
How to preserve a rare version of a book?
What is a common way to tell if an academic is "above average," or outstanding in their field? Is their h-index (Hirsh index) one of them?
Efficient deletion of specific list entries
What does のそ mean on this picture?
Can anyone identify this unknown 1988 PC card from The Palantir Corporation?
How to use awk to extract data from a file based on the content of another file?
Can an earth elemental drag a tiny creature underground with Earth Glide?
How long did it take Captain Marvel to travel to Earth?
Is it normal for gliders not to have attitude indicators?
What do you call a painting painted on a wall?
Convert Numbers To Emoji Math
Collision domain question
Two denim hijabs
Picking a theme as a discovery writer
Antivirus for Ubuntu 18.04
What does the phrase "go for the pin" mean here?
Find the cheapest shipping option based on item weight
Assigning sentiment to each tweet - Twitter trendShipping charge based on weightDynamic class instancing (with conditional parameters and methods) based on a dictionaryFind csv item indexREST API Communication script | ArgparsingCompute a students grade based on weight of assignmentsPiling Up with PythonFind the longest sequencePhone call routing cost checkerLongest palindromes in a string
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty margin-bottom:0;
$begingroup$
I've been taking the Computer Science course on Codecademy. The course includes this project where we have to create a python script to find the cheapest shipping method based on the weight of an item and I'm just wondering if this is the best way to code the solution?
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
def cheapest_shipping(weight):
ground_cost, premium_cost = ground_shipping_cost(weight)
drone_cost = drone_shipping_cost(weight)
if drone_cost < ground_cost and drone_cost < premium_cost:
return "You should use drone shipping as it will only cost " + str(drone_cost)
if ground_cost < drone_cost and ground_cost < premium_cost:
return "You should use standard ground shipping as it will only cost " + str(ground_cost)
if premium_cost < ground_cost and premium_cost < drone_cost:
return "You should use premium shipping as it will only cost " + str(premium_cost)
print(cheapest_shipping(4.8)) # You should use standard ground shipping as it will only cost 34.4
print(cheapest_shipping(41.5)) # You should use premium shipping as it will only cost 125
print(cheapest_shipping(1.5)) # You should use drone shipping as it will only cost 6.75
print(cheapest_shipping(4.0)) # You should use standard ground shipping as it will only cost 32.0
The issue I see is maybe there's too many if, elif statements and maybe there is a way to simplify the code
python python-3.x
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
$begingroup$
I've been taking the Computer Science course on Codecademy. The course includes this project where we have to create a python script to find the cheapest shipping method based on the weight of an item and I'm just wondering if this is the best way to code the solution?
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
def cheapest_shipping(weight):
ground_cost, premium_cost = ground_shipping_cost(weight)
drone_cost = drone_shipping_cost(weight)
if drone_cost < ground_cost and drone_cost < premium_cost:
return "You should use drone shipping as it will only cost " + str(drone_cost)
if ground_cost < drone_cost and ground_cost < premium_cost:
return "You should use standard ground shipping as it will only cost " + str(ground_cost)
if premium_cost < ground_cost and premium_cost < drone_cost:
return "You should use premium shipping as it will only cost " + str(premium_cost)
print(cheapest_shipping(4.8)) # You should use standard ground shipping as it will only cost 34.4
print(cheapest_shipping(41.5)) # You should use premium shipping as it will only cost 125
print(cheapest_shipping(1.5)) # You should use drone shipping as it will only cost 6.75
print(cheapest_shipping(4.0)) # You should use standard ground shipping as it will only cost 32.0
The issue I see is maybe there's too many if, elif statements and maybe there is a way to simplify the code
python python-3.x
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
4
$begingroup$
Welcome to Code Review! Here, it is more about best way to code a solution rather than best solution re. usability or resource usage - you may want to narrow down best.
$endgroup$
– greybeard
May 2 at 9:40
1
$begingroup$
Ah! Okay thanks for that - will update the question
$endgroup$
– Stephen
May 2 at 9:47
add a comment |
$begingroup$
I've been taking the Computer Science course on Codecademy. The course includes this project where we have to create a python script to find the cheapest shipping method based on the weight of an item and I'm just wondering if this is the best way to code the solution?
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
def cheapest_shipping(weight):
ground_cost, premium_cost = ground_shipping_cost(weight)
drone_cost = drone_shipping_cost(weight)
if drone_cost < ground_cost and drone_cost < premium_cost:
return "You should use drone shipping as it will only cost " + str(drone_cost)
if ground_cost < drone_cost and ground_cost < premium_cost:
return "You should use standard ground shipping as it will only cost " + str(ground_cost)
if premium_cost < ground_cost and premium_cost < drone_cost:
return "You should use premium shipping as it will only cost " + str(premium_cost)
print(cheapest_shipping(4.8)) # You should use standard ground shipping as it will only cost 34.4
print(cheapest_shipping(41.5)) # You should use premium shipping as it will only cost 125
print(cheapest_shipping(1.5)) # You should use drone shipping as it will only cost 6.75
print(cheapest_shipping(4.0)) # You should use standard ground shipping as it will only cost 32.0
The issue I see is maybe there's too many if, elif statements and maybe there is a way to simplify the code
python python-3.x
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
I've been taking the Computer Science course on Codecademy. The course includes this project where we have to create a python script to find the cheapest shipping method based on the weight of an item and I'm just wondering if this is the best way to code the solution?
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
def cheapest_shipping(weight):
ground_cost, premium_cost = ground_shipping_cost(weight)
drone_cost = drone_shipping_cost(weight)
if drone_cost < ground_cost and drone_cost < premium_cost:
return "You should use drone shipping as it will only cost " + str(drone_cost)
if ground_cost < drone_cost and ground_cost < premium_cost:
return "You should use standard ground shipping as it will only cost " + str(ground_cost)
if premium_cost < ground_cost and premium_cost < drone_cost:
return "You should use premium shipping as it will only cost " + str(premium_cost)
print(cheapest_shipping(4.8)) # You should use standard ground shipping as it will only cost 34.4
print(cheapest_shipping(41.5)) # You should use premium shipping as it will only cost 125
print(cheapest_shipping(1.5)) # You should use drone shipping as it will only cost 6.75
print(cheapest_shipping(4.0)) # You should use standard ground shipping as it will only cost 32.0
The issue I see is maybe there's too many if, elif statements and maybe there is a way to simplify the code
python python-3.x
python python-3.x
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
edited May 2 at 9:48
Stephen
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
asked May 2 at 9:17
StephenStephen
1588
1588
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
New contributor
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
Stephen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
4
$begingroup$
Welcome to Code Review! Here, it is more about best way to code a solution rather than best solution re. usability or resource usage - you may want to narrow down best.
$endgroup$
– greybeard
May 2 at 9:40
1
$begingroup$
Ah! Okay thanks for that - will update the question
$endgroup$
– Stephen
May 2 at 9:47
add a comment |
4
$begingroup$
Welcome to Code Review! Here, it is more about best way to code a solution rather than best solution re. usability or resource usage - you may want to narrow down best.
$endgroup$
– greybeard
May 2 at 9:40
1
$begingroup$
Ah! Okay thanks for that - will update the question
$endgroup$
– Stephen
May 2 at 9:47
4
4
$begingroup$
Welcome to Code Review! Here, it is more about best way to code a solution rather than best solution re. usability or resource usage - you may want to narrow down best.
$endgroup$
– greybeard
May 2 at 9:40
$begingroup$
Welcome to Code Review! Here, it is more about best way to code a solution rather than best solution re. usability or resource usage - you may want to narrow down best.
$endgroup$
– greybeard
May 2 at 9:40
1
1
$begingroup$
Ah! Okay thanks for that - will update the question
$endgroup$
– Stephen
May 2 at 9:47
$begingroup$
Ah! Okay thanks for that - will update the question
$endgroup$
– Stephen
May 2 at 9:47
add a comment |
2 Answers
2
active
oldest
votes
$begingroup$
Welcome to CodeReview! Having others review your code is one of the very best ways to find bugs and improve your coding. And we're going to improve your coding, no matter how much it hurts! :-)
I'm going to repeat many of the points made in Maarten's review, although with slightly different results.
From your post, I know you're still learning. From your code, I believe you have learned: if/elif/else statements, Python tuple types, and functions. So I'm going to focus on these in any improvements.
else means 'Not true'
The most glaring problem, and one you intuited a little yourself, is that you are doing "else" wrong. Consider:
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
In this sequence, you first check if weight <= 2. Now pretend that if statement fails. What do you know? You know that if any one of the else statements executes, then weight must be > 2 because otherwise the if statement would have executed!
So never "test" something you know to be true. Or false. If you know a thing, you don't need to test it. (You might assert it for sanity checking, but that's different.)
Note: With compound statements, like if A and B you might have to (re) test one of the statements when the compound fails:
if A and B:
elif A:
But that's technically different because they are different conditions.
So let's rewrite your conditions:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
Becomes:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Note two things: first, the weight > 10 case becomes a blanket else statement, since you are covering all the possible numbers; and second, there's no reason to set cost = 0 initially, since you cover all possible numbers:
def drone_shipping_cost(weight):
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Keep separate things separate
You could rewrite your ground_shipping_cost function in a similar way, but let's take a harder look at that:
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
You're doing a couple of things wrong here. First, you're "accumulating" when you should be "adding". And second, you're returning a tuple just to get the premium cost. In reality, the premium shipping cost is another form of shipping.
Let's get the low-hanging-fruit out of the way:
def premium_shipping_cost(weight):
''' Compute cost of premium shipping for a package. '''
return 125
That was easy, wasn't it!
Now, let's remove the premium_cost from the ground shipping, and fix the if/else statements:
def ground_shipping_cost(weight):
flat_cost = 20
if weight <= 2:
flat_cost += weight * 1.50
elif weight <= 6:
flat_cost += weight * 3.00
elif weight <= 10:
flat_cost += weight * 4.00
else:
flat_cost += weight * 4.75
return flat_cost
This looks better, but you're still "accumulating" instead of "adding." In this case, that's the wrong thing to do because you're only going to add one thing. Phrasing the computations as accumulating costs gives the wrong impression to the reader. Let's make it clear that there's a flat charge and a by-weight charge:
def ground_shipping_cost(weight):
''' Compute cost of ground shipping for a package. '''
flat_cost = 20
if weight <= 2:
weight_charge = weight * 1.50
elif weight <= 6:
weight_charge = weight * 3.00
elif weight <= 10:
weight_charge = weight * 4.00
else:
weight_charge = weight * 4.75
return flat_cost + weight_charge
This version makes it clear that there's a flat cost and a weight charge. Future-Stephen will thank you.
You make the call!
Now here's where I'll diverge from Maarten Fabre's review. The DRY principle tells us that those two chains of if/elif/else statements should be moved into a separate function.
First I have to ask, what's the objective? If you are in part of a class where they are focused on writing functions, then that is absolutely right and you should do it.
But if you are in a part of the class where they are starting to focus on classes and objects, and encapsulating behavior, then maybe it would be the wrong thing to do. Why? Because maybe the weights and cost multipliers were the same only by coincidence, and maybe the next thing they ask will be for you to separate them!
So you have to use your own judgement. You could write a function that returns the cost multiplier. You could write a function that returns a cost "category" and use that to look up the multiplier. Or you could leave the two cost functions with duplicated code, so that you can change the cost layers or cost multipliers independently.
One built-in function
Python has a built-in function called min. By default, min compares objects in a sequence, or objects passed as positional parameters. It compares them using the default Python comparison, and for tuples that comparison compares the elements of the tuple in ascending order. This is discussed nicely in this SO answer.
What this means for you is that you can use min on a sequence of tuple values, in different ways:
- You could compute name, cost tuples for the shipping types, and find the lowest cost.
- You could store name, cost-function tuples and compute the lowest cost using a special
keyfunction.
Let's try the most direct approach, since it's easier to understand. And let's put the cost before the name, so that we get the right results (comparison is in tuple order!):
def cheapest_shipping(weight):
''' Determine the cheapest shipping method for a package. '''
drone_cost = drone_shipping_cost(weight)
ground_cost = ground_shipping_cost(weight)
premium_cost = premium_shipping_cost(weight)
cheapest_tuple = min((drone_cost, 'drone shipping'),
(ground_cost, 'ground shipping'),
(premium_cost, 'premium shipping'))
return cheapest_tuple
At this point, you can do what is called "tuple unpacking" (remember the word "unpacking" -- you'll want to search for it later). That lets you return multiple values into multiple separate variables:
cost, name = cheapest_shipping(weight)
print(f"You should use name, it costs only cost!")
$endgroup$
$begingroup$
Thanks for the detailed answer Austin! This has helped me understand a lot more about what was incorrect with my code like the "accumulating" cost. Again thank you for taking your time to write such a detailed response I appreciate it
$endgroup$
– Stephen
May 2 at 18:10
3
$begingroup$
Another improvement toground_shipping_costwould be to just setweight_rate(or a similarly named variable) inside theifblocks and thenreturn flat_cost + weight_rate * weight. You could also bypass the extra variables in the finalcheapest_shippingmethod, but I understand why you might want them, especially for a beginning. +1 for solid advice that doesn't make things more convoluted.
$endgroup$
– jpmc26
May 2 at 22:10
$begingroup$
This is the better answer compared to the accepted one.
$endgroup$
– stefan
2 days ago
$begingroup$
@stefan Yeah I have accepted this one as the answer as it explains the refactor as more beginner friendly than the answer I accepted originally
$endgroup$
– Stephen
2 days ago
add a comment |
$begingroup$
premium_cost
There is no reason to include the return of this premium_cost in ground_shipping_cost. Use a different method here
DRY
You are correct. There are too many if statements. If you want to introduce an new threshold for the cost, this will include a adding an extra clause, and changing the limits for the preceding and following thresholds. This is just waiting for errors to pop up. Better would be to keep a dict of the thresholds, and iterate over them to get this price factor:
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
for threshold, factor in sorted(thresholds.items()):
if weight <= threshold:
break
return flat_cost + weight * factor
thresholds is a dict, with the cost per weight unit as value, and the threshold as key.
The drone_shipping_cost can be tackled comparably.
Now you have 2 methods that, starting from a list of thresholds, tries to determine the cost factor. We can easily refactor this out:
def get_factor(thresholds, value):
for threshold, factor in sorted(thresholds.items()):
if value <= threshold:
return factor
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
return flat_cost + get_factor(thresholds, weight)
def drone_shipping_cost(weight):
thresholds = 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75
return weight * get_factor(thresholds, weight)
Cheapest costs
Your cheapest_shipping method calculates the costs for the different shipping methods, finds the cheapest one and formats this into a string. This string formatting is also very repetitive, and should be done somewhere else. cheapest_shipping will be the most clear if it only returned which method is the cheapest, and the corresponding cost. This also allow you to test this method with unit tests further on.
Since methods are real objects in Python, and you can pass references to them on and store these in dicts, calculating the costs for the different methods can be simplified a lot:
def cheapest_shipping(weight):
methods =
"drone": drone_shipping_cost,
"standard ground": ground_shipping_cost,
"premium ground": lambda weight: 125,
results = method: calculation(weight) for method, calculation in methods.items()
To look for the cheapest option among those, you can use the built-in min:
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
Note: the lambda weight: 125 is the equivalent of
def premium_shipping(weight):
return 125
and "premium ground": premium_shipping, in the methods dict
And this can be called and formatted using str.format or f-strings in Python 3.6+
method, cost = cheapest_shipping(4)
f"You should use method shipping as it will only cost cost"
'You should use standard ground shipping as it will only cost 23.0'
Further refactoring
You can even refactor this one step further, and have 1 generalized cost calculation method that take a flat_cost and thresholds as arguments
def get_shipping_cost(weight, thresholds=None, flat_cost=0):
if thresholds is None:
return flat_cost
return flat_cost + weight * get_factor(thresholds, weight)
then you can define the different shipping methods like this:
shipping_methods =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "flat_cost": 125,
def cheapest_shipping2(methods, weight):
results =
method: get_shipping_cost(weight, **parameters)
for method, parameters in methods.items()
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
method, cost = cheapest_shipping2(shipping_methods, 4)
different min
Instead of using min with the cost as key, you can reverse the results dictionary:
def cheapest_shipping2(methods, weight):
results =
get_shipping_cost(weight, **parameters): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
In case of an ex aequo, this can have different results than the other method
even further refactoring.
Now all shipping methods are calculated with get_shipping_cost. If you want to use different functions for the different shipping methods, you can do something like this:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
shipping_methods2 =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "method": lambda weight: 125,
method, cost = cheapest_shipping3(shipping_methods2, 4)
please note that the parameters.pop mutates the original shipping_methods2 after the execution:
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, inf: 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, inf: 4.75,
,
"premium ground": ,
To prevent this, we need to make a copy of the methods:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
methods_copy =
method: parameters.copy()
for method, parameters
in methods.items()
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods_copy.items()
cost, method = min(results.items())
return method, cost
$endgroup$
$begingroup$
Thanks for taking your time to give me this very in-depth response I really appreciate it. Although I don't understand everything going on here (as I'm still new to Python) I can certainly say I've learnt a thing or two from it!
$endgroup$
– Stephen
May 2 at 10:56
$begingroup$
"thresholdsis a sorted list with the factor for the weights." No it's not. It is a dictionary with the thresholds and factors which is sorted in Python 3.7+ and in arbitrary order before that.sorted(thresholds.items())however is a sorted iterable in all Python versions, but it also contains both the threshold and the factor.
$endgroup$
– Graipher
May 2 at 14:20
2
$begingroup$
I would not use adictto represent a range relationship. This is very unintuitive.
$endgroup$
– jpmc26
May 2 at 22:06
1
$begingroup$
@jpmc26 How about a list of objects that have max_weight and cost properties?
$endgroup$
– JollyJoker
2 days ago
1
$begingroup$
I disagree about the "further refactoring" sections. IMHO they are violating some principles of the "Zen of Python", especially "simple is better than complex" and "readability counts".
$endgroup$
– stefan
2 days ago
|
show 3 more comments
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Stephen is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f219572%2ffind-the-cheapest-shipping-option-based-on-item-weight%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
Welcome to CodeReview! Having others review your code is one of the very best ways to find bugs and improve your coding. And we're going to improve your coding, no matter how much it hurts! :-)
I'm going to repeat many of the points made in Maarten's review, although with slightly different results.
From your post, I know you're still learning. From your code, I believe you have learned: if/elif/else statements, Python tuple types, and functions. So I'm going to focus on these in any improvements.
else means 'Not true'
The most glaring problem, and one you intuited a little yourself, is that you are doing "else" wrong. Consider:
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
In this sequence, you first check if weight <= 2. Now pretend that if statement fails. What do you know? You know that if any one of the else statements executes, then weight must be > 2 because otherwise the if statement would have executed!
So never "test" something you know to be true. Or false. If you know a thing, you don't need to test it. (You might assert it for sanity checking, but that's different.)
Note: With compound statements, like if A and B you might have to (re) test one of the statements when the compound fails:
if A and B:
elif A:
But that's technically different because they are different conditions.
So let's rewrite your conditions:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
Becomes:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Note two things: first, the weight > 10 case becomes a blanket else statement, since you are covering all the possible numbers; and second, there's no reason to set cost = 0 initially, since you cover all possible numbers:
def drone_shipping_cost(weight):
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Keep separate things separate
You could rewrite your ground_shipping_cost function in a similar way, but let's take a harder look at that:
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
You're doing a couple of things wrong here. First, you're "accumulating" when you should be "adding". And second, you're returning a tuple just to get the premium cost. In reality, the premium shipping cost is another form of shipping.
Let's get the low-hanging-fruit out of the way:
def premium_shipping_cost(weight):
''' Compute cost of premium shipping for a package. '''
return 125
That was easy, wasn't it!
Now, let's remove the premium_cost from the ground shipping, and fix the if/else statements:
def ground_shipping_cost(weight):
flat_cost = 20
if weight <= 2:
flat_cost += weight * 1.50
elif weight <= 6:
flat_cost += weight * 3.00
elif weight <= 10:
flat_cost += weight * 4.00
else:
flat_cost += weight * 4.75
return flat_cost
This looks better, but you're still "accumulating" instead of "adding." In this case, that's the wrong thing to do because you're only going to add one thing. Phrasing the computations as accumulating costs gives the wrong impression to the reader. Let's make it clear that there's a flat charge and a by-weight charge:
def ground_shipping_cost(weight):
''' Compute cost of ground shipping for a package. '''
flat_cost = 20
if weight <= 2:
weight_charge = weight * 1.50
elif weight <= 6:
weight_charge = weight * 3.00
elif weight <= 10:
weight_charge = weight * 4.00
else:
weight_charge = weight * 4.75
return flat_cost + weight_charge
This version makes it clear that there's a flat cost and a weight charge. Future-Stephen will thank you.
You make the call!
Now here's where I'll diverge from Maarten Fabre's review. The DRY principle tells us that those two chains of if/elif/else statements should be moved into a separate function.
First I have to ask, what's the objective? If you are in part of a class where they are focused on writing functions, then that is absolutely right and you should do it.
But if you are in a part of the class where they are starting to focus on classes and objects, and encapsulating behavior, then maybe it would be the wrong thing to do. Why? Because maybe the weights and cost multipliers were the same only by coincidence, and maybe the next thing they ask will be for you to separate them!
So you have to use your own judgement. You could write a function that returns the cost multiplier. You could write a function that returns a cost "category" and use that to look up the multiplier. Or you could leave the two cost functions with duplicated code, so that you can change the cost layers or cost multipliers independently.
One built-in function
Python has a built-in function called min. By default, min compares objects in a sequence, or objects passed as positional parameters. It compares them using the default Python comparison, and for tuples that comparison compares the elements of the tuple in ascending order. This is discussed nicely in this SO answer.
What this means for you is that you can use min on a sequence of tuple values, in different ways:
- You could compute name, cost tuples for the shipping types, and find the lowest cost.
- You could store name, cost-function tuples and compute the lowest cost using a special
keyfunction.
Let's try the most direct approach, since it's easier to understand. And let's put the cost before the name, so that we get the right results (comparison is in tuple order!):
def cheapest_shipping(weight):
''' Determine the cheapest shipping method for a package. '''
drone_cost = drone_shipping_cost(weight)
ground_cost = ground_shipping_cost(weight)
premium_cost = premium_shipping_cost(weight)
cheapest_tuple = min((drone_cost, 'drone shipping'),
(ground_cost, 'ground shipping'),
(premium_cost, 'premium shipping'))
return cheapest_tuple
At this point, you can do what is called "tuple unpacking" (remember the word "unpacking" -- you'll want to search for it later). That lets you return multiple values into multiple separate variables:
cost, name = cheapest_shipping(weight)
print(f"You should use name, it costs only cost!")
$endgroup$
$begingroup$
Thanks for the detailed answer Austin! This has helped me understand a lot more about what was incorrect with my code like the "accumulating" cost. Again thank you for taking your time to write such a detailed response I appreciate it
$endgroup$
– Stephen
May 2 at 18:10
3
$begingroup$
Another improvement toground_shipping_costwould be to just setweight_rate(or a similarly named variable) inside theifblocks and thenreturn flat_cost + weight_rate * weight. You could also bypass the extra variables in the finalcheapest_shippingmethod, but I understand why you might want them, especially for a beginning. +1 for solid advice that doesn't make things more convoluted.
$endgroup$
– jpmc26
May 2 at 22:10
$begingroup$
This is the better answer compared to the accepted one.
$endgroup$
– stefan
2 days ago
$begingroup$
@stefan Yeah I have accepted this one as the answer as it explains the refactor as more beginner friendly than the answer I accepted originally
$endgroup$
– Stephen
2 days ago
add a comment |
$begingroup$
Welcome to CodeReview! Having others review your code is one of the very best ways to find bugs and improve your coding. And we're going to improve your coding, no matter how much it hurts! :-)
I'm going to repeat many of the points made in Maarten's review, although with slightly different results.
From your post, I know you're still learning. From your code, I believe you have learned: if/elif/else statements, Python tuple types, and functions. So I'm going to focus on these in any improvements.
else means 'Not true'
The most glaring problem, and one you intuited a little yourself, is that you are doing "else" wrong. Consider:
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
In this sequence, you first check if weight <= 2. Now pretend that if statement fails. What do you know? You know that if any one of the else statements executes, then weight must be > 2 because otherwise the if statement would have executed!
So never "test" something you know to be true. Or false. If you know a thing, you don't need to test it. (You might assert it for sanity checking, but that's different.)
Note: With compound statements, like if A and B you might have to (re) test one of the statements when the compound fails:
if A and B:
elif A:
But that's technically different because they are different conditions.
So let's rewrite your conditions:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
Becomes:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Note two things: first, the weight > 10 case becomes a blanket else statement, since you are covering all the possible numbers; and second, there's no reason to set cost = 0 initially, since you cover all possible numbers:
def drone_shipping_cost(weight):
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Keep separate things separate
You could rewrite your ground_shipping_cost function in a similar way, but let's take a harder look at that:
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
You're doing a couple of things wrong here. First, you're "accumulating" when you should be "adding". And second, you're returning a tuple just to get the premium cost. In reality, the premium shipping cost is another form of shipping.
Let's get the low-hanging-fruit out of the way:
def premium_shipping_cost(weight):
''' Compute cost of premium shipping for a package. '''
return 125
That was easy, wasn't it!
Now, let's remove the premium_cost from the ground shipping, and fix the if/else statements:
def ground_shipping_cost(weight):
flat_cost = 20
if weight <= 2:
flat_cost += weight * 1.50
elif weight <= 6:
flat_cost += weight * 3.00
elif weight <= 10:
flat_cost += weight * 4.00
else:
flat_cost += weight * 4.75
return flat_cost
This looks better, but you're still "accumulating" instead of "adding." In this case, that's the wrong thing to do because you're only going to add one thing. Phrasing the computations as accumulating costs gives the wrong impression to the reader. Let's make it clear that there's a flat charge and a by-weight charge:
def ground_shipping_cost(weight):
''' Compute cost of ground shipping for a package. '''
flat_cost = 20
if weight <= 2:
weight_charge = weight * 1.50
elif weight <= 6:
weight_charge = weight * 3.00
elif weight <= 10:
weight_charge = weight * 4.00
else:
weight_charge = weight * 4.75
return flat_cost + weight_charge
This version makes it clear that there's a flat cost and a weight charge. Future-Stephen will thank you.
You make the call!
Now here's where I'll diverge from Maarten Fabre's review. The DRY principle tells us that those two chains of if/elif/else statements should be moved into a separate function.
First I have to ask, what's the objective? If you are in part of a class where they are focused on writing functions, then that is absolutely right and you should do it.
But if you are in a part of the class where they are starting to focus on classes and objects, and encapsulating behavior, then maybe it would be the wrong thing to do. Why? Because maybe the weights and cost multipliers were the same only by coincidence, and maybe the next thing they ask will be for you to separate them!
So you have to use your own judgement. You could write a function that returns the cost multiplier. You could write a function that returns a cost "category" and use that to look up the multiplier. Or you could leave the two cost functions with duplicated code, so that you can change the cost layers or cost multipliers independently.
One built-in function
Python has a built-in function called min. By default, min compares objects in a sequence, or objects passed as positional parameters. It compares them using the default Python comparison, and for tuples that comparison compares the elements of the tuple in ascending order. This is discussed nicely in this SO answer.
What this means for you is that you can use min on a sequence of tuple values, in different ways:
- You could compute name, cost tuples for the shipping types, and find the lowest cost.
- You could store name, cost-function tuples and compute the lowest cost using a special
keyfunction.
Let's try the most direct approach, since it's easier to understand. And let's put the cost before the name, so that we get the right results (comparison is in tuple order!):
def cheapest_shipping(weight):
''' Determine the cheapest shipping method for a package. '''
drone_cost = drone_shipping_cost(weight)
ground_cost = ground_shipping_cost(weight)
premium_cost = premium_shipping_cost(weight)
cheapest_tuple = min((drone_cost, 'drone shipping'),
(ground_cost, 'ground shipping'),
(premium_cost, 'premium shipping'))
return cheapest_tuple
At this point, you can do what is called "tuple unpacking" (remember the word "unpacking" -- you'll want to search for it later). That lets you return multiple values into multiple separate variables:
cost, name = cheapest_shipping(weight)
print(f"You should use name, it costs only cost!")
$endgroup$
$begingroup$
Thanks for the detailed answer Austin! This has helped me understand a lot more about what was incorrect with my code like the "accumulating" cost. Again thank you for taking your time to write such a detailed response I appreciate it
$endgroup$
– Stephen
May 2 at 18:10
3
$begingroup$
Another improvement toground_shipping_costwould be to just setweight_rate(or a similarly named variable) inside theifblocks and thenreturn flat_cost + weight_rate * weight. You could also bypass the extra variables in the finalcheapest_shippingmethod, but I understand why you might want them, especially for a beginning. +1 for solid advice that doesn't make things more convoluted.
$endgroup$
– jpmc26
May 2 at 22:10
$begingroup$
This is the better answer compared to the accepted one.
$endgroup$
– stefan
2 days ago
$begingroup$
@stefan Yeah I have accepted this one as the answer as it explains the refactor as more beginner friendly than the answer I accepted originally
$endgroup$
– Stephen
2 days ago
add a comment |
$begingroup$
Welcome to CodeReview! Having others review your code is one of the very best ways to find bugs and improve your coding. And we're going to improve your coding, no matter how much it hurts! :-)
I'm going to repeat many of the points made in Maarten's review, although with slightly different results.
From your post, I know you're still learning. From your code, I believe you have learned: if/elif/else statements, Python tuple types, and functions. So I'm going to focus on these in any improvements.
else means 'Not true'
The most glaring problem, and one you intuited a little yourself, is that you are doing "else" wrong. Consider:
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
In this sequence, you first check if weight <= 2. Now pretend that if statement fails. What do you know? You know that if any one of the else statements executes, then weight must be > 2 because otherwise the if statement would have executed!
So never "test" something you know to be true. Or false. If you know a thing, you don't need to test it. (You might assert it for sanity checking, but that's different.)
Note: With compound statements, like if A and B you might have to (re) test one of the statements when the compound fails:
if A and B:
elif A:
But that's technically different because they are different conditions.
So let's rewrite your conditions:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
Becomes:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Note two things: first, the weight > 10 case becomes a blanket else statement, since you are covering all the possible numbers; and second, there's no reason to set cost = 0 initially, since you cover all possible numbers:
def drone_shipping_cost(weight):
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Keep separate things separate
You could rewrite your ground_shipping_cost function in a similar way, but let's take a harder look at that:
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
You're doing a couple of things wrong here. First, you're "accumulating" when you should be "adding". And second, you're returning a tuple just to get the premium cost. In reality, the premium shipping cost is another form of shipping.
Let's get the low-hanging-fruit out of the way:
def premium_shipping_cost(weight):
''' Compute cost of premium shipping for a package. '''
return 125
That was easy, wasn't it!
Now, let's remove the premium_cost from the ground shipping, and fix the if/else statements:
def ground_shipping_cost(weight):
flat_cost = 20
if weight <= 2:
flat_cost += weight * 1.50
elif weight <= 6:
flat_cost += weight * 3.00
elif weight <= 10:
flat_cost += weight * 4.00
else:
flat_cost += weight * 4.75
return flat_cost
This looks better, but you're still "accumulating" instead of "adding." In this case, that's the wrong thing to do because you're only going to add one thing. Phrasing the computations as accumulating costs gives the wrong impression to the reader. Let's make it clear that there's a flat charge and a by-weight charge:
def ground_shipping_cost(weight):
''' Compute cost of ground shipping for a package. '''
flat_cost = 20
if weight <= 2:
weight_charge = weight * 1.50
elif weight <= 6:
weight_charge = weight * 3.00
elif weight <= 10:
weight_charge = weight * 4.00
else:
weight_charge = weight * 4.75
return flat_cost + weight_charge
This version makes it clear that there's a flat cost and a weight charge. Future-Stephen will thank you.
You make the call!
Now here's where I'll diverge from Maarten Fabre's review. The DRY principle tells us that those two chains of if/elif/else statements should be moved into a separate function.
First I have to ask, what's the objective? If you are in part of a class where they are focused on writing functions, then that is absolutely right and you should do it.
But if you are in a part of the class where they are starting to focus on classes and objects, and encapsulating behavior, then maybe it would be the wrong thing to do. Why? Because maybe the weights and cost multipliers were the same only by coincidence, and maybe the next thing they ask will be for you to separate them!
So you have to use your own judgement. You could write a function that returns the cost multiplier. You could write a function that returns a cost "category" and use that to look up the multiplier. Or you could leave the two cost functions with duplicated code, so that you can change the cost layers or cost multipliers independently.
One built-in function
Python has a built-in function called min. By default, min compares objects in a sequence, or objects passed as positional parameters. It compares them using the default Python comparison, and for tuples that comparison compares the elements of the tuple in ascending order. This is discussed nicely in this SO answer.
What this means for you is that you can use min on a sequence of tuple values, in different ways:
- You could compute name, cost tuples for the shipping types, and find the lowest cost.
- You could store name, cost-function tuples and compute the lowest cost using a special
keyfunction.
Let's try the most direct approach, since it's easier to understand. And let's put the cost before the name, so that we get the right results (comparison is in tuple order!):
def cheapest_shipping(weight):
''' Determine the cheapest shipping method for a package. '''
drone_cost = drone_shipping_cost(weight)
ground_cost = ground_shipping_cost(weight)
premium_cost = premium_shipping_cost(weight)
cheapest_tuple = min((drone_cost, 'drone shipping'),
(ground_cost, 'ground shipping'),
(premium_cost, 'premium shipping'))
return cheapest_tuple
At this point, you can do what is called "tuple unpacking" (remember the word "unpacking" -- you'll want to search for it later). That lets you return multiple values into multiple separate variables:
cost, name = cheapest_shipping(weight)
print(f"You should use name, it costs only cost!")
$endgroup$
Welcome to CodeReview! Having others review your code is one of the very best ways to find bugs and improve your coding. And we're going to improve your coding, no matter how much it hurts! :-)
I'm going to repeat many of the points made in Maarten's review, although with slightly different results.
From your post, I know you're still learning. From your code, I believe you have learned: if/elif/else statements, Python tuple types, and functions. So I'm going to focus on these in any improvements.
else means 'Not true'
The most glaring problem, and one you intuited a little yourself, is that you are doing "else" wrong. Consider:
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
In this sequence, you first check if weight <= 2. Now pretend that if statement fails. What do you know? You know that if any one of the else statements executes, then weight must be > 2 because otherwise the if statement would have executed!
So never "test" something you know to be true. Or false. If you know a thing, you don't need to test it. (You might assert it for sanity checking, but that's different.)
Note: With compound statements, like if A and B you might have to (re) test one of the statements when the compound fails:
if A and B:
elif A:
But that's technically different because they are different conditions.
So let's rewrite your conditions:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight > 2 and weight <= 6:
cost = weight * 9.00
elif weight > 6 and weight <= 10:
cost = weight * 12.00
elif weight > 10:
cost = weight * 14.25
return cost
Becomes:
def drone_shipping_cost(weight):
cost = 0
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Note two things: first, the weight > 10 case becomes a blanket else statement, since you are covering all the possible numbers; and second, there's no reason to set cost = 0 initially, since you cover all possible numbers:
def drone_shipping_cost(weight):
if weight <= 2:
cost = weight * 4.50
elif weight <= 6:
cost = weight * 9.00
elif weight <= 10:
cost = weight * 12.00
else:
cost = weight * 14.25
return cost
Keep separate things separate
You could rewrite your ground_shipping_cost function in a similar way, but let's take a harder look at that:
def ground_shipping_cost(weight):
flat_cost = 20
premium_cost = 125
if weight <= 2:
flat_cost += weight * 1.50
elif weight > 2 and weight <= 6:
flat_cost += weight * 3.00
elif weight > 6 and weight <= 10:
flat_cost += weight * 4.00
elif weight > 10:
flat_cost += weight * 4.75
return flat_cost, premium_cost
You're doing a couple of things wrong here. First, you're "accumulating" when you should be "adding". And second, you're returning a tuple just to get the premium cost. In reality, the premium shipping cost is another form of shipping.
Let's get the low-hanging-fruit out of the way:
def premium_shipping_cost(weight):
''' Compute cost of premium shipping for a package. '''
return 125
That was easy, wasn't it!
Now, let's remove the premium_cost from the ground shipping, and fix the if/else statements:
def ground_shipping_cost(weight):
flat_cost = 20
if weight <= 2:
flat_cost += weight * 1.50
elif weight <= 6:
flat_cost += weight * 3.00
elif weight <= 10:
flat_cost += weight * 4.00
else:
flat_cost += weight * 4.75
return flat_cost
This looks better, but you're still "accumulating" instead of "adding." In this case, that's the wrong thing to do because you're only going to add one thing. Phrasing the computations as accumulating costs gives the wrong impression to the reader. Let's make it clear that there's a flat charge and a by-weight charge:
def ground_shipping_cost(weight):
''' Compute cost of ground shipping for a package. '''
flat_cost = 20
if weight <= 2:
weight_charge = weight * 1.50
elif weight <= 6:
weight_charge = weight * 3.00
elif weight <= 10:
weight_charge = weight * 4.00
else:
weight_charge = weight * 4.75
return flat_cost + weight_charge
This version makes it clear that there's a flat cost and a weight charge. Future-Stephen will thank you.
You make the call!
Now here's where I'll diverge from Maarten Fabre's review. The DRY principle tells us that those two chains of if/elif/else statements should be moved into a separate function.
First I have to ask, what's the objective? If you are in part of a class where they are focused on writing functions, then that is absolutely right and you should do it.
But if you are in a part of the class where they are starting to focus on classes and objects, and encapsulating behavior, then maybe it would be the wrong thing to do. Why? Because maybe the weights and cost multipliers were the same only by coincidence, and maybe the next thing they ask will be for you to separate them!
So you have to use your own judgement. You could write a function that returns the cost multiplier. You could write a function that returns a cost "category" and use that to look up the multiplier. Or you could leave the two cost functions with duplicated code, so that you can change the cost layers or cost multipliers independently.
One built-in function
Python has a built-in function called min. By default, min compares objects in a sequence, or objects passed as positional parameters. It compares them using the default Python comparison, and for tuples that comparison compares the elements of the tuple in ascending order. This is discussed nicely in this SO answer.
What this means for you is that you can use min on a sequence of tuple values, in different ways:
- You could compute name, cost tuples for the shipping types, and find the lowest cost.
- You could store name, cost-function tuples and compute the lowest cost using a special
keyfunction.
Let's try the most direct approach, since it's easier to understand. And let's put the cost before the name, so that we get the right results (comparison is in tuple order!):
def cheapest_shipping(weight):
''' Determine the cheapest shipping method for a package. '''
drone_cost = drone_shipping_cost(weight)
ground_cost = ground_shipping_cost(weight)
premium_cost = premium_shipping_cost(weight)
cheapest_tuple = min((drone_cost, 'drone shipping'),
(ground_cost, 'ground shipping'),
(premium_cost, 'premium shipping'))
return cheapest_tuple
At this point, you can do what is called "tuple unpacking" (remember the word "unpacking" -- you'll want to search for it later). That lets you return multiple values into multiple separate variables:
cost, name = cheapest_shipping(weight)
print(f"You should use name, it costs only cost!")
answered May 2 at 15:56
Austin HastingsAustin Hastings
8,7911438
8,7911438
$begingroup$
Thanks for the detailed answer Austin! This has helped me understand a lot more about what was incorrect with my code like the "accumulating" cost. Again thank you for taking your time to write such a detailed response I appreciate it
$endgroup$
– Stephen
May 2 at 18:10
3
$begingroup$
Another improvement toground_shipping_costwould be to just setweight_rate(or a similarly named variable) inside theifblocks and thenreturn flat_cost + weight_rate * weight. You could also bypass the extra variables in the finalcheapest_shippingmethod, but I understand why you might want them, especially for a beginning. +1 for solid advice that doesn't make things more convoluted.
$endgroup$
– jpmc26
May 2 at 22:10
$begingroup$
This is the better answer compared to the accepted one.
$endgroup$
– stefan
2 days ago
$begingroup$
@stefan Yeah I have accepted this one as the answer as it explains the refactor as more beginner friendly than the answer I accepted originally
$endgroup$
– Stephen
2 days ago
add a comment |
$begingroup$
Thanks for the detailed answer Austin! This has helped me understand a lot more about what was incorrect with my code like the "accumulating" cost. Again thank you for taking your time to write such a detailed response I appreciate it
$endgroup$
– Stephen
May 2 at 18:10
3
$begingroup$
Another improvement toground_shipping_costwould be to just setweight_rate(or a similarly named variable) inside theifblocks and thenreturn flat_cost + weight_rate * weight. You could also bypass the extra variables in the finalcheapest_shippingmethod, but I understand why you might want them, especially for a beginning. +1 for solid advice that doesn't make things more convoluted.
$endgroup$
– jpmc26
May 2 at 22:10
$begingroup$
This is the better answer compared to the accepted one.
$endgroup$
– stefan
2 days ago
$begingroup$
@stefan Yeah I have accepted this one as the answer as it explains the refactor as more beginner friendly than the answer I accepted originally
$endgroup$
– Stephen
2 days ago
$begingroup$
Thanks for the detailed answer Austin! This has helped me understand a lot more about what was incorrect with my code like the "accumulating" cost. Again thank you for taking your time to write such a detailed response I appreciate it
$endgroup$
– Stephen
May 2 at 18:10
$begingroup$
Thanks for the detailed answer Austin! This has helped me understand a lot more about what was incorrect with my code like the "accumulating" cost. Again thank you for taking your time to write such a detailed response I appreciate it
$endgroup$
– Stephen
May 2 at 18:10
3
3
$begingroup$
Another improvement to
ground_shipping_cost would be to just set weight_rate (or a similarly named variable) inside the if blocks and then return flat_cost + weight_rate * weight. You could also bypass the extra variables in the final cheapest_shipping method, but I understand why you might want them, especially for a beginning. +1 for solid advice that doesn't make things more convoluted.$endgroup$
– jpmc26
May 2 at 22:10
$begingroup$
Another improvement to
ground_shipping_cost would be to just set weight_rate (or a similarly named variable) inside the if blocks and then return flat_cost + weight_rate * weight. You could also bypass the extra variables in the final cheapest_shipping method, but I understand why you might want them, especially for a beginning. +1 for solid advice that doesn't make things more convoluted.$endgroup$
– jpmc26
May 2 at 22:10
$begingroup$
This is the better answer compared to the accepted one.
$endgroup$
– stefan
2 days ago
$begingroup$
This is the better answer compared to the accepted one.
$endgroup$
– stefan
2 days ago
$begingroup$
@stefan Yeah I have accepted this one as the answer as it explains the refactor as more beginner friendly than the answer I accepted originally
$endgroup$
– Stephen
2 days ago
$begingroup$
@stefan Yeah I have accepted this one as the answer as it explains the refactor as more beginner friendly than the answer I accepted originally
$endgroup$
– Stephen
2 days ago
add a comment |
$begingroup$
premium_cost
There is no reason to include the return of this premium_cost in ground_shipping_cost. Use a different method here
DRY
You are correct. There are too many if statements. If you want to introduce an new threshold for the cost, this will include a adding an extra clause, and changing the limits for the preceding and following thresholds. This is just waiting for errors to pop up. Better would be to keep a dict of the thresholds, and iterate over them to get this price factor:
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
for threshold, factor in sorted(thresholds.items()):
if weight <= threshold:
break
return flat_cost + weight * factor
thresholds is a dict, with the cost per weight unit as value, and the threshold as key.
The drone_shipping_cost can be tackled comparably.
Now you have 2 methods that, starting from a list of thresholds, tries to determine the cost factor. We can easily refactor this out:
def get_factor(thresholds, value):
for threshold, factor in sorted(thresholds.items()):
if value <= threshold:
return factor
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
return flat_cost + get_factor(thresholds, weight)
def drone_shipping_cost(weight):
thresholds = 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75
return weight * get_factor(thresholds, weight)
Cheapest costs
Your cheapest_shipping method calculates the costs for the different shipping methods, finds the cheapest one and formats this into a string. This string formatting is also very repetitive, and should be done somewhere else. cheapest_shipping will be the most clear if it only returned which method is the cheapest, and the corresponding cost. This also allow you to test this method with unit tests further on.
Since methods are real objects in Python, and you can pass references to them on and store these in dicts, calculating the costs for the different methods can be simplified a lot:
def cheapest_shipping(weight):
methods =
"drone": drone_shipping_cost,
"standard ground": ground_shipping_cost,
"premium ground": lambda weight: 125,
results = method: calculation(weight) for method, calculation in methods.items()
To look for the cheapest option among those, you can use the built-in min:
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
Note: the lambda weight: 125 is the equivalent of
def premium_shipping(weight):
return 125
and "premium ground": premium_shipping, in the methods dict
And this can be called and formatted using str.format or f-strings in Python 3.6+
method, cost = cheapest_shipping(4)
f"You should use method shipping as it will only cost cost"
'You should use standard ground shipping as it will only cost 23.0'
Further refactoring
You can even refactor this one step further, and have 1 generalized cost calculation method that take a flat_cost and thresholds as arguments
def get_shipping_cost(weight, thresholds=None, flat_cost=0):
if thresholds is None:
return flat_cost
return flat_cost + weight * get_factor(thresholds, weight)
then you can define the different shipping methods like this:
shipping_methods =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "flat_cost": 125,
def cheapest_shipping2(methods, weight):
results =
method: get_shipping_cost(weight, **parameters)
for method, parameters in methods.items()
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
method, cost = cheapest_shipping2(shipping_methods, 4)
different min
Instead of using min with the cost as key, you can reverse the results dictionary:
def cheapest_shipping2(methods, weight):
results =
get_shipping_cost(weight, **parameters): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
In case of an ex aequo, this can have different results than the other method
even further refactoring.
Now all shipping methods are calculated with get_shipping_cost. If you want to use different functions for the different shipping methods, you can do something like this:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
shipping_methods2 =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "method": lambda weight: 125,
method, cost = cheapest_shipping3(shipping_methods2, 4)
please note that the parameters.pop mutates the original shipping_methods2 after the execution:
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, inf: 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, inf: 4.75,
,
"premium ground": ,
To prevent this, we need to make a copy of the methods:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
methods_copy =
method: parameters.copy()
for method, parameters
in methods.items()
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods_copy.items()
cost, method = min(results.items())
return method, cost
$endgroup$
$begingroup$
Thanks for taking your time to give me this very in-depth response I really appreciate it. Although I don't understand everything going on here (as I'm still new to Python) I can certainly say I've learnt a thing or two from it!
$endgroup$
– Stephen
May 2 at 10:56
$begingroup$
"thresholdsis a sorted list with the factor for the weights." No it's not. It is a dictionary with the thresholds and factors which is sorted in Python 3.7+ and in arbitrary order before that.sorted(thresholds.items())however is a sorted iterable in all Python versions, but it also contains both the threshold and the factor.
$endgroup$
– Graipher
May 2 at 14:20
2
$begingroup$
I would not use adictto represent a range relationship. This is very unintuitive.
$endgroup$
– jpmc26
May 2 at 22:06
1
$begingroup$
@jpmc26 How about a list of objects that have max_weight and cost properties?
$endgroup$
– JollyJoker
2 days ago
1
$begingroup$
I disagree about the "further refactoring" sections. IMHO they are violating some principles of the "Zen of Python", especially "simple is better than complex" and "readability counts".
$endgroup$
– stefan
2 days ago
|
show 3 more comments
$begingroup$
premium_cost
There is no reason to include the return of this premium_cost in ground_shipping_cost. Use a different method here
DRY
You are correct. There are too many if statements. If you want to introduce an new threshold for the cost, this will include a adding an extra clause, and changing the limits for the preceding and following thresholds. This is just waiting for errors to pop up. Better would be to keep a dict of the thresholds, and iterate over them to get this price factor:
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
for threshold, factor in sorted(thresholds.items()):
if weight <= threshold:
break
return flat_cost + weight * factor
thresholds is a dict, with the cost per weight unit as value, and the threshold as key.
The drone_shipping_cost can be tackled comparably.
Now you have 2 methods that, starting from a list of thresholds, tries to determine the cost factor. We can easily refactor this out:
def get_factor(thresholds, value):
for threshold, factor in sorted(thresholds.items()):
if value <= threshold:
return factor
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
return flat_cost + get_factor(thresholds, weight)
def drone_shipping_cost(weight):
thresholds = 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75
return weight * get_factor(thresholds, weight)
Cheapest costs
Your cheapest_shipping method calculates the costs for the different shipping methods, finds the cheapest one and formats this into a string. This string formatting is also very repetitive, and should be done somewhere else. cheapest_shipping will be the most clear if it only returned which method is the cheapest, and the corresponding cost. This also allow you to test this method with unit tests further on.
Since methods are real objects in Python, and you can pass references to them on and store these in dicts, calculating the costs for the different methods can be simplified a lot:
def cheapest_shipping(weight):
methods =
"drone": drone_shipping_cost,
"standard ground": ground_shipping_cost,
"premium ground": lambda weight: 125,
results = method: calculation(weight) for method, calculation in methods.items()
To look for the cheapest option among those, you can use the built-in min:
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
Note: the lambda weight: 125 is the equivalent of
def premium_shipping(weight):
return 125
and "premium ground": premium_shipping, in the methods dict
And this can be called and formatted using str.format or f-strings in Python 3.6+
method, cost = cheapest_shipping(4)
f"You should use method shipping as it will only cost cost"
'You should use standard ground shipping as it will only cost 23.0'
Further refactoring
You can even refactor this one step further, and have 1 generalized cost calculation method that take a flat_cost and thresholds as arguments
def get_shipping_cost(weight, thresholds=None, flat_cost=0):
if thresholds is None:
return flat_cost
return flat_cost + weight * get_factor(thresholds, weight)
then you can define the different shipping methods like this:
shipping_methods =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "flat_cost": 125,
def cheapest_shipping2(methods, weight):
results =
method: get_shipping_cost(weight, **parameters)
for method, parameters in methods.items()
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
method, cost = cheapest_shipping2(shipping_methods, 4)
different min
Instead of using min with the cost as key, you can reverse the results dictionary:
def cheapest_shipping2(methods, weight):
results =
get_shipping_cost(weight, **parameters): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
In case of an ex aequo, this can have different results than the other method
even further refactoring.
Now all shipping methods are calculated with get_shipping_cost. If you want to use different functions for the different shipping methods, you can do something like this:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
shipping_methods2 =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "method": lambda weight: 125,
method, cost = cheapest_shipping3(shipping_methods2, 4)
please note that the parameters.pop mutates the original shipping_methods2 after the execution:
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, inf: 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, inf: 4.75,
,
"premium ground": ,
To prevent this, we need to make a copy of the methods:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
methods_copy =
method: parameters.copy()
for method, parameters
in methods.items()
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods_copy.items()
cost, method = min(results.items())
return method, cost
$endgroup$
$begingroup$
Thanks for taking your time to give me this very in-depth response I really appreciate it. Although I don't understand everything going on here (as I'm still new to Python) I can certainly say I've learnt a thing or two from it!
$endgroup$
– Stephen
May 2 at 10:56
$begingroup$
"thresholdsis a sorted list with the factor for the weights." No it's not. It is a dictionary with the thresholds and factors which is sorted in Python 3.7+ and in arbitrary order before that.sorted(thresholds.items())however is a sorted iterable in all Python versions, but it also contains both the threshold and the factor.
$endgroup$
– Graipher
May 2 at 14:20
2
$begingroup$
I would not use adictto represent a range relationship. This is very unintuitive.
$endgroup$
– jpmc26
May 2 at 22:06
1
$begingroup$
@jpmc26 How about a list of objects that have max_weight and cost properties?
$endgroup$
– JollyJoker
2 days ago
1
$begingroup$
I disagree about the "further refactoring" sections. IMHO they are violating some principles of the "Zen of Python", especially "simple is better than complex" and "readability counts".
$endgroup$
– stefan
2 days ago
|
show 3 more comments
$begingroup$
premium_cost
There is no reason to include the return of this premium_cost in ground_shipping_cost. Use a different method here
DRY
You are correct. There are too many if statements. If you want to introduce an new threshold for the cost, this will include a adding an extra clause, and changing the limits for the preceding and following thresholds. This is just waiting for errors to pop up. Better would be to keep a dict of the thresholds, and iterate over them to get this price factor:
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
for threshold, factor in sorted(thresholds.items()):
if weight <= threshold:
break
return flat_cost + weight * factor
thresholds is a dict, with the cost per weight unit as value, and the threshold as key.
The drone_shipping_cost can be tackled comparably.
Now you have 2 methods that, starting from a list of thresholds, tries to determine the cost factor. We can easily refactor this out:
def get_factor(thresholds, value):
for threshold, factor in sorted(thresholds.items()):
if value <= threshold:
return factor
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
return flat_cost + get_factor(thresholds, weight)
def drone_shipping_cost(weight):
thresholds = 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75
return weight * get_factor(thresholds, weight)
Cheapest costs
Your cheapest_shipping method calculates the costs for the different shipping methods, finds the cheapest one and formats this into a string. This string formatting is also very repetitive, and should be done somewhere else. cheapest_shipping will be the most clear if it only returned which method is the cheapest, and the corresponding cost. This also allow you to test this method with unit tests further on.
Since methods are real objects in Python, and you can pass references to them on and store these in dicts, calculating the costs for the different methods can be simplified a lot:
def cheapest_shipping(weight):
methods =
"drone": drone_shipping_cost,
"standard ground": ground_shipping_cost,
"premium ground": lambda weight: 125,
results = method: calculation(weight) for method, calculation in methods.items()
To look for the cheapest option among those, you can use the built-in min:
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
Note: the lambda weight: 125 is the equivalent of
def premium_shipping(weight):
return 125
and "premium ground": premium_shipping, in the methods dict
And this can be called and formatted using str.format or f-strings in Python 3.6+
method, cost = cheapest_shipping(4)
f"You should use method shipping as it will only cost cost"
'You should use standard ground shipping as it will only cost 23.0'
Further refactoring
You can even refactor this one step further, and have 1 generalized cost calculation method that take a flat_cost and thresholds as arguments
def get_shipping_cost(weight, thresholds=None, flat_cost=0):
if thresholds is None:
return flat_cost
return flat_cost + weight * get_factor(thresholds, weight)
then you can define the different shipping methods like this:
shipping_methods =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "flat_cost": 125,
def cheapest_shipping2(methods, weight):
results =
method: get_shipping_cost(weight, **parameters)
for method, parameters in methods.items()
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
method, cost = cheapest_shipping2(shipping_methods, 4)
different min
Instead of using min with the cost as key, you can reverse the results dictionary:
def cheapest_shipping2(methods, weight):
results =
get_shipping_cost(weight, **parameters): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
In case of an ex aequo, this can have different results than the other method
even further refactoring.
Now all shipping methods are calculated with get_shipping_cost. If you want to use different functions for the different shipping methods, you can do something like this:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
shipping_methods2 =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "method": lambda weight: 125,
method, cost = cheapest_shipping3(shipping_methods2, 4)
please note that the parameters.pop mutates the original shipping_methods2 after the execution:
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, inf: 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, inf: 4.75,
,
"premium ground": ,
To prevent this, we need to make a copy of the methods:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
methods_copy =
method: parameters.copy()
for method, parameters
in methods.items()
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods_copy.items()
cost, method = min(results.items())
return method, cost
$endgroup$
premium_cost
There is no reason to include the return of this premium_cost in ground_shipping_cost. Use a different method here
DRY
You are correct. There are too many if statements. If you want to introduce an new threshold for the cost, this will include a adding an extra clause, and changing the limits for the preceding and following thresholds. This is just waiting for errors to pop up. Better would be to keep a dict of the thresholds, and iterate over them to get this price factor:
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
for threshold, factor in sorted(thresholds.items()):
if weight <= threshold:
break
return flat_cost + weight * factor
thresholds is a dict, with the cost per weight unit as value, and the threshold as key.
The drone_shipping_cost can be tackled comparably.
Now you have 2 methods that, starting from a list of thresholds, tries to determine the cost factor. We can easily refactor this out:
def get_factor(thresholds, value):
for threshold, factor in sorted(thresholds.items()):
if value <= threshold:
return factor
def ground_shipping_cost(weight):
thresholds = 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75
flat_cost = 20
return flat_cost + get_factor(thresholds, weight)
def drone_shipping_cost(weight):
thresholds = 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75
return weight * get_factor(thresholds, weight)
Cheapest costs
Your cheapest_shipping method calculates the costs for the different shipping methods, finds the cheapest one and formats this into a string. This string formatting is also very repetitive, and should be done somewhere else. cheapest_shipping will be the most clear if it only returned which method is the cheapest, and the corresponding cost. This also allow you to test this method with unit tests further on.
Since methods are real objects in Python, and you can pass references to them on and store these in dicts, calculating the costs for the different methods can be simplified a lot:
def cheapest_shipping(weight):
methods =
"drone": drone_shipping_cost,
"standard ground": ground_shipping_cost,
"premium ground": lambda weight: 125,
results = method: calculation(weight) for method, calculation in methods.items()
To look for the cheapest option among those, you can use the built-in min:
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
Note: the lambda weight: 125 is the equivalent of
def premium_shipping(weight):
return 125
and "premium ground": premium_shipping, in the methods dict
And this can be called and formatted using str.format or f-strings in Python 3.6+
method, cost = cheapest_shipping(4)
f"You should use method shipping as it will only cost cost"
'You should use standard ground shipping as it will only cost 23.0'
Further refactoring
You can even refactor this one step further, and have 1 generalized cost calculation method that take a flat_cost and thresholds as arguments
def get_shipping_cost(weight, thresholds=None, flat_cost=0):
if thresholds is None:
return flat_cost
return flat_cost + weight * get_factor(thresholds, weight)
then you can define the different shipping methods like this:
shipping_methods =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "flat_cost": 125,
def cheapest_shipping2(methods, weight):
results =
method: get_shipping_cost(weight, **parameters)
for method, parameters in methods.items()
cheapest_method = min(results, key=lambda method: results[method])
return cheapest_method, results[cheapest_method]
method, cost = cheapest_shipping2(shipping_methods, 4)
different min
Instead of using min with the cost as key, you can reverse the results dictionary:
def cheapest_shipping2(methods, weight):
results =
get_shipping_cost(weight, **parameters): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
In case of an ex aequo, this can have different results than the other method
even further refactoring.
Now all shipping methods are calculated with get_shipping_cost. If you want to use different functions for the different shipping methods, you can do something like this:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods.items()
cost, method = min(results.items())
return method, cost
shipping_methods2 =
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, float("inf"): 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, float("inf"): 4.75,
,
"premium ground": "method": lambda weight: 125,
method, cost = cheapest_shipping3(shipping_methods2, 4)
please note that the parameters.pop mutates the original shipping_methods2 after the execution:
"drone": "thresholds": 2: 4.5, 6: 9.0, 10: 12.0, inf: 14.75,
"standard ground":
"flat_cost": 20,
"thresholds": 2: 1.5, 6: 3.0, 10: 4.0, inf: 4.75,
,
"premium ground": ,
To prevent this, we need to make a copy of the methods:
def cheapest_shipping3(methods, weight, default_cost_method=get_shipping_cost):
methods_copy =
method: parameters.copy()
for method, parameters
in methods.items()
results =
parameters.pop("method", default_cost_method)(
weight, **parameters
): method
for method, parameters in methods_copy.items()
cost, method = min(results.items())
return method, cost
edited 2 days ago
answered May 2 at 10:48
Maarten FabréMaarten Fabré
5,680719
5,680719
$begingroup$
Thanks for taking your time to give me this very in-depth response I really appreciate it. Although I don't understand everything going on here (as I'm still new to Python) I can certainly say I've learnt a thing or two from it!
$endgroup$
– Stephen
May 2 at 10:56
$begingroup$
"thresholdsis a sorted list with the factor for the weights." No it's not. It is a dictionary with the thresholds and factors which is sorted in Python 3.7+ and in arbitrary order before that.sorted(thresholds.items())however is a sorted iterable in all Python versions, but it also contains both the threshold and the factor.
$endgroup$
– Graipher
May 2 at 14:20
2
$begingroup$
I would not use adictto represent a range relationship. This is very unintuitive.
$endgroup$
– jpmc26
May 2 at 22:06
1
$begingroup$
@jpmc26 How about a list of objects that have max_weight and cost properties?
$endgroup$
– JollyJoker
2 days ago
1
$begingroup$
I disagree about the "further refactoring" sections. IMHO they are violating some principles of the "Zen of Python", especially "simple is better than complex" and "readability counts".
$endgroup$
– stefan
2 days ago
|
show 3 more comments
$begingroup$
Thanks for taking your time to give me this very in-depth response I really appreciate it. Although I don't understand everything going on here (as I'm still new to Python) I can certainly say I've learnt a thing or two from it!
$endgroup$
– Stephen
May 2 at 10:56
$begingroup$
"thresholdsis a sorted list with the factor for the weights." No it's not. It is a dictionary with the thresholds and factors which is sorted in Python 3.7+ and in arbitrary order before that.sorted(thresholds.items())however is a sorted iterable in all Python versions, but it also contains both the threshold and the factor.
$endgroup$
– Graipher
May 2 at 14:20
2
$begingroup$
I would not use adictto represent a range relationship. This is very unintuitive.
$endgroup$
– jpmc26
May 2 at 22:06
1
$begingroup$
@jpmc26 How about a list of objects that have max_weight and cost properties?
$endgroup$
– JollyJoker
2 days ago
1
$begingroup$
I disagree about the "further refactoring" sections. IMHO they are violating some principles of the "Zen of Python", especially "simple is better than complex" and "readability counts".
$endgroup$
– stefan
2 days ago
$begingroup$
Thanks for taking your time to give me this very in-depth response I really appreciate it. Although I don't understand everything going on here (as I'm still new to Python) I can certainly say I've learnt a thing or two from it!
$endgroup$
– Stephen
May 2 at 10:56
$begingroup$
Thanks for taking your time to give me this very in-depth response I really appreciate it. Although I don't understand everything going on here (as I'm still new to Python) I can certainly say I've learnt a thing or two from it!
$endgroup$
– Stephen
May 2 at 10:56
$begingroup$
"
thresholds is a sorted list with the factor for the weights." No it's not. It is a dictionary with the thresholds and factors which is sorted in Python 3.7+ and in arbitrary order before that. sorted(thresholds.items()) however is a sorted iterable in all Python versions, but it also contains both the threshold and the factor.$endgroup$
– Graipher
May 2 at 14:20
$begingroup$
"
thresholds is a sorted list with the factor for the weights." No it's not. It is a dictionary with the thresholds and factors which is sorted in Python 3.7+ and in arbitrary order before that. sorted(thresholds.items()) however is a sorted iterable in all Python versions, but it also contains both the threshold and the factor.$endgroup$
– Graipher
May 2 at 14:20
2
2
$begingroup$
I would not use a
dict to represent a range relationship. This is very unintuitive.$endgroup$
– jpmc26
May 2 at 22:06
$begingroup$
I would not use a
dict to represent a range relationship. This is very unintuitive.$endgroup$
– jpmc26
May 2 at 22:06
1
1
$begingroup$
@jpmc26 How about a list of objects that have max_weight and cost properties?
$endgroup$
– JollyJoker
2 days ago
$begingroup$
@jpmc26 How about a list of objects that have max_weight and cost properties?
$endgroup$
– JollyJoker
2 days ago
1
1
$begingroup$
I disagree about the "further refactoring" sections. IMHO they are violating some principles of the "Zen of Python", especially "simple is better than complex" and "readability counts".
$endgroup$
– stefan
2 days ago
$begingroup$
I disagree about the "further refactoring" sections. IMHO they are violating some principles of the "Zen of Python", especially "simple is better than complex" and "readability counts".
$endgroup$
– stefan
2 days ago
|
show 3 more comments
Stephen is a new contributor. Be nice, and check out our Code of Conduct.
Stephen is a new contributor. Be nice, and check out our Code of Conduct.
Stephen is a new contributor. Be nice, and check out our Code of Conduct.
Stephen is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f219572%2ffind-the-cheapest-shipping-option-based-on-item-weight%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
4
$begingroup$
Welcome to Code Review! Here, it is more about best way to code a solution rather than best solution re. usability or resource usage - you may want to narrow down best.
$endgroup$
– greybeard
May 2 at 9:40
1
$begingroup$
Ah! Okay thanks for that - will update the question
$endgroup$
– Stephen
May 2 at 9:47