使用pickle和多个模块时无法加载文件

83
我将尝试创建一个用户系统,该系统使用设置和Gui模块。当Gui模块使用pickle请求加载文件时,我一直收到属性错误。以下是来自设置模块的内容:
import pickle
import hashlib

class User(object):
    def __init__(self, fname, lname, dob, gender):
        self.firstname = fname
        self.lastname = lname
        self._dob = dob
        self.gender = gender
        self.type = 'General'
        self._username = ''
        self._hashkey = ''

    def Report(self):
        print("Full Name: {0} {1}\nDate of Birth: {2}\nGender: {3}\nAccess Level: {4}".format(self.firstname,self.lastname, self._dob, self.gender, self.type))
        print(self._username)

    def Genusername(self):
        self._username = str(str(self._dob)[:2] + self.firstname[:2] + self.lastname[:2])
        saveUsers(users)

    def Genhashkey(self, password):
        encoded = password.encode('utf-8','strict')
        return hashlib.sha256(encoded).hexdigest()

    def Verifypassword(self, password):
        if self._hashkey == self.Genhashkey(password):
            return True
        else:
            return False

class SAdmin(User):
    def __init__(self, fname, lname, dob, gender):
        super().__init__(fname, lname, dob, gender)
        self.type = 'Stock Admin'

class Manager(User):
    def __init__(self, fname, lname, dob, gender):
        super().__init__(fname, lname, dob, gender)
        self.type = 'Manager'

def saveUsers(users):
    with open('user_data.pkl', 'wb') as file:
        pickle.dump(users, file, -1) # PICKLE HIGHEST LEVEL PROTOCOL

def loadUsers(users):
    try:        
        with open('user_data.pkl', 'rb') as file:
            temp = pickle.load(file)
            for item in temp:
                users.append(item)
    except IOError:
        saveUsers([])

def userReport(users):
    for user in users:
        print(user.firstname, user.lastname)

def addUser(users):
    fname = input('What is your First Name?\n > ')
    lname = input('What is your Last Name?\n > ')
    dob = int(input('Please enter your date of birth in the following format, example 12211996\n> '))
    gender = input("What is your gender? 'M' or 'F'\n >")
    level = input("Enter the access level given to this user 'G', 'A', 'M'\n > ")
    password = input("Enter a password:\n > ")
    if level == 'G':
        usertype = User
    if level == 'A':
        usertype = SAdmin
    if level == 'M':
        usertype = Manager
    users.append(usertype(fname, lname, dob, gender))
    user = users[len(users)-1]
    user.Genusername()
    user._hashkey = user.Genhashkey(password)
    saveUsers(users)

def deleteUser(users):
    userReport(users)
    delete = input('Please type in the First Name of the user do you wish to delete:\n > ')
    for user in users:
        if user.firstname == delete:
            users.remove(user)
    saveUsers(users)

def changePass(users):
    userReport(users)
    change = input('Please type in the First Name of the user you wish to change the password for :\n > ')
    for user in users:
        if user.firstname == change:
            oldpass = input('Please type in your old password:\n > ')
            newpass = input('Please type in your new password:\n > ')
            if user.Verifypassword(oldpass):
                user._hashkey = user.Genhashkey(newpass)
                saveUsers(users)
            else:
                print('Your old password does not match!')

def verifyUser(username, password):
    for user in users:
        if user._username == username and user.Verifypassword(password):
            return True
        else:
            return False  

if __name__ == '__main__':
    users = []
    loadUsers(users)

这是GUI模块:

from PyQt4 import QtGui, QtCore
import Settings

class loginWindow(QtGui.QDialog):    
    def __init__(self):
        super().__init__()        
        self.initUI()

    def initUI(self):
        self.lbl1 = QtGui.QLabel('Username')
        self.lbl2 = QtGui.QLabel('Password')
        self.username = QtGui.QLineEdit()
        self.password = QtGui.QLineEdit()

        self.okButton = QtGui.QPushButton("OK")
        self.okButton.clicked.connect(self.tryLogin)
        self.cancelButton = QtGui.QPushButton("Cancel")

        grid = QtGui.QGridLayout()
        grid.setSpacing(10)

        grid.addWidget(self.lbl1, 1, 0)
        grid.addWidget(self.username, 1, 1)
        grid.addWidget(self.lbl2, 2, 0)
        grid.addWidget(self.password, 2, 1)
        grid.addWidget(self.okButton, 3, 1)
        grid.addWidget(self.cancelButton, 3, 0)

        self.setLayout(grid)

        self.setGeometry(300, 300, 2950, 150)
        self.setWindowTitle('Login')
        self.show()

    def tryLogin(self):
        print(self.username.text(), self.password.text())
        if Settings.verifyUser(self.username.text(),self.password.text()):
            print('it Woks')
        else:
            QtGui.QMessageBox.warning(
                self, 'Error', 'Incorrect Username or Password')

class Window(QtGui.QMainWindow):
    def __init__(self):
        super().__init__()        


if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    users = []
    Settings.loadUsers(users)
    if loginWindow().exec_() == QtGui.QDialog.Accepted:
        window = Window()
        window.show()
        sys.exit(app.exec_())

每个用户都是一个类,它们被放入一个列表中,然后使用pickle保存该列表。当我仅加载设置文件并验证登录时,一切正常。但是,当我打开GUI模块并尝试进行验证时,它不允许我这样做,我收到的错误信息是:

Traceback (most recent call last):
  File "C:\Users`Program\LoginGUI.py", line 53, in <module>
    Settings.loadUsers(users)
  File "C:\Users\Program\Settings.py", line 51, in loadUsers
    temp = pickle.load(file)
AttributeError: Can't get attribute 'Manager' on <module '__main__' (built-in)>

1
我不知道这是否与您的问题有关,但如果您使用with打开文件,则无需close。上下文管理器将在with块结束时自动关闭它。此外,您的verifyUser方法无法正常工作。它只查看用户列表中的第一个用户。 - Kevin
你能发布user_data.pkl文件的内容吗?假设它现在是测试数据。 - zehnpaard
€•Ñ ]”Œ__main__”ŒManager”“”)}”’”}”(Œlastname”ŒHammer”Œgender”ŒM”Œ_dob”JüxËŒ_hashkey”Œ@99b3bcf690e653a177c602dd9999093b9eb29e50a3af9a059af3fcbfab476a16”Œ _username”Œ30JaHa”Œtype”hŒ firstname”ŒJack”uba. 这是一个pickle文件,它包含了名字、姓氏、出生日期、用户名、哈希密钥(密码)和访问级别等信息。 - Inthu
好的,谢谢。我认为相关代码不是你的登录窗口细节,而是个别用户的类定义。你的设置模块有多大?它是否足够小,让你编辑问题并添加整个模块,而不仅仅是三个函数和__name__=='main'子句? - zehnpaard
好的,添加了完整的模块,就是这样了,它其实并不大。 - Inthu
显示剩余3条评论
5个回答

121
问题在于您正在通过实际运行“Settings”模块来对定义在该模块中的对象进行“pickling”,然后尝试从“GUI”模块中对这些对象进行“unpickling”。
请记住,pickle实际上不会存储有关如何构建类/对象的信息,并且在“unpickling”时需要访问类。有关更多详细信息,请参见使用Pickle的wiki
在pkl数据中,您可以看到被引用的对象是__main__.Manager,因为当您创建pickle文件时,“Settings”模块是主要模块(即您运行“Settings”模块作为主脚本来调用“addUser”函数)。

然后,您尝试在 'Gui' 中对反序列化进行操作 - 因此该模块的名称为 __main__,并且您正在该模块中导入 Setting。因此,Manager 类实际上将是 Settings.Manager。但是,pkl 文件不知道这一点,并且在 __main__ 中查找 Manager 类,因为它不存在而引发 AttributeError(Settings.Manager 存在,但是 __main__.Manager 不存在)。

这是一个最小的代码集来演示。

class_def.py 模块:

import pickle

class Foo(object):
    def __init__(self, name):
        self.name = name

def main():
    foo = Foo('a')
    with open('test_data.pkl', 'wb') as f:
        pickle.dump([foo], f, -1)

if __name__=='__main__':
    main()

你需要运行以上代码来生成pickle数据。 main_module.py模块:
import pickle

import class_def

if __name__=='__main__':
    with open('test_data.pkl', 'rb') as f:
        users = pickle.load(f)

您运行上述代码尝试打开pickle文件,但是出现了与之前看到的大致相同的错误。(稍有不同,但我猜测这是因为我使用的是Python 2.7)
解决方案可以是:
1.通过显式导入,将类放在顶层模块(即GUI或main_module)的命名空间中。 2.在创建pickle文件时,使用与您将在其中打开它的顶层模块相同的模块(即从GUI调用`Settings.addUser`或从main_module调用`class_def.main`)。这意味着pkl文件将保存对象作为`Settings.Manager`或`class_def.Foo`,然后可以在`GUI main_module`名称空间中找到它们。
选项1示例:
import pickle

import class_def
from class_def import Foo # Import Foo into main_module's namespace explicitly

if __name__=='__main__':
    with open('test_data.pkl', 'rb') as f:
        users = pickle.load(f)

选项2示例:
import pickle

import class_def

if __name__=='__main__':
    class_def.main() # Objects are being pickled with main_module as the top-level
    with open('test_data.pkl', 'rb') as f:
        users = pickle.load(f)

1
谢谢!那真的很有帮助,程序现在可以工作了。你说得对,导入用户类似乎比选项2更有效率! - Inthu
很高兴听到这个消息!我同意,在这个阶段,将每个类显式地导入GUI模块会更简单。另一方面,一旦你完成了你的代码,我想你将使用GUI界面添加/删除用户 - 所以你的对象将被pickle为__main__模块的GUI,并且类将是Settings.User、Settings.Manager等。 - zehnpaard
是的,因为这只是登录界面,我想现在它很有效率。顺便问一下,如果我有另一个问题,我应该再问吗? - Inthu
是的,如果这是一个独立的问题,那么最好创建另一个问题,而不是继续在这个问题上添加。 - zehnpaard
你会如何执行同一项目中的这个部分呢?http://stackoverflow.com/questions/27745094/pyqt4-qsql-how-to-get-data-from-a-relational-database-and-display-them - Inthu
这对我来说是非常有用的答案。我在PyCharm调试器中遇到了pickling错误。解决方法是在调用main()之前和之后导入模块(file.py)和类(模块内部)。问题得到了解决。谢谢!即: "if name == 'main': import server from server import track_server_process main()" - tobi delbruck

56

请先阅读由zehnpaard提供的答案,了解属性错误的原因。 除了他已经提供的解决方案之外,在python3中,您可以使用pickle.Unpickler类并覆盖下面提到的find_class方法:

import pickle

class CustomUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        if name == 'Manager':
            from settings import Manager
            return Manager
        return super().find_class(module, name)

pickle_data = CustomUnpickler(open('file_path.pkl', 'rb')).load()

5
太棒了!我发现这个解决方案比zehnpaard的被接受的答案更加健壮,因为它允许在代码中更改类(或函数)的位置,而不是在生成的pickle文件中更改。 - EliadL
1
对于任何想知道的人,这个解决方案可以扩展到多个导入,只需添加额外的if语句即可。 - thedeg123
@thedeg123 是的,你也可以这样做。 - Pankaj Saini
@PankajSaini,你应该像用户thedeg123建议的那样添加多个导入情况。这对其他读者很有帮助。这个答案确实很强大。它有助于修复旧存储库中的错误。我使用了这个答案来运行我们领域的这个流行存储库。用户和答案已经得到了认可 https://github.com/caltech-netlab/acnportal-experiments/pull/11 - lifezbeautiful

6
如果你在加载模块时导入了适当的类,仍然出现此错误(zehnpaard's solution #1),那么可以重写 pickle.Unpicklerfind_class 函数,并明确指示查找当前模块的命名空间。
import pickle
from settings import Manager

class CustomUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        try:
            return super().find_class(__name__, name)
        except AttributeError:
            return super().find_class(module, name)

pickle_data = CustomUnpickler(open('file_path.pkl', 'rb')).load()
## No exception trying to get 'Manager'

注意:此方法会丢失存储在module中的相对导入路径信息。因此,在您的pickled类中要小心命名空间冲突。

4
如果您在模块外定义了一个类,并且该类的对象存在于pickle数据中,那么您需要导入该类。
from outside_module import DefinedClass1, DefinedClass2, DefinedClass3 

with open('pickle_file.pkl', 'rb') as f:
    pickle_data = pickle.load(f)

如果模块存在于不同的文件夹中,您可能需要为模块文件夹创建一个__init__.py文件。请参见(https://dev59.com/GW855IYBdhLWcg3wXTEI#21995949) - Chidi

3
如果你使用dill进行转储/加载,模型将能够正常运行。
import dill
from sklearn.preprocessing import FunctionTransformer

sp_clf = FunctionTransformer(lambda X:X.astype('float').fillna(0).applymap(abs))

with open('temp.joblib','wb') as io:
    dill.dump(sp_clf,io)

with open('temp.joblib','rb') as io:
    dd=dill.load(io)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接