我的大部分编程经验都是使用 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 样式)重载。
所有这些感觉都非常像尝试用 python 编写 c++ 代码,如果我最喜欢的一本书 Code Complete 2 被认真对待,这是一种大罪。我觉得我是在逆流而上,而不是让它带我前进。
我非常想学习编写 python 'pythonic-ally',解决这种问题域的最佳方法是什么?什么是学习正确的 Pythonic 风格的好资源?
最佳答案
标准库、流行的 PyPI 模块和 ActiveState 配方中有大量代码可以执行此类操作,因此您最好阅读示例,而不是试图从基本原理中弄清楚。另外,请注意,这与创建 list
非常相似。 -like 或 dict
-like class,还有更多的例子。
但是,对于您想要做什么,有一些答案。我将从最严重的开始,然后向后工作。
Things get even more complicated when I start to want to construct BoundInt objects from normal python numbers (integers?), and other BoundInt objects … Which, as far as I'm aware requires the use of rather large/ugly if/else type checking statements within the BoundInt() constructor, as python does not support (c style) overloading.
啊,但是想想你在做什么:你正在构建一个
BoundInt
来自任何可以像整数一样的东西,例如,一个实际的 int
或 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);
本副本
bar
至 param
, param
至 local
, local
到一个未命名的“返回值”变量,然后到 baz
.其中一些将被优化掉,而其他(在 C++11 中)将使用移动而不是复制,但是,您仍然有 4 个复制/移动构造函数/赋值运算符的概念调用。现在看看 Python 的等价物:
def foo(param): local = param; return local
bar = BoundInt();
baz = foo(bar)
在这里,我们只有一个
BoundInt
实例——显式创建的那个——而我们所做的只是给它绑定(bind)新的名字。偶分配baz
作为超出 bar
范围的新对象的成员和 baz
不会复制。制作副本的唯一方法是显式调用 BoundInt(baz)
再次。 (这不是 100% 正确,因为有人总是可以检查您的对象并尝试从外部克隆它,而 pickle
、 deepcopy
等实际上可能会这样做……但在这种情况下,它们是仍然没有调用您或编译器编写的“复制构造函数”。)现在,将所有这些运算符转发到值怎么样?
好吧,一种可能性是动态进行。详细信息取决于您使用的是 Python 3 还是 2(对于 2,您需要支持多远)。但是这个想法是你只有一个名称列表,对于每个名称,你定义一个具有该名称的方法,该方法调用值对象上的同名方法。如果您想要一个草图,请提供额外的信息并询问,但您最好寻找动态方法创建的示例。
那么,那是 Pythonic 吗?这要看情况。
如果您正在创建数十个“类似整数”的类,那么是的,它肯定比复制粘贴代码或添加“编译时”生成步骤要好,并且可能比添加其他不需要的基类要好。
如果您尝试跨多个版本的 Python 工作并且不想记住“我应该停止提供哪个版本
__cmp__
来再次像 int
一样?”输入问题,我可能会更进一步并从 int
中获取方法列表本身(取 dir(int())
并将一些名字列入黑名单)。但是如果你只是在做这门课,比如说,只是 Python 2.6-2.7 或只是 3.3+,我认为这是一个折腾。
一个很好的阅读类(class)是
fractions.Fraction
标准库中的类。它是清晰编写的纯 Python 代码。它部分演示了动态和显式机制(因为它根据通用动态转发功能明确定义了每个特殊消息),如果您同时拥有 2.x 和 3.x,则可以比较和对比两者。同时,您的类(class)似乎未指定。如
x
是 BoundInt
和 y
是 int
, 应该 x+y
真回个int
(就像在您的代码中一样)?如果没有,你需要绑定(bind)它吗?怎么样y+x
?应该怎么做x+=y
做?等等。最后,在 Python 中,通常值得让“值类”像这样不可变,即使直观的 C++ 等价物是可变的。例如,考虑这个:
>>> i = BoundInt(3, 0, 10)
>>> j = i
>>> i.set(5)
>>> j
5
我不认为你会期待这一点。这不会发生在 C++ 中(对于典型的值类),因为
j = i
会创建一个新副本,但在 Python 中,它只是将一个新名称绑定(bind)到同一个副本。 (它相当于 BoundInt &j = i
,而不是 BoundInt j = i
。)如果你想要
BoundInt
保持不变,除了消除像 set
这样明显的东西,还要确保不执行 __iadd__
和 friend 。如果您遗漏了 __iadd__
, i += 2
将变成i = i.__add__(2)
: 换句话说,它会创建一个新实例,然后重新绑定(bind) i
到那个新实例,留下旧实例。
关于实现数据类型的 Pythonic 方式(Python 2.7),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13242501/