8

我的大部分编程经验都是使用 C++。受 Bjarne Stroustrup 的演讲启发我最喜欢的编程技术之一是“类型丰富”的编程;开发新的健壮数据类型,不仅可以通过将功能包装到类型中来减少我必须编写的代码量(例如向量加法,而不是 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

现在代码的大小正在迅速爆炸式增长,并且对正在编写的内容有大量重复,而回报却很少,这似乎一点也不像 Python。

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

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

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

所有这一切都感觉非常像尝试在 python 中编写 c++ 代码,如果认真对待我最喜欢的书之一, Code Complete 2 ,这是一个大罪。我觉得我在逆流而上,而​​不是让它带我前进。

我非常想学习编写 python 'pythonic-ally' 的代码,解决这类问题域的最佳方法是什么?什么是学习正确 Python 风格的好资源?

4

4 回答 4

4

标准库、流行的 PyPI 模块和 ActiveState 配方中有很多代码可以做这种事情,所以你最好阅读示例而不是试图从第一原理中弄清楚。list另外,请注意,这与创建-like 或-like 类非常相似dict,还有更多示例。

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

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

啊,但是想想你在做什么:你是BoundInt从任何可以像整数一样的东西构造 a ,包括,比如说,一个实际的int或 a BoundInt,对吧?那么,为什么不:

def __init__(self, target, low, high):
    self.target, self.low, self.high = int(target), int(low), int(high)

我假设您已经添加了一个__int__方法BoundInt,当然(相当于 C++ explicit operator int() const)。

另外,请记住,缺少重载并不像您从 C++ 中想象的那么严重,因为没有用于制作副本的“复制构造函数”;您只需将对象传递出去,所有这些都会在幕后得到处理。

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

BoundInt foo(BoundInt param) { BoundInt local = param; return local; }
BoundInt bar;
BoundInt baz = foo(bar);

这将复制barparamparamlocallocal到一个未命名的“返回值”变量,然后复制到baz。其中一些将被优化,而其他(在 C++11 中)将使用移动而不是复制,但仍然有 4 个复制/移动构造函数/赋值运算符的概念调用。

现在看一下 Python 等价物:

def foo(param): local = param; return local
bar = BoundInt();
baz = foo(bar)

在这里,我们只有一个BoundInt实例——显式创建的实例——我们所做的只是将新名称绑定到它。即使分配baz为超出范围barbaz不会复制的新对象的成员。制作副本的唯一方法是BoundInt(baz)再次显式调用。(这不是 100% 正确的,因为有人总是可以检查您的对象并尝试从外部克隆它,并且pickle,deepcopy等实际上可能会这样做......但在这种情况下,他们仍然没有调用“副本”构造函数”,您或编译器编写的。)

现在,如何将所有这些运算符转发到该值?

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

那么,这是Pythonic吗?这要看情况。

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

而且,如果您尝试跨多个版本的 Python 工作并且不想记住“我应该停止提供哪个版本以再次__cmp__像以前一样工作int?” 输入问题,我可能会走得更远,并从int自身中获取方法列表(dir(int())取出一些名称并将其列入黑名单)。

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

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

同时,您的班级似乎未指定。If xis a BoundIntand yis an intx+y真的应该返回一个int(就像在你的代码中那样)?如果没有,你需要绑定它吗?怎么样y+x?应该x+=y怎么做?等等。

最后,在 Python 中,通常值得将这样的“值类”设为不可变,即使直观的 C​​++ 等效项是可变的。例如,考虑一下:

>>> i = BoundInt(3, 0, 10)
>>> j = i
>>> i.set(5)
>>> j
5

我不认为你会期待这个。这在 C++ 中不会发生(对于典型的值类),因为j = i会创建一个新副本,但在 Python 中,它只是将一个新名称绑定到同一个副本。(它相当于BoundInt &j = i,而不是BoundInt j = i。)

如果你想BoundInt不可变,除了消除明显的东西之外set,还要确保不要实现__iadd__和朋友。如果你遗漏__iadd__,i += 2将变成i = i.__add__(2): 换句话说,它将创建一个新实例,然后重新绑定i到该新实例,而留下旧实例。

于 2012-11-06T01:05:47.787 回答
2

对此可能有很多意见。但是对于特殊方法的泛滥,您只需要这样做即可使其完整。但至少你只在一个地方做一次。内置的数字类型也可以子类化。这就是我为类似的实现所做的,您可以查看它

于 2012-11-06T01:10:07.680 回答
1

你的set方法令人憎恶。您不要创建一个默认值为 0 的数字,然后将该数字更改为其他数字。这是非常尝试在 Python 中编写 C++,如果你真的想以与处理数字相同的方式处理这些,将会给你带来无穷无尽的头痛,因为每次将它们传递给函数时,它们都是通过引用传递的(就像Python)。因此,您最终会在您认为可以将其视为数字的事物中遇到大量的别名,并且您几乎肯定会遇到错误,因为您没有意识到是别名的数字的值发生了变异,或者期望能够检索通过为另一个提供相同的值,将值存储在以 aBoundInt作为键的字典中。BoundInt

对我来说,high它们low不是与特定值关联的数据值BoundInt,它们是类型参数。我想要7typeBoundInt(1, 10)中的数字7,而不是限制在 1 到 10 之间的数字,所有这些都是 type 中的值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类型的行为与完全一样int,除了当您创建一个值时,该值被类型限制。不幸的是,这意味着对这些类型的所有操作都返回int对象,而不是BoundInt对象。

如果您可以想出一种方法来协调 eg 中涉及的两个不同值的低/高值x + y,那么您可以覆盖特殊方法以使它们返回BoundInt值。想到的方法是:

  1. x + y取左操作数的边界并忽略右操作数(看起来混乱且不对称;违反=的假设y + x
  2. 取最大值low和最小值high。它非常对称,您可以将没有的数值lowhigh值视为sys.minintsys.maxint(即只使用另一个值的边界)。如果范围根本不重叠,则没有多大意义,因为您最终会得到一个空范围,但是对这些数字一起操作可能无论如何都没有多大意义。
  3. 取最小值low和最大值high。也是对称的,但在这里你可能想明确地忽略正常数字,而不是假装它们是BoundInt可以在整个整数范围内变化的值。

以上任何一个都可以工作,并且上述任何一个都可能在某些时候让你感到惊讶(例如,否定一个被限制在正范围内的数字总是会给你这个范围内最小的正数,这对我来说似乎很奇怪)。

如果您采用这种方法,您可能不想继承int. 因为如果您有normalInt + boundedInt, thennormalInt将在不尊重您的代码的情况下处理添加。相反,您希望它不被识别boundedInt为一个int值,因此这int__add__不会”起作用,并且会给您的班级一个尝试的机会__radd__。但是我仍然会将您的类视为“不可变的”,并使每个产生新数字的操作都构造一个新对象;实际上可以保证在某个地方改变数字会导致错误。

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

class BoundIntBase(object):
    # Don't use this class directly; use a subclass that specifies low and high as
    # class attributes.
    def __init__(self, value):
        self.value = min(self.high, max(self.low, int(value)))

    def __int__(self):
        return self.value

# add binary operations to BoundInt
for method in ['__add__', '__radd__', ...]:
    def tmp(self, other):
        try:
            low = min(self.low, other.low)
            high = max(self.high, other.high)
        except AttributError:
            cls = type(self)
        else:
            cls = BountInd(low, high)
        v = getattr(int(self), method)(int(other))
        return cls(v)
    tmp.__name__ = method
    setattr(BountIntBase, method, tmp)


_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)]

似乎仍然比应有的代码多,但是您尝试做的实际上比您想象的要复杂。

于 2012-11-06T02:54:07.590 回答
0

由于 Python 中丰富的语法支持,在所有情况下行为与数字完全一样的类型需要许多特殊方法(似乎没有其他类型需要这么多方法,例如,在 Python 中定义行为类似于列表、dict 的类型要简单得多:几个方法,你有一个 Sequence)。有几种方法可以减少代码的重复性。

ABC 类,例如 numbers.Integral 为某些方法提供默认实现,例如,如果__add__, __radd__在子类中实现,则__sub__,__rsub__ 自动可用。

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

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

Python 允许在工厂函数/元类中动态生成/修改一个类,例如, 任何人都可以帮助压缩这个 Python 代码吗?. Even exec可以在(非常)罕见的情况下使用,例如 namedtuple().

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

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

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

于 2012-11-06T10:54:36.900 回答