【问题标题】:Using Instance Attributes as Dictionary Values使用实例属性作为字典值
【发布时间】:2019-12-20 18:42:15
【问题描述】:

预脚本:我已经搜索了很多关于 SO 的线程,但似乎没有人回答我的问题。

我制作了一个小脚本,它处理一个在网格周围移动的点,同时用它遍历的所有点更新一个集合。 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. 即使原始代码段被认为对功能“没有那么糟糕”,是否有办法按照我的意图传递实例属性?

【问题讨论】:

  • dir_dict = {'U': (0, 1), 'D': (0, -1), ... }
  • 这如何告诉我要更新哪个属性?
  • 你更新both
  • 哦,好的,我明白了。谢谢。在某些方面,我发现这种清洁剂。 P.S.:我认为,如果您指出您在元组中表示的内容与 OP 中提到的内容不同,那将会很有帮助。 :)

标签: python dictionary oop attributes instance


【解决方案1】:

您的第一个重构肯定是在正确的轨道上。您看到的问题是 addsub 返回新值,而不是现有值。 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)

【讨论】:

  • 啊,一个足够简单的调整就可以完成工作。我之前正在研究getattr,但当它不起作用时就放弃了。谢谢。
【解决方案2】:

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

给类move_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) )

【讨论】:

  • 感谢您的回复。我曾经考虑过这一点,但后来觉得我只是在四处移动,并没有真正使代码简洁。
【解决方案3】:

在重构方面,有很多不同的方法可以做到这一点。 就个人而言,我认为调用者应该负责确定该点应该做什么,而不是根据字符串输入确定该点做什么。这里的问题是这种方法并没有使 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. 每个方向都包含在自己的函数中。这将允许子类轻松覆盖基点行为。例如,如果您想跟踪每步向上移动 2 个网格空间的点,您可以扩展 BasePoint 并重写 move_up 函数,使其看起来像这样:
def move_up(steps=1):
    super().move_up(distance=2, steps=steps)
  1. _move 函数更简洁,因为它不需要知道移动点的方向。只需沿 x 和 y 轴移动距离,移动点并保存新位置。这使得该类更具可扩展性,因为您可以通过添加新函数轻松地为要移动的点(即对角点)创建新方向
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')

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

【讨论】:

  • 感谢您的帮助。我的特定用例相对简单,我不希望随时扩展代码。不过,我很欣赏整个思考过程。
猜你喜欢
  • 1970-01-01
  • 2013-05-20
  • 2013-11-18
  • 1970-01-01
  • 1970-01-01
  • 2010-12-10
  • 1970-01-01
  • 2010-11-02
  • 1970-01-01
相关资源
最近更新 更多