2012-08-09 20 views
15

Tôi có một lớp chứa tùy chỉnh trong Python 2.7 và mọi thứ hoạt động như mong đợi ngoại trừ nếu tôi cố gắng mở rộng một thể hiện là **kwargs cho một chức năng:Làm cho vùng chứa tùy chỉnh hoạt động với ** kwargs (cách Python mở rộng các arg?)

cm = ChainableMap({'a': 1}) 
cm['b'] = 2 
assert cm == {'a': 1, 'b': 2} # Is fine 
def check_kwargs(**kwargs): 
    assert kwargs == {'a': 1, 'b': 2} 
check_kwargs(**cm) # Raises AssertionError 

tôi đã ghi đè __getitem__, __iter__, iterkeys, keys, items, và iteritems, (và __eq____repr__) chưa ai trong số họ dường như được tham gia vào việc mở rộng như **kwargs, những gì tôi làm sai rồi?

Edit - Các công tác nguồn cập nhật mà bây giờ được thừa hưởng từ MutableMapping và cho biết thêm các phương pháp còn thiếu:

from itertools import chain 
from collections import MutableMapping 

class ChainableMap(MutableMapping): 
    """ 
    A mapping object with a delegation chain similar to JS object prototypes:: 

     >>> parent = {'a': 1} 
     >>> child = ChainableMap(parent) 
     >>> child.parent is parent 
     True 

    Failed lookups delegate up the chain to self.parent:: 

     >>> 'a' in child 
     True 
     >>> child['a'] 
     1 

    But modifications will only affect the child:: 

     >>> child['b'] = 2 
     >>> child.keys() 
     ['a', 'b'] 
     >>> parent.keys() 
     ['a'] 
     >>> child['a'] = 10 
     >>> parent['a'] 
     1 

    Changes in the parent are also reflected in the child:: 

     >>> parent['c'] = 3 
     >>> sorted(child.keys()) 
     ['a', 'b', 'c'] 
     >>> expect = {'a': 10, 'b': 2, 'c': 3} 
     >>> assert child == expect, "%s != %s" % (child, expect) 

    Unless the child is already masking out a certain key:: 

     >>> del parent['a'] 
     >>> parent.keys() 
     ['c'] 
     >>> assert child == expect, "%s != %s" % (child, expect) 

    However, this doesn't work:: 

     >>> def print_sorted(**kwargs): 
     ...  for k in sorted(kwargs.keys()): 
     ...   print "%r=%r" % (k, kwargs[k]) 
     >>> child['c'] == 3 
     True 
     >>> print_sorted(**child) 
     'a'=10 
     'b'=2 
     'c'=3 

    """ 
    __slots__ = ('_', 'parent') 

    def __init__(self, parent, **data): 
     self.parent = parent 
     self._ = data 

    def __getitem__(self, key): 
     try: 
      return self._[key] 
     except KeyError: 
      return self.parent[key] 

    def __iter__(self): 
     return self.iterkeys() 

    def __setitem__(self, key, val): 
     self._[key] = val 

    def __delitem__(self, key): 
     del self._[key] 

    def __len__(self): 
     return len(self.keys()) 

    def keys(self, own=False): 
     return list(self.iterkeys(own)) 

    def items(self, own=False): 
     return list(self.iteritems(own)) 

    def iterkeys(self, own=False): 
     if own: 
      for k in self._.iterkeys(): 
       yield k 
      return 
     yielded = set([]) 
     for k in chain(self.parent.iterkeys(), self._.iterkeys()): 
      if k in yielded: 
       continue 
      yield k 
      yielded.add(k) 

    def iteritems(self, own=False): 
     for k in self.iterkeys(own): 
      yield k, self[k] 

    def __eq__(self, other): 
     return sorted(self.iteritems()) == sorted(other.iteritems()) 

    def __repr__(self): 
     return dict(self.iteritems()).__repr__() 

    def __contains__(self, key): 
     return key in self._ or key in self.parent 

    def containing(self, key): 
     """ 
     Return the ancestor that directly contains ``key`` 

     >>> p2 = {'a', 2} 
     >>> p1 = ChainableMap(p2) 
     >>> c = ChainableMap(p1) 
     >>> c.containing('a') is p2 
     True 
     """ 
     if key in self._: 
      return self 
     elif hasattr(self.parent, 'containing'): 
      return self.parent.containing(key) 
     elif key in self.parent: 
      return self.parent 

    def get(self, key, default=None): 
     """ 
     >>> c = ChainableMap({'a': 1}) 
     >>> c.get('a') 
     1 
     >>> c.get('b', 'default') 
     'default' 
     """ 
     if key in self: 
      return self[key] 
     else: 
      return default 

    def pushdown(self, top): 
     """ 
     Pushes a new mapping onto the top of the delegation chain: 

     >>> parent = {'a': 10} 
     >>> child = ChainableMap(parent) 
     >>> top = {'a': 'apple', 'b': 'beer', 'c': 'cheese'} 
     >>> child.pushdown(top) 
     >>> assert child == top 

     This creates a new ChainableMap with the contents of ``child`` and makes it 
     the new parent (the old parent becomes the grandparent): 

     >>> child.parent.parent is parent 
     True 
     >>> del child['a'] 
     >>> child['a'] == 10 
     True 
     """ 
     old = ChainableMap(self.parent) 
     for k, v in self.items(True): 
      old[k] = v 
      del self[k] 
     self.parent = old 
     for k, v in top.iteritems(): 
      self[k] = v 
+0

Thử bước qua một trình gỡ rối (hoặc viết các câu lệnh 'in' trong mọi hàm quá tải) để xem hàm nào đang được gọi khi mở rộng đối số. – Lanaru

+0

Lưu ý rằng ngay cả khi thao tác này hoạt động, 'check_args' sẽ nhận được từ điển * mới *, không phải lớp con của bạn. Xem [tài liệu định nghĩa hàm] (http://docs.python.org/reference/compound_stmts.html#function-definitions); cụ thể * "Nếu biểu mẫu" ** số nhận dạng "hiện diện, nó được khởi tạo từ điển mới nhận bất kỳ đối số từ khóa dư thừa nào, mặc định là từ điển trống mới." *. –

+0

@Lanaru đặt một 'pdb nhập; pdb.set_trace() 'ngay trước lệnh gọi' check_kwargs' và thực hiện một bước duy nhất đặt tôi qua điểm arg được mở rộng. Đặt cùng một 'set_trace' trong mỗi hàm ghi đè cho thấy không có hàm nào trong số chúng được gọi. – grncdr

Trả lời

9

Khi tạo một từ điển tranh cãi từ khóa, hành vi cũng giống như đi qua đối tượng của bạn vào dict() initializer, mà kết quả trong dict {'b': 2} cho cm đối tượng của bạn:

>>> cm = ChainableMap({'a': 1}) 
>>> cm['b'] = 2 
>>> dict(cm) 
{'b': 2} 

Một lời giải thích chi tiết hơn về lý do tại sao đây là trường hợp dưới, nhưng tóm tắt là lập bản đồ của bạn là conv erted cho một từ điển Python trong mã C mà không một số tối ưu hóa nếu đối số là chính nó một dict, bằng cách bỏ qua các chức năng Python gọi và kiểm tra đối tượng C cơ bản trực tiếp.

Có một vài cách để tiếp cận giải pháp này, đảm bảo rằng dict cơ bản chứa mọi thứ bạn muốn hoặc ngừng kế thừa từ dict (cũng sẽ yêu cầu các thay đổi khác, ít nhất là phương pháp __setitem__) .

chỉnh sửa: Nghe có vẻ như BrenBarn's suggestion kế thừa từ collections.MutableMapping thay vì dict đã làm các trick.

Bạn có thể thực hiện phương pháp đầu tiên đơn giản chỉ bằng cách thêm self.update(parent) đến ChainableMap.__init__(), nhưng tôi không chắc liệu điều đó có gây ra các tác dụng phụ khác cho hành vi của lớp học hay không.

Giải thích về lý do tại sao dict(cm) cho {'b': 2}:

Kiểm tra mã CPython sau cho đối tượng dict:
http://hg.python.org/releasing/2.7.3/file/7bb96963d067/Objects/dictobject.c#l1522

Khi dict(cm) được gọi là (và khi đối số từ khóa là giải nén), các PyDict_Merge chức năng được gọi với cm làm tham số b. Bởi vì ChainableMap thừa hưởng từ dict, câu lệnh if tại dòng 1539 được nhập:

if (PyDict_Check(b)) { 
    other = (PyDictObject*)b; 
    ... 

Từ đó trở đi, các mục từ other được thêm vào dict mới đang được tạo ra bằng cách truy cập các đối tượng C trực tiếp, mà bỏ qua tất cả các phương pháp mà bạn ghi đè.

Điều này có nghĩa là bất kỳ mục nào trong cá thể ChainableMap được truy cập thông qua thuộc tính parent sẽ không được thêm vào từ điển mới được tạo bởi dict() hoặc giải nén đối số từ khóa.

+0

Rất tiếc, các tác dụng phụ khác sẽ không được chấp nhận đối với trường hợp sử dụng của tôi. Tôi đã thay đổi để kế thừa từ 'collections.MutableMapping' và điều đó đã khắc phục được sự cố của tôi. – grncdr

+0

@grncdr: Ngay cả khi kế thừa từ 'collections.MutableMapping', bạn vẫn nên cung cấp' __len __() ', bị thiếu trong mã ví dụ của bạn. –

+0

@Fj: Việc giải đối số từ khóa không thực sự gọi 'dict()' trên đối số sau '**', mà là 'PyDict_Update()' với từ điển mới được tạo và ánh xạ, nhưng cuộc gọi này cũng sẽ kết thúc tại 'PyDictMerge()', vì vậy nó cơ bản như bạn đã nói. –

Các vấn đề liên quan