I am following "Fluent Python" to learn Function and Design Pattern:
In chapter 6 example-code/strategy.py
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self) # <1>
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
I am very confused about:
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
What's the purpose of if condition here? I guess it could be more readable if:
def total(self):
return sum(item.total() for item in self.cart)
What's the key point I missed? could you please provide any hints?
What happens if you call total more than once? If self.cart hasn't changed, then you're needlessly recalculating the total, a potentially expensive operation.
You're checking if you've already stored the value. If you haven't you calculate it, but if you have you simply return the stored value without recalculating.
As an aside, I would expect name mangling to make your life difficult here because of the double underscore at the beginning of __total. You may want to consider switching to a single underscore.
Related
I have a class holding some scientific data. Depending on an internal state, the values of this class can appear as normalized (i.e. unitless), or non-normalized. The values are always stored as normalized, but if the object is set in non-normalized status, the user-accessible properties (and methods) will give the non-normalized values. This way the class appears as non-normalized, while there's no need to duplicate the stored values.
Right now I implemented this using getters. While it works, it gives a lot of repeating structure, and I wonder if there's a more Pythonic way of managing this without overcomplicating things.
Am I doing this right? Is there a more elegant way to switch between two sets of data in a similar fashion?
class CoolPhysicsData(object):
def __init__(self, lambda0, *args, normed=False):
self.lambda0 = lambda0 # some normalization factor (wavelength of some wave)
self.normalized = normed # user can change this state as he pleases
self._normed_tmin, self._normed_tmax, self._normed_r = self.calculate_stuffs(*args)
...
#property
def tmin(self):
if self.normalized:
return self._normed_tmin
else:
return denormalize(self.lambda0, self._normed_tmin, unit_type="time")
#property
def tmax(self):
if self.normalized:
return self._normed_tmax
else:
return denormalize(self.lambda0, self._normed_tmax, unit_type="time")
#property
def r(self):
if self.normalized:
return self._normed_r
else:
return denormalize(self.lambda0, self._normed_r, unit_type="len")
... # about 15 getters alike these
One way is to avoid using properties, and implement __getattr__, __setattr__ and __delattr__. Since you need to know which quantity you're denormalizing, there's really no way to escape definitions: these must be handcoded somewhere. I'd do this way:
class CoolPhysicsData:
def _get_normalization_params(self, value):
# set up how individual properties should be denormalized..
params = {
# 'property_name' : (norm_factor, norm_value, 'unit_type')
'tmin': (self.lambda0, self._normed_tmin, 'time'),
'tmax': (self.lambda0, self._normed_tmax, 'time'),
'r': (self.lambda0, self._normed_r, 'len'),
}
return params[value]
and I would implement __getattr__ something like this:
...
def __getattr__(self, value):
# extract the parameters needed
norm_factor, normed_value, unit_type = self._get_normalization_params(f'{value}')
if self.normed:
return normed_value
else:
return self.denormalize(norm_factor, normed_value, unit_type)
...
Note that you might want to write __setattr__ and __delattr__ too.
One little addition: dataclasses might be useful to you. I'm not sure if *args in your __init__ function is the exact signature, or you just simplified for the sake of the example. If you have known arguments (no varargs), this can be easily turned into a dataclass.
from dataclasses import dataclass, field
#dataclass
class CoolPhysicsData:
lambda0: float
normed: bool = field(default=False)
def __post_init__(self):
# set up some test values for simplicity
# of course you can run custom calculations here..
self._normed_tmin = 1
self._normed_tmax = 2
self._normed_r = 3
def __getattr__(self, value):
norm_factor, normed_value, unit_type = self._get_normalization_params(f'{value}')
if self.normed:
return normed_value
else:
return self.denormalize(norm_factor, normed_value, unit_type)
# you may want to implement the following methods too:
# def __setattr__(self, name, value):
# # your custom logic here
# ...
# def __delattr__(self, name):
# # your custom logic here
# ...
def denormalize(self, v1, v2, v3):
# just for simplicity
return 5
def _get_normalization_params(self, value):
# setup how individual properties should be denormalized..
params = {
# 'property_name' : (norm_factor, norm_value, 'unit_type')
'tmin': (self.lambda0, self._normed_tmin, 'time'),
'tmax': (self.lambda0, self._normed_tmax, 'time'),
'r': (self.lambda0, self._normed_r, 'len'),
}
return params[value]
Is it more pythonic? It's up to you to decide. It surely takes away some repetition, but you introduce a little more complexity, and - in my opinion - it's more prone to bugs.
So I will have to finish a half-done code to get the desired output.
the half-done code goes as follows AND I AM NOT ALLOWED TO CHANGE THIS CODE:
class Wadiya():
def __init__(self):
self.name = 'Aladeen'
self.designation = 'President Prime Minister Admiral General'
self.num_of_wife = 100
self.dictator = True
the desired output goes as follows:
Part 1:
Name of President: Aladeen
Designation: President Prime Minister Admiral General
Number of wife: 100
Is he/she a dictator: True
Part 2:
Name of President: Donald Trump
Designation: President
Number of wife: 1
Is he/she a dictator: False
Now to get this output, I will have to use the same object which is wadiya in this case to change the values of the instance variables. Then print if it affected the previous values of Part 1. If it did, I'll have to print 'Previous information lost' otherwise I'll have to print 'No, changing had no effect in previous values.'
Now my question is, how can I change the values of the instance variables using the same object? This is what I've done, but I don't think this what the question has asked me to do. What do you think? Am I on the right track? Here's my approach:
class Wadiya():
def __init__(self):
self.name = 'Aladeen'
self.designation = 'President Prime Minister Admiral General'
self.num_of_wife = 100
self.dictator = True
def my_method(self):
print('Name of the President:', self.name)
print('Designation:', self.designation)
print('Number of wife:', self.num_of_wife)
print('Is he/she a dictator:', self.dictator)
def change_values(self, name, designation, num_of_wife, dictator):
self.name = name
self.designation = designation
self.num_of_wife = num_of_wife
self.dictator = dictator
print('Part 1:')
wadiya = Wadiya()
wadiya.my_method()
print('Part 2:')
wadiya = Wadiya()
wadiya.change_values('Donald Trump', 'President', 1, False)
wadiya.my_method()
Question is a bit ambiguous why would you want to change all values of an instance. If you want you can reassign new instance to same variable just pass arguments to init instead of change_method
if you want default values to class then you don't need to do init and then change values.
def __init__(self, name: str = None): # None is default value
self.name: str = name if name else 'Aladeen'
For some reason if you want to change values of instanced objects then do
wadiya.name = 'Donald'
what you are doing will work, but generally not suggested
I need to be able to implement dictionaries into this code. Not all needs to be changed just were i can change it and it still does the same job.
In a test file I have a list of three strings (1, once),(2,twice).(2, twice).
I'm guessing the number will represent the value.
This code passes the tests but I am struggling to understand how I can use dictionaries to make it do the same job.
If any one can help it'll be grateful.
The current is:
The list items are in a test file elsewhere.
class Bag:
def __init__(self):
"""Create a new empty bag."""
self.items = []
def add(self, item):
"""Add one copy of item to the bag. Multiple copies are allowed."""
self.items.append(item)
def count(self, item):
"""Return the number of copies of item in the bag.
Return zero if the item doesn't occur in the bag.
"""
counter = 0
for an_item in self.items:
if an_item == item:
counter += 1
return counter
def clear(self, item):
"""Remove all copies of item from the bag.
Do nothing if the item doesn't occur in the bag.
"""
index = 0
while index < len(self.items):
if self.items[index] == item:
self.items.pop(index)
else:
index += 1
def size(self):
"""Return the total number of copies of all items in the bag."""
return len(self.items)
def ordered(self):
"""Return the items by decreasing number of copies.
Return a list of (count, item) pairs.
"""
result = set()
for item in self.items:
result.add((self.count(item), item))
return sorted(result, reverse=True)
I have been scratching my head over it for a while now. I can only use these also for dictionaries.
Items[key] = value
len(items)
dict()
items[key]
key in items
Del items[key]
Thank you
Start with the simplest possible problem. You have an empty bag:
self.items = {}
and now a caller is trying to add an item, with bag.add('twice').
Where shall we put the item?
Well, we're going to need some unique index.
Hmmm, different every time, different every time, what changes with each .add()?
Right, that's it, use the length!
n = len(self.items)
self.items[n] = new_item
So items[0] = 'twice'.
Now, does this still work after a 2nd call?
Yes. items[1] = 'twice'.
Following this approach you should be able to refactor the other methods to use the new scheme.
Use unit tests, or debug statements like print('after clear() items is: ', self.items), to help you figure out if the Right Thing happened.
Here I created a module.
class Employee:
def __init__(self):
self.name = input("Enter your name: ")
self.account_number = int(input("Enter your account number: "))
def withdraw(self): # it receives values from for
if withdraw1 > current_balance:
print ("You have entered a wrong number: ")
else:
print ("The current balance is: ", current_balance - withdraw1)
import TASK2 # I am importing the module I created
c = TASK2.Employee()
def for(self):
c.withdraw1 = int(input("enter number: "))
c.current_balance = int(input("Enter the current balance: "))
d = method(c.withdraw) # here I am trying to pass the values to withdraw
print (d)
The problem I get is that although it asks for the values instead of giving me an answer it gives me None.
Here's my take on your code.
# TASK2.py
class Employee:
def __init__(self):
self.name = input("Enter your name: ")
self.account_number = int(input("Enter your account number: "))
# make sure you initialise your member variables!
self.withdraw_val = 0 # withdraw1 is ambiguous, so I use withdraw_val instead
self.current_balance = 0
# receives values from for ### no it doesn't, right now, it GIVES values TO your "for" function
def withdraw(self):
if self.withdraw_val > self.current_balance: # remember to use "self." to
# access members within the class
print ("You have entered a wrong number: ")
else:
# again, remember "self."
print ("The current balance is: ", self.current_balance - self.withdraw_val)
# TASK2sub.py
import TASK2
c = TASK2.Employee()
def for_employee(employee): # (1) don't use "self" outside a class
# it's contextually unconventional
# (2) "for" is a keyword in Python, don't use it for naming
# variables/functions, it'll mess things up
employee.withdraw_val = int(input("Enter value to withdraw: "))
employee.current_balance = int(input("Enter the current balance: "))
return employee.withdraw_val # not entirely sure what you want to return
# but you should definitely return something
# if you're going to assign it to some variable
d = for_employee(c.withdraw()) # "for_employee" function needs a return statement
# ".withdraw()" method should also require a return statement
print(d)
Note: I'll be referring to your original for function as for_employee from now on. Also note that I'm still hazy about what you're trying to accomplish and that there is most probably a more suitable name for it.
Since your original for_employee function didn't return anything, it returns None by default. (This explains the output you saw.)
I think you're misunderstanding how functions work in general. For example,
d = for_employee(c.withdraw())
print(d)
Your comment for the .withdraw() method is inaccurate.
"it receives values from for"
More accurately, c.withdraw() will first be computed, then whatever it returns is passed into the for_employee function as a parameter. Instead of "receiving values from", the withdraw method "gives values to" the for_employee function.
Something more reasonable would be
c.withdraw() # on a line by itself, since it doesn't return anything
d = for_employee(c) # pass the entire object, since you'll be using self.withdraw_val and whatnot
print(d)
Another issue is with conventional naming. This is what I get from the IDLE (with Python 3.7) when defining a function named for
>>> def for(a): return a
SyntaxError: invalid syntax
Again, for is a keyword in Python, don't use it for naming your variables, functions, or classes.
With self, it's less severe (but I could see that it's confusing you). self is more of a convention used in class methods. But for_employee isn't a class method. So conventionally speaking, the parameter shouldn't be named self.
(I find the code spaghetti-ish, it might benefit if you refactor the code by moving the for_employee method into the class itself. Then it would completely make sense to use self.)
In short; I started coding a few days ago and thought trying to make a simple text based adventure will let me face a lot of problems that I will encounter in other harder projects as well. My class init function produces items with some variables, one of which is it's equipment slot position [0-6]. I would like to unequip a slot, but the way I have it set up at the moment requires me to know which item is in that particular slot.
In english: unequip("mainhand"), mainhand has slotnumber 0. Get the info of all equipped items and check which one has the corresponding slotnumber, then remove that particular item.
(Some items have 2 slotnumbers ("althand") which means I will have to find a way to make sure I remove the right item from the list, but that is something I can do later). For now, I can't seem to figure out how to dynamically call items and do stuff with them.
PS. I am pretty sure I can do this in a way more phytonic manner and any suggestions are welcome, but regardless of this, I would still like to know how to dynamically call the function.
The code with which I try this:
def Unequip(Slotname): #Slotname is just a string, so I could say: unequip("Mainhand")
for count,i in enumerate(Item.slotname): #Item.slotname is a list of strings for slots which corresponds with Item.Slot which are the values determining the which slot is occupied.
if Slotname == Item.slotname[count]: #so when the name I put into the function equals the Item.slotname, I know which number in Item.Slot I need.
for items in Item: #Here we enter the problem (again, I understand that this code could be a lot better and would love some suggestions).
#Item is a object, but it's __init__ functions produces items, such as item1, item2 etc. I would like to check if any of these items is currently in my Item.Equipped and has the Item.Slot value I want to remove.
#I tried global(), locals() and dir(Item) but none give me what I need. I really hope this makes it clear.
if Item.Slot[count] == items.slot and items.name == Item.Equipped: #I need a susbtitute for the items variable which will represent item1, item2 or item3 etc. So I can do Item.Slot.remove(item2.slot).
Slot = Item.Slot.remove(items.slot)
Equipped = Item.Equipped.remove(items.name)
Player.stats = list(map(operator.sub,list(Player.stats),self.itemstats))
elif Item.Slot[i] == items.altslot and Items.name == items.Equipped:
pass
Full code (I tried using self etc, but it may not be super readable, my apologies), it includes a item.unequip function but this requires me to select the particular item instead of just the slot from which I want my item to be removed
Edit1: Removed all unneeded stuff per request:
import random
import operator
class Item:
Equipped = []
Slot = []
Inventory = []
num_of_items = 0
statnames = ["Strength", "Agility", "Dexterity", "Wisdom", "Constitution", "Intelligence"]
slotname = ["MainHand","Offhand","Head", "Necklace","Chest","Legs", "Cape" ]
def __init__(self, name, itemstats, slot, altslot = None, atk = None, Def = None):
self.itemstats = itemstats #itemstats in order: S, A, D, W, C, I
self.name = name
Item.num_of_items += 1
self.slot = slot
self.altslot = altslot
if atk != None and atk != 0:
self.atk = atk
if Def != None and Def != 0:
self.Def = Def
def Unequip(Slotname):
for count,i in enumerate(Item.slotname):
if Slotname == Item.slotname[count]:
for items in dir(Item):
if Item.Slot[count] == items.slot and items.name == Item.Equipped:
Slot = Item.Slot.remove(items.slot)
Equipped = Item.Eqiupped.remove(items.name)
Player.stats = list(map(operator.sub,list(Player.stats),self.itemstats))
elif Item.Slot[i] == items.altslot and Items.name == items.Equipped:
pass
class Player:
stats= [8,8,8,8,8,8]
item1 = Item("Sword of damaocles",[5, 1, 0,1,2,-2],0,1,20)
item2 = Item("Helmet of steel",[9,9,9,9,9,9],2,None,0,20)