Python 对象的良好风格

Posted

技术标签:

【中文标题】Python 对象的良好风格【英文标题】:Good Style in Python Objects 【发布时间】:2012-12-22 00:06:35 【问题描述】:

在 Python 之前,我的大部分编程都是用 C++ 或 Matlab 进行的。我没有计算机科学学位(几乎完成了物理学博士学位),但已经完成了一些课程和大量的实际编程。现在,我正在 Coursera 上学习算法课程(顺便说一句,与斯坦福大学的教授一起学习的课程非常好)。我决定用 Python 实现作业。然而,有时我发现自己想要一些语言不容易支持的东西。我非常习惯于在 C++ 中为事物创建类和对象,只是为了将数据组合在一起(即当没有方法时)。然而,在 Python 中,您可以动态添加字段,而我基本上一直想要的是 Matlab 结构。我认为这可能表明我没有使用良好的风格并且没有以“Pythonic”的方式做事。

下面是我对联合查找数据结构的实现(用于 Kruskal 算法)。尽管实现相对较短并且运行良好(没有太多错误检查),但还是有一些奇怪的地方。例如,我的代码假定最初传递给 union-find 的数据是一个对象列表。但是,如果传入的是显式数据片段列表(即整数列表),则代码将失败。有没有更清晰、更 Pythonic 的方式来实现它?我试图用谷歌搜索这个,但大多数示例都非常简单,并且更多地与程序代码相关(即在 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)    

【问题讨论】:

你想达到什么目的?乍一看,是某种树,但后来就不太确定了…… 如果你传入一个整数列表,代码失败并不奇怪,因为它们没有代码所期望的属性(构造函数中的 while 循环),这是“问题”与动态类型。为了解决这个问题,您可以随时使用“type()”检查类型。 嗨,乔恩,你能澄清一下你的问题吗?我写道,这是用于 Kruskal 算法的联合查找(不相交集)数据结构。丹尼尔,我不觉得奇怪。我只是想找到一种干净的方法来让它工作。这很重要,因为您需要能够以某种方式传入可以访问已定义字段的指针。 您应该包含您对此类的data 参数的定义。 问题是数据是否应该严格类型化,还是如何以最 Python 的方式处理/支持多种数据类型? 【参考方案1】:

一般而言,以 Python 方式执行此类操作意味着您尝试让您的代码不在乎给予它的内容,至少不超过它真正需要的。

让我们以联合查找算法为例。 union-find 算法实际上对传递给它的值所做的唯一一件事就是比较它们是否相等。因此,要创建一个普遍有用的UnionFind 类,您的代码不应依赖于它接收到的具有除相等性测试之外的任何行为的值。特别是,您不应该依赖能够为值分配任意属性。

我建议解决这个问题的方法是让UnionFind 使用包含给定值和使算法工作所需的任何属性的包装器对象。您可以按照另一个答案的建议使用namedtuple,或者制作一个小型包装类。当一个元素被添加到UnionFind 时,你首先将它包装在其中一个对象中,并使用包装器对象来存储属性leadersize 等。唯一一次访问被包装的东西是检查它是否等于另一个值。

实际上,至少在这种情况下,假设您的值是可散列的应该是安全的,这样您就可以将它们用作 Python 字典中的键来查找与给定值对应的包装器对象。当然,并非 Python 中的所有对象都必须是可散列的,但那些不是比较少见的对象,并且要创建一个能够处理这些对象的数据结构需要做更多的工作。

【讨论】:

我真的很喜欢 namedTuples,只是觉得更改它们的值(通过替换函数)相当难看。【参考方案2】:

如果你没有必要,更 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”的用途。你能解释一下吗?我想我可能会切换到非常相似的东西。我唯一要添加的可能是一个可选标志,以便用户可以使用其他条目(即“数据”)创建字典,并且仍然让构造函数初始化默认状态(所有条目不相交)。我必须承认,我对字典有偏见;与结构相比,它们的语法非常笨拙。你最终会克服这个吗?感谢您的帮助。 好的,我改了。您可以在 Python 中轻松地将字典转换为对象。 添加到对象(“结构”)的属性很难枚举。您无法确定它们是否是您设置的属性、来自超类的方法、描述符/属性魔术等。如果我有一个预先知道属性名称的定义明确的结构,我会使用元组或命名元组。否则,如果我有一个类似索引的结构,我会使用一个 dict,它提供简单可靠的枚举。 Francis,我想用一个命名元组,我只是觉得替换值的语法很烦人。【参考方案3】:

一种选择是使用字典来存储您需要的有关数据项的信息,而不是直接存储该项目的属性。例如,与其引用d.size,不如引用size[d](其中sizedict 实例)。这要求您的数据项是可散列的,但它们不需要允许对其分配属性。

以下是使用此样式的当前代码的简单翻译:

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

【讨论】:

【参考方案4】:

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

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

另外,在函数、类和成员函数中添加docstrings是一个好习惯。这样一个函数或方法的文档字符串应该描述它的作用、要传递给它的参数以及如果适用的话返回什么以及可以引发哪些异常。

【讨论】:

这对提问者毫无帮助。 namedtuple 创建的类型实例是不可变的,因此您无法更新它们的值,而 UnionFind 数据结构需要这样做。事实上,解决不可变值的问题正是问题所在!【参考方案5】:

我猜这里的缩进问题只是将代码输入 SO 的简单错误。您能否创建一个简单的内置数据类型的子类?例如,您可以通过将数据类型放在括号中来创建列表数据类型的子类:

class UnionFind(list):
'''extends list object'''

【讨论】:

除非你真的要使用它的大部分功能,否则对另一个数据结构进行子类化并没有什么意义,不是吗?在这种情况下,union-find 只有两个操作,并且与简单的数据结构都不重叠。 union-find 中的列表仅用于存储对象(实际上是指针);该数据结构通过添加指向现有列表的指针并操作这些指针来完成所有工作。 是的。不幸的是,SO 只让我选择在这一点上回答而不是发表评论。因为这本身并不是真正的“答案”,而是评论。我非常感谢您的回复,我是初学者,这绝对是一本有用的读物​​。谢谢!

以上是关于Python 对象的良好风格的主要内容,如果未能解决你的问题,请参考以下文章

一位攻城狮的自我修养,在于良好的编程规范

python风格对象

python 符合Python风格的对象

流畅的python 符合python风格的对象

『流畅的Python』第9章_符合Python风格的对象

Python 面向对象和实例属性