4

I'm writing an API parsing Twitter bot and am very new to OOP. I have some existing Python code that relies on global variables and figured I could take this opportunity to learn.

I have the following Team class that gets updated when the API is parsed and is like to be able to call a totally unrelated (external) method when a class attribute changes.

class Team(object):
  def __init__(self, team_name, tri_code, goals, shots, goalie_pulled):
    self.team_name = team_name
    self.tri_code = tri_code
    self.goals = goals
    self.shots = shots
    self.goalie_pulled = goalie_pulled

When goalie_pulled is changed for an existing instance of Team I'd like the following method to be called (pseudo code):

def goalie_pulled_tweet(team):
  tweet = "{} has pulled their goalie with {} remaining!".format(team.team_name, game.period_remain)
  send_tweet(tweet)

Two things -

  1. How do I call goalie_pulled_tweet from within my Team class once I detect that goalie_pulled attribute has changed?
  2. Can I access an instance of my Game object from anywhere or does it need to be passed to that variable as well?
mattdonders
  • 1,328
  • 1
  • 19
  • 42
  • Does this answer your question? [How do you change the value of one attribute by changing the value of another? (dependent attributes)](https://stackoverflow.com/questions/27308560/how-do-you-change-the-value-of-one-attribute-by-changing-the-value-of-another) – pppery Jul 04 '20 at 20:35

2 Answers2

10

You should take a look at the property class. Basically, it lets you encapsulate behaviour and private members without the consumer even noticing it.

In your example, you may have a goalie_pulled property:

class Team(object):
    def __init__(self, team_name, tri_code, goals, shots, goalie_pulled):
        # Notice the identation here. This is very important.
        self.team_name = team_name
        self.tri_code = tri_code
        self.goals = goals
        self.shots = shots

        # Prefix your field with an underscore, this is Python standard way for defining private members
        self._goalie_pulled = goalie_pulled

    @property
    def goalie_pulled(self):
        return self._goalie_pulled

    @goalie_pulled.setter
    def goalie_pulled(self, new_value):
        self._goalie_pulled = new_value
        goalie_pulled_tweet(self) #self is the current Team instance

From the consumer's point of view:

team = create_team_instance()

# goalie_pulled_tweet is called
team.goalie_pulled = 'some_value'

I'd recommend you to use properties whenever you can (and must), as they are a nice way of abstraction.

Matias Cicero
  • 25,439
  • 13
  • 82
  • 154
  • This makes a lot of sense - just two minor things if you don't mind. 1 - is there a way to access an attribute of another class from the 'goalie_pulled_tweet(self)' method if I needed an attribute from an instance of the Team object (obviously my code for this class isn't there but as an example). And 2 - the property basically becomes a getter for what I've made a private variable in the Class? – mattdonders Jan 22 '18 at 23:42
  • This method definitely makes sense to me. If I want to check for a changed value and determine if I want to tweet, can I reference the old value like such? `old_value = self.goalie_pulled; self._goalie_pulled = new_value; if new_value == True and old_value == False: goalie_pulled_tweet(self)` – mattdonders Jan 23 '18 at 01:51
2

From a design standpoint, it would make more sense to have a pull_goalie method.

Classes are a tool to create more meaningful abstractions. Pulling a goalie is an action. If you think of Team as representing a real-life team, it makes more sense to say "The team pulled their goalie!" rather than "The team set their pulled-goalie attribute to X player!"

class Team(object):
    ...

    def pull_goalie(self, player):
        self.pulled_goalie = player
        tweet = '<your format string>'.format(
            self.pulled_goalie,
            # Yes, your Team *could* store a reference to the current game.
            # It's hard to tell if that makes sense in your program without more context.
            self.game.period_remaining  
        )

I was going to recommend a property, but I think that would solve the immediate problem without considering broader design clarity.

NOTE: There is no such thing as a "private" attribute in Python. There is a convention that attributes beginning with a single underscore (self._pulled_goalie) is treated as private, but that's just so that people using your code know that they can't depend on that value always doing what they think it will. (i.e., it's not part of the public contract of your code, and you can change it without warning.)

EDIT: To create a register_team method on a Game object, you might do something like this:

class Game(object):
    def __init__(<stuff>):
        ...
        self.teams = {}

    ...

    def register_team(self, team):
        if len(self.teams) > 1:
            raise ValueError(
                "Too many teams! Cannot register {} for {}"
                .format(team, game)
            )

        self.teams[team] = team
        team.game = self

    def unregister_team(self, team):
        try:
            del self.teams[team]
        except KeyError:
            pass

        team.game = None

Note that by using a dictionary, register_team and unregiser_team can be called multiple times without ill effect.

Phil
  • 141
  • 1
  • 7
  • That definitely makes sense as well and I could check before calling the method if the previous value is different than the value returned from the API. My follow up to this is that if period_remaining isn't an attribute of Team how do I reference it in that class? – mattdonders Jan 22 '18 at 23:49
  • Give the `Team` instance a reference to your `Game` instance. If `Game` is created before teams are, add a `game` parameter to `Team.__init__`. If teams are created first, add `self.game = None` to the body of `Team.__init__` and then set it manually after game is created. – Phil Jan 22 '18 at 23:53
  • You could even get fancy and have a `Game.register_team(team)` method that would set the appropriate attributes of both objects from a central place, if they need to know about each other. – Phil Jan 22 '18 at 23:54
  • Without knowing what all your `Game` instance needs to do, it's hard to recommend one alternative over another. – Phil Jan 22 '18 at 23:54
  • Game is created before each Team so adding a reference to the Game inside each Team makes a lot of sense - maybe I'll give that a shot and report back. If I wanted to go with the Game.register_team(team) option and each Game has two Teams how would I go about that? – mattdonders Jan 23 '18 at 00:00
  • Makes so much sense. So if I understand correctly that register team method adds a dictionary of teams to the Game object but also adds the Game object to teach individual Team object so attributes for each can be referenced from within either object? – mattdonders Jan 23 '18 at 00:55
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/163672/discussion-between-mattdonders-and-phil). – mattdonders Jan 23 '18 at 01:51