3

I am attempting to print employee object information from a dictionary of employee objects without using a for loop. Here is what I have done so far:

employee_dict = {}

class Employee:

  def __init__(self, id, salary):
    self.id = id
    self.salary = salary
    self.employee_dictionary(self)

  def info(self):
    return "Employee ID:{} \nSalary:{}".format(self.id, self.salary)

  def employee_dictionary(self):
    employee_dict = {self.id: self}

emp = Employee(1, 10)
emp1 = Employee(2, 5)

employee = employee_dict[1]

print(employee.info())

Could someone point me in the right direction? I feel like I am close. The error this code gives me is:

Traceback (most recent call last):
  File "/home/rob/PycharmProjects/untitled4/sdfghsfdgjrtyrty.py", line 19, in <module>
    employee = employee_dict[1]
KeyError: 1
scharette
  • 9,437
  • 8
  • 33
  • 67
Rob
  • 545
  • 3
  • 21
  • Is your indentation correct in this question? As in, it represents what you're using? – roganjosh Jul 12 '18 at 18:57
  • 1
    What you almost certainly want here is `employee_dict[self.id] = self`, not `employee_dict = {self.id: self}`. You want to _add this employee to the dictionary_, not _replace the dictionary with one that only handles this employee_, right? If so, that means your _other_ problem, the one related to locals vs. globals, won't come up in the first place, so you don't need to solve it. – abarnert Jul 12 '18 at 19:01

3 Answers3

3

The underlying problem here is that you're not using the dictionary right. Instead of adding the new employee to the existing global employee_dict, you're trying to replace that global dict with a new one, containing just the new employee:

def employee_dictionary(self):
  employee_dict = {self.id: self}

If you fix this, the problem you're actually asking about won't even come up:

def employee_dictionary(self):
  employee_dict[self.id] = self

This will add a new mapping from self.id to self in the dict. So, after your two Employee constructions, with IDs 1 and 2, you'll end up with two entries in the dict, for keys 1 and 2, and everything will just work.


Also, you should definitely at least consider two other changes:

  • Replace the global variable with a class attribute, and possibly also replace the employee_dictionary method with a class method, as detailed in scharette's answer.
  • Rename the method from employee_dictionary to something (a) marked private (methods starting with _ are private by convention, meaning your users know they aren't supposed to be calling it unless they have a weird use case), and (b) more reflective of what it does. Maybe _register or _add_to_dict?

The first one of these would also (as, again, detailed in scharette's answer) have made the problem you're asking about go away. (But you still need the main fix anyway.)


But you probably want to understand the more immediate scope problem anyway—and how to solve it when you can't just make it go away.

In Python, any name that you assign to in a function is a local variable. If there's a spam = … anywhere in the function (or a with eggs as spam: or certainly other kinds of things that also count as assignment), every reference to spam in that function is to that local. So, employee_dict = {self.id: self} creates a local variable named employee_dict, assigns a value to it, and then returns, at which point all the locals go away. The fact that it happens to have the same name as a global doesn't mean anything to Python (although to a human reader, like you, it's obviously confusing).

Any name that you use without assigning to it anywhere is a local-or-enclosing-or-global-or-builtin variable. When you do employee_dict[self.id], because there's no employee_dict = … anywhere, Python searches the local, enclosing, global, and builtin scopes to find what you meant by employee_dict, and it finds the global.

You can force Python to treat a name as a global variable, even if you assign to it, with a global statement at the top of the function:

def employee_dictionary(self):
  global employee_dict
  employee_dict = {self.id: self}

Adding global is safe even if the name would already be a global. This is usually pointless—but if you're not sure whether something counts as an assignment (or just not sure future readers of your code would be sure…), and you want to make it clear that the variable is a global, you can declare it:

def employee_dictionary(self):
  global employee_dict
  employee_dict[self.id] = self
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Awesome I appreciate the help! – Rob Jul 12 '18 at 20:09
  • I really don't think global is the way go here. Recommanding him to use global is far from optimal in my opinion. – scharette Jul 12 '18 at 20:27
  • @scharette But this answer isn’t recommending to use a global. It (1) explains the difference between adding to vs. replacing a dict, which is the key problem, (2) recommends unrelated changes (including using a class attribute—which I mostly replaced with a link to your answer), then (3) explains the immediate problem and shows how you _would_ fix it when you _do_ need `global`. It’s definitely not intended to read as a recommendation to use `global` for this problem. If it really does come across that way, what part do you think needs to be reworded or reorganized? – abarnert Jul 12 '18 at 20:31
  • Sorry I had a cached version of your answer before some of your edits. I was wrong on your recommandation sorry about that. I just think that explaining the global process in your last paragraph could lead a new user to use it since it is valid even though its not optimal. Anyway, thats probably just me. Thanks for the insights I actually learned some details reading through your answer. – scharette Jul 12 '18 at 20:38
  • @scharette I was originally planning to build to an explanation for why a class attribute would be better, but then your edit covered the same ground even better, so I reorganized things to just refer to yours instead. If an intermediate edit was confusing, that’s probably my fault for committing edits too often… – abarnert Jul 12 '18 at 20:42
  • @abarnet Or mine for not refreshing before commentting on a post. – scharette Jul 12 '18 at 20:53
2

This is a scope problem. employee_dict = {} is not acting like you think.

You're not assigning the value in the right scope. Therefore, you're never actually adding an Employee to it. Which cause the KeyError.

What you are looking for in your case is probably using a class variable like so,

class Employee:
    employee_dict = {}
    def __init__(self, id, salary):
        self.id = id
        self.salary = salary
        self.employee_dictonary(self)

    def info(self):
        return "Employee ID:{} \nSalary:{}".format(self.id, self.salary)

    @classmethod
    def employee_dictonary(cls,current_employee):
        cls.employee_dict[current_employee.id]=current_employee

emp = Employee(1, 10)
emp1 = Employee(2, 5)

print(Employee.employee_dict[1].info())

Basically, for the sake of explanation, you're creating a employee_dict which will be share among all instances of Employee.

Also, there's one last thing you have to keep in mind is if someone create 2 employee like so,

emp = Employee(1, 10)
emp2 = Employee(1, 100)

calling print(Employee.employee_dict[1].info()) will output

Employee ID:1 
Salary:100

which is probably what you want, but still thought it was relevant to underline that an employee can be overridden.

scharette
  • 9,437
  • 8
  • 33
  • 67
  • The class attribute is definitely better than the global here, and the `@classmethod` is a good idea too… but calling the parameter `self` is a little weird. – abarnert Jul 12 '18 at 20:21
  • @abarnert why is that? In this particular case we are literally adding himself. I'm open to suggestions though. – scharette Jul 12 '18 at 20:28
  • But `self` has a specific idiomatic meaning, as the bound parameter of normal methods. Especially since, as written, this is a part of the class’s public interface, so `Employee.employee_dictionary(emp3)` ought to make sense (even though there’s no reason to call it). – abarnert Jul 12 '18 at 20:35
  • (There is the precedent of `__new__` methods typically doing `self = object.__new__(cls)`, but I think that idiom is pretty closely tied to the specialness of `__new__`, and what it’s for.) – abarnert Jul 12 '18 at 20:36
  • @abarnert I see and understand. Thanks a lot for the insight. I just edited. I truly hate posting code whitout test no matter how small the change is. Could you do me a favor and review my edit ? – scharette Jul 12 '18 at 20:44
  • It looks good. And it works. (Also, good note on the fact that dict keys have to be unique. I don’t think the OP was going to be confused by that, but a future reader who needs the same answer might well be.) – abarnert Jul 12 '18 at 20:44
  • @abarnet Truly appreciate your time. Not enough people take time to improve other answers. You can often learn a lot more from writing an answer than asking a question on this site, thanks to you. – scharette Jul 12 '18 at 20:50
-1

You should define employee_dict as global:

def employee_dictonary(self):
  global employee_dict

Without the global keyword you are assigning to local variable.

The second problem is using employee_dict = {self.id: self} for assigning to employee_dict. With that, you overwriting the variable with each call. That line should be employee_dict[self.id] = self.

With that change, the output is:

Employee ID:1 
Salary:10

Using global variables is strongly discouraged in most cases, see Why are global variables evil?.

Andrej Kesely
  • 168,389
  • 15
  • 48
  • 91
  • 1
    Using `global` to solve this issue is not a strong suggestion, IMO – roganjosh Jul 12 '18 at 18:57
  • And, in fact, does not solve the issue, whether or not the function is indented to be part of the class body. You can test this locally. – roganjosh Jul 12 '18 at 18:59
  • Yes, it does work now. You didn't originally have `employee_dict[self.id] = self` in your answer – roganjosh Jul 12 '18 at 19:03
  • After the edits, this is misleading. Once you fix the `employee_dict = {self.id: self} to be `employee_dict[self.id] = self`, it's no longer true that the OP should declare it as global, and no longer try that without the global he's assigning to a local. So the whole first half is wrong. And then the second half is correct, but it's confusing because it says he needs to change the code to use `employee_dict[self.id] = self`, right after showing code that's already doing that. – abarnert Jul 12 '18 at 20:02
  • @abarnert I've removed the assignement from the function to make it more clear – Andrej Kesely Jul 12 '18 at 20:04
  • _you should define `employee_dict` as global_ and then link _Why are global variables evil_ ... Wut ? – scharette Jul 12 '18 at 21:50
  • @scharette Yes, I fixed op's problem and then said, why his attempt isn't probably that good. Is it a problem? – Andrej Kesely Jul 12 '18 at 21:51
  • Hmmm, yes there is. OP never mentioned globals. You're the one that brought it up and suggest he use it. In your honest opinion, is fixing a problem using bad pratices really fixing a problem ? – scharette Jul 12 '18 at 21:54
  • @scharette Bad practice would be if I didn't mention the problem with his code, which I did. It's only a matter of view. – Andrej Kesely Jul 12 '18 at 21:57
  • @AndrejKesely it actually has nothing to do with view. You're solution is problematic, there is a flaw it what you're suggesting. You even linked it. I mean you literally said _You should define employee_dict as global_... global is even bold. According to your linked article this should be forbidden. You're entire solution to his problem is actually forbidden by the exact article you linked. It makes no sense. – scharette Jul 13 '18 at 01:40