Python class inheritance/Logic gate & circuit example - python

I'm currently self-learning Python, and I'm reading 'Problem Solving with Algorithms and Data Structures' (Brad Miller, David Ranum). I've stumbled upon the basic example of inheritance. Although I can see what it does, I need an explanation, how it works actually. The Code is as follows:
class LogicGate:
def __init__(self,n):
self.name = n
self.output = None
def getName(self):
return self.name
def getOutput(self):
self.output = self.performGateLogic()
return self.output
class BinaryGate(LogicGate):
def __init__(self,n):
LogicGate.__init__(self,n)
self.pinA = None
self.pinB = None
def getPinA(self):
if self.pinA == None:
return int(input("Enter Pin A input for gate "+self.getName()+"-->"))
else:
return self.pinA.getFrom().getOutput()
def getPinB(self):
if self.pinB == None:
return int(input("Enter Pin B input for gate "+self.getName()+"-->"))
else:
return self.pinB.getFrom().getOutput()
def setNextPin(self,source):
if self.pinA == None:
self.pinA = source
else:
if self.pinB == None:
self.pinB = source
else:
print("Cannot Connect: NO EMPTY PINS on this gate")
class AndGate(BinaryGate):
def __init__(self,n):
BinaryGate.__init__(self,n)
def performGateLogic(self):
a = self.getPinA()
b = self.getPinB()
if a==1 and b==1:
return 1
else:
return 0
class OrGate(BinaryGate):
def __init__(self,n):
BinaryGate.__init__(self,n)
def performGateLogic(self):
a = self.getPinA()
b = self.getPinB()
if a ==1 or b==1:
return 1
else:
return 0
class UnaryGate(LogicGate):
def __init__(self,n):
LogicGate.__init__(self,n)
self.pin = None
def getPin(self):
if self.pin == None:
return int(input("Enter Pin input for gate "+self.getName()+"-->"))
else:
return self.pin.getFrom().getOutput()
def setNextPin(self,source):
if self.pin == None:
self.pin = source
else:
print("Cannot Connect: NO EMPTY PINS on this gate")
class NotGate(UnaryGate):
def __init__(self,n):
UnaryGate.__init__(self,n)
def performGateLogic(self):
if self.getPin():
return 0
else:
return 1
class Connector:
def __init__(self, fgate, tgate):
self.fromgate = fgate
self.togate = tgate
tgate.setNextPin(self)
def getFrom(self):
return self.fromgate
def getTo(self):
return self.togate
def main():
g1 = AndGate("G1")
g2 = AndGate("G2")
g3 = OrGate("G3")
g4 = NotGate("G4")
c1 = Connector(g1,g3)
c2 = Connector(g2,g3)
c3 = Connector(g3,g4)
print(g4.getOutput())
main()
I'm mostly doubted by the tgate.setNextPin(self) statement in Connector class __init__. Is it a method call? If it is, why is it called with just one parameter, when there are two actually required by the setNexPin function in UnaryGate and BinaryGate classes (self, source)? How does the fromgate variable ends up as the source arrgument? Does this statement 'initiliaze' anything actually, and what?
Next thing which troubles me is, for example, when I print(type(g4)) before declaring g4.getOutput(), I get <class '__main__.OrGate'>, but when the g4.getOutput() starts, and functions start calling each other, to the point of calling getPin function, if I put print (self.pinA) before return self.pinA.getFrom().getOutput(), I get the <__main__.Connector object at 0x2b387e2f74d0>, although self.Pin is a variable from g4 OrGate instance. How can one variable from one class instance can become object of another class, which isn't inheriting it? Does this have something with a work of setNextPin() function?
Can someone explain this to me as I am new to OOP and thorougly confused by this piece of code. Thank you.

Regarding your first question, tgate.setNextPin(self) is a method call. tgate is an object, presumably an instance of one of the gate types. When you access instance.method, Python gives you a "bound method" object, which works pretty much like a function, but which puts the instance in as the first argument when it is actually called. So, tgate.setNextPin(self) is really calling type(tgate).setNextPin(tgate, self)
Your second question seems to reflect a misunderstanding of what attributes are. There's no requirement that an object's attributes be of its own type. In the various LogicGate sub-classes, the pin/pinA/pinB attributes are either going to be None (signaling that the user should be prompted for an input value) or an instance of a Connector (or something else with a getFrom method). Neither of those values is a LogicGate instance.
As for where the Connector instance you saw came from, its going to be one of the c1 through c3 values you created. Connector instances install themselves onto a pin of their tgate argument, with the setNextPin call you were asking about in your first question. I can't really speak to the g4 gate you are looking at, as it seems to be different than the g4 variable created in your example code's main() function (it is a different type), but I suspect that it is working as designed, and it's just a bit confusing. Try accessing the pinX attributes via g4.pinA rather than inspecting them inside of the methods and you may have a bit less confusion.
Here's a bit of code with outputs that should help you understand things a bit better:
# create a few gates
g1 = AndGate("G1")
g2 = OrGate("G2")
# at this point, no connectors have been hooked up, so all pinX attrs are None:
print("G1 pins:", g1.pinA, g1.pinB) # "None, None"
print("G2 pins:", g2.pinA, g2.pinB) # "None, None"
# test that the gates work independently of each other:
print("G1 output:", g1.getOutput()) # will prompt for two inputs, then print their AND
print("G2 output:", g2.getOutput()) # will prompt for two inputs, then print their OR
# create a connection between the gates
c1 = Connector(g1, g2) # connects output of g1 to first available pin (pinA) of g2
# we can see that g2.pinA has changed
print("G2 pins after connection:", g2.pinA, g2.pinB)
# "<__main__.Connector object at SomeHexAddress>, None"
# now, if we get g2's output, it will automatically request g1's output via the Connector
print("G2 output:", g2.getOutput())
# will prompt for 2 G1 inputs, and one G2 input and print (G1_A AND G1_B) OR G2_B
If you want to play around with these classes more, you might want to add a __str__ (and/or __repr__) method to some or all of the classes. __str__ is used by Python to convert an instance of the class into a string whenever necessary (such as when you pass it as an argument to print or str.format). Here's a quick __str__ implementation for Connector:
def __str__(self):
return "Connector between {} and {}".format(self.fgate.name, self.tgate.name)

Related

Avoiding too much "self." when working with classes methods

I have created this class that works as expected, I want only to expose one method, get_enriched_dataso the other are pretty much private w/ the underscore.
The functionality works, just pretty convinced I am not doing the most pythonic/OOP way:
class MergeClients:
def __init__(self,source_df,extra_info_df,type_f):
self.df_all = pd.merge(source_df,extra_info_df, on='clientID', how='left')
self.avg_age = self._get_avg_age()
self.type_f = 'Medium'
def _filter_by_age(self, age):
return self.df_all[self.df_all['Age'] > age]
def _filter_by_family_type(self, f_type):
return self.df_all[self.df_all['familyType'] == f_type]
def _get_avg_age(self):
return self.df_all['Age'].mean()
def get_enriched_data(self):
self.df_all = self._filter_by_age(self.avg_age)
self.df_all=self._filter_by_family_type(self.type_f)
return self.df_all
But I find the code looks so ugly with so many self references, for example in the get_enriched_datamethod there are three self references per line, how can I correct this? Any direction on how to correctly Python classes is welcome.
Edit:
Example of working code:
main_df = pd.DataFrame({'clientID':[1,2,3,4,5],
'Name':['Peter','Margaret','Marc','Alice','Maria']})
extra_info = pd.DataFrame({'clientID':[1,2,3,4,5],'Age':[19,35,18,65,57],'familyType':['Big','Medium','Single','Medium','Medium']})
family_stats = MergeClients(main_df,extra_info,'Medium')
family_filtered = family_stats.get_enriched_data()
There are some odd things about your code. I will point out one thing about instances: every method has access to all attributes, so you don't always need to pass them as parameters:
class MergeClients:
def __init__(self,source_df,extra_info_df,type_f):
self.df_all = pd.merge(source_df,extra_info_df, on='clientID', how='left')
self.avg_age = self._get_avg_age()
self.type_f = 'Medium'
def _filter_by_age(self): #No need for age param
return self.df_all[self.df_all['Age'] > self.avg_age]
def _filter_by_family_type(self): #No need for f_type param
return self.df_all[self.df_all['familyType'] == self.type_f]
def _get_avg_age(self):
return self.df_all['Age'].mean()
def get_enriched_data(self):
self.df_all = self._filter_by_age()
self.df_all = self._filter_by_family_type()
return self.df_all
Since the two methods in question: _filter_by_age() and _filter_by_family_type() are private by convention, this means that clients of your class are not expected to call them. So if only other methods of this class call these methods and only the ones you have shown, then there is no need to pass parameters which are already attributes.
Alternatively there is the argument that for other private methods where sometimes they should use attributes, but at other times they should take a parameter, then I would make those methods take a parameter as you had originally.
Functions declared within a Python Class can be effectively made 'private' by preceding the name with double underscore. For example:
class Clazz():
def __work(self):
print('Working')
def work(self):
self.__work()
c = Clazz()
c.work()
c.__work()
The output of this would be:
Working
Traceback (most recent call last):
File "/Volumes/G-DRIVE Thunderbolt 3/PythonStuff/play.py", line 575, in
c = Clazz()
AttributeError: 'Clazz' object has no attribute '__work'
In other words, the __work function has been 'hidden'

How to automatically detect the arguments to pass in Python?

So here's one example. I have a module with multiple classes for computing properties, let's say, Density. Then, I have substances, whom I make inherit the properties, so I can call them afterwards:
class Density():
def __init__(self):
self.rho_method_1_A = None
self.rho_method_1_B = None
self.rho_method_2_A = None
self.rho_method_2_B = None
def density_method_1(self,T):
return self.rho_method_1_A*T + self.rho_method_1_B*T**2
def density_method_2(self,T,P):
return P*(self.rho_method_2_A/T + self.rho_method_1_B*log(T))
class Water(Density):
def __init__(self):
self.rho_method_1_A = 0.2
self.rho_method_1_B = 0.0088
self.rho_method_2_A = 1.9
self.rho_method_2_B = 10
Water.density_method_1(T=300)
Basically, I want the user to be able to set beforehand the method of choice. The problem is, depending on the model that he/she chose, it will either accept only T, or both T and P (in some other cases, maybe T won't even be accepted). Essentially:
density_method = density_method_2 # This is chosen by the user in an outer module
Water.density_method(code magically knows what to put here by detecting the arguments that
density_method_2 accepts)
To be clear: The user itself will know which arguments the method accepts, so if he chose method_2, P will be known on the outer module. I suspect **kwargs and/or decorators are part of the solution, but I can't quite figure it out. Any help is appreciated. Thanks and have a great weekend.
Update: It is relevant to notice that density_method will be called inside a temporal loop of tens of thousands of iterations. So, I'm trying to avoid if/else statements by completely defining density_method before starting the temporal loop.
Possible solution (tested with Python 3.8):
from math import log
class Density():
def __init__(self, method=1):
self.method = method
self.rho_method_1_A = None
self.rho_method_1_B = None
self.rho_method_2_A = None
self.rho_method_2_B = None
def density_method_1(self, T):
return self.rho_method_1_A*T + self.rho_method_1_B*T**2
def density_method_2(self, T, P):
return P*(self.rho_method_2_A/T + self.rho_method_1_B*log(T))
def density_method(self, *args, **kwargs):
if self.method == 1:
return self.density_method_1(*args, **kwargs)
elif self.method == 2:
return self.density_method_2(*args, **kwargs)
else:
raise ValueError("No density method found")
class Water(Density):
def __init__(self, method):
super(Water, self).__init__(method=method)
self.rho_method_1_A = 0.2
self.rho_method_1_B = 0.0088
self.rho_method_2_A = 1.9
self.rho_method_2_B = 10
class Air(Density):
def __init__(self, method):
super(Air, self).__init__(method=method)
self.rho_method_1_A = 0.2
self.rho_method_1_B = 0.0088
self.rho_method_2_A = 1.9
self.rho_method_2_B = 10
water = Water(method=1)
air = Air(method=2)
print(water.density_method(T=1))
print(air.density_method(T=1, P=2))
The solution can be improved but the general idea is to have a "wrapper" method density_method that calls the relevant density_method_* for each sub-class of Density.
Edit (following the comments to my answer):
You should look into python method overloading, like the example here:
https://www.geeksforgeeks.org/python-method-overloading/
from multipledispatch import dispatch
#passing one parameter
#dispatch(int,int)
def product(first,second):
result = first*second
print(result);
#passing two parameters
#dispatch(int,int,int)
def product(first,second,third):
result = first * second * third
print(result);
#you can also pass data type of any value as per requirement
#dispatch(float,float,float)
def product(first,second,third):
result = first * second * third
print(result);
#calling product method with 2 arguments
product(2,3,2) #this will give output of 12
product(2.2,3.4,2.3) # this will give output of 17.985999999999997

Is there a way to fix Name Error due to scope?

I have a function that creates a player object but when referencing the object, I get a NameError. I think it is happening due to local scope but global should fix it...
I just started out OOP and this code is working in the python shell but it is not working in script mode.
endl = lambda a: print("\n"*a)
class Score:
_tie = 0
def __init__(self):
self._name = ""
self._wins = 0
self._loses = 0
def get_name(self):
print
self._name = input().upper()
def inc_score(self, wlt):
if wlt=="w": self._wins += 1
elif wlt=="l": self._loses += 1
elif wlt=="t": _tie += 1
else: raise ValueError("Bad Input")
def player_num(): #Gets number of players
while True:
clear()
endl(10)
print("1 player or 2 players?")
endl(5)
pnum = input('Enter 1 or 2: '.rjust(55))
try:
assert int(pnum) == 1 or int(pnum) == 2
clear()
return int(pnum)
except:
print("\n\nPlease enter 1 or 2.")
def create_player(): #Creates players
global p1
p1 = Score()
yield 0 #stops here if there is only 1 player
global p2
p2 = Score()
def pr_(): #testing object
input(p1._wins)
input(p2._wins)
for i in range(player_num()):
create_player()
input(p1)
input(p1._wins())
pr_()
wherever I reference p1 I should get the required object attributes but I'm getting this error
Traceback (most recent call last):
File "G:/Python/TicTacTwo.py", line 83, in <module>
input(p1)
NameError: name 'p1' is not defined
Your issue is not with global but with the yield in create_player(), which turns the function into a generator.
What you could do:
Actually run through the generator, by executing list(create_player()) (not nice, but works).
But I suggest you re-design your code instead, e.g. by calling the method with the number of players:
def create_player(num): #Creates players
if num >= 1:
global p1
p1 = Score()
if num >= 2:
global p2
p2 = Score()
If you fix this issue, the next issues will be
1) input(p1) will print the string representation of p1 and the input will be lost, you probably want p1.get_name() instead.
2) input(p1._wins()) will raise TypeError: 'int' object is not callable
I will redesign the app to introduce really powerful python constructs that may help you when getting into OOP.
Your objects are going to represent players, then don't call them Score, call them Player.
Using _tie like that makes it a class variable, so the value is shared for all the players. With only two participants this may be true but this will come to hurt you when you try to extend to more players. Keep it as a instance variable.
I am a fan of __slots__. It is a class special variable that tells the instance variables what attributes they can have. This will prevent to insert new attributes by mistake and also improve the memory needed for each instance, you can remove this line and it will work but I suggest you leave it. __slots__ is any kind of iterable. Using tuples as they are inmutable is my recomendation.
Properties are also a really nice feature. They will act as instance attribute but allow you to specify how they behave when you get the value (a = instance.property), assign them a value (instance.property = value), or delete the value (del instance.property). Name seems to be a really nice fit for a property. The getter will just return the value stored in _name, the setter will remove the leading and trailing spaces and will capitalize the first letter of each word, and the deletter will set the default name again.
Using a single function to compute a result is not very descriptive. Let's do it with 3 functions.
The code could look like this:
# DEFAULT_NAME is a contant so that we only have to modify it here if we want another
# default name instead of having to change it in several places
DEFAULT_NAME = "Unknown"
class Player:
# ( and ) are not needed but I'll keep them for clarity
__slots__ = ("_name", "_wins", "_loses", "_ties")
# We give a default name in case none is provided when the instance is built
def __init__(self, name=DEFAULT_NAME):
self._name = name
self._wins = 0
self._loses = 0
self._ties = 0
# This is part of the name property, more specifically the getter and the documentation
#property
def name(self):
""" The name of the player """
return self._name
# This is the setter of the name property, it removes spaces with .strip() and
# capitalizes first letters of each word with .title()
#name.setter
def name(self, name):
self._name = name.strip().title()
# This is the last part, the deleter, that assigns the default name again
#name.deleter
def name(self):
self._name = DEFAULT_NAME
def won(self):
self._wins += 1
def lost(self):
self._loses += 1
def tied(self):
self._ties += 1
Now that's all we need for the player itself. The game should have a different class where the players are created.
class Game:
_min_players = 1
_max_players = 2
def __init__(self, players):
# Check that the number of players is correct
if not(self._min_players <= players <= self._max_players):
raise ValueError("Number of players is invalid")
self._players = []
for i in range(1, players+1):
self._players.append(Player(input("Insert player {}'s name: ".format(i))))
#property
def players(self):
# We return a copy of the list to avoid mutating the inner list
return self._players.copy()
Now the game would be created as follows:
def new_game():
return Game(int(input("How many players? ")))
After that you would create new methods for the game like playing matches that will call the players won, lost or tied method, etc.
I hope that some of the concepts introduced here are useful for you, like properties, slots, delegating object creation to the owner object, etc.

Is this pythonic? Class self variable

Long story short, I need to write a data analysing tool using mainly OOP principles. I'm not quite a beginner at python but still not the best. I wrote a function which returned true or false value based on what the user inputted (below):
def secondary_selection():
"""This prints the options the user has to manipulate the data"""
print("---------------------------")
print("Column Statistics [C]")
print("Graph Plotting [G]")
d = input(str("Please select how you want the data to be processed:")).lower()
# Returns as a true/false boolean as it's easier
if d == "c":
return True
elif d == "g":
return False
else:
print("Please enter a valid input")
This function works the way I want it to, but I then tried to import it into different file for use with a class (below):
class Newcastle:
def __init__(self, data, key_typed):
self.data = data[0]
self.key_typed = key_typed
def newcastle_selection(self):
# If function returns True
if self:
column_manipulation()
# If function returns False
if not self:
graph_plotting()
The newcastle_selection(self) function takes the secondary_selection() function as a argument, but the only way I got it to work was the if self statement. As writing something like if true lead to both the column_manipulation and the graph_plotting functions being printed.
I am wondering if there's any better way to write this as I am not quite a beginner at python but still relatively new to it.
Disclaimer: This is for a first year course, and I asked this as a last result.
I'm not sure I've understood really well your code structure, but it looks like a little factory could helps you:
def column_manipulation():
print("Column manipulation")
def graph_plotting():
print("Graph plotting")
class Newcastle:
def __init__(self, data, func):
self.data = data[0]
self._func = func
def newcastle_selection(self):
return self._func()
#classmethod
def factorize(cls, data, key_typed):
if key_typed is True:
return cls(data, column_manipulation)
elif key_typed is False:
return cls(data, graph_plotting)
else:
raise TypeError('Newcastle.factorize expects bool, {} given'.format(
type(key_typed).__name__
))
nc = Newcastle.factorize(["foo"], True)
nc.newcastle_selection()
nc = Newcastle.factorize(["foo"], False)
nc.newcastle_selection()
Output
Column manipulation
Graph plotting
The main idea is to define your class in a generic way, so you store a function given as __init__ parameter within self._func and just call it in newcastle_selection.
Then, you create a classmethod that takes your data & key_typed. This method is in charge of choosing which function (column_manipulation or graph_plotting) will be used for the current instance.
So you don't store useless values like key_typed in your class and don't have to handle specific cases everywhere, only in factorize.
Cleaner and powerful I think (and btw it answer your question "Is this pythonic", this is).
This is simple and basic example of secondary_selection method using class. This would help probably.
class castle:
def __init__(self):
self.data = ''
def get_input(self):
print("1: Column Statistics")
print("2: Graph Plotting")
self.data = input("Please select how you want the data to be processed: ")
def process(self):
if self.data == 1:
return self.column_manipulation()
else:
return self.graph_plotting()
def column_manipulation(self):
return True
def graph_plotting(self):
return False
c = castle()
c.get_input()
result = c.process()
print(result)

Python 2.7, what's the benefit of this kind of initialization in class?

class LogicGate(object):
def __init__(self, n):
self.label = n
self.output = None # ????????????
def getOutput(self):
self.output = self.performGateLogic()
return self.output
def getLabel(self):
return self.label
class BinaryGate(LogicGate):
def __init__(self, n): # ?????????????????
LogicGate.__init__(self, n)
self.pinA = None # ??????????????
self.pinB = None # ??????????????
def getPinA(self):
return int(raw_input('Enter Pin A input for gate' + self.getLabel() + '-->'))
def getPinB(self):
return int(raw_input('Enter Pin A input for gate' + self.getLabel() + '-->'))
class UnaryGate(LogicGate):
def __init__(self, n): # ??????????????
LogicGate.__init__(self, n)
self.pin = None # ?????????????
def getPin(self):
return int(raw_input('Enter Pin input for gate' + self.getLabel() + '-->'))
class AndGate(BinaryGate):
def __init__(self, n): # ????????????
BinaryGate.__init__(self, n)
def performGateLogic(self):
a = self.getPinA()
b = self.getPinB()
if a == 1 and b == 1:
return 1
else:
return 0
This code belongs to Problem Solving with Algorithms and Date Structures.
When I remove the lines before the comment '# ????????', the code can run normally.
Why does the author write the code like this?
Whether is it a good code style?
Can I always remove these lines before the comment '# ????????' ?
The author writes the code like that because it is good practice to never have uninitialised members and class parents, static checkers moan if you do.
The reason that it is not good practice is for future maintainability - let us say that the base class, LogicGate, was to gain a new property - say propagation_delay and a new method that allowed simulations to called get_response_time which relied on the current output state and the required, possibly new, state. If all the code that was derived from that class did the correct initialisations then it would all work fine, without any changes. If you remove those lines and such a new method was introduced you would have to go back through all of the child classes adding them back in before your final class would work for that method, with the chance that you would miss one.
Daft as it sounds doing things properly now is actually future laziness - it only takes you seconds when you are creating a class to make sure everything is initialised - debugging an uninitialised class can take hours.
First:
The __init__ functions are the constructors of the classes, you can read about them here.
Second:
Your code will run without those lines but the question is why and is it ok to remove them?
For example if you remove the following init
class UnaryGate(LogicGate): # LogicGate is the superclass
def __init__(self, n):
LogicGate.__init__(self, n)
The constructor of the super-class LogicGate will be called directly.
Third:
Ok, so can we remove the self.xxx = None?
class BinaryGate(LogicGate):
def __init__(self, n):
LogicGate.__init__(self, n)
self.pinA = None
self.pinB = None
We could remove those 2 Lines too but consider this code
bg = BinaryGate("binaryGate1")
print bg.pinA
This would throw an error because pinA is undefined.
If you do not remove the self.pinA = None in __init__ the code will run and None will be printed.

Categories