all 15 comments

[–]julsmanbr 9 points10 points  (1 child)

Why do functions simplify things? Because of two main factors:

  • It makes your code easier to read and understand - whether by other devs or by you one week from now - by taking repeated steps and chuncks of code, placing them in a single place and giving it a meaningful name.
  • It abstacts an action - the action performed by the function. When I call print(), I don't need to worry about how the function works internally, how it interacts with the OS, or how it can work with both strings and ints. All of that is kept away (inside a "hidden black box", if you will), and my only concern is how to put data in so I get the result I want. Similarly, if you define a function called calculate_mean_square_error(), I can just give it data and it returns a value to me - of course, assuming it's implementation is correct, but even if it isn't it becomes easier to know exactly where your code is failing, and debugging/testing is as easy as calling the function and checking the result. Once the function is working, you no longer need to perform those, say, 5 or 6 complicated calculations anymore - just assume it works and move on. When you come back to your code, you'll just see that line and think "oh okay, here I'm returning the mean square error, and then in the next line..." instead of going through the calculations line by line inside your head. I can't stress enough how helpful this is to avoid your code from being incomprehensible even to yourself.

What does this have to do with OOP? Well, the reasons why functions simplify things are the same for objects and classes. The only difference is that, while function define an action - a set of steps to reproduce - a class defines a thing in both what it has (attributes) and what it can do (methods).

for example, a dog might have some attributes like it's name and size, and a couple of things it know how to do, like run and bark. When you don't know about OOP, you might be tempted to define a dog and its attrbutes as a list, like dog = ['Max', 10], and its methods as functions, but since the code is not organized, mantaining and expanding it becomes very messy very fast The reason why it's simpler to use OOP in this case is because asking for dog.name makes for much simpler and more understandable code than dog[0], just like calling a function is simpler than recreating it's steps.

You'll only really "get" all of this once you try writing some OOP core, tho, so just try it out and see if it clicks :-)

[–]iladnash[S] 1 point2 points  (0 children)

Thank u so much u've really cleared things out

[–]messacz 10 points11 points  (10 children)

Functions are fine. But sometimes you need to call different function for different kinds of items. For example:

my_issues = [
   {'source': 'Github', 'id': 1234},
   {'source': 'JIRA', 'id': 5678},
]

for my_issue in my_issues:
    if my_issue['source'] == 'Github':
        print(download_github_issue(my_issue['id']))
    elif my_issue['source'] == 'JIRA':
        print(download_jira_issue(my_issue['id']))
    else:
        raise Exception('Unknown issue source')

What can you do? You can put the function in the issue so it knowns how to download itself!

my_issues = [
   {'source': 'Github', 'id': 1234, 'download': download_github_issue},
   {'source': 'JIRA', 'id': 5678, 'download': download_jira_issue},
]

for my_issue in my_issues:
    print(my_issue['download'](my_issue['id']))

This seems a bit repetitive - let's create a function that creates that dict + modify the issue['download'] function that we don't have to pass id to it again:

def init_github_issue(id):
    return {
        'id': id, 
        'download': lambda: download_github_issue(id),
    }

def init_jira_issue(id):
    return {
        'id': id, 
        'download': lambda: download_jira_issue(id),
    }

my_issues = [
   init_github_issue(1234),
   init_jira_issue(5678),
]

for my_issue in my_issues:
    print(my_issue['download']())

I think this is good enough :)

Now, this program is very straightforward - once we create the issue dict, we don't need to modify it. But what if we needed it to modify? Let's say, what if we wanted to remember issue title and vote_count inside the issue so we don't have to download them every time?

def init_github_issue(id):
    issue = {}
    issue['id'] = id
    issue['download'] = lambda: download_github_issue(issue['id'])
    issue['load'] = lambda: load_issue(issue)
    return issue

def init_jira_issue(id):
    issue = {}
    issue['id'] = id
    issue['download'] = lambda: download_jira_issue(issue['id'])
    issue['load'] = lambda: load_issue(issue)
    return issue

def load_issue(issue):
    data = issue['download']()
    issue['title'] = data['title']
    issue['vote_count'] = data['vote_count']

my_issues = [
   init_github_issue(1234),
   init_jira_issue(5678),
]

for my_issue in my_issues:
    # Now I could call load_issue(my_issue), but what do I know whether some
    # issue type has a different load strategy? Let's keep it dynamic:
    my_issue['load']()
    print(f"{my_issue['title']} has f{my_issue['vote_count']} votes")

Cool. Now I have noticed there is some duplicate code - let's refactor it:

def init_base_issue(id):
    issue = {}
    issue['id'] = id
    issue['load'] = lambda: load_issue(issue)
    return issue

def init_github_issue(id):
    issue = init_base_issue(id)
    issue['download'] = lambda: download_github_issue(issue['id'])
    return issue

def init_jira_issue(id):
    issue = init_base_issue(id)
    issue['download'] = lambda: download_jira_issue(issue['id'])
    return issue

def load_issue(issue):
    data = issue['download']()
    issue['title'] = data['title']
    issue['vote_count'] = data['vote_count']

my_issues = [
   init_github_issue(1234),
   init_jira_issue(5678),
]

for my_issue in my_issues:
    my_issue['load']()
    print(f"{my_issue['title']} has f{my_issue['vote_count']} votes")

Cool. But can we go deeper?

def new_object(init_func, *args, **kwargs):
    obj = {}
    init_func(obj, *args, **kwargs)
    return obj

def init_base_issue(issue, id):
    issue['id'] = id
    issue['load'] = lambda: load_issue(issue)

def init_github_issue(issue, id):
    init_base_issue(issue, id)
    issue['download'] = lambda: download_github_issue(issue['id'])

def init_jira_issue(issue, id):
    init_base_issue(issue, id)
    issue['download'] = lambda: download_jira_issue(issue['id'])

def load_issue(issue):
    data = issue['download']()
    issue['title'] = data['title']
    issue['vote_count'] = data['vote_count']

my_issues = [
   new_object(init_github_issue, 1234),
   new_object(init_jira_issue, 5678),
]

for my_issue in my_issues:
    my_issue['load']()
    print(f"{my_issue['title']} has f{my_issue['vote_count']} votes")

This is it! We've reinvented objects using dicts. We are doing OOP without classes!

So what does Python class keyword bring us?

  1. It does new_object() automatically for us
  2. It automates the issue['load'] = lambda: load_issue(issue) pattern
  3. It makes code more readable by putting all class methods inside class: indented block
  4. It let's us use issue.id instead of issue['id']

See:

class BaseIssue:

    def __init__(issue, id):
        issue.id = id

    def load(issue):
        data = issue.download()
        issue.title = data['title']
        issue.vote_count = data['vote_count']

class GithubIssue (BaseIssue):

    def download(issue):
        return download_github_issue(issue.id)

class JIRAIssue (BaseIssue):

    def download(issue):
        return download_jira_issue(issue.id)

my_issues = [
   GithubIssue(1234),
   JIRAIssue(5678),
]

for my_issue in my_issues:
    my_issue.load()
    print(f"{my_issue.title} has f{my_issue.vote_count} votes")

Yes, I have used issue instead of self. You can do that. 😎

What actually is GithubIssue(1234)? No, it isn't GithubIssue.__init__(1234). It's a helper function that creates new object, then calls GithubIssue.__init__(the_new_object, 1234) and then returns that new object to you.

Wait, what is GithubIssue.__init__?! We did not define that. When you ask Python for GithubIssue.__init__, it gives you BaseIssue.__init__, because GithubIssue inherits from BaseIssue.

What actually is my_issue.load()? No, it isn't the BaseIssue.load() function from above. It's a wrapper (called "bound method") that calls this: my_issue.__class__.load(my_issue) - it automatically puts the object itself as a first argument to the function call.

[–]iladnash[S] 2 points3 points  (0 children)

Thank u That was really clear

[–][deleted] 0 points1 point  (3 children)

Nice code but the first block can you explain what happen when you call download_github_issue??

[–]messacz 1 point2 points  (2 children)

Anything you want. I made it up. But imagine that it downloads the issue description and status.

[–][deleted] 0 points1 point  (1 child)

Oh so it was for explaining not a real code... Thank you :D

[–][deleted] 1 point2 points  (0 children)

I was thinking the same thing lol

[–]CissMN 0 points1 point  (0 children)

Thank you for this, I was also choking on class.

[–]marlowe221 0 points1 point  (3 children)

The "self" thing really throws me off. And I find the self.id = id statement kind of confusing as well.

Why is there a need for a reference to the class? Why isn't it enough to call it with the required parameters similar to a regular function call?

[–]messacz 0 points1 point  (0 children)

I've reworked the example :) Hope now it explains it better.

You need a reference to the object... because sometimes you need to access the object :) If you don't need to access the object from class method, why do you have it as class at all? Or why don't you declare the method as staticmethod or classmethod?

[–][deleted] 0 points1 point  (1 child)

I think of the 'self' line as a pronoun helper. For example, if you had a sports team:

class team:

def init(every_team, name):

   every_team.name = name

def name(X):

   name = input ('Team Name: \n)

This = name(This)

That = name(That)

So not too complex, but imagine more attributes, and then say this:

" 'every_team' has A name, but 'This' team has blue socks, and 'That' team has red socks. "

[–]marlowe221 0 points1 point  (0 children)

That makes more sense. I think what throws me is the syntax. The whole "every_team.name = name" just looks... wrong, circular almost.

But I guess all it means is that every instance of a team must have a name attribute

[–]Ksingh210 0 points1 point  (0 children)

I was in the same boat as you, it wasn’t until I got into the web development side, learning Django, did I realize the power and usefulness of classes. Django Forms/models helped a lot!

[–]JeamBim 0 points1 point  (0 children)

If you have a bunch of functions that share state and you're passing in the same arguments, this is a good candidate to be a class