Python 3 迁移常见陷阱与代码转换指南
1. 方法重定义错误
在某些情况下,如果使用特定的元类,当方法被重新定义时会引发错误。示例代码如下:
if classdict.multiple:
raise TypeError("Multiple definitions exist")
return type.__new__(cls,name,bases,classdict)
若将此元类应用于其他类定义,任何方法的重新定义都会报错。例如:
class Foo(metaclass=MultiMeta):
def __init__(self):
pass
def __init__(self,x): # Error. __init__ multiply defined.
pass
2. Python 2 迁移至 Python 3 的常见陷阱
2.1 文本与字节的区别
Python 3 严格区分文本字符串(字符)和二进制数据(字节)。例如,
"hello"
是存储为 Unicode 的文本字符串,而
b"hello"
是字节字符串(此例中包含 ASCII 字母)。
在 Python 3 中,
str
和
bytes
类型绝不能混合使用。例如,尝试将字符串和字节连接会引发
TypeError
异常,这与 Python 2 不同,Python 2 会根据需要自动将字节字符串转换为 Unicode。
将文本字符串
s
转换为字节,需使用
s.encode(encoding)
,如
s.encode('utf-8')
可将
s
转换为 UTF - 8 编码的字节字符串;将字节字符串
t
转换回文本,需使用
t.decode(encoding)
。
字节字符串在行为上与文本有很大不同,例如:
x = b'Hello World'
print(x) # Produces b'Hello World'
print(b"You said '%s'" % x) # TypeError: % operator not supported
下面是一个更复杂的示例,展示了 Python 3 对文本/字节分离的严格执行:
# Create a response message using strings (Unicode)
status = 200
msg = "OK"
proto = "HTTP/1.0"
response = "%s %d %s" % (proto, status, msg)
print(response)
# Create a response message using only bytes (ASCII)
status = 200
msg = b"OK"
proto = b"HTTP/1.0"
response = b"%s %d %s" % (proto, status, msg) # TypeError
response = proto + b" " + str(status) + b" " + msg # TypeError
bytes(status) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00....'
bytes(str(status)) # TypeError
bytes(str(status),'ascii') # b'200'
response = proto + b" " + bytes(str(status),'ascii') + b" " + msg
print(response)
print(response.decode('ascii'))
在处理不同的库模块时,文本/字节的区别更为微妙。例如,
os.listdir(dirname)
函数,若
dirname
是字符串,仅返回能成功解码为 Unicode 的文件名;若
dirname
是字节字符串,则返回的所有文件名都是字节字符串。
2.2 新的 I/O 系统
Python 3 实现了全新的 I/O 系统,该系统也体现了文本和二进制数据的严格区分。
若进行文本 I/O,Python 3 要求以“文本模式”打开文件,并可提供可选的编码(默认通常为 UTF - 8);若进行二进制数据 I/O,则必须以“二进制模式”打开文件并使用字节字符串。常见的错误是将输出数据传递给以错误模式打开的文件或 I/O 流。例如:
f = open("foo.txt","wb")
f.write("Hello World\n") # TypeError: can't write str to binary stream
套接字、管道等 I/O 通道通常应假定为二进制模式。网络代码中,由于许多网络协议涉及基于文本的请求/响应处理,而套接字是二进制的,这种二进制 I/O 和文本处理的混合可能会导致问题。
2.3
print()
和
exec()
函数
Python 2 中的
print
和
exec
语句在 Python 3 中变为函数。
print()
函数的使用示例如下:
| Python 2 | Python 3 |
| ---- | ---- |
|
print x, y, z
|
print(x,y,z)
|
|
print x, y, z,
|
print(x,y,z,end=' ')
|
|
print >>f, a
|
print(a,file=f)
|
exec()
在 Python 3 中的行为与 Python 2 略有不同。例如:
def foo():
exec("a = 42")
print(a)
在 Python 2 中,调用
foo()
会打印
42
;在 Python 3 中,会引发
NameError
异常。可采用以下解决方法:
def foo():
_locals = locals()
exec("a = 42",globals(),_locals)
a = _locals['a']
print(a)
2.4 迭代器和视图的使用
Python 3 比 Python 2 更广泛地使用迭代器和生成器。内置函数如
zip()
、
map()
和
range()
现在返回可迭代对象,若需要列表,可使用
list()
函数。
Python 3 从字典中提取键和值信息的方式略有不同。在 Python 2 中,可使用
d.keys()
、
d.values()
或
d.items()
方法获取键、值或键值对列表;在 Python 3 中,这些方法返回视图对象。例如:
s = { 'GOOG': 490.10, 'AAPL': 123.45, 'IBM': 91.10 }
k = s.keys()
v = s.values()
for x in k:
print(x)
视图对象与创建它们的字典相关联,若字典发生变化,视图对象的内容也会改变。若需要列表,可使用
list()
函数,如
list(s.keys())
。
2.5 整数和整数除法
Python 3 不再区分 32 位整数的
int
类型和长整数的
long
类型,
int
类型现在表示任意精度的整数。此外,整数除法现在总是产生浮点数结果,如
3/5
为
0.6
,
8/2
为
4.0
。
2.6 比较操作
Python 3 对值的比较更为严格。在 Python 2 中,任何两个对象都可比较,即使没有意义;在 Python 3 中,不兼容类型的比较会引发
TypeError
。例如:
# Python 2
3 < "Hello" # True
# Python 3
3 < "Hello" # TypeError
2.7 迭代器和生成器
Python 3 对迭代器协议做了细微更改,
next()
方法重命名为
__next__()
。若手动迭代可迭代对象或定义自定义迭代器对象,需确保更改类中
next()
方法的名称,可使用内置的
next()
函数以兼容方式调用迭代器的
__next__()
方法。
2.8 文件名、参数和环境变量
在 Python 3 中,文件名、
sys.argv
中的命令行参数和
os.environ
中的环境变量是否被视为 Unicode 取决于本地设置。由于操作系统环境中 Unicode 的使用并非普遍,可能会出现问题。提供文件和目录名作为字节字符串可解决许多问题,如
os.listdir(b'/foo')
。
2.9 库的重新组织
Python 3 对标准库的多个部分进行了重新组织和重命名,特别是与网络和 Internet 数据格式相关的模块。许多旧模块已被弃用,如
gopherlib
、
rfc822
等。现在模块通常使用小写名称,如
ConfigParser
重命名为
configparser
,
Queue
重命名为
queue
,
SocketServer
重命名为
socketserver
。
同时,创建了一些包来重新组织以前分散在不同模块中的代码,如
http
包包含用于编写 HTTP 服务器的所有模块,
html
包包含用于解析 HTML 的模块,
xmlrpc
包包含用于 XML - RPC 的模块等。
2.10 绝对导入
与库的重新组织相关,包的子模块中的所有导入语句都使用绝对名称。例如,若有如下包结构:
foo/
__init__.py
spam.py
bar.py
在
spam.py
中使用
import bar
会引发
ImportError
异常,需使用
import foo.bar
或相对导入
from . import bar
。这与 Python 2 不同,Python 2 会先检查当前目录是否匹配,再检查
sys.path
中的其他目录。
3. 代码迁移与 2to3 工具
3.1 迁移步骤
-
移植到 Python 2.6
:建议先将代码移植到 Python 2.6,它不仅与 Python 2.5 向后兼容,还支持 Python 3 的部分新特性,如高级字符串格式化、新的异常语法、字节字面量、I/O 库和抽象基类。运行 Python 2.6 时使用
-3命令行选项,可针对弃用特性发出警告信息,应确保程序在 Python 2.6 上无警告运行后再迁移到 Python 3。 -
提供测试覆盖
:使用 Python 的测试模块(如
doctest和unittest)确保应用程序有全面的测试覆盖,所有测试应在 Python 2.6 上无警告通过。
3.2 2to3 工具的使用
Python 3 包含
2to3
工具,可协助从 Python 2.6 迁移到 Python 3。该工具通常位于 Python 源代码分发的
Tools/scripts
目录,也会安装在与
python3.0
二进制文件相同的目录。它是一个命令行工具,可在 UNIX 或 Windows 命令 shell 中运行。
例如,有如下包含多个弃用特性的程序:
# example.py
import ConfigParser
for i in xrange(10):
print i, 2*i
def spam(d):
if not d.has_key("spam"):
d["spam"] = load_spam()
return d["spam"]
运行
2to3 example.py
会输出程序中可能需要更改的部分,显示为上下文差异。默认情况下,
2to3
不会实际修复扫描的源代码,只是报告可能需要更改的部分。
2to3
面临的挑战是它通常只有不完整的信息。例如,对于
spam()
函数中的
d.has_key()
方法,虽然字典的
has_key()
已被
in
运算符取代,但
2to3
无法确定
d
是否为字典。此外,
2to3
在处理字节字符串和 Unicode 时也存在问题,因为 Python 2 会自动将字节字符串提升为 Unicode,代码中可能会随意混合这两种字符串类型,而
2to3
无法完全处理。
可通过以下操作使用 2to3 工具:
1. 获取可用的“修复器”列表:
2to3 -l
。
2. 查看选定修复的实际更改:
2to3 -f fixname filename
。
3. 应用多个修复:为每个修复指定
-f
选项。
4. 实际应用修复到源文件:
2to3 -f fixname -w filename
。
3.3 流程图
graph TD;
A[开始迁移] --> B[移植到Python 2.6];
B --> C[检查警告信息];
C --> D{是否无警告};
D -- 是 --> E[提供测试覆盖];
D -- 否 --> B;
E --> F[使用2to3工具];
F --> G[查看修复建议];
G --> H{是否需要修复};
H -- 是 --> I[选择修复器并应用];
H -- 否 --> J[完成迁移];
I --> J;
综上所述,从 Python 2 迁移到 Python 3 需要注意诸多细节,严格遵循文本/字节分离原则,适应新的 I/O 系统、函数使用方式等变化。同时,借助 2to3 工具和全面的测试覆盖,可更顺利地完成代码迁移。
4. 常见陷阱总结
4.1 陷阱类型与影响
| 陷阱类型 | 影响 | 示例代码 |
|---|---|---|
| 方法重定义错误 | 若使用特定元类,方法重定义会报错 |
class Foo(metaclass=MultiMeta):
def __init__(self):
pass
def __init__(self,x): # Error. __init__ multiply defined.
pass
``` |
| 文本与字节混合 | 会引发 `TypeError` 异常,处理复杂 |
```python
status = 200
msg = b"OK"
proto = b"HTTP/1.0"
response = b"%s %d %s" % (proto, status, msg) # TypeError
``` |
| 错误的 I/O 模式 | 会导致写入文件时出错 |
```python
f = open("foo.txt","wb")
f.write("Hello World\n") # TypeError: can't write str to binary stream
``` |
| `exec()` 行为变化 | 可能导致变量未定义的错误 |
```python
def foo():
exec("a = 42")
print(a)
``` |
| 迭代器和视图变化 | 内置函数返回可迭代对象,字典方法返回视图对象 |
```python
s = { 'GOOG': 490.10, 'AAPL': 123.45, 'IBM': 91.10 }
k = s.keys()
``` |
| 整数除法结果改变 | 整数除法总是产生浮点数结果 |
```python
print(3/5) # 0.6
``` |
| 比较操作严格化 | 不兼容类型比较会引发 `TypeError` |
```python
3 < "Hello" # TypeError
``` |
| 迭代器协议更改 | 手动迭代或自定义迭代器需更改方法名 |
```python
# 旧的迭代器使用方式在Python 3中需调整
``` |
| 文件名等编码问题 | 可能因 Unicode 支持不一致导致问题 |
```python
os.listdir(b'/foo') # 解决部分问题
``` |
| 库重命名和组织变化 | 需更新导入语句和使用新模块 |
```python
# 旧的import ConfigParser需改为import configparser
``` |
| 绝对导入要求 | 子模块导入需使用绝对名称或相对导入 |
```python
# 在特定包结构中,import bar会出错
``` |
### 4.2 陷阱应对策略
- **方法重定义**:避免在使用特定元类的类中重定义方法。
- **文本与字节处理**:严格区分文本和字节,使用 `encode()` 和 `decode()` 方法进行转换。
- **I/O 操作**:根据数据类型正确选择文件打开模式。
- **`exec()` 函数**:使用 `locals()` 和 `globals()` 字典来处理变量赋值。
- **迭代器和视图**:使用 `list()` 函数将可迭代对象转换为列表,了解视图对象的特性。
- **整数除法**:明确需要整数结果时使用 `//` 运算符。
- **比较操作**:确保比较的对象类型兼容。
- **迭代器协议**:更新自定义迭代器的 `next()` 方法为 `__next__()`。
- **文件名等编码**:必要时使用字节字符串作为文件名和路径。
- **库重命名**:更新代码中的导入语句,使用新的模块名称。
- **绝对导入**:使用绝对名称或相对导入来加载子模块。
## 5. 代码迁移实践案例
### 5.1 示例代码迁移
假设有以下 Python 2 代码:
```python
import ConfigParser
for i in xrange(10):
print i, 2*i
def spam(d):
if not d.has_key("spam"):
d["spam"] = load_spam()
return d["spam"]
使用 2to3 工具进行迁移:
1. 运行
2to3 -l
获取可用的修复器列表。
2. 运行
2to3 -f import -f xrange -f has_key example.py
查看选定修复器的更改建议。
3. 运行
2to3 -f import -f xrange -f has_key -w example.py
应用修复到源文件。
迁移后的代码如下:
import configparser
for i in range(10):
print(i, 2*i)
def spam(d):
if "spam" not in d:
d["spam"] = load_spam()
return d["spam"]
5.2 迁移过程中的问题与解决
-
2to3信息不完整 :如spam()函数中d.has_key()方法,2to3无法确定d是否为字典。解决方法是结合代码上下文进行手动检查和调整。 -
字节字符串和 Unicode 处理
:
2to3无法完全处理代码中混合的字节字符串和 Unicode。解决方法是在迁移前对代码进行梳理,明确文本和字节的使用,迁移后进行全面测试。
6. 总结与建议
6.1 总结
从 Python 2 迁移到 Python 3 是一个需要谨慎对待的过程,涉及到语言特性、库使用、代码结构等多个方面的变化。常见的陷阱包括方法重定义、文本与字节混合、新的 I/O 系统等,这些陷阱可能导致代码出错或行为不符合预期。通过遵循正确的迁移步骤,如先移植到 Python 2.6、提供测试覆盖、使用 2to3 工具等,可以有效减少迁移过程中的问题。
6.2 建议
- 提前规划 :在开始迁移前,对代码进行全面评估,了解可能存在的问题和需要修改的部分。
- 逐步迁移 :按照推荐的步骤,先移植到 Python 2.6,再进行测试和使用 2to3 工具,逐步完成迁移。
- 测试驱动 :确保代码有全面的测试覆盖,在迁移过程中不断运行测试,及时发现和解决问题。
- 学习新特性 :深入学习 Python 3 的新特性,如文本/字节分离、新的 I/O 系统等,以便更好地编写和维护代码。
6.3 未来展望
随着 Python 2 的逐渐淘汰,越来越多的开发者将迁移到 Python 3。未来,Python 3 可能会继续发展和完善,提供更多强大的功能和更好的性能。开发者应积极适应这些变化,不断提升自己的编程技能,以跟上技术的发展步伐。
graph LR;
A[提前规划] --> B[逐步迁移];
B --> C[测试驱动];
C --> D[学习新特性];
D --> E[适应未来发展];
通过以上的总结和建议,希望能帮助开发者更顺利地从 Python 2 迁移到 Python 3,充分利用 Python 3 的新特性和优势。
超级会员免费看
56

被折叠的 条评论
为什么被折叠?



