Class Objects and comparing specific attributes - coding-style

I have the following code.
class person(object):
def __init__(self, keys):
for item in keys:
setattr(self, item, None)
def __str__(self):
return str(self.__dict__)
def __eq__(self, other) :
return self.__dict__ == other.__dict__
Now I want to take this code and only do __eq__ on a specific set of attrs ("keys"). So I changed it to do this:
class person(object):
def __init__(self, keys):
self.valid_keys = keys
for item in keys:
setattr(self, item, None)
def __str__(self):
return dict([(i, getattr(self, i)) for i in self.valid_keys ])
def __eq__(self, other) :
assert isinstance(other, person)
self_vals = [ getattr(self, i) for i in self.valid_keys ]
other_vals = [ getattr(other, i) for i in self.valid_keys ]
return self_vals == other_vals
I have read the following two awesome posts (here and here) and my fundamental question is:
Is this the right approach or is there a better way to do this in python?
Obviously TMTOWTDI - but I'd like to keep and follow a standard pythonic approach. Thanks!!
Updates
I was asked why do I not fix the attrs in my class. This is a great question and here's why. The purpose of this is to take several dis-jointed employee records and build a complete picture of an employee. For example I get my data from ldap, lotus notes, unix passwd files, bugzilla data, etc. Each of those has uniq attrs and so I generalized them into a person. This gives me a quick consistent way to compare old records to new records. HTH. Thanks
** Updates Pt.2 **
Here is what I ended up with:
class personObj(object):
def __init__(self, keys):
self.__dict__ = dict.fromkeys(keys)
self.valid_keys = keys
def __str__(self):
return str([(i, getattr(self, i)) for i in self.valid_keys ])
def __eq__(self, other):
return isinstance(other, personObj) and all(getattr(self, i) == getattr(other, i) for i in self.valid_keys)
Thanks to both gents for reviewing!

There are minor enhancements (bug fixes) I'd definitely do.
In particular, getattr called with two arguments raises an ArgumentError if the attribute's not present, so you could get that exception if you were comparing two instances with different keys. You could just call it with three args instead (the third one is returned as the default value when the attribute is not present) -- just don't use None as the third arg in this case since it's what you normally have as the value (use a sentinel value as the third arg).
__str__ is not allowed to return a dict: it must return a string.
__eq__ between non-comparable objects should not raise -- it should return False.
Bugs apart, you can get the object's state very compactly with self.__dict__, or more elegantly with vars(self) (you can't reassign the whole dict with the latter syntax, though). This bit of knowledge lets you redo your class entirely, in a higher-level-of-abstraction way -- more compact and expeditious:
class person(object):
def __init__(self, keys):
self.__dict__ = dict.fromkeys(keys)
def __str__(self):
return str(vars(self))
def __eq__(self, other):
return isinstance(other, person) and vars(self) == vars(other)

You can simplify your comparison from:
self_vals = [ getattr(self, i) for i in self.valid_keys ]
other_vals = [ getattr(other, i) for i in self.valid_keys ]
return self_vals == other_vals
to:
return all(getattr(self, i) == getattr(other, i) for i in self.valid_keys)

Related

How to optimize extracting data from nested hashes in ruby?

Background
I have a collection of nested hashes which present a set of parameters to define application behavior:
custom_demo_options: {
verticals: {
fashion: true,
automotive: false,
fsi: false
},
channels: {
b2b: true,
b2c: true
}
}
website_data: {
verticals: {
fashion: {
b2b: {
code: 'luma_b2b',
url: 'b2b.luma.com'
},
b2c: {
code: 'base',
url: 'luma.com'
}
}
}
}
The choices made in the custom_demo_options hash relate to data stored in the website_data hash and serve to return values from it:
data = []
collection = {}
custom_demo_options[:verticlas].each do |vertical_name, vertical_choice|
# Get each vertical selection
if vertical_choice == true
# Loop through the channels for each selected vertical
custom_demo_options[:channels].each do |channel_name, channel_choice|
# Get each channel selection for each vertical selection
if channel_choice == true
# Loop through the website data for each vertical/channel selection
website_data[:verticals].each do |site_vertical, vertical_data|
# Look at the keys of the [:website_data][:verticals] hash
# If we have a vertical selection that matches a website_data vertical...
if site_vertical == vertical_name
# For each website_data vertical collection...
vertical_data.each do |vertical_channel, channel_value|
# If we have a matching channel in the collection...
if vertical_channel == channel_name
# Add the channel's url and code to the collection hash
collection[:url] = channel_value[:url]
collection[:code] = channel_value[:code]
# Push the collection hash(es) onto the data array
data.push(collection)
}
}
}
}
}
}
}
}
The data pushed to the data array is ultimately used to create the following nginx map definition:
map $http_host $MAGE_RUN_CODE {
luma.com base;
b2b.luma.com luma_b2b;
}
As an example of the relationship between the hashes, if a user sets custom_demo_options[:channels][:b2b] tofalse, the b2b code/url pair stored in thewebsite_data` hash would be removed from the nginx block:
map $http_host $MAGE_RUN_CODE {
luma.com base;
}
Question
The above code works, but I know it's horribly inefficient. I'm relatively new to ruby, but I think this is most likely a logical challenge rather than a language-specific one.
My question is, what is the proper way to connect these hashes rather than using loops as I've done? I've done some reading on hash.select and it seems like this might be the best route, but I'd like to know: are there are other approaches I should consider that would optimize this operation?
UPDATE
I've been able to implement the first suggestion (thanks again to the poster); however, I think the second solution will be a better approach. Everything works as described; however, my data structure has changed slightly, and although I understand what the solution is doing, I'm having trouble adapting accordingly. Here's the new structure:
custom_demo_options = {
verticals: {
fashion: true,
automotive: false,
fsi: false
},
channels: {
b2b: true,
b2c: true
},
geos: [
'us_en'
]
}
website_data = {
verticals: {
fashion: {
us_en: {
b2b: {
code: 'luma_b2b',
url: 'b2b.luma.com'
},
b2c: {
code: 'base',
url: 'luma.com'
}
}
}
}
}
So, I add another level to the hashes, :geo.
I've tried to adapt the second solution has follows:
class CustomOptionsMap
attr_accessor :custom_options, :website_data
def initialize(custom_options, website_data)
#custom_options = custom_options
#website_data = website_data[:verticals]
end
def data
verticals = selected_verticals
channels = selected_channels
geos = selected_geos
# I know this is the piece I'm not understanding. How to map channels and geos accordingly.
verticals.map{ |vertical| #website_data.fetch(vertical).slice(*channels) }
end
private
def selected_geos
#custom_options[:geos].select{|_,v| v } # I think this is correct, as it extracts the geo from the array and we don't have additional keys
end
def selected_verticals
#custom_options[:verticals].select{|_,v| v }.keys
end
def selected_channels
#custom_options[:channels].select{|_,v| v }.keys
end
end
demo_configuration = CustomOptionsMap.new(custom_demo_options, website_data)
print demo_configuration.data
Any guidance on what I'm missing regarding the map statement would be very much appreciated.
Object Oriented approach.
Using OOP might be more readable and consistent in this context, as Ruby is Object Oriented language.
Introducing simple Ruby class and using activesupport module, which is extending Hash with some useful methods, same result can be achieved in the following way:
class WebsiteConifg
attr_accessor :custom_options, :website_data
def initialize(custom_options, website_data)
#custom_options = custom_options
#website_data = website_data[:verticals]
end
def data
verticals = selected_verticals
channels = selected_channels
verticals.map{ |vertical| #website_data.fetch(vertical).slice(*channels) }
end
private
def selected_verticals
#custom_options[:verticals].select{|_,v| v }.keys
end
def selected_channels
#custom_options[:channels].select{|_,v| v }.keys
end
Based on passed custom_demo_options we can select verticals and channels of only those keys, which values are set as true.
For your configuration will return
selected_verticals # [:fashion]
selected_channels # [:b2b, :b2c]
+data()
Simple public interface is iterating through all selected verticals based on the passed options and return Array of hashes for the given channels by using slice(keys).
fetch(key)
return value for the given key it is an equivalent of h[:key]
h = {a: 2, b: 3}
h.fetch(:a) # 2
h.fetch(:b) # 3
slice(key1, key2) does require activesupport
returns hash which contains passed as an arguments, keys. Method is accepting multiple arguments, as in our example we are getting an Array of those keys, we can use * splat operator to comply with this interface.
h = {a: 2, b: 3}
h.slice(:a) # {:a=>2}
h.slice(:a, :b) # {:a=>2, :b=>3}
h.slice(*[:a, :b]) # {:a=>2, :b=>3}
Usage
website_config = WebsiteConifg.new(custom_demo_options, website_data)
website_config.data
# returns
# [{:b2b=>{:code=>"luma_b2b", :url=>"b2b.luma.com"}, :b2c=>{:code=>"base", :url=>"luma.com"}}]
UPDATE
Changed relevant parts:
def data
verticals = selected_verticals
channels = selected_channels
geos = selected_geos
verticals.map do |vertical|
verticals_data = #website_data.fetch(vertical)
# in case of multiple geolocations
# collecting relevant entries of all of them
geos_data = geos.map{|geo| verticals_data.fetch(geo) }
# for each geo-location getting selected channels
geos_data.map {|geo_data| geo_data.slice(*channels) }
end.flatten
end
private
# as `website_data' hash is using symbols, we need to covert string->sym
def selected_geos
#custom_options[:geos].map(&:to_sym)
end
def selected_verticals
selected_for(:verticals).keys
end
def selected_channels
selected_for(:channels).keys
end
def selected_for(key)
#custom_options[key].select{|_,v| v }
end
Easiest way to understand what kind of output(data) you have on each of the steps in the each(map) iterator, would be to place there debugger
like: pry, byebug.
Say you have key = :foo and hash = { foo: 1, bar: 2 } - you want to know the hash's value for that key.
The approach you're using here is essentially
result = nil
hsh.each { |k,v| result = v if k == :foo }
But why do that when you can simply say
result = hsh[:foo]
It seems like you understand how hashes can be iterable structures, and you can traverse them like arrays. But you're overdoing it, and forgetting that hashes are indexed structures. In terms of your code I would refactor it like so:
# fixed typo here: verticlas => verticals
custom_demo_options[:verticals].each do |vertical_name, vertical_choice|
# == true is almost always unnecessary, just use a truthiness check
next unless vertical_choice
custom_demo_options[:channels].each do |channel_name, channel_choice|
next unless channel_choice
vertical_data = website_data[:verticals][site_vertical]
channel_value = vertical_data[channel_name]
# This must be initialized here:
collection = {}
collection[:url] = channel_value[:url]
collection[:code] = channel_value[:code]
data.push(collection)
end
end
You can see that a lot of the nesting and complexity is removed. Note that I am initializing collection at the time it has attributes added to it. This is a little too much to go into here but I highly advise reading up on mutability in Ruby. You're current code will probably not do what you expect it to because you're pushing the same collection hash into the array multiple times.
At this point, you could refactor it into a more functional-programming style, with some chained methods, but I'd leave that exercise for you

Ruby passing arbitrary function and parameters into another function

I'm trying to learn Ruby. I want to pass an arbitrary function and an arbitrary list of arguments and keyword arguments into another function.
for example, I have this arbitrary function below
def dummy_func(a, b)
return a+b
end
And I have this wrapper function
def wrapper(func, *args, **kwargs)
func(args, kwargs))
end
I want it so I can pass my arguments in any of the following ways and still return the correct answer
wrapper(dummy_func, a=1, b=2)
wrapper(dummy_func, 1, b=2)
wrapper(dummy_func, a=1, b=2)
wrapper(dummy_func, 1, 2)
Is this possible in Ruby? What would be an idiomatic way of approaching it?
The idiomatic way is to instead yield to a block.
def dummy_func(a, b, key:)
return a+b+key
end
def wrapper
puts yield
end
a = 4
b = 5
c = 6
wrapper do
dummy_func(a ,b, key: c)
end
Since the block is closure it can see all the same variables that the call to wrapper can. Now there's no need to pass wrapper's arguments through.
If you really want to make your wrapper, you can do some introspection to determine what arguments the wrapped function takes.
def dummy_func(a, b=23, key: 42)
return a+b+key
end
def no_keys(a, b=23)
return a+b
end
def wrapper(func, *array, **hash)
method = self.method(func)
takes_array = method.parameters.any? { |p| [:req, :opt, :rest].include?(p[0]) }
takes_hash = method.parameters.any? { |p| [:keyreq, :key, :keyrest].include?(p[0]) }
if takes_array && takes_hash
self.send(func, *array, **hash)
elsif takes_array
self.send(func, *array)
elsif takes_hash
self.send(func, **hash)
else
self.send(func)
end
end
a = 4
b = 5
c = 6
puts wrapper(:dummy_func, a, b, key:c)
puts wrapper(:no_keys, a, b)
But this is quite a bit more complex and less flexible than yielding to a block. This also limits you to "functions" which are really methods on the main object (there are no function references in Ruby). That's why they're called with self.send. Blocks require no assumptions about what is being wrapped.
The closest you can get is keyword arguments:
https://www.justinweiss.com/articles/fun-with-keyword-arguments/
def hello_message(greeting, time_of_day, first_name:, last_name:)
"#{greeting} #{time_of_day}, #{first_name} #{last_name}!"
end
args = ["Morning"]
keyword_args = {last_name: "Weiss"}
hello_message("Good", *args, first_name: "Justin", **keyword_args)
=> "Good Morning, Justin Weiss!"

BFS, trying to implement a method that find out the way between two words

First of all, it's a school assignment, what I wanna do, is adding a method to the bfs that will track the way between two words, which also will be parameters. Currently its looking like this:
class Queue:
def __init__(self):
self.lista=[]
def put(self,x):
self.lista.append(x)
def get(self):
if not len(self.lista)==0:
return self.lista.pop(0)
def isempty(self):
if len(self.lista)==0:
return True
else:
False
def length(self):
return len(self.lista)
def dequeue(self):
if not len(self.lista)==0:
n=self.lista.pop(0)
return n
def enqueue(self,item):
self.lista.insert(0,item)
class Word:
def __init__(self,w,f=None):
self.word=w
self.parent=f
def hamta():
ordlista=[]
fil=open("labb9text.txt")
ordlista=[]
for line in fil.readlines():
ordlista.append(line.strip())
return ordlista
def setlista():
ordlista=hamta()
setlista=set()
for a in ordlista:
if a not in setlista:
setlista.add(a)
return setlista
def hittabarn(parent):
mangd=setlista()
children=[]
lparent=list(parent)
mangd.remove(parent)
for word in mangd:
letters=list(word)
count=0
i=0
for a in letters:
if a==lparent[i]:
count+=1
i+=1
else:
i+=1
if count==2:
if word not in children:
children.append(word)
if i>2:
break
return children
def way(start,slut):
queue=Queue()
queue.enqueue(start)
visited=set()
while not queue.isempty():
vertex=queue.get()
if vertex==slut:
return True
else:
visited.add(vertex)
s=hittabarn(start)
for vertex in s:
if vertex not in visited:
queue.put(vertex)
else:
visited.add(vertex)
return False
updated the code, this should work!

How to find a value that match a first condition from mulpitple conditions

For example there is a array like this:
a = [1,2,3]
I want to find a value that is 2, but if that doesn't exist find a 3 and so on 5.
result = a.find{|i| i == 2} || a.find{|i| i == 3} || a.find{|i| i == 5}
Of course the real values are more complicated, Can I write the function more succinct?
I would recommend using separate validators that encapsulate each condition. It will help you decouple your business logic.
Consider this - instead of Integer array you have Car class instances
class Car
attr_accessor :producer, :colour, :max_speed
def initialize(producer, colour, max_speed)
#producer = producer
#colour = colour
#max_speed = max_speed
end
end
Now you want to select cars, by max_speed, colour and lastly by producer
cars = [
Car.new(:ford, :black, 200),
Car.new(:fiat, :yellow, 170),
Car.new(:renault, :green, 200),
]
instead of putting all conditions in one place, use separate validators
class ColourValidator
def initialize(colour)
#colour = colour
end
def matching?(car)
car.colour == #colour
end
end
class MaxSpeedValidator
def initialize(max_speed)
#max_speed = max_speed
end
def matching?(car)
car.max_speed == #max_speed
end
end
class ProducerValidator
def initialize(producer)
#producer = producer
end
def matching?(car)
car.producer == #producer
end
end
of course vaidators may be much more complex - this is just an idea
and no in one place you create validators
validators = [
ProducerValidator.new(:renault)
]
and later in your code you may write
cars.find { |car| validators.any? { |v| v.matching?(car) } }
benefit is here you don't have to check concrete business logic but just the mechanism of returning element for which any validator matches. Additionally when new validator comes up - you just have to test its logic without reference to the rest
You could use Array#&:
([2,3,5] & [1,2,3]).first #=> 2
([0,3,5] & [1,2,3]).first #=> 3
([0,4,5] & [1,2,3]).first #=> nil
From the doc: "The order is preserved from the original array."
#argus' suggestion (in a comment) should not be overlooked.

Using a tree in python to get values

So I am trying to create a Tree using Python to be able to try and read a text file, which has repeating quantities within the file, and try to create a tree out of these values and return the sentences with the Top 3 values (Explained in more detail below).
First of all I searched on wikipedia on how a tree is created and have also seen previous examples on stackoverflow like: This one. and This one. However I have only been able to do this so far as code goes:
import fileinput
setPhrasesTree = 0
class Branch():
def __init__(self, value):
self.left = None
self.right = None
self.value = value
class Tree():
def __init__(self):
self.root = None
self.found = False
#lessThan function needed to compare strings
def lessThan(self, a, b):
if len(a) < len(b):
loopCount = len(a)
else:
loopCount = len(b)
for pos in range(0, loopCount):
if a[pos] > b[pos]:
return False
return True
def insert(self, value):
self.root = self.insertAtBranch(self.root, value)
def exists(self, value):
#set the class variable found to False to assume it is not there
self.found = False
self.findAtBranch(self.root, value)
return self.found
#Used to fine a value in a tree
def findAtBranch(self, branch, value):
if branch == None:
pass
else:
if branch.value == value:
self.found = True
else:
self.findAtBranch(branch.left, value)
self.findAtBranch(branch.right, value)
def insertAtBranch(self, branch, value):
if branch == None:
return Branch(value)
else:
if self.lessThan(branch.value, value):
branch.right = self.insertAtBranch(branch.right, value)
else:
branch.left = self.insertAtBranch(branch.left, value)
return branch
def loadTree(filename, treeType):
if treeType == setPhrasesTree:
for sentence in fileinput.input("setPhrases.txt"):
print(sentence)
setPhrases.insert(sentence[:-1])
def findSentenceType(sentence):
if sentence.exists(sentence):
return setPhrasesTree
Here is what text file looks like. Bare in mind that it is purposefully laid out like this and not with a quantity value next to it (file name = setPhrases.txt):
Hi my name is Dave.
Thank-You.
What is your name?
I have done all my homework.
What time is dinner?
What is your name?
Thank-You.
Hi my name is Dave.
What is your name?
I have done all my homework.
What is your name?
Can you bring me a drink Please?
Can you bring me a drink Please?
What is your name?
Hi my name is Dave.
What is your name?
Can you bring me a drink Please?
Here is what I am trying to get my code to do. I need it to recognize that the first sentence, in the file, is the starting node. And then it needs to tally up all the other sentences that are the same and add a value to that sentence and just use the tree to be able to do this. (I have originally done this in another way, however I need to use a tree to be able to tally up and do all the other stuff) Here is what I mean:
I then want to be able to return the top 3 Phrases with the highest frequencies. So in this case the system would return the sentences (in this order):
What is your name?
Hi my name is Dave.
Can you bring me a drink please?
Any help is much appreciated. Also thank-you for your time.
Here you go, an implementation using a dictionary. Is this what you want?
import collections
def count_lines():
d = collections.defaultdict(int)
for line in open( "phrases.txt" ):
d[ line.strip() ] += 1
# we use the negative count as sort key, so the biggest ends up first
a = sorted( d.items(), key=lambda x : -x[1] )
for n, u in enumerate( a[:3] ):
print( u[0], "# count=", u[1] )
count_lines()

Resources