理解__getitem__方法

我已经浏览了Python文档中__getitem__的大部分文档,但我仍然无法理解它的含义。

所以我所能理解的是__getitem__是用来实现像self[key]这样的调用的。但这有什么用呢?

假设我有一个这样定义的python类:

class Person:
def __init__(self,name,age):
self.name = name
self.age = age


def __getitem__(self,key):
print ("Inside `__getitem__` method!")
return getattr(self,key)


p = Person("Subhayan",32)
print (p["age"])

这将返回预期的结果。但是为什么要首先使用__getitem__呢?我还听说Python在内部调用__getitem__。但是为什么会这样呢?

有人能详细解释一下吗?

283571 次浏览

根据键或索引获取项的[]语法只是语法糖。

当你计算a[i]时,Python调用a.__getitem__(i)(或type(a).__getitem__(a, i),但这个区别是关于继承模型的,在这里不重要)。即使a类可能没有显式地定义这个方法,它通常继承自一个祖先类。

所有(Python 2.7)特殊方法名称及其语义都列在这里:https://docs.python.org/2.7/reference/datamodel.html#special-method-names

Cong Ma很好地解释了__getitem__的用途-但我想给你一个可能有用的例子。 想象一个为建筑物建模的类。在建筑物的数据中,它包括许多属性,包括占用每层楼的公司的描述:

如果不使用__getitem__,我们将得到这样一个类:

class Building(object):
def __init__(self, floors):
self._floors = [None]*floors
def occupy(self, floor_number, data):
self._floors[floor_number] = data
def get_floor_data(self, floor_number):
return self._floors[floor_number]


building1 = Building(4) # Construct a building with 4 floors
building1.occupy(0, 'Reception')
building1.occupy(1, 'ABC Corp')
building1.occupy(2, 'DEF Inc')
print( building1.get_floor_data(2) )

然而,我们可以使用__getitem__(及其对应的__setitem__)来使Building类的用法“更好”。

class Building(object):
def __init__(self, floors):
self._floors = [None]*floors
def __setitem__(self, floor_number, data):
self._floors[floor_number] = data
def __getitem__(self, floor_number):
return self._floors[floor_number]


building1 = Building(4) # Construct a building with 4 floors
building1[0] = 'Reception'
building1[1] = 'ABC Corp'
building1[2] = 'DEF Inc'
print( building1[2] )

是否像这样使用__setitem__实际上取决于你计划如何抽象数据——在这种情况下,我们决定将建筑物视为楼层的容器(你也可以为建筑物实现迭代器,甚至可能具有切片的能力——即一次获得多个楼层的数据——这取决于你需要什么。

神奇的__getitem__方法主要用于访问列表项、字典项、数组元素等。它对于快速查找实例属性非常有用。

在这里,我用一个示例类Person来展示这一点,它可以通过'name'、'age'和'dob'(出生日期)来实例化。__getitem__方法的编写方式是可以访问已索引的实例属性,例如姓名、日期、月份或年份等。

import copy


# Constants that can be used to index date of birth's Date-Month-Year
D = 0; M = 1; Y = -1


class Person(object):
def __init__(self, name, age, dob):
self.name = name
self.age = age
self.dob = dob


def __getitem__(self, indx):
print ("Calling __getitem__")
p = copy.copy(self)


p.name = p.name.split(" ")[indx]
p.dob = p.dob[indx] # or, p.dob = p.dob.__getitem__(indx)
return p

假设一个用户输入如下:

p = Person(name = 'Jonab Gutu', age = 20, dob=(1, 1, 1999))

__getitem__方法的帮助下,用户可以访问索引属性。例如,

print p[0].name # print first (or last) name
print p[Y].dob  # print (Date or Month or ) Year of the 'date of birth'

对于可读性一致性。这个问题是为什么操作符重载存在的一部分,因为__getitem__是实现它的函数之一。

如果你得到一个未知的类,由一个未知的作者编写,并且你想将它的第3个元素添加到它的第5个元素,你可以很好地假设obj[3] + obj[5]将工作。

在不支持操作符重载的语言中,这一行看起来会是什么样子??可能类似obj.get(3).add(obj.get(5))??或者obj.index(3).plus(obj.index(5))??

第二种方法的问题是(1)可读性差得多,(2)你猜不到,你必须查找文档。

使用这种技术的一个常用库是'email'模块。它在email.message.Message类中使用__getitem__方法,该方法由mime相关类继承。

然后,在and中,您所需要的是添加您的头,以获得具有正常默认值的有效mime类型消息。在引擎盖下有很多东西,但使用很简单。

message = MIMEText(message_text)
message['to'] = to
message['from'] = sender
message['subject'] = subject

作为旁注,__getitem__方法还允许将对象转换为可迭代的

例子:如果与iter()一起使用,它可以生成任意多的int平方值:

class MyIterable:
def __getitem__(self, index):
return index ** 2




obj = MyIterable()
obj_iter = iter(obj)


for i in range(1000):
print(next(obj_iter))

Django core对magic方法有一些有趣而漂亮的用法,包括__getitem__。以下是我最近的发现:

  1. < p > Django HTTP请求

    • 当你在Django中提交GET/POST数据时,它会作为request.GET/request.POST dict存储在Django的request对象中。该字典类型为QueryDict,继承自MultiValueDict

    • 当你提交数据,说user_id=42, QueryDict将是存储/表示为:

      <QueryDict: {'user_id': ['42']}>

      因此,传递的数据变成

      'user_id': ['42']

      而不是直觉

      'user_id': '42'

      MultiValueDict所以解释了为什么它需要自动将其转换为列表格式:

      这个类的存在是为了解决cgi引起的恼人问题。Parse_qs,为每个键返回一个列表。

    • 假定QueryDict值被转换为列表,则需要像这样访问它们(与request.GET的想法相同):

      • < p > request.POST['user_id'][0]

      • < p > request.POST['user_id'][-1]

      • < p > request.POST.get('user_id')[0]

      • < p > request.POST.get('user_id)[-1]

        但是,这些都是访问数据的可怕方式。所以。Django重写了MultiValueDict中的__getitem____get__。这是简化版:

        def __getitem__(self, key):
        """
        Accesses the list value automatically
        using the `-1` list index.
        """
        list_ = super().__getitem__(key)
        return list_[-1]
        
        
        def get(self, key, default=None):
        """
        Just calls the `__getitem__` above.
        """
        return self[key]
        

        有了这些,你现在可以有一个更直观的访问器:

        • request.POST['user_id']
        • request.POST.get('user_id')
  2. < p > Django形式

    • 在Django中,你可以这样声明表单(包括ModelForm):

      class ArticleForm(...):
      title = ...
      
    • 这些表单继承自标识,并具有以下重写的魔术方法(简化版):

      def __iter__(self):
      for name in self.fields:
      yield self[name]
      
      
      def __getitem__(self, name):
      return self.fields[name]
      

      导致这些方便的模式:

      # Instead of `for field in form.fields`.
      # This is a common pattern in Django templates.
      for field in form
      ...
      
      
      # Instead of `title = form.fields['title']`
      title = form['title']
      

总之,魔术方法(或它们的重写)增加了代码的可读性和开发人员的体验/便利性。

__getitem__的使用包括实现控制流措施,由于某些奇怪的原因,这些措施不能在执行堆栈的较低位置执行:

class HeavenlyList(list):
"""don't let caller get 666th element"""
    

def __getitem__(self, key):
"""return element"""
if isinstance(key, slice):
return [
super().__getitem__(i)
for i in range(key.start, key.stop, key.step)
if i != 666
]
return super().__getitem__(key) if key != 666 else None

一个类似但更有趣的原因是允许基于__abc0的访问容器/序列类型中通常不允许的元素:

class SliceDict(dict):
"""handles slices"""
    

def __setitem__(self, key, value):
"""map key to value"""
if not isinstance(key, int)
raise TypeError("key must be an integer")
super().__setitem__(key, value)
    

def __getitem__(self, key):
"""return value(s)"""
if not isinstance(key, slice):
return super().__getitem__(key)
return [
super().__getitem__(i)
for i in range(key.start, key.stop, key.step)
]

另一个有趣的用法是重写str.__getitem__以接受str对象以及__abc2和slices,这样str输入是一个正则表达式,返回值是re.finditer返回的匹配对象迭代器:

from re import finditer


class REString(str):
"""handles regular expressions"""
    

re_flags = 0
    

def __getitem__(self, key):
"""return some/all of string or re.finditer"""
if isinstance(key, str):
return finditer(key, self, flags=self.re_flags)
return super().__getitem__(key)

在现实世界中,重写dict.__getitem__特别有用的一个问题是,当程序需要通过互联网分布并可通过HTTP获得的信息时。因为这些信息是远程的,所以流程可以采用某种程度的惰性——只检索它没有的项或已经更改的项的数据。具体的例子是让一个字典实例惰性地检索并存储Python增强建议。有很多这样的文档,有时它们会被修改,它们都驻留在域名为peps.python.org的主机上。因此,这个想法是为传递到__getitem__的PEP编号发出一个HTTP GET请求,如果字典中还没有包含它或PEP HTTP ETAG改变,则获取它。

from http import HTTPStatus, client




class PEPDict(dict):
"""lazy PEP container"""
    

conn = client.HTTPSConnection("peps.python.org")
    

def __getitem__(self, pep):
"""return pep pep"""
        

# if lazy for too long
if self.conn.sock is None:
self.conn.connect()
        

# build etag check in request header
requestheaders = dict()
if pep in self:
requestheaders = {
"if-none-match": super().__getitem__(pep)[0]
}
        

# make request and fetch response
self.conn.request(
"GET",
"/%s/" % str(pep).zfill(4),
headers=requestheaders
)
response = self.conn.getresponse()
        

# (re)set the pep
if response.status = HTTPStatus.OK:
self.__setitem__(
pep, (
response.getheader("etag"),
response.read()
)
)
        

# raise if status is not ok or not modified
if response.status != HTTPStatus.NOT_MODIFIED:
raise Exception("something weird happened")
        

return super().__getitem__(pep)[1]
        

要进一步了解它的用途,一个很好的参考资料是在Python数据模型文档的模拟容器类型部分中回顾其相关的special/dunder方法。