chosen |
Jun 4, 2008 1:45:51 AM
http://blog.csdn.net/lanphaday
http://groups.google.com/group/python-cn/browse_thread/thread/97a568c3d58b7781/52e1e72aa83f70d8
http://www.linuxjournal.com/node/4540/print
声明:本文适合初中级Python程序员阅读,另外本文措词可能会导致阅读者不适,敬请PG。
因为出现了Mixin这样一个东西呀,就像C++社区很多人谈论template一样啊,Python社区也很多会谈论Mixin的(以后会的,嘻嘻),所以我就来凑凑热闹了。
这个,基本上已经是我这篇文章里要讲的东西了,所以,我会用本文的大部分篇幅来回答你这个超有深度的问题。现在,就开始吧~
小时候,我家开百货店,当然,也兼营水果蔬菜啊。
小时候的事,总是特别有趣,也特别的有意义,所以今天我们就以水果店开始吧~
记得,以前很多人买水果的时候,都会问我妈一个问题,就是价格了啦~但还有两个问题也经常问到哦,就是送人应该买什么水果和什么水果可以用来送人?
嗯,年青人大多都不懂这些礼节。送水果也是很讲兆头的,比如梨和香蕉一般不用来送人,因为它们意味着离别和焦躁哦;而苹果和桔子就很受欢迎,因为它们意味着平安和吉利哦~
当然有啦,不然我扯那么多皮干嘛咧?现在,国产凌凌漆接到一个任务,要求他为一个水果连锁店开发一套软件;显然这个不关心国计民生这些鸡毛蒜皮的小事的武夫是搞不定这项艰巨任务的了,他就找到了你。
通过调研,客户发现两件事实:一是现在的年青人还是不懂送人应该买什么水果和什么水果可以用来送人这两个问题;二是水果连锁店的营业员100%都是年青人,他们中大部分人也不懂。
所以,客户要求在软件中必须提供一个这样的功能--可以查询一种水果是否适宜送人。
最初,你可能这样设计:
class Fruit(object):
pass
现在你打算实现最受顾客欢迎的苹果:
1 class Apple(Fruit):
2 def is_gift_fruit(self):
3 return True
接下来让我们实现梨子吧:
1 class Pear(Fruit):
2 def is_gift_fruit(self):
3 return False
可惜,需求很多,你还要实现桔子和香蕉呢。你写下了这几行代码:
1 class Orange(Fruit):
2 def is_gift_fruit(self):
3 return True
4 class Banana(Fruit):
5 def is_gift_fruit(self):
6 return False
好臭啊,代码的坏味道!
类Apple和类Orange除了类名不同,几乎是完全重复的代码;类Pear和类Banana也是一样。 更进一层的说,这四个类都差不多啊,所以我们有必要重构一下已有代码,改善它们的设计。
阅读代码,你可以发现水果只分两类:一类是可以作为礼品的,一类是不可以的。所以希望可以这样设计:
Fruit
/ \
GiftFruit NotGiftFruit
/ \ / \
Apple Orange Pear Banana
嗯,加了两个中间类,看起来不错:
1 class GiftFruit(Fruit):
2 def is_gift_fruit(self):
3 return True
4 class NotGiftFruit(Fruit):
5 def is_gift_fruit(self):
6 return False
7 class Apple(GiftFruit):pass
8 class Orange(GiftFruit):pass
9 class Pear(NotGiftFruit):pass
10 class Banana(NotGiftFruit):pass
好啦,看上去很不错哦,代码精简了不少,任务完成~
Fruit
/ \
GiftFruit NotGiftFruit
/ \ / \
PareG... HuskG... PareNot... HuskNot...
/ / / /
Apple Orange Pear Banana
不得已,我们添加了四个类:
1 class PareGiftFruit(GiftFruit):
2 def eat_method(self):
3 return 'Pare'
4 class HustGiftFruit(GiftFruit):
5 def eat_method(self):
6 return 'Husk'
7 class PareNotGiftFruit(NotGiftFruit):
8 def eat_method(self):
9 return 'Pare'
10 class HuskNotGiftFruit(NotGiftFruit):
11 def eat_method(self):
12 return 'Husk'
先忍忍,把AOPB四种水果的实现改改:
1 class Apple(PareGiftFruit):pass
2 class Orange(HuskGiftFruit):pass
3 class Pear(PareNotGiftFruit):Pass
4 class Banana(HuskNotGiftFruit):pass
我已经忍无可忍了。这个设计不仅仅又引入了好不容易消除的重复代码,而且还修改了AOPB这四个类的实现。这种设计的扩展性也不好,如果以后要提供水果的其它特点,比如是进口水果还是国产水果。天啊,这还了得!加上这个特性,我要实现NativePareGiftFruit、NativeHuskGiftFruit等类共8个(2的三次方)啊。水果的特征多得很,随便算算可能超过16种啊,65536个类?叫我去死吧~单是长达16个单词的类名我就崩溃了!
该是Mixin出场的时候了! 先来看看Mixin的实现吧:
1 class Fruit(object):
2 pass
3 class GiftMixin(object):
4 def is_gift_fruit(self):
5 return True
6 class NotGiftMixin(object):
7 def is_gift_fruit(self):
8 return False
9 class PareMixin(object):
10 def eat_method(self):
11 return 'Pare'
12 class HuskMixin(object):
13 def eat_method(self):
14 return 'Husk'
15 class Apple(GiftMixin, PareMixin, Fruit):pass
16 class Orange(GiftMixin, HuskMixin, Fruit):pass
17 class Pear(NotGiftMixin, PareMixin, Fruit):pass
18 class Banana(NotGiftMixin, HuskMixin, Fruit):pass
编码完成!这就是Mixin,就是这么简单,以致我无法再说出任何言语,因为我觉得上面的代码已经完整地表达了我想要表达的思想。
注意, 因为 Python 里面多重继承时如果被调用的成员函数只存在于父类中,则按类声明的父类从左到右查找调用的, 所以主类被放在右边, MixIn 被放在左边,才能正确地调用到Mixin的成员函数。
Mixin的好处是可以为主类(如Fruit)添加任意多的Mixin来实现多态,比如刚才说的水果有进口和国产两个特征,现在相当容易实现:
1 class NativeMixin(object):
2 def Locality(self):
3 return 'Native'
4 class ForeignMixin(object):
5 def Locality(self):
6 return 'Foreign'
7 class Apple(ForeignMixin, GiftMixin, PareMixin, Fruit):pass #进口红富士
8 class Orange(NativeMixin, GiftMixin, HuskMixin, Fruit):pass
9 class Pear(NativeMixin, NotGiftMixin, PareMixin, Fruit):pass
10 class Banana(NativeMixin, NotGiftMixin, HuskMixin, Fruit):pass
另外,我们还获得了可重用性。比如NativeMixin和ForeignMixin跟主类Human结合,可以做出国人和老外两个类哦~也许水果连锁店软件以后会考虑记录关于客户是否外国人的信息呢。
当然有啦!其实Mixin并不是什么高阶的Python技巧,早有就很多开源项目使用这个技巧了,典型的,比如Python项目啊!在Python自带的SocketServer.py里就应用了Mixin来实现基于进程和基于线程的两种TCP/UDP服务模型,在Tkinter和Python的其它模块也可以见到它的踪迹,如果你留意的话。
# SocketServer.py 里的Mixin
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
如果有一天你不能忍受每增加一种特征你就必须编写N(N>=2)个Mixin,然后都必须给已经存在的AOPB代码增加一个基类(想想,水果店卖的可不止四种水果,你会更头大),那,就考虑把OO抛弃吧!
鸣谢
在本文成文过程中,
沈崴(http://eishn.blog.163.com)给我很大帮助,特此鸣谢。
1 #!/usr/bin/env python
2 # 仿黄毅大师的版本
3
4 class Instance:
5 def __init__(self, *args, **kw):
6 self.__dict__.update(kw)
7 for m in args:
8 m(self)
9
10 def config(self, *args, **kw):
11 self.__dict__.update(kw)
12 for m in args:
13 m(self)
14
15 return self
16
17 def i_am_gift(self):
18 # self.is_gift = True # Why not :)
19
20 self.is_gift = lambda: True
21
22 def i_am_not_gift(self):
23 self.is_gift = lambda: False
24
25 def eatable(eat_method = ''):
26 def config_eat_method(self):
27 self.eat_method = lambda: eat_method
28
29 return config_eat_method
30
31 def Apple():
32 return Instance(i_am_gift, eatable('Bare'))
33
34 def Banana():
35 return Instance(i_am_not_gift, eatable('Hust'))
36
37 if __name__ == '__main__':
38 apple = Apple()
39 print apple.is_gift()
40
41 apple.config(i_am_not_gift)
42 print apple.is_gift()
43
44 banana = Banana()
45 print apple.eat_method()
46 print banana.eat_method()
#!/usr/bin/env python
#
# Copyright(c) 2007 Leon Dong
# E-mail : Leon.D...@gmail.com
#
class Fruit(type):
def __new__(cls, name, bases, methods):
return type.__new__(cls,name,bases,methods)
def __init__(cls, name, bases, methods):
super(Fruit,cls).__init__(name, bases, methods)
Apple = Fruit('Apple',(),{'isGift':lambda self: True, 'eatMethod':lambda self: 'Pare'})
Orange = Fruit('Orange',(),{'isGift':lambda self: True, 'eatMethod':lambda self: 'Husk'})
Pear = Fruit('Pear',(),{'isGift':lambda self: False, 'eatMethod':lambda self: 'Pare'})
Banana = Fruit('Banana',(),{'isGift':lambda self: False, 'eatMethod':lambda self: 'Husk'})
if __name__=='__main__':
print map(lambda f: (f.isGift(),f.eatMethod()),[Apple(),Orange(),Pear(),Banana()])
对于MixIn机制的实现比较而言我更认可默兜兄UliPad中的做法
我的理解是PlugIn是指在实例化时动态增加方法和属性
比如eclipse的plugin只要丢到指定的folder下重启动之后就可以被加载
MixIn指实例化以后再动态的增加对新的方法和属性的调用入口
这是我的理解的MixIn机制的示例,欢迎拍砖:
#!/usr/bin/env python
#
# Copyright(c) 2007 Leon Dong
# E-mail : Leon.D...@gmail.com
#
__mixinset__ = {}
class Fruit(object):
__mixinname__ = ''
def __init__(self):
if __mixinset__.has_key(self.__mixinname__):
plugins,mixins = __mixinset__[self.__mixinname__]
setattr(self.__class__,'__plugins__',plugins)
setattr(self.__class__,'__mixins__',mixins)
self.initMixIn()
self.execPlugIn()
def initMixIn(self):
for key,value in self.__mixins__.items():
setattr(self.__class__,key,value)
def execPlugIn(self):
for key,value in self.__plugins__.items():
setattr(self.__class__,key,value)
try:
getattr(self,key)()
except SystemExit:
raise
def execMixIn(self, name, *args, **kwargs):
try:
f = getattr(self,name)
f(args[0])
except SystemExit:
raise
class Apple(Fruit):
__mixinname__ = 'Apple'
class Orange(Fruit):
__mixinname__ = 'Orange'
class Pear(Fruit):
__mixinname__ = 'Pear'
class Banana(Fruit):
__mixinname__ = 'Banana'
def isGift(self):
self.isGift = True
print self.isGift
def notGift(self):
self.isGift = False
print self.isGift
def eatMethod(self,m):
print m
return m
if __name__ == '__main__':
__mixinset__ = {'Apple':({'isGift':isGift},{'eatMethod':eatMethod}),
'Orange':({'isGift':isGift},{'eatMethod':eatMethod}),
'Pear':({'notGift':notGift},{'eatMethod':eatMethod}),
'Banana':({'notGift':notGift},{'eatMethod':eatMethod}),
}
apple = Apple()
orange = Orange()
pear = Pear()
banana = Banana()
apple.execMixIn('eatMethod','Pare')
orange.execMixIn('eatMethod','Husk')
pear.execMixIn('eatMethod','Pare')
banana.execMixIn('eatMethod','Husk')
Mix-in programming is a style of software development where units of functionality are created in a class and then mixed in with other classes. This might sound like simple inheritance at first, but a mix-in differs from a traditional class in one or more of the following ways. Often a mix-in is not the “primary” superclass of any given class, does not care what class it is used with, is used with many classes scattered throughout the class hierarchy and is introduced dynamically at runtime.
There are several reasons to use mix-ins: they extend existing classes in new areas without having to edit, maintain or merge with their source code; they keep project components (such as domain frameworks and interface frameworks) separate; they ease the creation of new classes by providing a grab bag of functionalities that can be combined as needed; and they overcome a limitation of subclassing, whereby a new subclass has no effect if objects of the original class are still being created in other parts of the software.
So while mix-ins are not a distinct technical feature of Python, the benefits of this technique are worth studying.
Python provides an ideal language for mix-in development because it supports multiple inheritance, supports full-dynamic binding and allows dynamic changes to classes. Before we dive into Python, let me admit that mix-ins are old hat. The first time I saw mix-in programming by that name was when reviewing the now-defunct Taligent Project, known for its Pink operating system and CommonPoint application framework. However, since C++ does not support language feature #2, full-dynamic binding, or language feature #3, dynamic changes at runtime, I'm not surprised that the approach didn't bring to fruition all its inventors had hoped for.
I have also seen another instance of mix-in programming under a different name. Objective-C has a nifty language feature called categories that allows you to add and replace methods of existing classes, even without access to their source code.
This is great for repairing existing system classes and extending their capabilities. Also, combined with an ability to load libraries dynamically, categories are quite effective in improving the structure of applications and reducing code.
The grapevine informs me that Symbolics' object-oriented Flavors system is most likely the earliest appearance of bona fide mix-ins. The designers were inspired by Steve's Ice Cream Parlor in Cambridge, Massachusetts where customers started with a basic flavor of ice cream (vanilla, chocolate, etc.) and added any combination of mix-ins (nuts, fudge, chocolate chips, etc.). In the Symbolics system, large, standalone classes were known as flavors while smaller helper classes designed for enhancing other classes were known as mix-ins. A reference can be found on the Web at www.kirkrader.com/examples/cpp/mixin.htm.
Having paid our respects to the dead (Taligent), nearly dead (Objective-C) and legendary (Symbolics), let's start digging into the features that make Python a great language for mix-in programming. For one, Python supports multiple inheritance. That is, in Python, a class can inherit more than one class:
class Server(Object, Configurable):
pass
also, Python supports full-dynamic binding. When passing a message to an object such as:
obj.load(filename)Python will determine, entirely at runtime, what method to invoke, based on the name of the message and the class inheritance of obj. This behavior works as expected and is easy to remember. It continues to work even if the class inheritance or method definitions are altered at runtime.
One thing to keep in mind is the order of searching with regard to multiple inheritance. The search order goes from left to right through the base classes, and for any given base class, goes deep into its ancestor classes.
When you create mix-ins, keep in mind the potential for method names to clash. By creating distinct mix-ins with well-named methods you can generally avoid any surprises. Lastly, Python supports dynamic changes to the class hierarchy.
Most Python “things”, whether they are lists, dictionaries, classes or instances, have a set of accessible attributes. Python classes have an attribute named __bases__, which is a tuple of their base classes. Consistent with Python design, you can play with it at runtime. In the following session with the Python interactive interpreter seen in Listing 1, we create two classes and then later change the inheritance. Our person in Listing 1 isn't very friendly so let's change it. In fact, let's change all people so that we'll never have this problem again:
<<< Person.__bases__ += (Friendly,)
<<< p.hello()
Hello
Listing 1. Installing a Mix-in Dynamically
The first statement above changes the base classes of Person. By using += (as opposed to =) we avoid accidentally removing existing base classes, especially if a future version of the code makes Person inherit from another class. Also, the funny looking expression, (Friendly,), specifies a tuple that would normally simply be enclosed in parenthesis. However, while Python readily recognizes <If“Courier”>(x,y)<I$f$> as a tuple of two elements, it recognizes <If“Courier”>(x)<I$f$> as a parenthesized expression. Appending the comma forces tuple recognition.
The most straightforward way to apply mix-ins is at design time within the construction of a module. One of the more famous third-party modules for Python, MySQLdb, does exactly this.
Python defines a standard programmatic interface for database access named DB API (http://www.python.org/topics/database/). Andy Dustman's MySQLdb module implements this interface so that Python programmers can make connections and send queries to a MySQL server. It can be found at http://dustman.net/andy/python/MySQLdb/.
MySQLdb provides three major features for the cursor objects it creates. It reports warnings when necessary; it stores result sets on the client side or uses them on the server side as needed, and it returns results as tuples (e.g., immutable lists) or dictionaries.
Rather than combining all of these into one monolithic class, MySQLdb defines mix-in classes for each of them:
class CursorWarningMixIn:
class CursorStoreResultMixIn:
class CursorUseResultMixIn
class CursorTupleRowsMixIn:
class CursorDictRowsMixIn(CursorTupleRowsMixIn):
Remember that mix-ins are classes, so they can take advantage of inheritance, as we see with CursorDictRowsMixIn, which inherits CursorTupleRowsMixIn.
None of the mix-ins above can stand on their own: a BaseCursor class provides the required core functionality for any type of cursor. Using these mix-ins in combination with BaseCursor, MySQLdb offers every combination of warnings, storage and result types (eight in all). When creating a database connection, you can pass the cursor class you desire:
conn = MySQLdb.connection (cursorclass=MySQLdb.DictCursor)
Mix-ins don't only help in the creation of MySQLdb itself. They also make it more extensible by allowing you to pick and choose features for your own custom cursor classes.
Note that these class names are suffixed with MixIn to emphasize their nature. Another common convention is to append “-able” or “-ible” to the end of the name as in Configurable or NamedValueAccessible.
Let's use that last one as an example. The NamedValueAccessible mix-in adds the method valueForKey( ) to whatever class with which it is joined. For obj.valueForKey(name), this method will return one of the following:
obj.name( )
obj._name( )
obj.name
obj._name
In other words, valueForKey( ) looks for methods or attributes, either public or private, in order to return a value for the given key. The design of this method reflects the fact that Python objects often provide information through both attributes and methods. See Listing 2 for the implementation.
Listing 2. A Mix-in for Uniform Value Access
A useful application of this mix-in is to implement generic code for writing logs (see Listing 3).
Listing 3. Applying the NamedValueAccessible Mix-in for Logging
By simply adding new keys to the logColumns( ) method, the log can be expanded without having to modify the code that generates it, which is found in logEntry( ). More importantly, you can imagine that logColumns( ) could read its list of fields from a simple configuration file.
The transaction object itself is free to provide the given values via either methods or attributes, due to the flexibility of the valueForKey( ) method. Making mix-ins flexible increases their utility and is an art that can be developed over time.
So far we have seen examples of using mix-ins during the construction of classes. However, Python's dynamic nature also allows us to mix in functionality at runtime. The simplest technique for doing so is to modify the base classes of the given class, as described earlier. A function allows us to keep this operation opaque and enhance it later if need be:
def MixIn(pyClass, mixInClass):
pyClass.__bases__ += mixInClass
Let's consider a situation that makes the utility of MixIn( ) obvious. In the construction of internet applications, keeping domain classes separate from interface classes is generally a good idea. Domain classes represent the concepts, data and operations of a specific application. They are independent of operating system, user interface, database, etc. Some writers refer to domain objects as business objects, model objects or problem space objects.
Keeping the domain and interface separate makes sense for various reasons. An individual focus is created for two key areas that are largely independent: What is the subject material of the problem? And, how should that be presented? New interfaces can be constructed without modifying or rewriting the domain classes. In fact, multiple interfaces can be provided.
Domain classes for a story publishing system might include Story, Author and Site. These classes contain essential attributes (such as title, body, name, e-mail, etc.) and various operations (save, load, publish, etc.).
One interface for such a system could be a web site that allows users to create, edit, delete and publish these stories. When developing such a site, it would be useful if our domain classes, such as Story, have the methods renderView( ) and renderForm( ), which write HTML for either displaying the story or editing it with a form.
Using mix-ins, we can develop such functionality outside of the domain classes:
class StoryInterface:
def renderView(self):
# write the HTML representation of the story
pass
def renderForm(self):
# write the HTML form to edit the story
pass
And within the code that backs the web site, mix it in like so:
from MixIn import MixInIf you decide to create a GUI interface for the publishing system, you don't have to take the HTML machinery with you (or vice versa). The domain classes focus on providing necessary data and operations, ensuring that when developing the GUI, you will have what you need.
from Domain.Story import Story
MixIn(Story, StoryInterface)
One could argue that a new class might be created to bring the two together:
class StoryInterface:
...
from Domain.Story import Story
class Story(Story, StoryInterface): pass
Or one could argue that StoryInterface might be made a subclass of Story in order to achieve the same benefit. However, consider the case when Story already has other domain subclasses:
class Story: ...Existing subclasses of Story are in no way affected by simply creating a new Story class or subclass. But a dynamic mix-in for Story will also affect Editorial, Feature and Column. That is why many times the static approach does not work in practice, thereby making the dynamic approach not only clever, but necessary.
class Editorial(Story): ...
class Feature(Story): ...
class Column(Story): ...
Also, consider the case where Story objects are created in parts of the code where Story is hard-coded. While poor practice, this is common. In this situation, creating subclasses of Story will have no effect on the code that ignores them.
One warning regarding dynamic mix-ins: they can change the behavior of existing objects (because they change the classes of those objects). This could lead to unpredictable results, as most classes are not designed with that type of change in mind. The safe way to use dynamic mix-ins is to install them when the application first starts, before any objects are created.
The first enhancement we can add to MixIn( ) is to check that we're not mixing in the same class twice:
def MixIn(pyClass, mixInClass):
if mixInClass not in pyClass.__bases__
pyClass.__bases__ += (mixInClass,)
In practice, I find more often than not, that I want my mix-in methods to take a high priority, even superseding inherited methods if needed. The next version of the function puts the mix-in class at the front of the sequence of base classes but allows you to override this behavior with an optional argument:
def MixIn(pyClass, mixInClass, makeLast=0):To make Python invocations more readable, I suggest using keyword arguments for flags:
if mixInClass not in pyClass.__bases__
if makeLast:
pyClass.__bases__ += (mixInClass,)
else:
pyClass.__bases__ = (mixInClass,) + pyClass.__bases__
# not so readable:Listing 4. Our Final Version of MixIn
MixIn(Story, StoryInterface, 1)
# much better:
MixIn(Story, StoryInterface, makeLast=1)
This new version still doesn't allow methods in the actual class to be overridden with methods in the mix-in. In order to accomplish that, the mix-in methods must actually be installed in the class. Fortunately, Python is dynamic enough to accomplish this. Listing 4 gives the source code for our final version of MixIn( ). By default it will install the methods of the mix-in directly into the target class, even taking care to traverse the base classes of the mix-in. The invocation is the same:
MixIn(Story, StoryInterface)
An extra makeAncestor=1 argument can be provided for the new MixIn( ) to get the old semantics (e.g., make the mix-in a base class of the target class). The ability to put the mix-in at the end of the base classes has been removed, since I have never needed this in practice.
An even more sophisticated version of this function could return (perhaps optionally) a list of methods that clash between the two, or raise an exception accompanied by such a list, if the overlap exists.
When making heavy use of after-the-fact mix-ins, invocations of the MixIn( ) function become repetitious. For example, a GUI application might have a mix-in for every domain class in existence, thereby requiring a call such as this for each one:
from Domain.User import User
MixIn(User, UserMixIn)
One solution is to bind the mix-ins to their target classes by name and have the application install these at startup. For example, all mix-ins could be named directly after the class they modify and put into a MixIns/ directory. The code in Listing 5 will install them.
Listing 5. Detecting and Installing Mix-ins Named after Their Classes
While it's fun to explore more sophisticated versions of the MixIn( ) function, the most important key is the ability to apply them in order to improve your software. Here are some additional uses to stimulate your imagination:
A class could augment itself with a mix-in after reading a configuration file. For example, a web server class could mix in Threading or Forking depending on how it's configured.
A program could provide for plug-ins: software packages that are located and loaded at launch time to enhance the program. Those who implement plug-ins could make use of MixIn( ) to enhance core program classes.
Mix-ins are great for improving modularity and enhancing existing classes without having to get intimate with their source code. This in turn supports other design paradigms, like separation of domain and interface, dynamic configuration and plug-ins. Python's inherent support for multiple inheritance, dynamic binding and dynamic changes to classes enables a very powerful technique. As you continue to write Python code, consider ways in which mix-ins can enhance your software.
Tag: Programming Python
Comment:
(no reply)