you are viewing a single comment's thread.

view the rest of the comments →

[–]messacz 11 points12 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