繁体   English   中英

理解 __getitem__ 方法

[英]Understanding __getitem__ method

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

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

假设我有一个 python class 以这种方式定义:

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__ 但它为什么这样做呢?

有人可以更详细地解释一下吗?

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__实际上取决于您计划如何抽象数据 - 在这种情况下,我们决定将建筑物视为楼层的容器(并且您还可以为建筑物实现迭代器,甚至可能能够切片 - 即一次获取多个楼层的数据 - 这取决于您的需要。

通过键或索引获取项目的[]语法只是语法糖。

当您评估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

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

在这里,我用一个示例类 Person 展示了这一点,该类可以通过“姓名”、“年龄”和“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'

Django 内核对魔术方法有几个有趣和漂亮的用法,包括__getitem__ 这些是我最近的发现:

  1. Django HTTP 请求

    • 当您在 Django 中提交 GET/POST 数据时,它将作为request.GET / request.POST dict 存储在 Django 的request object 中。 此 dict 是QueryDict类型,它继承自MultiValueDict

    • 当您提交数据时,比如user_id=42 , QueryDict 将被存储/表示为

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

      因此,传递的数据变为

      'user_id': ['42']

      而不是直观的

      'user_id': '42'

      MultiValueDict文档字符串解释了为什么需要将其自动转换为列表格式:

      这个 class 的存在是为了解决 cgi.parse_qs 提出的恼人问题,它为每个键返回一个列表。

    • 鉴于QueryDict值已转换为列表,因此需要像这样访问它们(与request.GET相同的想法):

      • request.POST['user_id'][0]

      • request.POST['user_id'][-1]

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

      • 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. Django Forms

    • 在 Django 中,您可以像这样声明 forms (包括ModelForm ):

       class ArticleForm(...): title =...
    • 这些 forms 继承自BaseForm ,并具有这些重写的魔术方法(简化版):

       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__可用于实现“懒惰”的dict子类。 目的是避免一次实例化一个字典,该字典要么在现有容器中已经有大量的键值对,要么在键值对的现有容器之间具有昂贵的散列过程,或者如果字典表示单个组分布在互联网上的资源。

举一个简单的例子,假设你有两个列表, keysvalues ,其中{k:v for k,v in zip(keys, values)}是你需要的字典,为了速度或效率的目的,它必须是惰性的:

class LazyDict(dict):
    
    def __init__(self, keys, values):
        self.keys = keys
        self.values = values
        super().__init__()
        
    def __getitem__(self, key):
        if key not in self:
            try:
                i = self.keys.index(key)
                self.__setitem__(self.keys.pop(i), self.values.pop(i))
            except ValueError, IndexError:
                raise KeyError("No such key-value pair!!")
        return super().__getitem__(key)

用法:

>>> a = [1,2,3,4]
>>> b = [1,2,2,3]
>>> c = LazyDict(a,b)
>>> c[1]
1
>>> c[4]
3
>>> c[2]
2
>>> c[3]
2
>>> d = LazyDict(a,b)
>>> d.items()
dict_items([])

为了可读性一致性 这个问题是运算符重载存在的部分原因,因为__getitem__是实现它的函数之一。

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

在不支持运算符重载的语言中,该行会是什么样子? 可能类似于obj.get(3).add(obj.get(5)) ?? 或者obj.index(3).plus(obj.index(5)) ??

第二种方法的问题在于(1)它的可读性要低得多,(2)你无法猜测,你必须查找文档。

使用这种技术的常见库是“电子邮件”模块。 它使用email.message.Message类中的__getitem__方法,该类又由 MIME 相关类继承。

然后,在获得具有合理默认值的有效 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))

该方法是您如何从容器或序列 object 中检索一个或多个项目,如list

Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> [1,2,3].__getitem__(-1)
3
>>> [1,2,3].__getitem__(slice(0,3,1))
[1, 2, 3]

__getitem__的一些实现旨在处理不同类型的输入,例如当绑定到list时,如上处理intslice 同样, []运算符可用作__getitem__的专用别名,

>>> [1,2,3][-1]
3
>>> [1,2,3][slice(0,3,1)]
[1, 2, 3]

对于 Python 中的许多容器/序列类型,它更熟悉地隐式处理slice对象的int:int:int语法糖:

>>> [1,2,3][0:3:1]
[1, 2, 3]
>>> ""[0:3:1]
''

鉴于这种架构, __getitem__的使用包括实现控制流措施,由于某些奇怪的原因,这些措施不能在执行堆栈中执行得更低:

class HeavenlyList(list):
    """don't let caller get 666th element"""
    
    def __getitem__(self, key):
        """return element"""
        try:
            super().__getitem(666)
        except IndexError:
            return super().__getitem__(key)
        else:
            if isinstance(key, slice):
                if key.start > 666 or key.stop < 666:
                    return super().__getitem__(key)
                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

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

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)
        ]

覆盖dict.__getitem__被证明特别有用的实际问题是,当程序需要通过 Internet 分发且可通过 HTTP 获得的信息时。 因为这些信息是远程的,所以该过程可以采用某种程度的惰性——只检索它没有或已更改的项目的数据。 具体示例是让字典实例延迟检索和存储Python Enhancement Proposals 这些文档有很多,有时会进行修订,它们都驻留在域名peps.python.org的主机上。 因此,想法是对传递给__getitem__的 PEP 编号发出 HTTP GET 请求,如果字典尚未包含它或 PEP HTTP ETAG changed则获取它。

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 的数据 model 文档的模拟容器类型部分中查看其相关的特殊/dunder 方法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM