Python对象的良好编码风格

4
在接触Python之前,我主要使用C++或Matlab进行编程。我没有计算机科学学位(几乎完成了物理学博士学位),但我参加过一些课程并实际进行了大量编程。现在,我正在Coursera上学习算法课程(顺便说一句,这是一门非常好的课程,由斯坦福大学的教授讲授)。我决定用Python实现作业。然而,有时候我发现自己想要的东西语言并不容易支持。我很习惯于在C++中为事物创建类和对象,只是为了将数据分组在一起(即当没有方法时)。然而,在Python中,您可以随时添加字段,因此我基本上一直想要的是Matlab结构体。我认为这可能是我没有使用良好的风格并且没有按照“Pythonic”方式处理事情的迹象。
下面是我的并查集数据结构实现(用于Kruskal算法)。虽然实现相对简短且效果良好(没有太多错误检查),但还有一些奇怪的地方。例如,我的代码假定最初传递给并查集的数据是对象列表。但是,如果传递的是显式数据片段的列表(即整数列表),则代码会失败。是否有更清晰、更符合Python风格的实现方法?我尝试过搜索,但大多数示例都非常简单,与过程式代码相关(即在Python中执行for循环的“正确”方式)。
class UnionFind:
    def __init__(self,data):
        self.data = data

        for d in self.data:
            d.size = 1
            d.leader = d
            d.next = None
            d.last = d

    def find(self,element):
        return element.leader

    def union(self,leader1,leader2):
        if leader1.size >= leader2.size:
            newleader = leader1
            oldleader = leader2
        else:
            newleader = leader2
            oldleader = leader1

        newleader.size = leader1.size + leader2.size

        d = oldleader
        while d != None:
            d.leader = newleader
            d = d.next

        newleader.last.next = oldleader
        newleader.last = oldleader.last

        del(oldleader.size)
        del(oldleader.last)    

你想要实现什么?乍一看,可能是某种树形结构,但是后来就不太确定了... - Jon Clements
如果您传入整数列表,代码失败并不奇怪,因为它们没有代码期望的属性(构造函数中的while循环)。这是动态类型的“问题”之一。为了解决这个问题,您可以始终使用“type()”来检查类型。 - Daniel Figueroa
嗨Jon,你能澄清一下你的问题吗?我写了这是一个并查集(不相交集)数据结构,用于Kruskal算法。丹尼尔,我觉得这并不奇怪。我只是试图找到一种简洁的方法使其工作。这很不容易,因为你需要能够传递指向已定义字段的指针。 - Nir Friedman
你应该在这个类中包含 data 参数的定义。 - yurisich
问题是数据是否应该是严格类型,还是如何以最符合Python风格的方式处理/支持多个数据类型? - Nisan.H
Droogans:数据参数没有严格的定义。数据只需要是一个对象列表即可。Nisan:我知道(认为?)在Python中不需要进行严格的类型定义,这并不符合Pythonic风格。问题是如何能够轻松、通用地支持任何传入的数据类型。例如,假设您想从给定的任意数据数组生成一个链表对象。在Python中,这非常容易泛化:您只需生成节点对象列表,并将节点数据设置为相应的输入参数即可。我希望这个并查集也可以类似地通用化。 - Nir Friedman
5个回答

2
通常来说,Python化地完成这种事情意味着你要尽量让你的代码不关心它接收到了什么,至少不会比它实际需要的更多。
我们以并查集算法为例。并查集算法实际上对你传递给它的值只做了一个操作,就是比较它们是否相等。所以为了创建一个通用的 UnionFind 类,你的代码不应该依赖于传递给它的值有任何其他行为,除了相等性测试。特别是,你不应该依赖于能够将任意属性分配给这些值。
我建议的解决方法是使用包装对象来包含给定的值和使算法工作所需的任何属性。你可以像另一个答案所建议的那样使用 namedtuple,或者创建一个小型的包装类。当一个元素被添加到 UnionFind 中时,你首先将其包装在其中一个这些对象中,并使用包装对象来存储属性 leadersize 等。唯一访问被包装的东西的时间是检查它是否等于另一个值。
在实践中,至少在这种情况下,可以安全地假设您的值是可哈希的,因此您可以将它们用作Python字典中的键,以查找对应于给定值的包装器对象。当然,并非所有Python中的对象都必须是可哈希的,但那些不可哈希的对象相对较少,而且要创建一个能够处理这些对象的数据结构需要更多的工作。

我非常喜欢namedTuples,但是我觉得通过replace函数更改它们的值有点丑陋。 - Nir Friedman

0
更加Pythonic的方式是尽量避免使用繁琐的对象,除非你必须要用。
class UnionFind(object):
    def __init__(self, members=10, data=None):
        """union-find data structure for Kruskal's algorithm
        members are ignored if data is provided
        """
        if not data:
            self.data = [self.default_data() for i in range(members)]
            for d in self.data:
                d.size   = 1
                d.leader = d
                d.next   = None
                d.last   = d
        else:
            self.data = data

    def default_data(self):
        """create a starting point for data"""
        return Data(**{'last': None, 'leader':None, 'next': None, 'size': 1})

    def find(self, element):
        return element.leader

    def union(self, leader1, leader2):
        if leader2.leader is leader1:
            return
        if leader1.size >= leader2.size:
            newleader = leader1
            oldleader = leader2
        else:
            newleader = leader2
            oldleader = leader1

        newleader.size = leader1.size + leader2.size

        d = oldleader
        while d is not None:
            d.leader = newleader
            d = d.next

        newleader.last.next = oldleader
        newleader.last = oldleader.last

        oldleader.size = 0
        oldleader.last = None

class Data(object):
    def __init__(self, **data_dict):
        """convert a data member dict into an object"""
        self.__dict__.update(**data_dict)

非常好。但我仍不确定“id”的目的是什么。你能解释一下吗? 我想我可能会转换到非常相似的东西。唯一我会添加的是一个可选标志,以便用户可以创建带有其他条目(即“数据”)的字典,并且仍然使构造函数初始化默认状态(所有条目不相交)。 我必须承认,我对字典持有偏见;与结构体相比,它们的语法非常笨拙。你最终会克服这个问题吗?感谢您的帮助。 - Nir Friedman
好的,我已经修改了。在Python中,你可以很容易地将一个字典转换成一个对象。 - yurisich
添加到对象(“结构体”)的属性很难枚举。您无法确定它们是您设置的属性,来自超类的方法,描述符/属性魔术等。如果我有一个预先知道属性名称的明确定义结构,我会使用元组或命名元组。否则,如果我有一个类似于索引的结构,我会使用字典,这提供了简单可靠的枚举。 - Francis Avila
Francis,我想使用namedtuple,但我觉得替换值的语法非常烦人。 - Nir Friedman

0

要检查参数是否为预期类型,请使用内置的{{link1:isinstance()}}函数:

if not isinstance(leader1, UnionFind):
    raise ValueError('leader1 must be a UnionFind instance')

此外,将docstrings添加到函数、类和成员函数中是一个好习惯。这样一个函数或方法的文档字符串应该描述它的功能、需要传递哪些参数,如果适用的话,还应该说明返回值以及可能引发的异常。

这根本没有帮助到提问者。由namedtuple创建的类型实例是不可变的,因此您无法更新它们的值,而这正是UnionFind数据结构所需的。实际上,解决不可变值问题正是提出该问题的问题! - Blckknght

0
一个选择是使用字典来存储与数据项相关的信息,而不是直接将信息作为属性存储在数据项上。例如,您可以使用 size[d] 来引用大小,而不是使用 d.size(其中 size 是一个 dict 实例)。这要求您的数据项是可哈希的,但它们不需要允许在其上分配属性。
下面是将您当前的代码直接翻译为使用此样式的代码:
class UnionFind:
    def __init__(self,data):
        self.data = data
        self.size = {d:1 for d in data}
        self.leader = {d:d for d in data}
        self.next = {d:None for d in data}
        self.last = {d:d for d in data}

    def find(self,element):
        return self.leader[element]

    def union(self,leader1,leader2):
        if self.size[leader1] >= self.size[leader2]:
            newleader = leader1
            oldleader = leader2
        else:
            newleader = leader2
            oldleader = leader1

        self.size[newleader] = self.size[leader1] + self.size[leader2]

        d = oldleader
        while d != None:
            self.leader[d] = newleader
            d = self.next[d]

        self.next[self.last[newleader]] = oldleader
        self.last[newleader] = self.last[oldleader]

一个最小化的测试案例:
>>> uf = UnionFind(list(range(100)))
>>> uf.find(10)
10
>>> uf.find(20)
20
>>> uf.union(10,20)
>>> uf.find(10)
10
>>> uf.find(20)
10

除此之外,您还可以考虑稍微改变您的实现方式,以减少初始化的工作量。这里有一个版本不需要任何初始化(它甚至不需要知道它要处理的数据集合)。它使用路径压缩和按秩合并,而不是始终维护一个集合中所有成员的最新leader值。如果您正在进行大量的合并操作,它应该比您当前的代码在渐进意义下更快。
class UnionFind:
    def __init__(self):
        self.rank = {}
        self.parent = {}

    def find(self, element):
        if element not in self.parent: # leader elements are not in `parent` dict
            return element
        leader = self.find(self.parent[element]) # search recursively
        self.parent[element] = leader # compress path by saving leader as parent
        return leader

    def union(self, leader1, leader2):
        rank1 = self.rank.get(leader1,1)
        rank2 = self.rank.get(leader2,1)

        if rank1 > rank2: # union by rank
            self.parent[leader2] = leader1
        elif rank2 > rank1:
            self.parent[leader1] = leader2
        else: # ranks are equal
            self.parent[leader2] = leader1 # favor leader1 arbitrarily
            self.rank[leader1] = rank1+1 # increment rank

-1
我猜这里的缩进问题只是在将代码输入到SO时出现的简单错误。你能否创建一个内置数据类型的子类?例如,您可以通过将数据类型放在括号中来创建列表数据类型的子类:
class UnionFind(list):
'''extends list object'''

1
除非您真的要使用其功能的重要部分,否则对另一个数据结构进行子类化并没有太多意义,不是吗?在这种情况下,Union-Find仅具有两个操作,并且与简单数据结构都没有重叠。 Union-Find中的列表仅用于对象(实际上是指针)的存储;此数据结构通过添加到现有列表的指针并操纵这些指针来完成所有工作。 - Nir Friedman
很遗憾,SO目前只允许我回答而不是评论。因为那并不是一个真正的“答案”,更像是一条评论。我非常感谢你对我的回复提供的意见,作为一个初学者,这确实是一篇有帮助的文章。谢谢! - Sandwich Heat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接