============================== 9.20 利用函数注解实现方法é‡è½½ ============================== ---------- 问题 ---------- ä½ å·²ç»å¦è¿‡æ€Žæ ·ä½¿ç”¨å‡½æ•°å‚æ•°æ³¨è§£ï¼Œé‚£ä¹ˆä½ å¯èƒ½ä¼šæƒ³åˆ©ç”¨å®ƒæ¥å®žçŽ°åŸºäºŽç±»åž‹çš„æ–¹æ³•é‡è½½ã€‚ ä½†æ˜¯ä½ ä¸ç¡®å®šåº”è¯¥æ€Žæ ·åŽ»å®žçŽ°ï¼ˆæˆ–è€…åˆ°åº•è¡Œå¾—é€šä¸ï¼‰ã€‚ ---------- 解决方案 ---------- 本å°èŠ‚çš„æŠ€æœ¯æ˜¯åŸºäºŽä¸€ä¸ªç®€å•的技术,那就是Pythonå…è®¸å‚æ•°æ³¨è§£ï¼Œä»£ç å¯ä»¥åƒä¸‹é¢è¿™æ ·å†™ï¼š .. code-block:: python class Spam: def bar(self, x:int, y:int): print('Bar 1:', x, y) def bar(self, s:str, n:int = 0): print('Bar 2:', s, n) s = Spam() s.bar(2, 3) # Prints Bar 1: 2 3 s.bar('hello') # Prints Bar 2: hello 0 䏋颿˜¯æˆ‘们第一æ¥çš„å°è¯•,使用到了一个元类和æè¿°å™¨ï¼š .. code-block:: python # multiple.py import inspect import types class MultiMethod: ''' Represents a single multimethod. ''' def __init__(self, name): self._methods = {} self.__name__ = name def register(self, meth): ''' Register a new method as a multimethod ''' sig = inspect.signature(meth) # Build a type signature from the method's annotations types = [] for name, parm in sig.parameters.items(): if name == 'self': continue if parm.annotation is inspect.Parameter.empty: raise TypeError( 'Argument {} must be annotated with a type'.format(name) ) if not isinstance(parm.annotation, type): raise TypeError( 'Argument {} annotation must be a type'.format(name) ) if parm.default is not inspect.Parameter.empty: self._methods[tuple(types)] = meth types.append(parm.annotation) self._methods[tuple(types)] = meth def __call__(self, *args): ''' Call a method based on type signature of the arguments ''' types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else: raise TypeError('No matching method for types {}'.format(types)) def __get__(self, instance, cls): ''' Descriptor method needed to make calls work in a class ''' if instance is not None: return types.MethodType(self, instance) else: return self class MultiDict(dict): ''' Special dictionary to build multimethods in a metaclass ''' def __setitem__(self, key, value): if key in self: # If key already exists, it must be a multimethod or callable current_value = self[key] if isinstance(current_value, MultiMethod): current_value.register(value) else: mvalue = MultiMethod(key) mvalue.register(current_value) mvalue.register(value) super().__setitem__(key, mvalue) else: super().__setitem__(key, value) class MultipleMeta(type): ''' Metaclass that allows multiple dispatch of methods ''' def __new__(cls, clsname, bases, clsdict): return type.__new__(cls, clsname, bases, dict(clsdict)) @classmethod def __prepare__(cls, clsname, bases): return MultiDict() ä¸ºäº†ä½¿ç”¨è¿™ä¸ªç±»ï¼Œä½ å¯ä»¥åƒä¸‹é¢è¿™æ ·å†™ï¼š .. code-block:: python class Spam(metaclass=MultipleMeta): def bar(self, x:int, y:int): print('Bar 1:', x, y) def bar(self, s:str, n:int = 0): print('Bar 2:', s, n) # Example: overloaded __init__ import time class Date(metaclass=MultipleMeta): def __init__(self, year: int, month:int, day:int): self.year = year self.month = month self.day = day def __init__(self): t = time.localtime() self.__init__(t.tm_year, t.tm_mon, t.tm_mday) 䏋颿˜¯ä¸€ä¸ªäº¤äº’示例æ¥éªŒè¯å®ƒèƒ½æ£ç¡®çš„工作: .. code-block:: python >>> s = Spam() >>> s.bar(2, 3) Bar 1: 2 3 >>> s.bar('hello') Bar 2: hello 0 >>> s.bar('hello', 5) Bar 2: hello 5 >>> s.bar(2, 'hello') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "multiple.py", line 42, in __call__ raise TypeError('No matching method for types {}'.format(types)) TypeError: No matching method for types (<class 'int'>, <class 'str'>) >>> # Overloaded __init__ >>> d = Date(2012, 12, 21) >>> # Get today's date >>> e = Date() >>> e.year 2012 >>> e.month 12 >>> e.day 3 >>> ---------- 讨论 ---------- å¦ç™½æ¥è®²ï¼Œç›¸å¯¹äºŽé€šå¸¸çš„代ç è€Œå·²æœ¬èŠ‚ä½¿ç”¨åˆ°äº†å¾ˆå¤šçš„é”æ³•代ç 。 但是,它å´èƒ½è®©æˆ‘们深入ç†è§£å…ƒç±»å’Œæè¿°å™¨çš„底层工作原ç†ï¼Œ å¹¶èƒ½åŠ æ·±å¯¹è¿™äº›æ¦‚å¿µçš„å°è±¡ã€‚å› æ¤ï¼Œå°±ç®—ä½ å¹¶ä¸ä¼šç«‹å³åŽ»åº”ç”¨æœ¬èŠ‚çš„æŠ€æœ¯ï¼Œ å®ƒçš„ä¸€äº›åº•å±‚æ€æƒ³å´ä¼šå½±å“到其它涉åŠåˆ°å…ƒç±»ã€æè¿°å™¨å’Œå‡½æ•°æ³¨è§£çš„编程技术。 本节的实现ä¸çš„ä¸»è¦æ€è·¯å…¶å®žæ˜¯å¾ˆç®€å•的。``MultipleMeta`` 元类使用它的 ``__prepare__()`` 方法 æ¥æä¾›ä¸€ä¸ªä½œä¸º ``MultiDict`` 实例的自定义å—典。这个跟普通å—å…¸ä¸ä¸€æ ·çš„æ˜¯ï¼Œ ``MultiDict`` ä¼šåœ¨å…ƒç´ è¢«è®¾ç½®çš„æ—¶å€™æ£€æŸ¥æ˜¯å¦å·²ç»å˜åœ¨ï¼Œå¦‚æžœå˜åœ¨çš„è¯ï¼Œé‡å¤çš„å…ƒç´ ä¼šåœ¨ ``MultiMethod`` 实例ä¸åˆå¹¶ã€‚ ``MultiMethod`` 实例通过构建从类型ç¾ååˆ°å‡½æ•°çš„æ˜ å°„æ¥æ”¶é›†æ–¹æ³•。 在这个构建过程ä¸ï¼Œå‡½æ•°æ³¨è§£è¢«ç”¨æ¥æ”¶é›†è¿™äº›ç¾åç„¶åŽæž„å»ºè¿™ä¸ªæ˜ å°„ã€‚ 这个过程在 ``MultiMethod.register()`` 方法ä¸å®žçŽ°ã€‚ è¿™ç§æ˜ å°„çš„ä¸€ä¸ªå…³é”®ç‰¹ç‚¹æ˜¯å¯¹äºŽå¤šä¸ªæ–¹æ³•ï¼Œæ‰€æœ‰å‚æ•°ç±»åž‹éƒ½å¿…é¡»è¦æŒ‡å®šï¼Œå¦åˆ™å°±ä¼šæŠ¥é”™ã€‚ 为了让 ``MultiMethod`` 实例模拟一个调用,它的 ``__call__()`` 方法被实现了。 这个方法从所有排除 ``self`` çš„å‚æ•°ä¸æž„建一个类型元组,在内部map䏿Ÿ¥æ‰¾è¿™ä¸ªæ–¹æ³•, ç„¶åŽè°ƒç”¨ç›¸åº”的方法。为了能让 ``MultiMethod`` 实例在类定义时æ£ç¡®æ“作,``__get__()`` 是必须得实现的。 å®ƒè¢«ç”¨æ¥æž„建æ£ç¡®çš„绑定方法。比如: .. code-block:: python >>> b = s.bar >>> b <bound method Spam.bar of <__main__.Spam object at 0x1006a46d0>> >>> b.__self__ <__main__.Spam object at 0x1006a46d0> >>> b.__func__ <__main__.MultiMethod object at 0x1006a4d50> >>> b(2, 3) Bar 1: 2 3 >>> b('hello') Bar 2: hello 0 >>> ä¸è¿‡æœ¬èŠ‚çš„å®žçŽ°è¿˜æœ‰ä¸€äº›é™åˆ¶ï¼Œå…¶ä¸ä¸€ä¸ªæ˜¯å®ƒä¸èƒ½ä½¿ç”¨å…³é”®å—傿•°ã€‚例如: .. code-block:: python >>> s.bar(x=2, y=3) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __call__() got an unexpected keyword argument 'y' >>> s.bar(s='hello') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __call__() got an unexpected keyword argument 's' >>> ä¹Ÿè®¸æœ‰å…¶ä»–çš„æ–¹æ³•èƒ½æ·»åŠ è¿™ç§æ”¯æŒï¼Œä½†æ˜¯å®ƒéœ€è¦ä¸€ä¸ªå®Œå…¨ä¸åŒçš„æ–¹æ³•æ˜ å°„æ–¹å¼ã€‚ 问题在于关键å—傿•°çš„出现是没有顺åºçš„。当它跟ä½ç½®å‚æ•°æ··åˆä½¿ç”¨æ—¶ï¼Œ é‚£ä½ çš„å‚æ•°å°±ä¼šå˜å¾—æ¯”è¾ƒæ··ä¹±äº†ï¼Œè¿™æ—¶å€™ä½ ä¸å¾—ä¸åœ¨ ``__call__()`` 方法ä¸å…ˆåŽ»åšä¸ªæŽ’åºã€‚ åŒæ ·å¯¹äºŽç»§æ‰¿ä¹Ÿæ˜¯æœ‰é™åˆ¶çš„,例如,类似下é¢è¿™ç§ä»£ç å°±ä¸èƒ½æ£å¸¸å·¥ä½œï¼š .. code-block:: python class A: pass class B(A): pass class C: pass class Spam(metaclass=MultipleMeta): def foo(self, x:A): print('Foo 1:', x) def foo(self, x:C): print('Foo 2:', x) åŽŸå› æ˜¯å› ä¸º ``x:A`` 注解ä¸èƒ½æˆåŠŸåŒ¹é…å类实例(比如B的实例),如下: .. code-block:: python >>> s = Spam() >>> a = A() >>> s.foo(a) Foo 1: <__main__.A object at 0x1006a5310> >>> c = C() >>> s.foo(c) Foo 2: <__main__.C object at 0x1007a1910> >>> b = B() >>> s.foo(b) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "multiple.py", line 44, in __call__ raise TypeError('No matching method for types {}'.format(types)) TypeError: No matching method for types (<class '__main__.B'>,) >>> ä½œä¸ºä½¿ç”¨å…ƒç±»å’Œæ³¨è§£çš„ä¸€ç§æ›¿ä»£æ–¹æ¡ˆï¼Œå¯ä»¥é€šè¿‡æè¿°å™¨æ¥å®žçŽ°ç±»ä¼¼çš„æ•ˆæžœã€‚ä¾‹å¦‚ï¼š .. code-block:: python import types class multimethod: def __init__(self, func): self._methods = {} self.__name__ = func.__name__ self._default = func def match(self, *types): def register(func): ndefaults = len(func.__defaults__) if func.__defaults__ else 0 for n in range(ndefaults+1): self._methods[types[:len(types) - n]] = func return self return register def __call__(self, *args): types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else: return self._default(*args) def __get__(self, instance, cls): if instance is not None: return types.MethodType(self, instance) else: return self 为了使用æè¿°å™¨ç‰ˆæœ¬ï¼Œä½ 需è¦åƒä¸‹é¢è¿™æ ·å†™ï¼š .. code-block:: python class Spam: @multimethod def bar(self, *args): # Default method called if no match raise TypeError('No matching method for bar') @bar.match(int, int) def bar(self, x, y): print('Bar 1:', x, y) @bar.match(str, int) def bar(self, s, n = 0): print('Bar 2:', s, n) æè¿°å™¨æ–¹æ¡ˆåŒæ ·ä¹Ÿæœ‰å‰é¢æåˆ°çš„é™åˆ¶ï¼ˆä¸æ”¯æŒå…³é”®å—傿•°å’Œç»§æ‰¿ï¼‰ã€‚ 所有事物都是平ç‰çš„,有好有å,也许最好的办法就是在普通代ç ä¸é¿å…使用方法é‡è½½ã€‚ ä¸è¿‡æœ‰äº›ç‰¹æ®Šæƒ…况下还是有æ„义的,比如基于模å¼åŒ¹é…的方法é‡è½½ç¨‹åºä¸ã€‚ 举个例å,8.21å°èŠ‚ä¸çš„访问者模å¼å¯ä»¥ä¿®æ”¹ä¸ºä¸€ä¸ªä½¿ç”¨æ–¹æ³•é‡è½½çš„类。 但是,除了这个以外,通常ä¸åº”该使用方法é‡è½½ï¼ˆå°±ç®€å•的使用ä¸åŒå称的方法就行了)。 在Python社区对于实现方法é‡è½½çš„讨论已ç»ç”±æ¥å·²ä¹…。 对于引å‘è¿™ä¸ªäº‰è®ºçš„åŽŸå› ï¼Œå¯ä»¥å‚考下Guido van Rossum的这篇åšå®¢ï¼š `Five-Minute Multimethods in Python <http://www.artima.com/weblogs/viewpost.jsp?thread=101605>`_