Skip to content

Latest commit

 

History

History
217 lines (157 loc) · 6.78 KB

object_oriented34.md

File metadata and controls

217 lines (157 loc) · 6.78 KB

“遗失”的键

本文为大家介绍Python中字典的__missing__特殊方法。

defaultdict

这一期内容中,我们介绍了collections中的defaultdict扩展字典类型。defaultdict允许我们为字典的值定义一种类型,这样当我们访问一个不存在的键时,defaultdict会创建类型所对应的空对象,从而避免了KeyError错误,某种情况下能够简化程序。例如:

from collections import defaultdict

a = {}
b = defaultdict(list)
print(a['key'])
# KeyError: 'key'

print(b['key'])
# []

严格来说,defaultdict接受的是对象工厂,即可以产生某种对象的可调用对象(类或函数):

def factory():
    return 1
  
class Factory:
    def __repr__(self):
        return "Factory object"

a = defaultdict(factory)
b = defaultdict(Factory)

print(a['key'])
1

print(b['key'])
# Factory object

defaultdict也接受None作为参数,这样产生的对象和普通的字典将无区别:

a = defaultdict()
a['key']
# KeyError: 'key'

__missing__

那么,defaultdict的内部机制是什么呢?是特殊方法__missing__在起作用。在defaultdict中定义了特殊方法__missing__,当访问一个不存在的键时,defaultdict会调用__missing__方法来进行处理,并返回结果或抛出异常。__missing__的调用仅发生在__getitem__方法中,所以,利用get()方法或setdefault()方法访问不存在的键时不会触发__missing__方法:

a = defaultdict(list)
print(a['key'])
[]

print(a.get('key2'))
None

print(a.setdefault('key3'))
None

下面我们利用一个自定义的类型来看一下__missing__的运作方式。由于需要__getitem__方法的触发,我们从UserDict继承子类来使用。在collections中存在三个特别的容器类,分别是UserDictUserListUserString,它们用于自定义字典、列表或字符串类型时进行直接继承,免去实现一些抽象方法。此外,如果仅仅需要扩展字典的功能时,继承UserDict和直接继承dict是类似的,而如果需要改变dict自有方法,则最好继承UserDict

from collections import UserDict

class DefaultDict(UserDict):
    def __missing__(self, key):
        print(f'__missing__ is called with {key}')
        return "returned value"
      
dd = DefaultDict()
print(dd['hi'])
# __missing__ is called with hi
# returned value
dd['hi'] = 1
print(dd['hi'])
1

可以看到,当key不存在时,__missing__会被调用,__missing__的返回值会被作为访问key的结果返回。

利用这一个特性,我们就可以利用__missing__试着实现一个DefaultDict

class DefaultDict(UserDict):
    def __init__(self, default_factory=None):
        if (not callable(default_factory) and default_factory is not None):
            raise TypeError("first argument must be callable or None")
        self.default_factory = default_factory
        super().__init__()

    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        self.data[key] = value = self.default_factory()
        return value

需要说明的内容是:

  1. DefaultDict传入的参数要么是None,要么是可调用对象,否则抛出TypeError类型错误异常;
  2. 调用super().__init__()是一个良好的习惯,不过如果对UserDict比较了解的话,会发现super().__init__()仅仅初始化了一个self.data = {}字典项来作为内部存储项;
  3. __missing__有且仅有一个参数key
  4. 如果对象工厂是空的,则抛出一个KeyError,正如defaultdict的行为;

我们来测试一下DefaultDictdefaultdict的行为:

DD_none = DefaultDict()
dd_none = defaultdict()
print(DD['key'])
# KeyError: 'key'

print(dd['key'])
# KeyError: 'key'

DD_unc = DefaultDict(1) # Uncallable
# TypeError: first argument must be callable or None

dd_unc = defaultdict(1)
# TypeError: first argument must be callable or None

DD = DefaultDict(list)
dd = defaultdict(list)

print(DD['key'])
[]

print(dd['key'])
[]

可以看到DefaultDict基本复刻了defaultdict的行为。

奇怪的类属性

这一期文章中,我们介绍了Python创建类时经历的一个准备命名空间的过程。通过__prepare__类方法,我们可以创建一个特殊的命名空间来存储类属性。之前我们采用的例子是利用OrderedDict来作命名空间,从而可以记录类属性的定义顺序。这里,我们利用defaultdict来作命名空间,会发生一些奇怪的事情:

from collections import defaultdict

class DDNamespaceMeta(type): # __prepare__定义在元类里
    @classmethod # __prepare__必须是类方法
    def __prepare__(cls, name, bases): # 3个参数
        return defaultdict(list)
      
class SomeClass(metaclass=DDNamespaceMeta):
    a
    b
    c

print(SomeClass.a)
# []

这里,类属性的写法非常奇怪,看起来好像是语法错误,但确确实实是可以运行的,且类属性a b c均为空列表。如果这种写法放到普通类里,则会引起NameError,因为a从未定义,却直接进行了使用:

class NormalClass:
    a
    
# NameError: name 'a' is not defined

秘密存在于__prepare__所返回的defaultdict,它为所有直接使用的类属性赋值了空列表。这样存在的一个问题在于,在类中,一些语句失去了作用:

class NormalClass:
    print('Class Definition')
    
# Class Definition

class SomeClass(metaclass=DDNamespaceMeta):
    print('Class Definition')
    
# TypeError: 'list' object is not callable

这是因为在类创建的时候,print被当做了类属性对待,因而它默认被赋予了一个空列表,空列表自然是不能print()来调用的。

如果希望避免部分关键字被误认为是类属性,我们需要自定义一个字典项来忽略这些关键字:

import builtins
class IgKwdDic(dict): # Ignore Keyword dict
# 必须继承自dict
    def __init__(self, factory=None):
        super().__init__()
        self.factory = factory # 省去了可调用判断
        
    def __missing__(self, key):
        try:
            return getattr(builtins, key)
        except AttributeError:
            self[key] = value = self.factory()
            return value

class DDNamespaceMeta(type):
    @classmethod
    def __prepare__(mcls, name, bases):
        return IgKwdDic(list)

class SpecialClass(metaclass=DDNamespaceMeta):
    print('hi')
    x
    y
    eval('z')

print(SpecialClass.z)
# hi
# []