使用实例属性作为字典值

Using Instance Attributes as Dictionary Values

前脚本:我在 SO 上搜索了很多线程,但 none 似乎回答了我的问题。

我制作了一个小脚本,用于处理围绕网格移动的点,同时使用它已遍历的所有这些点更新集合。 move() 方法的要点是:

# self.x and self.y are initialised to 0 at the time of object creation
# dir_ - direction in which to move
# steps - number of steps to move

def _move(self, dir_, steps):                
        if dir_ == 'U':
            for step in range(steps):
                self.y += 1
                self.locations.add((self.x, self.y))
        elif dir_ == 'R':
            for step in range(steps):
                self.x += 1
                self.locations.add((self.x, self.y))
        elif dir_ == 'L':
            for step in range(steps):
                self.x -= 1
                self.locations.add((self.x, self.y))
        elif dir_ == 'D':
            for step in range(steps):
                self.y -= 1
                self.locations.add((self.x, self.y))
        else:
            raise Exception("Invalid direction identifier.")

如您所见,有很多重复。在我急于清理东西的过程中,我尝试了这样的事情:

from operator import add, sub

def _move(self, dir_, steps):                
    dir_dict = {'U': (self.y, add), \
                'D': (self.y, sub), \
                'L': (self.x, sub), \
                'R': (self.x,add)}

    coord, func = dir_dict[dir_]
    for step in range(steps):
        coord = func(coord, 1)
        locations.add(self.x, self.y)

事实证明,我不能指望对象属性的 引用 会像那样传递,结果 self.xself.y 没有更新。

问题:

  1. 如何清理此代码以避免重复?

  2. 即使原始代码被认为是 "not all that bad" 的功能,有没有办法按照我打算的方式传递实例属性?

您的第一次重构肯定是在正确的轨道上。您看到的问题是 addsub return 新值,而不是现有值。 coordself.xself.y 不同。我会在这里使用属性查找

from operator import add, sub

def _move(self, dir_, steps):                
    dir_dict = {'U': ('y', self.y, add), \
                'D': ('y', self.y, sub), \
                'L': ('x', self.x, sub), \
                'R': ('x', self.x, add)}

    attr, coord, func = dir_dict[dir_]
    for step in range(steps):
        coord = func(coord, 1)
        # set the attribute on self here
        setattr(self, attr, coord)
        locations.add(self.x, self.y)

这是我的做法:您希望字典值代表运动;这些是应该更新对象状态的操作,因此将它们表示为执行状态更新的函数是有意义的,而不是关于要执行哪些状态更新的数据。 (通过将 addsub 存储为函数,您已经完成了一半。)

给出classmove_upmove_downmove_leftmove_right方法,然后在字典中存储对这些方法的引用。

    def move_up(self):
        self.y += 1
    def move_down(self):
        self.y -= 1
    def move_left(self):
        self.x -= 1
    def move_right(self):
        self.x += 1

    def _move(self, dir_, steps):
        dir_dict = {'U': self.move_up,
                    'D': self.move_down,
                    'L': self.move_left,
                    'R': self.move_right}

        func = dir_dict[dir_]
        for step in range(steps):
            func()
            self.locations.add( (self.x, self.y) )

就重构而言,有很多不同的方法。 个人认为,调用者应该负责决定点应该做什么,而不是点根据字符串输入决定做什么。这里的问题是这个方法并没有使 Point 真正可扩展(你不能在不更改 _move 函数的情况下更改基本功能)

所以这是我的看法:

最初,我们可以像这样简化 _move 函数:

def _move(self, distances, steps=1):
    """Move the Point a given distance along the x,y axis, a given amount of times and save the new location"""
    distance_x, distance_y = distances
    for step in range(steps):
        self.x += distance_x
        self.y += distance_y
        self.locations.add((self.x, self.y))     

情况如下。 _move 函数现在需要沿 x 轴和 y 轴移动点的距离,以及移动它的次数。 distances 在这种情况下是一个元组。为清楚起见,它被解包为 distance_xdistance_y 变量。然后将距离添加到点的 x 和 y 值,然后保存到 locations 列表。对于您的用例,调用者将查找该点应该做什么。

if action == 'U':
    point._move(0, 1)
elif action == 'D':
    point._move(0, -1)
...

现在,如果您想定义点可以进行的具体运动,您可以执行以下操作:

def move_up(self, distance=1, steps=1):
    """Move the Point up a given distance, a given amount of times

    Precondition: The distance is positive
    """
    assert distance >= 0
    self._move((0, distance), steps)

def move_right(self, distance=1, steps=1):
    """Move the Point right a given distance, a given amount of times

    Precondition: The distance is positive
    """
    assert distance >= 0
    self._move((distance, 0), steps)

def move_down(self, distance=1, steps=1):
    """Move the Point down a given distance, a given amount of times

    Precondition: The distance is positive
    """
    assert distance <= 0 
    self._move((0, -distance), steps)

def move_left(self, distance=1, steps=1):
    """Move the Point left a given distance, a given amount of times

    Precondition: The distance is positive
    """    
    assert distance <= 0
    self._move((-distance, 0), steps)

在这里,每个函数都定义了点在每个方向上的移动方式。每个方向都需要一个 distance 允许调用者定义在给定方向上移动多少个网格空间,以及数量 steps。默认情况下,每个函数在每个方向上移动一个。存在断言是因为能够使用负数移动某个方向似乎很奇怪(向右移动 -1 与向左移动 +1 相同),但根据您的用例,它们不是必需的。

调用者看起来像这样:

if action == 'U':
    point.move_up()
elif action == 'D':
    point.move_down()
...

虽然比较冗长,但这种方法有几个好处。

  1. 每个方向都包含在它自己的函数中。这将允许 subclasses 轻松覆盖基点行为。例如,如果您想要跟踪每步向上移动 2 个网格空间的点,您可以扩展 BasePoint 并重写 move_up 函数,使其看起来像这样:
def move_up(steps=1):
    super().move_up(distance=2, steps=steps)
  1. _move函数更简洁,因为它不需要知道将Point移动到哪个方向。它只需要沿 x 和 y 轴移动的距离,移动点并保存新位置。这使得 class 更具可扩展性,因为您可以通过添加新函数
  2. 轻松地为要移动的点(即对角线点)创建新的方向
def move_diagonal(distance_x=1, distance_y=1, steps=1)
    super()._move(distance=(distance_x, distance_y), steps=steps)
  1. 调用者控制点的调用方式。这允许调用者定义调用函数的规则
if action == 'U' and len(p.locations) < TOTAL_STEPS:
    p.move_up()
else:
    raise TooManyStepsException('You have exceeded the number of allowed steps')

希望这对您有所帮助,并根据您的用例为您提供几种不同的方法。