2

I've just recently wrapped my head around the self convention in Python and have begun making more complex code. However, an experienced programmer and friend of mine told me that to use self for every variable in a class method is wasteful.

I understand that self will cause the variable to become attributed to that class. So would it be true that, unless the need arises, it is good practice to avoid using self?

Below is some code that fetches League of Legends information from an API and stores each variable in self.var_name to illustrate how I'm (perhaps unnecessarily) using self.

async def getChampInfo(self, *args):
    """ Return play, ban, and win rate for a champ """
    self.uri = "http://api.champion.gg/v2/champions/{}?api_key={}"
    self.champ = " ".join(args)
    self.champID = lu.getChampID(self.champ)
    self.res = requests.get(self.uri.format(
        self.champID, League.champion_gg_api_key)).json()
    self.role = self.res[0]["role"]
    self.role_rate = self.res[0]["percentRolePlayed"]
    self.play_rate = self.res[0]["playRate"]
    self.win_rate = self.res[0]["winRate"]
    self.ban_rate = self.res[0]["banRate"]
James
  • 325
  • 1
  • 3
  • 15
  • 2
    Well which of those do you ever use *outside* that method? At a guess I'd say the first four should be local variables (the uri template maybe a class attribute), the last five attributes. – jonrsharpe Jun 22 '17 at 23:35
  • I guess I missed the `self.train` because I really _don't_ reference those variables outside of the method. I should be able to prune the code to make a hell of a lot more sense now. – James Jun 22 '17 at 23:57

2 Answers2

5

There are cases where using self is not needed.

Off the top of my head:

  • when the variable is only used in 1 function, or is created inside a function/method and only used in that function/method
  • when the variable doesn't need to be shared between methods
  • when the variable doesn't need to be exposed to other classes/scopes/contexts

Another partial answer is that when creating metaclass/factories/composition something like this might make more sense to move away from the convention of using self like:

class Factory(object):
    def __init__(cls, *args, **kwargs):
        thing = cls(args, kwargs)

I might be missing some stuff here, but those are what i can think of at the moment.

related:

jmunsch
  • 22,771
  • 11
  • 93
  • 114
  • 1
    Great that helps! So in the above example, if I don't need to access those variables outside of the instance or method, then it's basically wasteful. I'll tinker with removing self and see how it goes from there. Cheers! – James Jun 22 '17 at 23:53
  • @James added another link to a more in depth discussion on the usage of `self` under the related links. :) np. – jmunsch Jun 22 '17 at 23:57
3

self will cause a variable to become attributed to an instance of the class, not the class itself. I don't know if you meant that or not, but it's certainly worth thinking about.

Variables in the class-wide scope can be divided into two categories: class and instance variables. Class variables are defined at the beginning of the class definition, outside of any method. If a variable is constant for all instances, or it is only used in class/static methods, it should be a class variable. Often, such variables are true constants, though there are numerous cases where they aren't. Instance variables are generally defined in __init__, but there are numerous cases where they should be defined elsewhere. That being said, if you don't have a good reason not to, define instance variables in __init__, as this keeps your code (and class) organized. It is perfectly acceptable to give them placeholder values (such as None), if you know the variable is essential to the state of the instance but its value is not determined until a certain method is called.

Here's a good example:

class BaseGame:
    """Base class for all game classes."""

    _ORIGINAL_BOARD = {(0,0): 1, (2,0): 1, (4,0): 1, (6,0): 1, (8,0): 1,
                       (1,2): 1, (3,2): 1, (5,2): 1, (7,2): 1, (2,4): 1,
                       (4,4): 1, (6,4): 1, (3,6): 1, (5,6): 1, (4,8): 0}
    _POSSIBLE_MOVES = {(0,0): ((4,0),(2,4)),
                       (2,0): ((4,0),(2,4)),
                       (4,0): ((-4,0),(4,0),(2,4),(-2,4)),
                       (6,0): ((-4,0),(-2,4)),
                       (8,0): ((-4,0),(-2,4)),
                       (1,2): ((4,0),(2,4)),
                       (3,2): ((4,0),(2,4)),
                       (5,2): ((-4,0),(-2,4)),
                       (7,2): ((-4,0),(-2,4)),
                       (2,4): ((4,0),(2,4),(-2,-4),(2,-4)),
                       (4,4): ((-2,-4,),(2,-4)),
                       (6,4): ((-4,0),(-2,4),(-2,-4),(2,-4)),
                       (3,6): ((-2,-4),(2,-4)),
                       (5,6): ((-2,-4),(2,-4)),
                       (4,8): ((-2,-4),(2,-4))}
    started = False

    def __call__(self):
        """Call self as function."""
        self.started = True
        self.board = __class__._ORIGINAL_BOARD.copy()
        self.peg_count = 14
        self.moves = []

    @staticmethod
    def _endpoint(peg, move):
        """Finds the endpoint of a move vector."""
        endpoint = tuple(map(add, peg, move))
        return endpoint

    @staticmethod
    def _midpoint(peg, move):
        """Finds the midpoint of a move vector."""
        move = tuple(i//2 for i in move)
        midpoint = tuple(map(add, peg, move))
        return midpoint

    def _is_legal(self, peg, move):
        """Determines if a move is legal or not."""
        endpoint = self._endpoint(peg, move)
        midpoint = self._midpoint(peg, move)
        try:
            if not self.board[midpoint] or self.board[endpoint]:
                return False
            else:
                return True
        except KeyError:
            return False

    def find_legal_moves(self):
        """Finds all moves that are currently legal.

        Returns a dictionary whose keys are the locations of holes with
        pegs in them and whose values are movement vectors that the pegs
        can legally move along.
        """
        pegs = [peg for peg in self.board if self.board[peg]]
        legal_moves = {}
        for peg in pegs:
            peg_moves = []
            for move in __class__._POSSIBLE_MOVES[peg]:
                if self._is_legal(peg, move):
                    peg_moves.append(move)
            if len(peg_moves):
                legal_moves[peg] = peg_moves
        return legal_moves

    def move(self, peg, move):
        """Makes a move."""
        self.board[peg] = 0
        self.board[self._midpoint(peg, move)] = 0
        self.board[self._endpoint(peg, move)] = 1
        self.peg_count -= 1
        self.moves.append((peg, move))

    def undo(self):
        """Undoes a move."""
        peg, move = self.moves.pop()
        self.board[peg] = 1
        self.board[self._midpoint(peg, move)] = 1
        self.board[self._endpoint(peg, move)] = 0
        self.peg_count += 1

    def restart(self):
        """Restarts the game."""
        self.board = __class__._ORIGINAL_BOARD.copy()
        self.peg_count = 14
        self.moves.clear()

_ORIGINAL_BOARD and _POSSIBLE_MOVES are true constants. While started is not a constant, as its value depends on whether the __call__ method was invoked or not, its default value, False, IS constant for all instances, so I declared it as a class variable. Notice that in __call__ (don't worry about why I used __call__ instead of __init__), I redefined it as an instance variable, as __call__ starts the game, and therefore when it is invoked, the instance's state has changed from the class default, "not started", to "started".

Also notice that the other methods besides __call__ regularly change the value of the instance variables, but that they are not initially defined in said methods, as there is no compelling reason for them to be.

Isaac Saffold
  • 1,116
  • 1
  • 11
  • 20
  • I definitely did mean to reference the instance, so you're right. I'm a bit clumsy with my wording as I don't have a full grasp of OOP (or any programming for that matter) at this point. To your point, if I have a URI that will remain constant, it would make sense to put that outside of any function as it will be a constant? – James Jun 23 '17 at 00:33
  • If the class and/or instances need to access it, then yes. If only a single method needs to access it, then just declare it in that method. – Isaac Saffold Jun 23 '17 at 00:36
  • I'll give you a good example. I used it to represent the game itself (as opposed to graphics and non-essential methods used by the game) in an implementation of the Triangle Peg Game from Cracker Barrel. – Isaac Saffold Jun 23 '17 at 00:39
  • Alright, I added it at the end of my answer. Feel free to ask any questions. Learning about classes and OOP for the first time was far more difficult for me than more basic subjects such as loops and functions, but once I got the hang of it, I never turned back. My game, for example, contains no functions that are not defined in a class, although I could have defined them outside if I wanted to. Functional programming definitely has its uses, though. There are some languages, such as Java, that essentially force you to use OOP, so that's another good reason to learn it, and learn it well. – Isaac Saffold Jun 23 '17 at 00:56
  • 1
    Hey this is really cool! I really appreciate how in-depth this is. You've pretty much covered most questions I had about conventions here. I've managed to really clean up my code since my original posting! Cheers – James Jun 29 '17 at 14:49
  • Glad I could be of help. – Isaac Saffold Jun 30 '17 at 01:57