什么时候不使用 Python 中的自我约定?

When to NOT use the self convention in Python?

我最近刚刚开始研究 Python 中的 self 约定,并开始编写更复杂的代码。然而,一位经验丰富的程序员和我的朋友告诉我,对 class 方法中的每个变量使用 self 是一种浪费。

我知道 self 会导致变量归因于那个 class。那么,除非有必要,否则 避免 使用 self 是一种很好的做法吗?

下面是一些从 API 获取英雄联盟信息并将每个变量存储在 self.var_name 中的代码,以说明我如何(可能不必要地)使用 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"]

有些情况下不需要使用 self

超出我的想象:

  • 当变量仅在 1 个函数中使用,或在 function/method 中创建且仅在该 function/method
  • 中使用时
  • 当变量不需要在方法之间共享时
  • 当变量不需要暴露给其他人时classes/scopes/contexts

另一个部分答案是,在创建 metaclass/factories/composition 时,像这样的东西可能更有意义地摆脱使用 self 的惯例,例如:

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

我可能在这里遗漏了一些东西,但这些是我目前能想到的。

相关:

  • What is the purpose of self?

self 将导致变量归因于 class 的实例,而不是 class 本身。我不知道你是不是这个意思,但这确实值得考虑。

class范围内的变量可以分为两类:class和实例变量。 Class 变量定义在 class 定义的开头,在任何方法之外。如果一个变量对于所有实例都是常量,或者它只在 class/static 方法中使用,那么它应该是一个 class 变量。通常,这些变量是真正的常量,尽管在很多情况下它们不是。实例变量通常在 __init__ 中定义,但在许多情况下它们应该在别处定义。也就是说,如果您没有充分的理由不这样做,请在 __init__ 中定义实例变量,因为这可以使您的代码(和 class)保持井井有条。如果您知道该变量对实例的状态至关重要但其值直到调用某个方法才确定,则为它们提供占位符值(例如 None)是完全可以接受的。

这是一个很好的例子:

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_POSSIBLE_MOVES 是真正的常量。虽然 started 不是常量,因为它的值取决于是否调用了 __call__ 方法,但它的默认值 False 对于所有实例都是常量,所以我将其声明为一个 class 变量。注意在__call__中(不用担心我为什么用__call__而不是__init__),我将它重新定义为一个实例变量,因为__call__开始游戏,并且因此当它被调用时,实例的状态已经从class默认的"not started"变为"started".

另请注意,除 __call__ 之外的其他方法会定期更改实例变量的值,但它们最初并未在所述方法中定义,因为没有令人信服的理由。