首页 > 学技术 > 技术网文 > Python > 正文

[保留] 请问类的实例可以作为字典的key吗?


来源 chinaunix.net 酷勤网整理

写了一个类,实例化之后作为一个dic的key,但是出现 TypeError: unhashable instance。
我记得看过Learning Python中说过,甚至类的实例也可以作为字典的键,请问有什么特殊要求吗?



 wolfg 回复于:2005-09-23 10:49:02

The object must be immutable


 Rcfeng 回复于:2005-09-23 10:49:13

"And class instance objects (discussed in Part VI) can be used as keys too, as long as they have the proper protocol methods;roughly, they need to tell Python that their values won't change, or else they would be useless as fixed keys."
上面是Learning Python中说的,请问怎么告诉python我这个类的实例是不变的呢?


 limodou 回复于:2005-09-23 10:56:14

我试了一下没有问题。你的代码是什么,可能是别的错误。

>;>;>; class A:
...     pass
    
>;>;>; a=A()
>;>;>; b={a:1}
>;>;>; b[a]
1


 Rcfeng 回复于:2005-09-23 11:05:15

你试着定义一个__init__,然后定义两个成员变量再试试呢?
class ip:
    "A Ip class for finding user's area"
    def __init__(self, low_ip, up_ip):
            self.low_ip = low_ip
            self.up_ip = up_ip
#   def printip(self):
#       print "Low: %d; Up: %d" % (self.low_ip,self.up_ip)
    def __cmp__(self,ip):
        if( ip>;=low_ip and ip<=up_ip ):
            return 0
        elif( ip<low_ip ):
            return -1
        else: return 1

ip_map[ip(ip1,ip2)] = text
这样是不行的~


 wolfg 回复于:2005-09-23 11:21:41

引用:原帖由 "Rcfeng"]p2)] 这样是不行的~
 发表:



不使用内置的__cmp__方法就行了

class ip:

   "A Ip class for finding user's area"
   def __init__(self, low_ip, up_ip):
       self.low_ip = low_ip
       self.up_ip = up_ip
   def __str__(self):
       return "Low: %d; Up: %d" % (self.low_ip,self.up_ip)
   def cmpare(self,ip):
       if( ip>;=low_ip and ip<=up_ip ):
           return 0
       elif( ip<low_ip ):
           return -1
       else: return 1 


可能是有了__cmp__就认为是可变的了
Python文档里找到这么一句
引用:Instances of a class normally compare as non-equal unless the class defines the __cmp__() method.

__cmp__( self, other) 

Called by comparison operations if rich comparison (see above) is not defined. Should return a negative integer if self < other, zero if self == other, a positive integer if self >; other. If no __cmp__(), __eq__() or __ne__() operation is defined, class instances are compared by object identity (``address''). See also the description of __hash__() for some important notes on creating objects which support custom comparison operations and are usable as dictionary keys. (Note: the restriction that exceptions are not propagated by __cmp__() has been removed since Python 1.5.) 





看了__hash__的介绍,终于明白了
引用:__hash__( self) 

Called for the key object for dictionary operations, and by the built-in function hash(). Should return a 32-bit integer usable as a hash value for dictionary operations. The only required property is that objects which compare equal have the same hash value; it is advised to somehow mix together (e.g., using exclusive or) the hash values for the components of the object that also play a part in comparison of objects. If a class does not define a __cmp__() method it should not define a __hash__() operation either; if it defines __cmp__() or __eq__() but not __hash__(), its instances will not be usable as dictionary keys. If a class defines mutable objects and implements a __cmp__() or __eq__() method, it should not implement __hash__(), since the dictionary implementation requires that a key's hash value is immutable (if the object's hash value changes, it will be in the wrong hash bucket). 



If a class does not define a __cmp__() method it should not define a __hash__() operation either; if it defines __cmp__() or __eq__() but not __hash__(), its instances will not be usable as dictionary keys. 


 Rcfeng 回复于:2005-09-23 11:34:02

我的初衷是想拦截ip_map[ip(ip1,ip2)] = text 这个dic的get方法,让他从完全匹配到,只要在low_ip和up_ip这个范围即匹配,不知道这个实现思想是不是正确呢?请版主指点。
  def __cmp__(self,ip): 
       if( ip>;=self.low_ip and ip<=self.up_ip ): 
           return 0 
       elif( ip<self.low_ip ): 
           return -1 
       else: return 1 

ip_map.get(ip3)
如果ip3在ip1和ip2的范围之内就相当于匹配,去除后面text中的描述


 wolfg 回复于:2005-09-23 11:34:26

根据文档里的讲解,用__cmp__也可以,再实现__hash__就行了

我随便试了一下

def __hash__(self):

        return hash(self.low_ip + self.up_ip)


是可以的,只是不知道这样hash会不会有问题


 wolfg 回复于:2005-09-23 11:38:22

你可以这样做

不过要想用dict的话,就得自己实现__hash__方法,保证low_ip和up_ip值都一样的ip的instance其hash值一样,这样就可以做dict的key了

不知道说得对不对,呵呵


 Rcfeng 回复于:2005-09-23 11:44:05

真是多谢啦,我试试先~


 Rcfeng 回复于:2005-09-23 13:03:09

我仔细看了一下hash()的说明,必须__hash__的返回值和待比较的值相等时才能算是找到对应的key,唉,好像不是使用__cmp__来啊,这个和C++不同啊——只需要定义map,然后find,并重载operator<就可以了~
怎么办?


 wolfg 回复于:2005-09-23 13:17:14

哦,原来理解错误了

你可以继承dictionary,实现一个自己的


 wolfg 回复于:2005-09-23 13:27:26

引用:原帖由 "Rcfeng"]p2)] 如果ip3在ip1和ip2的范围之内就相当于匹配,去除后面text中的描述
 发表:



怎么叫ip3在ip1和ip2的范围之内?能否举一个例子


 Rcfeng 回复于:2005-09-23 13:31:28

是这样的,不知道你熟悉木子QQ的IP库不,他是按照
61.152.100.1 61.152.100.255 上海市
这样的格式来的,并且是按照ip值大小顺序从低到高。
我的想法是,使用一个class,将前面的两个ip范围作为成员变量,实例化一个类,然后把类作为key,把后面的地区作为valus,然后重载这个class的get()的匹配key的操作符,让他根据我输入的ip去查找他的所在的地区。
上面不知道我说清楚没有,呵呵。
下面是我的代码。
说一下我输入的infile的格式:
extrainf       ip
第一列是一些附加信息,第二列是要查的ip

def formatip(ip):
        if is_ip(ip):
                ipstr = map(int,(ip.split('.')))
                return (((ipstr[0]*256+ipstr[1])*256+ipstr[2])*256+ipstr[3])        
        else:
                return ip

def is_ip(ip_str) :
        "Check a string is a ip address or not"
        parts = ip_str.split('.')
        if len(parts) != 4 :
                return False
        for part in parts :
                if not (part.isdigit() and int(part) <= 255 and int(part) >;= 0):
                        return False
                return True

class ip:
        "A Ip class for finding user's area"
        def __init__(self, low_ip, up_ip):
                        self.low_ip = low_ip
                        self.up_ip = up_ip
#       def printip(self):
#               print "Low: %d; Up: %d" % (self.low_ip,self.up_ip)
#       def __cmp__(self,ip):
#               if( hash(ip)>;=hash(self.low_ip) and hash(ip)<=hash(self.up_ip) ):
#                       return 0
#               elif( hash(ip)<hash(self.low_ip) ):
#                       return -1
#               else: return 1
#       def __eq__(self,ip):
#               if( hash(ip)>;=hash(self.low_ip) and hash(ip)<=hash(self.up_ip) ): 
#                       return True
#               else: return False
#       def __ne__(self,ip):
#               if( hash(ip)<=hash(self.low_ip) or hash(ip)>;=hash(self.up_ip) ): 
#                       return True
#               else: return False
        def __hash__(self): 
                print hash(self.low_ip)
                return hash(self.low_ip)

if( __name__ == "__main__"):
        import sys
        if(len(sys.argv) != 3):
                print "Not current arguments: %d" % len(sys.argv)
                sys.exit()

        try:
                datafile=open(sys.argv[1],"r")
                infile=open(sys.argv[2],"r")
        except IOError:
                print "Open file error,exiting..."
                sys.exit()

        ip_map={}
        for line in datafile.readlines():
                onearea = map(formatip,line.split())
                ip_map[ip(onearea[0],onearea[1])] = onearea[2]

        for userip in infile.readlines():
                oneinf = userip.split()
                loc=ip_map.get(formatip(oneinf[1]))
                if( loc ):
                        print "Time:%s, Ip:%s, Area:%s" % (oneinf[0], oneinf[1], loc)
                else: print "Time:%s, Ip:%s, Area:%s" % (oneinf[0], oneinf[1], "unknow")



 Rcfeng 回复于:2005-09-23 13:33:15

另外,说一下,这个思路应该是没有问题的,因为我用C++用同样的思路,使用map.find(),并重载"operator<"是可以满足需求的。


 Rcfeng 回复于:2005-09-23 13:48:52

不过还是比较奇怪,要是我把__cmp__注掉,那么即使dic的hash和输入值的hash相等也不会正确判断,真不知道他是怎么进行key的匹配的了,唉。看来要去看源码了,呵呵。


 limodou 回复于:2005-09-23 13:56:01

如果你只是想实现get的特殊处理,那么可以自已从dict派生出一个类,然后覆盖get方法进行特殊处理即可。上面的方法似乎麻烦了一些。


 Rcfeng 回复于:2005-09-23 13:58:25

其实我只是想重载get判断key是否相等的那个运算符,如果为了这个而去实现一个get查找算法,觉得有些得不偿失。其实为什么要用一个类实现呢?就是不想自己重新写一个容器的查找算法。呵呵


 limodou 回复于:2005-09-23 14:56:38

那你完全可以让每个ip类提供一个判断的方法,如is_in(ip),然后在容器类中进行调用就行了。也不麻烦。


 Rcfeng 回复于:2005-09-24 18:55:44

我尝试将ip_map容器用list来代替,使用index方法来查找ip,重载__cmp__,结果是可行的,查出来了,结果也是正确的。但是……速度慢得一塌糊涂,还不如自己写的遍历算法(几乎是自写算法的两倍)。
看来必须使用dic才行啊,它应该有一套内部优化的排序和查找算法,因为用之前dic做容器,虽然只能查到ip完全与范围匹配的情况,但是查找的速度很快。
唉,看来必须深入了解dic.get()方法的实现才行啦,遍历的方法太慢!下面是用list作容器的代码。


#!/usr/bin/python

def formatip(ip):
        if is_ip(ip):
                ipstr = map(int,(ip.split('.')))
                return (((ipstr[0]*256+ipstr[1])*256+ipstr[2])*256+ipstr[3])
        else:
                return ip

def is_ip(ip_str) :
        "Check a string is a ip address or not"
        parts = ip_str.split('.')
        if len(parts) != 4 :
                return False
        for part in parts :
                if not (part.isdigit() and int(part) <= 255 and int(part) >;= 0):
                        return False
                return True

class ip:
        "A Ip class for finding user's area"
        def __init__(self, low_ip, up_ip, loc):
                        self.low_ip = low_ip
                        self.up_ip = up_ip
                        self.loc = loc
#       def printip(self):
#               print "Low: %d; Up: %d" % (self.low_ip,self.up_ip)
        def __cmp__(self,ip):
                if( ip>;=self.low_ip and ip<=self.up_ip ):
                        return 0
                elif( ip<self.low_ip ):
                        return -1
                else: return 1

if( __name__ == "__main__"):
        import sys
        if(len(sys.argv) != 3):
                print "Not current arguments: %d" % len(sys.argv)
                sys.exit()

        try:
                datafile=open(sys.argv[1],"r")
                infile=open(sys.argv[2],"r")
        except IOError:
                print "Open file error,exiting..."
                sys.exit()

        ip_map=[]
        for line in datafile.readlines():
                onearea = map(formatip,line.split())
                ip_map.append(ip(onearea[0],onearea[1],onearea[2]))

        for userip in infile.readlines():
                oneinf = userip.split()
                try:
                 loc=ip_map.index(formatip(oneinf[1]))
                except ValueError:
                 loc = False
                if( loc ):
                        print "Time:%s, Ip:%s, Area:%s" % (oneinf[0], oneinf[1], ip_map[loc].loc)
                else: print "Time:%s, Ip:%s, Area:%s" % (oneinf[0], oneinf[1], "unknow")



 limodou 回复于:2005-09-24 21:28:46

dict是一种hash算法,要比遍历快。而且算法简单清晰就行,不一定非要与对象,重载挂勾。有时采用对象,重载之类的反倒程序写起来麻烦,不如简单的实现。好比你不希望在容器类中进行判断,其实这种处理反倒是最直接,最直观的做法,自已与别人也更容易理解,速度也快。


 Rcfeng 回复于:2005-09-24 22:36:40

嗯,是的,这种方法确实是比较没那么直观。但是我也尝试过自己使用二分法进行查找,但是速度始终不能太如意。
用C++进行同样的遍历可以实现很理想的速度,并且之前我也试过用C++的类和重载operator<实现(看了标准库中map部分的代码发现的办法)。
其实纯论使用的话,已经写了好几个可用的版本,用Cpp遍历一个,Cpp重载运算符一个,perl二分一个,php遍历一个,python遍历一个,python重载操作符一个,呵呵,是想深入学习和研究一下吧。在大量查询的情况下——4w个ip左右,基本上只有cpp的两个版本可以很快的完成(当然,如果能继续研究一下python的dic查找key方式,应该速度还是能令人满意的)。
刚开始学python,对这门语言感觉挺不错的,希望以后能与大家多多交流啦。


 limodou 回复于:2005-09-24 23:37:35

如果是纯为了速度快,自然应该考虑算法的设计。如果只是简单地将ip段保存在一个list中,自然不会对运算速度的提高有任何的帮助。我想hash也并不能对无序的东西进行快速的索引。因此首先需要对存储的东西进行预处理,然后再进行检索才可以加快速度。如果只是顺序处理的话,那可以考虑使用swig或pyrex将检索处理转为扩展模块才行。


 Rcfeng 回复于:2005-09-25 01:43:42

用来查询的ip库是经过排序的,所以formatip()后的序列是有序的,dic在加入key:value时是会按照hash进行一定的排序,所以强制对dic进行某种规则的sort必定会降低它的性能。正因为如此,对dic的get操作肯定是快于list(无序的数组)的index的。
所以我如果能够借用dic的查找算法,只是重载判断相等的操作符,那样的效果就是最好的了。
似乎对脚本语言处理速度的挑战不是很明智吧,但是研究一些深入的东西对学习还是有好处的。


 西门吹风_CU 回复于:2007-02-16 13:56:13

学习了楼主这个帖子挺有收获的,对类内部的一些专用方法有了了解。虽然已经是个挺老的帖子了,但是俺还是尝试在楼主前面代码的基础上改动一下,增加了一个函数,修改了ip class,基本上实现了楼主的初衷。希望不会让各位大大见笑了:)

其实实现的方法也挺简单的,就是按照楼主的思路让同一子网内的ip都返回同一hash值。为了达到这个目的扩充了一下ip class,让它即可以接受作为boundary以low_ip, up_ip来初始化的实例,同时也可以接受以单个欲查找ip来初始化的实例。另外修改了__hash__函数,使得同一子网内的ip返回相同的hash值,其实就是把ip的第四段赋为0然后求formatip(ip)。


#!/usr/bin/python

def formatip(ip):
        if is_ip(ip):
                ipstr = map(int,(ip.split('.')))
                return (((ipstr[0]*256+ipstr[1])*256+ipstr[2])*256+ipstr[3])        
        else:
                return ip


def deformatip(ip):
        ipstr = []
        for i in range(4):
ipstr.append("%i"%(ip % 256))
ip /=  256
for i in range(2):
a = ipstr
ipstr = ipstr[-i-1]
ipstr[-i-1] = a
        return ipstr


def is_ip(ip_str) :
        "Check a string is a ip address or not"
        parts = ip_str.split('.')
        if len(parts) != 4 :
                return False
        for part in parts :
                if not (part.isdigit() and int(part) <= 255 and int(part) >= 0):
                        return False
                return True


class ip:
        "A Ip class for finding user's area"
        def __init__(self, *args):
if len(args) == 2:
self.low_ip = args[0]
                self.up_ip = args[1]
self.ip = None
self.type = "bound"
if len(args) == 1:
self.ip = args[0]
self.low_ip = None
                self.up_ip = None
self.type = "target"
        def printip(self):
if self.type == "bound":
print "Low: %d; Up: %d" % (self.low_ip,self.up_ip)
else:
print "Target: %d" % self.ip
        def __cmp__(self,ip):
                if( hash(ip) == hash(self) ):
                 return 0
                elif( hash(ip) < hash(self) ):
                        return -1
                else: return 1
        def __eq__(self,ip):
                if( hash(ip) == hash(self) ):
                        return True
                else: return False
        def __ne__(self,ip):
                if( hash(ip) < hash(self) or hash(ip) > hash(self) ):
                        return True
                else: return False
        def __hash__(self):
if self.type == "bound":
deip = deformatip(self.low_ip)
else:
deip = deformatip(self.ip)
deip[-1] = "0"
return(hash(formatip(".".join(deip))))


if( __name__ == "__main__"):
        import sys
        if(len(sys.argv) != 3):
                print "Not current arguments: %d" % len(sys.argv)
                sys.exit()

        try:
                datafile=open(sys.argv[1],"r")
                infile=open(sys.argv[2],"r")
        except IOError:
                print "Open file error,exiting..."
                sys.exit()

        ip_map={}
        for line in datafile.readlines():
                onearea = map(formatip,line.split())
                ip_map[ip(onearea[0],onearea[1])] = onearea[2]

        for userip in infile.readlines():
                oneinf = map(formatip, userip.split())
                loc=ip_map.get(ip(oneinf[1]))
                if( loc ):
                        print "Time:%s, Ip:%s, Area:%s" % (oneinf[0], oneinf[1], loc)
                else: print "Time:%s, Ip:%s, Area:%s" % (oneinf[0], oneinf[1], "unknow")



另外有一点在我实际执行时发现,只修改__hash__使得同一子网内的ip实例返回同样的hash值,并不能实现楼主的初衷,即用类的实例作为字典的key。用ip_map[ip(oneinf[1])]来取value,python会出现异常,用ip_map.get(ip(oneinf[1]))来取,python返回None。只有同时修改了__cmp__, __eq__, __ne__才可以实现以实例作为key的要求。看起来好像以实例作为key是和hash值以及__cmp__同时相关的。


 ghostwwl 回复于:2007-02-25 05:51:58

哇 刚才乱说了 没看清代码就乱说 
昏 不过我正准备说 把ip都弄成10进制 比较方便些

[ 本帖最后由 ghostwwl 于 2007-2-25 06:51 编辑 ]




原文链接:http://bbs.chinaunix.net/viewthread.php?tid=616577
转载请注明作者名及原文出处



收藏本页到: