前言

  最近打算逐渐熟悉一下 Python3 的语法效果,经过网上查找,如果在 Python2 中还原 Python3 的效果可以通过 __future__ 引入一些 Python3 的功能。
  于是我就引入了 from __future__ import unicode_literals,division,print_function 这一句。


  unicode_literals 可以让 Python2 的字符串全部编程 unicode 模式,也就是所有字符串默认前面带 u 了,中文就不会变成乱码。

  division 引入之后 Python2 的整数除法也可以输出浮点数了。
  比如说 1/2 默认输出是 0,因为 被除数不是浮点数 因此输出变整数了,以前我是通过 float(1)/2 来解决这个问题,现在就可以通过 __future__ 来解决

  print_function 引入之后就可以支持 Python3 的 print ,所以 print 的写法也要和 Python3 一样加括号。
  这样 print 命令就可以直接嵌入到 lambda 函数里面,不引入的话 lambda 函数打印只能使用 sys.stdout.write


  交代了一下背景,终于要扯出今天的主角 unicode_literals 的坑了。

setProperty 要设置 byte string

  最近在开发 QMVVM 模块状态管理模块,使用 Qt 的 DynamicProperty 发现无法正确设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# coding:utf-8
from __future__ import unicode_literals,division,print_function

import os
import sys

from Qt import QtWidgets
from Qt import QtCore
from Qt import QtGui

class WidgetTest(QtWidgets.QWidget):

def __init__(self):
super(WidgetTest, self).__init__()

# NOTE 设置 Property 为空
self.setProperty("test","hello,world")
print(self.dynamicPropertyNames())

# NOTE 解决方案是去掉 unicode_literals 或者 添加 byte string
self.setProperty(b"test2","hello,world")
print(self.dynamicPropertyNames())
# NOTE 打印 [PySide2.QtCore.QByteArray('test2')]


if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widget = WidgetTest()
widget.show()
sys.exit(app.exec_())

注: Qt 无法导入可以去 Github 下载兼容脚本

  上面的代码就很清晰了,这背后可能也和 Qt 框架有关, Qt 可能不支持 Python unicode string
  即便这里不引入 unicode_literals ,只是 setProperty(u'test','hello') 这样设置也照样不行了。

signal 调用造成递归永动机

  我觉得正常使用几乎不会遇到这个问题,碰巧我在开发 QMVVM 模块,所以用了一些比较偏门的方法来构造变量,结果引入 unicode_literals 出了大问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# coding:utf-8
from __future__ import unicode_literals,division,print_function

import os
import sys

from Qt import QtWidgets
from Qt import QtCore
from Qt import QtGui

class WidgetTest(QtWidgets.QWidget):

# ComputedSignal = QtCore.Signal()
# StateSignal = QtCore.Signal()

_var_dict = {}
_var_dict["ComputedSignal"] = QtCore.Signal()
_var_dict["StateSignal"] = QtCore.Signal()
locals().update(_var_dict)

def __init__(self):
super(WidgetTest, self).__init__()

# NOTE 信号槽套娃处理
getattr(self,"StateSignal").connect(getattr(self,"ComputedSignal").emit)

# NOTE 如果加上这行就会打印一堆 computed signal
# self.ComputedSignal.connect(lambda:print("computed signal"))

# ! 报错 RuntimeError: maximum recursion depth exceeded while calling a Python object
getattr(self,"StateSignal").emit()

if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widget = WidgetTest()
widget.show()
sys.exit(app.exec_())

  这里构建信号槽变量用了比较 tricky 的方法,因为 QMVVM 需要根据传入的字符串动态生成变量。
  Python 的变量背后其实就是一个 locals 的字典来产生对应关系,可以通过 update 方法更新字典从而更新到本地变量里面。
  当然除了这个方法之外 类里面 也可以使用 setattr 方法添加类属性,只不过用字典也方便我后面获取动态生成的变量。

  后面 __init__ 函数里面 QMVVM 通过 getattr 根据字符串动态获取类属性,因此这里测试我也不直接 self.StateSignal 来获取了。
  然后这里进行了套娃处理,两个信号槽,一个触发同时会触发另一个。
  然后触发 信号槽 就会导致调用永动机从而报错了。

  上面的代码就是我花了大半天调试精简出来的,在 QMVVM 内部的运行还是挺绕的,更何况一开始完全没有预料到问题出在 unicode_literals 里面 (:з」∠)
  解决方案也不复杂,可以去掉 unicode_literals 或者构建信号槽的变量的时候给字符串加上 b’’ 转换为 byte string


总结

  上面的代码测试我汇总到了 github 的仓库上 代码
  后面我又测试不同版本的Python,发现 Python 2.7.16 版本已经解决了这个问题,但是 Python 2.7.14 还是出错, Maya 的 Python 2.7.11 也照样有问题。
  Python 3.7.6 完全 OK ,所以可以相当肯定这个就是 Python2 的 BUG。
  经过了这个坑之后,我还是不推荐使用 unicode_literals 了,毕竟中文乱码至少还可以运行,开了 unicode_literals 之后出问题的代价太大了。

更新 2020-5-7

  在 Stack Overflow 上发现了类似的问题 链接
  或许这真的是 Qt 的 Bug 而已,如果用起来方便的话还是可以考虑一下的。