0

I'm trying to create a command line tool for saving contacts with Python using Pickle and Argparse. The parser looks for three arguments when adding a contact, first, last, and number.

I've looked though as many forums and stack overflow questions as possible, none of them really apply to my situation. The program has three options, -a (--add), -rm (--remove), and -l (--list). Three functions handle the arguments, add_contact, remove_contact, list_contacts.

My class looks like this:

class Contact:
    def __init__(self, first, last, number):
        self.first = first
        self.last = last
        self.number = number

    def __len__(self):
        global contacts
        return len(contacts)

    def __iter__(self):
        return self

A single contact should have the attributes first, last, and number. This function lists the contacts:

def list_contacts:
    try:
        print("Contacts:")
        f = open('c.data', 'rb')
        contacts = pickle.load(f)
        f.close()
        for contact in contacts:
            print(first+" "+last+": " + number)
        print(str(len(contacts)) + " contacts total.")
        if len(contacts) == 0:
            print("No contacts, why don't you add some first?")
     except FileNotFoundError:
         print("No contacts, why don't you add some first?")

Now, I believe the issue is with my Main() function, where the instance of Contact is called:

def Main():
    first = args.first
    last = args.last
    number = args.number
    # The issue:
    contact = Contact(first, last, number)
    if args.add and first and last and number:
        add_contact(contact)
    elif args.remove and first and last and number:
        delete_contact(contact)
    elif args.list:
        list_contacts()

I want my program to list the contacts like this:

First Last: Number
First Last: Number
2 contact total.

Instead, if I were to get it working somehow it would run like this:

First 
Last
Number
3 contacts total

I want to create an instance of the Contact class with the attributes first, last, and number as one object, and append that to the list of contacts. Instead when I list them, I get this:

Contacts:
Traceback (most recent call last):
  File "contacts.py", line 105, in <module>
    Main()
  File "contacts.py", line 102, in Main
    list_contacts()
  File "contacts.py", line 50, in list_contacts
    for contact in contacts:
TypeError: iter() returned non-iterator of type 'Contact'

If this didn't give you enough context, the whole program can be found here: https://pastebin.com/CqJwPVL2 I'm stuck , the iter function didn't fix this. Please help me! Thank you in advance

hpaulj
  • 221,503
  • 14
  • 230
  • 353
Zachary Haslam
  • 103
  • 1
  • 12
  • 1
    Why are you having `iter` return `self`? A Contract itself is not an iterator. – Carcigenicate Jun 28 '19 at 15:59
  • Saw this on stack overflow, it worked for some people. Before I added that, I would get a Type Error saying that 'Contact' is not iterable on line 47 – Zachary Haslam Jun 28 '19 at 16:08
  • It would only work if the class implements the required methods to be an iterator. Your class doesn't. – Carcigenicate Jun 28 '19 at 17:26
  • Okay, thanks. This leads me back to the original problem when I delete that `iter` function, I need to append first, last, and number as one object to a list, and print the list but I can't iterate through it since it gives me the type error – Zachary Haslam Jun 28 '19 at 17:31

1 Answers1

0

The problem is that you are using pickle.load(f) as if it could automatically load all the pickled objects as a list, but since you are adding every new contact as a single dump which gets appended to the file, you actually don't have a list.

You have two possibilities:

  1. Dump contacts as a list. In this way, when you will use pickle.load you will get all of the contacts at once in a list and then you will be able to iterate over it.

  2. Keep dumping contacts one-by-one, but build a list as you load them. pickle.load will return the next pickled object in your file, so if you have several Contacts in it you will have to iterate over it and build a list, or simply use it in the print function.

Eg:

tot = 0
while 1:
    try:
        contact = pickle.load(f)
        print(contact)
        tot += 1
    except:
        print("Total contacts: {}".format(tot))
        break

See this answer for some more details about using the list, which I would suggest anyway.


Edit

Given the updates in the comments, here are some fixes to the code.

class Contact:

def __init__(self, first, last, number):
    self.first = first
    self.last = last
    self.number = number

def __len__(self):
    global contacts
    return len(contacts)

def __str__(self):
    return self.first + " "+ self.last + ": " + self.number

def __eq__(self, other):
    if self.name == other.name and self.surname == other.surname:
        return True
    return False

__str__(self) method should return information (a string) about the object itself, what you were doing was returning a list which is even out of its scope. I would also like to add that returning the whole contact list length from within one Contact only is semantically wrong. I would instead use a simple len(contact) from somewhere else not inside Contact.

__eq__ method has been added for later use inside the delete_contact method.

def load_contacts(filename="c.data"):
    try:
        f = open("c.data", "rb")
        contact_list = pickle.load(f)
        f.close()
        return contact_list
    except FileNotFoundError:
        return []


def list_contacts(filename):
    contacts = load_contacts(filename)
    for contact in contacts:
        print(contact)
    print("Total contacts: {}".format(len(contacts))) 
    if len(contacts) == 0:
        print("No contacts, why don't you add some first?")


def add_contact(contact):
    contacts = load_contacts()
    contacts.append(contact)
    f = open('c.data', 'wb')
    pickle.dump(contacts, f)
    f.close()
    print("Contact added")

def delete_contact(contact):
    contact_list = load_contacts()
    if len(contact_list) == 0:
        print("No contacts, why don't you add some first?")
        return           
    try:
        contact_list.remove(contact)
        f = open("c.data", "wb")
        pickle.dump(contact_list, f)
        f.close()
    except ValueError:
        print("No such a contact to remove")

def Main():
    first = args.first
    last = args.last
    number = args.number

    if args.list:
        list_contacts('c.data')
    else:
        if not args.first or not args.last or not args.number:
            print("Contact info missing")
            return

        contact = Contact(first, last, number)
        if args.add:
            add_contact(contact)
        elif args.remove:
            delete_contact(contact)

Alright, there was a little bit of confusion in your code so I made some changes and reorganized it. I will try to explain my changes:

  1. Main: I re-organized the logic by moving the contact information check here rather than inside add_contact, also the contact gets created only if such info are not None. This is because it is semantically more correct to perform such a check here, since it is out of the scope of the add_contact method.

  2. load_contacts: As you always need to load contacts data first, it is better to wrap the logic for doing so into one single method. This method returns an empty list if no data is present.

  3. list_contacts: Now it simply iterates over the loaded contacts

  4. add_contact: After the contact list is loaded, it simply appends the new contact to it and dumps the whole list into the file. Note that the file is open in wb mode, not ab because you dump the whole list every time.

  5. delete_contact: The check on the contacts list length should be done at the beginning. For the rest it is very straight forward: if the list is not empty it will try to remove an item from it, otherwise nothing is done. Note that the removal from the list is possible because we have previously define the eq method inside Contact class.

Hope everything is clear.

DLM
  • 555
  • 5
  • 10
  • I'm still a little confused...when I use `print(contact)`, it says `<__main__.Contact object at 0x769cef70>`. The TypeError is also getting in the way of more output. And what to you mean by dump contacts as a list? Apologies for my inexperience – Zachary Haslam Jun 28 '19 at 17:13
  • That is because you have to override the `__str__()` method inside `Contact` class. By dumping as a list i mean to do `pickle.dump([contact], f)` when you add the first contact ever, then in the subsequent times you will have to do `contact_list = pickle.load(f); contact_list.append(contact); pickle.dump(contact_list, f)`. Did you take a look at the link I mentioned? – DLM Jun 28 '19 at 17:36
  • Yeah, thanks for that link. I implemented the method that the second person who commented suggested on that link. I still get the `<__main__.Contact...`, and says that I have one contact. I tried adding another one and got the same output regardless, it still said one contact even though I added two. Any idea on how to fix this? – Zachary Haslam Jun 28 '19 at 18:40
  • Can you please provide an updated version of your code? – DLM Jun 28 '19 at 23:44
  • Well, you basically have a bunch of issues. I will edit my answer. – DLM Jun 29 '19 at 08:06
  • @ZacharyHaslam Edited my answer, let me know if it is ok for you – DLM Jun 29 '19 at 09:06
  • @ DLM, it works perfectly, thank you so much! I also added a clear_contacts function, and it works, you can see it here: https://pastebin.com/90pvgLx. Thanks again – Zachary Haslam Jun 29 '19 at 22:08
  • @ZacharyHaslam I'm very glad it does! If your happy with the answer please consider upvoting and accepting it :) – DLM Jun 30 '19 at 15:22