Pythonic实现数据类型的方法(Python 2.7)

时间:2012-11-06 00:11:47

标签: python oop types

我的大部分编程经验都是使用C ++。灵感来自Bjarne Stroustrup的演讲here,我最喜欢的编程技巧之一是“类型丰富”的编程;开发新的健壮数据类型,不仅可以通过将功能包装到类型中来减少我必须编写的代码量(例如向量添加,而不是newVec.x = vec1.x + vec2.x; newVec.y = ...等,我们可以使用newVec = vec1 + vec2),但也会在编译时通过强类型系统揭示代码中的问题。

我在Python 2.7 中进行的最近项目需要具有上限和下限的整数值。我的第一直觉是创建一个新的数据类型(类),它在python中具有与普通数字相同的行为,但总是在其(动态)边界值内。

class BoundInt:
    def __init__(self, target = 0, low = 0, high = 1):
        self.lowerLimit = low
        self.upperLimit = high
        self._value = target
        self._balance()

    def _balance(self):
        if (self._value > self.upperLimit):
            self._value = self.upperLimit
        elif (self._value < self.lowerLimit):
            self._value = self.lowerLimit
        self._value = int(round(self._value))

    def value(self):
        self._balance()
        return self._value

    def set(self, target):
        self._value = target
        self._balance()

    def __str__(self):
        return str(self._value)

这是一个好的开始,但它需要访问这些BoundInt类型的内容,如此

x = BoundInt()
y = 4
x.set(y)           #it would be nicer to do something like x = y
print y            #prints "4"
print x            #prints "1"
z = 2 + x.value()  #again, it would be nicer to do z = 2 + x
print z            #prints "3" 

我们可以在类中添加大量python的“魔术方法”定义,以添加更多功能:

def __add__(self, other):
    return self._value + other

def __sub__(self, other):
    return self._value - other

def __mul__(self, other):
    return self._value * other

def __div__(self, other):
    return self._value / other

def __pow__(self, power):
    return self._value**power

def __radd__(self, other):
    return self._value + other

#etc etc

现在代码的大小正在快速爆炸,并且对于正在编写的内容有大量的重复,对于非常小的回报,这看起来根本不是pythonic。

当我开始想要从普通的python数字(整数?)和其他BoundInt对象构建BoundInt对象时,事情变得更加复杂

x = BoundInt()
y = BoundInt(x)
z = BoundInt(4)

据我所知,在BoundInt()构造函数中需要使用相当大/丑陋的if / else类型检查语句,因为python不支持(c style)重载。

所有这一切都非常像试图在python中编写c ++代码,如果我最喜欢的一本书 Code Complete 2 被认真对待,那么这是一个重大的罪恶。我觉得我正在游动动态的打字电流,而不是让它带我前进。

我非常想学习编码python'pythonic-ally',这种问题域的最佳方法是什么?学习正确的pythonic风格有哪些好资源?

4 个答案:

答案 0 :(得分:4)

标准库中有很多代码,在流行的PyPI模块中,以及执行此类操作的ActiveState配方中,因此您可能最好不要阅读示例而不是尝试从第一原则中找出它。另请注意,这与创建类似list或类似dict的类非常类似,其中包含更多示例。

但是,您想要做的事情有一些答案。我会从最严肃的开始,然后向后工作。

  

当我开始想要从普通的python数字(整数?)和其他BoundInt对象构造BoundInt对象时,事情变得更加复杂   ...   据我所知,在BoundInt()构造函数中需要使用相当大/丑陋的if / else类型检查语句,因为python不支持(c样式)重载。

啊,但想想你正在做什么:你正在构建一个BoundInt来自任何可以像整数一样的东西,包括实际的int或{{1} }, 对?那么,为什么不呢:

BoundInt

我假设您已经向def __init__(self, target, low, high): self.target, self.low, self.high = int(target), int(low), int(high) 添加了__int__方法,当然(相当于C ++ BoundInt)。

另外,请记住,缺少重载并不像你想到的那样严重,因为没有“复制构造函数”来制作副本;你只需要将物体传递给周围的物体,所有这些都可以解决。

例如,想象一下这个C ++代码:

explicit operator int() const

这会将BoundInt foo(BoundInt param) { BoundInt local = param; return local; } BoundInt bar; BoundInt baz = foo(bar); 复制到bar,将param复制到param,将local复制到未命名的“返回值”变量,并将其复制到{{1} }。其中一些将被优化,而其他(在C ++ 11中)将使用move而不是copy,但是,你仍然有4个复制/移动构造函数/赋值运算符的概念调用。

现在看看Python等价物:

local

在这里,我们只有一个baz实例 - 显式创建的实例 - 我们所做的就是将新名称绑定到它。即使将def foo(param): local = param; return local bar = BoundInt(); baz = foo(bar) 指定为超出BoundIntbaz范围的新对象的成员,也不会复制。制作副本的唯一方法是再次显式调用bar。 (这不是100%真实,因为有人可以随时检查您的对象并尝试从外部克隆它,而bazBoundInt(baz)等实际上可能会这样做......但在这种情况下,他们仍然没有调用你或编译器所写的“复制构造函数”。)

现在,如何将所有这些运营商转发给该值?

嗯,一种可能性是动态地做。详细信息取决于您是使用Python 3还是2(以及2,需要支持的距离)。但是这个想法是你只有一个名称列表,并且对于每个名称,你定义一个具有该名称的方法,该方法在值对象上调用相同名称的方法。如果你想要一个草图,提供额外的信息并询问,但你最好还是寻找动态方法创建的例子。

那么,那是Pythonic吗?嗯,这取决于。

如果你正在创建几十个“类整数”类,那么是的,它肯定比复制粘贴代码或添加“编译时”生成步骤更好,并且它可能比添加其他不必要的基类更好类。

如果您尝试使用多个版本的Python并且不想记住“我应该停止提供哪个版本pickle再次像deepcopy那样行事?”键入问题,我可能会更进一步,从__cmp__本身获取方法列表(取int并将一些名称列入黑名单。)

但是如果你只是在做一个课程,比如说,只有Python 2.6-2.7或者只有3.3+,我认为这是一个折腾。

要阅读的好课程是标准库中的int类。它是清晰编写的纯Python代码。它部分地演示了动态和显式机制(因为它根据通用动态转发函数明确定义了每个特殊消息),如果你同时拥有2.x和3.x,你可以比较和对比两者。

与此同时,您的班级似乎未被指定。如果dir(int())fractions.FractionxBoundInt,那么y是否真的会返回int(就像在代码中一样)?如果没有,你需要绑定吗?那么x+y呢? int应该做什么?等等。

最后,在Python中,即使直观的C ++等价物是可变的,通常也值得制作像这样不可变的“值类”。例如,考虑一下:

y+x

我认为你没想到这一点。这不会发生在C ++中(对于典型的值类),因为x+=y会创建一个新副本,但在Python中,它只是将一个新名称绑定到同一个副本。 (它相当于>>> i = BoundInt(3, 0, 10) >>> j = i >>> i.set(5) >>> j 5 ,而不是j = i。)

如果您希望BoundInt &j = i不可变,除了消除BoundInt j = i之类的明显内容之外,还要确保不要实现BoundInt和朋友。如果您遗漏set__iadd__将变为__iadd__:换句话说,它会创建一个新实例,然后将i += 2重新绑定到该新实例,老一个人。

答案 1 :(得分:2)

对此可能有很多意见。但是对于特殊方法的扩散,你只需要这样做就可以完成它。但至少你只在一个地方做过一次。内置数字类型也可以是子类。这就是我为类似的实现所做的,即can look it

答案 2 :(得分:1)

你的set方法令人厌恶。您执行创建默认值为零的数字,然后将该数字更改为其他数字。这非常想用Python编写C ++,如果你真的想以与数字相同的方式处理这些问题,会引起无穷无尽的麻烦,因为每次将它们传递给函数时,它们都会通过引用传递< / em>(就像Python中的所有内容)。所以你最终会在你认为可以像数字一样对待的东西中出现大量别名,并且你几乎肯定会遇到错误,因为它们会改变你没有意识到的数字值,或者希望能够检索通过提供具有相同值的另一个BoundInt,将BoundInt作为键存储在字典中的值。

对我而言,highlow不是与特定BoundInt值相关联的数据值,而是类型参数。我想在7类型中使用数字BoundInt(1, 10),而不是数字7,它被限制在1到10之间,所有这些都是BoundInt类型中的值

如果我真的想做这样的事情,我将采用的方法是将int作为一个类工厂来处理BoundInt。你给它一个范围,它给你一个限制在该范围内的整数类型。您可以将该类型应用于任何“类似int”的对象,它将为您提供一个限制在该范围内的值。类似的东西:

_bound_int_cache = {}
def BoundInt(low, low):
    try:
        return _bound_int_cache[(low, high)]
    except KeyError:
        class Tmp(int):
            low = low
            high = high
            def __new__(cls, value):
                value = max(value, cls.low)
                value = min(value, cls.max)
                return int.__new__(cls, value)

        Tmp.__name__ = 'BoundInt({}, {})'.format(low, high)
        _bound_int_cache[(low, high)] = Tmp
        return _bound_int_cache[(low, high)]

(缓存只是为了确保两个不同的尝试获得相同的低/高值的BoundInt类型给出完全相同的类,而不是两个行为相同的不同类。可能不会'在大多数情况下在实践中都很重要,但似乎更好。)

您可以使用它:

B = BoundInt(1, 10)
x = B(7)

“类工厂”方法意味着如果您想要绑定整数的有意义的范围很少,则可以全局创建这些范围的类(具有有意义的名称),然后使用它们常规课程。

子类化int使这些对象不可变(这就是为什么初始化必须在__new__中完成),这使你摆脱了别名错误(人们不希望在什么时候担心他们使用简单的值类型编程,如数字,并且有充分的理由)。它还为您提供所有免费的整数方法,因此这些BoundInt类型的行为与<{1}}完全相同,除非您创建一个值被类型限制。不幸的是,这意味着这些类型的所有操作都返回int个对象,而不是int个对象。

如果你能想出一种方法来协调例如两个不同值的低值/高值。 BoundInt,然后您可以覆盖特殊方法,使它们返回x + y值。想到的方法是:

  1. 取左操作数的边界并忽略右操作数(看起来凌乱且不对称;违反了BoundInt = x + y)的假设
  2. 取最大y + x值和最小low值。它非常对称,您可以将没有highlow值的数值视为highsys.minint(即只使用来自其他价值)。如果范围根本没有重叠,那么没有多大意义,因为你最终会得到一个空的范围,但是一起操作这些数字可能并不是很有意义。
  3. 取最小sys.maxint值和最大low值。也是对称的,但在这里你可能想要明确地忽略正常数字而不是假装它们是high值,它们可以在整个整数范围内。
  4. 上述任何一种都可以起作用,上述任何一种都可能会让你感到惊讶(例如,否定一个被限制在正范围内的数字总会给你一个范围内最小的正数,这似乎很奇怪我)。

    如果你采用这种方法,你可能想要继承BoundInt。因为如果您有int,那么normalInt + boundedInt会在不尊重您的代码的情况下处理添加内容。您反而希望它不会将normalInt识别为boundedInt值,以便int的{​​{1}}不会工作,并且会让您的班级有机会尝试{{1} }}。但我会将您的类视为“不可变”,并使每个带有新数字的操作构造一个新对象;实际上,变异数字确实可以在某个时候引起错误。

    所以我会处理这样的方法:

    int

    看起来似乎还有更多的代码,但是你想要做的事情实际上比你想象的要复杂得多。

答案 3 :(得分:0)

在所有情况下都需要与数字完全相同的类型 由于Python中丰富的语法支持,许多特殊方法(似乎没有 其他类型需要如此多的方法,例如,定义起来要简单得多 行为类似于列表的类型,Python中的dict:a couple of methods and you have a Sequence)。有几个 使代码重复性降低的方法。

ABC classes such as numbers.Integral 为某些方法提供默认实现,例如__add____radd__在子类中实现__sub____rsub__ 可自动使用。

fractions.Fraction 使用 _operator_fallbacks 定义__r*__并提供后备运算符 处理其他数字类型:

__op__, __rop__ = _operator_fallbacks(monomorphic_operator, operator.op)

Python允许在工厂中动态生成/修改类 函数/元类例如, Can anyone help condense this Python code?。甚至 exec可用于(非常)罕见的情况,例如, namedtuple()

数字在Python中是不可变的,因此您应该使用__new__而不是__init__

__new__未涵盖的罕见案例可以在中定义 from_sometype(cls, d: sometype) -> your_type类方法。并在 相反,特殊方法未涵盖的案例可以使用 as_sometype(self) -> sometype方法。

在您的情况下,更简单的解决方案可能是定义更高级别的类型 特定于您的应用程序域。数字抽象也可能 低级别,例如, decimal.Decimal是 超过6个KLOC。