从pywifi的一个设计缺陷解析python类的成员访问

目录

1、缘起

2、群中的一次讨论

2.1 类与子类

2.1.1 从现象说起1:属性成员

2.1.2 从现象说起2:方法成员

2.1.3 小结

2.1.4 拓展:可见与拥有的关系

2.2 python类体系:一切皆对象,对象的访问可通过“可见+拥有”分析

2.3 这是一个彩蛋:什么是知识

3、C++和python在实现继承上的几点比较

3.1 “继承”概念

3.2 C++中的继承实现

3.3 python中的继承实现

3.4 网络小品:duck typing


1、缘起

前几天,我有一个设想,想用C语言写出一系列底层访问(获取与操作)WIFI的具体实现,然后封装成库引入到python中来,从技能上完成python和C/C++的连通。设想的施行条件VS2007、SDK、libcap、windump、python我都准备好(N,N,N年前就用过),不过还是在施行前从网上查了一下python操作WIFI现状,发现有个python库ctypes可以直接操作WINAPI,还发现有个pywifi包可以直接使用,于是就先深入一把ctypes,并pip install pywifi,看看这个包怎么样。(除pywifi外当然还有其他有意思的包)。

奔跑吧,兄弟:

pywifi 2020-05-22 15:30:47,918 INFO Get interface: Qualcomm Atheros AR956x Wireless Network Adapter
pywifi 2020-05-22 15:30:47,920 INFO iface 'Qualcomm Atheros AR956x Wireless Network Adapter' scans
pywifi 2020-05-22 15:30:52,935 INFO Scan found 11 networks.
pywifi 2020-05-22 15:30:52,942 INFO ---------------------------------->strProfileName: DXG3F
pywifi 2020-05-22 15:30:52,944 INFO   dot11Ssid: DXG3F
pywifi 2020-05-22 15:30:52,947 INFO   dot11BssType: dot11_BSS_type_infrastructure
pywifi 2020-05-22 15:30:52,950 INFO   uNumberOfBssids: 1
pywifi 2020-05-22 15:30:52,952 INFO       Bssids: ['78:eb:14:d4:09:06']
pywifi 2020-05-22 15:30:52,955 INFO       sigFre: [(-47, 2472000)]
pywifi 2020-05-22 15:30:52,957 INFO   bNetworkConnectable: True
pywifi 2020-05-22 15:30:52,961 INFO   wlanNotConnectableReason(if TRUE): 操作成功。
pywifi 2020-05-22 15:30:52,963 INFO   uNumberOfPhyTypes: 1
pywifi 2020-05-22 15:30:52,964 INFO   dot11PhyTypes:
pywifi 2020-05-22 15:30:52,965 INFO       dot11_phy_type_ht
pywifi 2020-05-22 15:30:52,967 INFO   bMorePhyTypes: False
pywifi 2020-05-22 15:30:52,968 INFO   wlanSignalQuality (-100,-50)-->(0,100): 100
pywifi 2020-05-22 15:30:52,969 INFO   bSecurityEnabled: True
pywifi 2020-05-22 15:30:52,971 INFO   dot11DefaultAuthAlgorithm: DOT11_AUTH_ALGO_RSNA_PSK
pywifi 2020-05-22 15:30:52,972 INFO   dot11DefaultCipherAlgorithm: DOT11_CIPHER_ALGO_CCMP
pywifi 2020-05-22 15:30:52,975 INFO   dwFlags: 6
pywifi 2020-05-22 15:30:52,975 INFO   dwReserved: 0
pywifi 2020-05-22 15:30:53,074 INFO ---------------------------------->strProfileName: CPS_Hotel
pywifi 2020-05-22 15:30:53,076 INFO   dot11Ssid: CPS_Hotel
pywifi 2020-05-22 15:30:53,078 INFO   dot11BssType: dot11_BSS_type_infrastructure
pywifi 2020-05-22 15:30:53,080 INFO   uNumberOfBssids: 6
pywifi 2020-05-22 15:30:53,084 INFO       Bssids: ['00:1b:2f:ae:77:e5', 'c0:3f:0e:82:7f:3e', '00:18:4d:2b:c3:82', 'c0:3f:0e:82:7f:37', '00:24:b2:61:84:0f', '84:1b:5e:7b:b3:90']
pywifi 2020-05-22 15:30:53,086 INFO       sigFre: [(-91, 2412000), (-90, 2437000), (-85, 2437000), (-91, 2462000), (-90, 2422000), (-92, 2462000)]
pywifi 2020-05-22 15:30:53,088 INFO   bNetworkConnectable: True
pywifi 2020-05-22 15:30:53,091 INFO   wlanNotConnectableReason(if TRUE): 操作成功。
pywifi 2020-05-22 15:30:53,094 INFO   uNumberOfPhyTypes: 2
pywifi 2020-05-22 15:30:53,096 INFO   dot11PhyTypes:
pywifi 2020-05-22 15:30:53,099 INFO       dot11_phy_type_erp
pywifi 2020-05-22 15:30:53,101 INFO       dot11_phy_type_ht
pywifi 2020-05-22 15:30:53,103 INFO   bMorePhyTypes: False
pywifi 2020-05-22 15:30:53,107 INFO   wlanSignalQuality (-100,-50)-->(0,100): 30
pywifi 2020-05-22 15:30:53,109 INFO   bSecurityEnabled: True
pywifi 2020-05-22 15:30:53,111 INFO   dot11DefaultAuthAlgorithm: DOT11_AUTH_ALGO_RSNA_PSK
pywifi 2020-05-22 15:30:53,113 INFO   dot11DefaultCipherAlgorithm: DOT11_CIPHER_ALGO_CCMP
pywifi 2020-05-22 15:30:53,115 INFO   dwFlags: 0
pywifi 2020-05-22 15:30:53,118 INFO   dwReserved: 0
pywifi 2020-05-22 15:30:53,252 INFO ---------------------------------->strProfileName: HUAWEI-421
pywifi 2020-05-22 15:30:53,254 INFO   dot11Ssid: HUAWEI-421
pywifi 2020-05-22 15:30:53,257 INFO   dot11BssType: dot11_BSS_type_infrastructure
pywifi 2020-05-22 15:30:53,259 INFO   uNumberOfBssids: 1
pywifi 2020-05-22 15:30:53,261 INFO       Bssids: ['b0:89:00:09:56:fc']
pywifi 2020-05-22 15:30:53,263 INFO       sigFre: [(-86, 2462000)]
pywifi 2020-05-22 15:30:53,266 INFO   bNetworkConnectable: True
pywifi 2020-05-22 15:30:53,269 INFO   wlanNotConnectableReason(if TRUE): 操作成功。
pywifi 2020-05-22 15:30:53,271 INFO   uNumberOfPhyTypes: 1
pywifi 2020-05-22 15:30:53,274 INFO   dot11PhyTypes:
pywifi 2020-05-22 15:30:53,278 INFO       dot11_phy_type_ht
pywifi 2020-05-22 15:30:53,281 INFO   bMorePhyTypes: False
pywifi 2020-05-22 15:30:53,283 INFO   wlanSignalQuality (-100,-50)-->(0,100): 28
pywifi 2020-05-22 15:30:53,286 INFO   bSecurityEnabled: True
pywifi 2020-05-22 15:30:53,289 INFO   dot11DefaultAuthAlgorithm: DOT11_AUTH_ALGO_RSNA_PSK
pywifi 2020-05-22 15:30:53,291 INFO   dot11DefaultCipherAlgorithm: DOT11_CIPHER_ALGO_CCMP
pywifi 2020-05-22 15:30:53,293 INFO   dwFlags: 0
pywifi 2020-05-22 15:30:53,295 INFO   dwReserved: 0
pywifi 2020-05-22 15:30:53,298 INFO ---------------------------------->strProfileName: DXG3F
pywifi 2020-05-22 15:30:53,301 INFO   dot11Ssid: DXG3F
pywifi 2020-05-22 15:30:53,303 INFO   dot11BssType: dot11_BSS_type_infrastructure
pywifi 2020-05-22 15:30:53,305 INFO   uNumberOfBssids: 1
pywifi 2020-05-22 15:30:53,308 INFO       Bssids: ['78:eb:14:d4:09:06']
pywifi 2020-05-22 15:30:53,310 INFO       sigFre: [(-47, 2472000)]
pywifi 2020-05-22 15:30:53,313 INFO   bNetworkConnectable: True
pywifi 2020-05-22 15:30:53,316 INFO   wlanNotConnectableReason(if TRUE): 操作成功。
pywifi 2020-05-22 15:30:53,318 INFO   uNumberOfPhyTypes: 1
pywifi 2020-05-22 15:30:53,320 INFO   dot11PhyTypes:
pywifi 2020-05-22 15:30:53,322 INFO       dot11_phy_type_ht
pywifi 2020-05-22 15:30:53,325 INFO   bMorePhyTypes: False
pywifi 2020-05-22 15:30:53,328 INFO   wlanSignalQuality (-100,-50)-->(0,100): 100
pywifi 2020-05-22 15:30:53,330 INFO   bSecurityEnabled: True
pywifi 2020-05-22 15:30:53,332 INFO   dot11DefaultAuthAlgorithm: DOT11_AUTH_ALGO_RSNA_PSK
pywifi 2020-05-22 15:30:53,334 INFO   dot11DefaultCipherAlgorithm: DOT11_CIPHER_ALGO_CCMP
pywifi 2020-05-22 15:30:53,337 INFO   dwFlags: 0
pywifi 2020-05-22 15:30:53,340 INFO   dwReserved: 0
pywifi 2020-05-22 15:30:53,343 INFO ---------------------------------->strProfileName: ChinaNet-2.4G-439
pywifi 2020-05-22 15:30:53,345 INFO   dot11Ssid: ChinaNet-2.4G-439
pywifi 2020-05-22 15:30:53,347 INFO   dot11BssType: dot11_BSS_type_infrastructure
pywifi 2020-05-22 15:30:53,350 INFO   uNumberOfBssids: 1
pywifi 2020-05-22 15:30:53,352 INFO       Bssids: ['b0:ac:d2:0a:2e:12']
pywifi 2020-05-22 15:30:53,354 INFO       sigFre: [(-91, 2427000)]
pywifi 2020-05-22 15:30:53,357 INFO   bNetworkConnectable: True
pywifi 2020-05-22 15:30:53,360 INFO   wlanNotConnectableReason(if TRUE): 操作成功。
pywifi 2020-05-22 15:30:53,363 INFO   uNumberOfPhyTypes: 1
pywifi 2020-05-22 15:30:53,365 INFO   dot11PhyTypes:
pywifi 2020-05-22 15:30:53,367 INFO       dot11_phy_type_ht
pywifi 2020-05-22 15:30:53,369 INFO   bMorePhyTypes: False
pywifi 2020-05-22 15:30:53,372 INFO   wlanSignalQuality (-100,-50)-->(0,100): 12
pywifi 2020-05-22 15:30:53,375 INFO   bSecurityEnabled: True
pywifi 2020-05-22 15:30:53,378 INFO   dot11DefaultAuthAlgorithm: DOT11_AUTH_ALGO_RSNA_PSK
pywifi 2020-05-22 15:30:53,380 INFO   dot11DefaultCipherAlgorithm: DOT11_CIPHER_ALGO_CCMP
pywifi 2020-05-22 15:30:53,383 INFO   dwFlags: 0
pywifi 2020-05-22 15:30:53,385 INFO   dwReserved: 0
pywifi 2020-05-22 15:30:53,389 INFO ---------------------------------->strProfileName: A Hide Network
pywifi 2020-05-22 15:30:53,391 INFO   dot11Ssid: A Hide Network
pywifi 2020-05-22 15:30:53,393 INFO   dot11BssType: dot11_BSS_type_infrastructure
pywifi 2020-05-22 15:30:53,395 INFO   uNumberOfBssids: 1
pywifi 2020-05-22 15:30:53,398 INFO       Bssids: ['80:3f:5d:5e:24:e3']
pywifi 2020-05-22 15:30:53,401 INFO       sigFre: [(-95, 2457000)]
pywifi 2020-05-22 15:30:53,403 INFO   bNetworkConnectable: True
pywifi 2020-05-22 15:30:53,406 INFO   wlanNotConnectableReason(if TRUE): 操作成功。
pywifi 2020-05-22 15:30:53,408 INFO   uNumberOfPhyTypes: 1
pywifi 2020-05-22 15:30:53,411 INFO   dot11PhyTypes:
pywifi 2020-05-22 15:30:53,414 INFO       dot11_phy_type_ht
pywifi 2020-05-22 15:30:53,417 INFO   bMorePhyTypes: False
pywifi 2020-05-22 15:30:53,419 INFO   wlanSignalQuality (-100,-50)-->(0,100): 4
pywifi 2020-05-22 15:30:53,423 INFO   bSecurityEnabled: True
pywifi 2020-05-22 15:30:53,426 INFO   dot11DefaultAuthAlgorithm: DOT11_AUTH_ALGO_RSNA_PSK
pywifi 2020-05-22 15:30:53,428 INFO   dot11DefaultCipherAlgorithm: DOT11_CIPHER_ALGO_CCMP
pywifi 2020-05-22 15:30:53,431 INFO   dwFlags: 0
pywifi 2020-05-22 15:30:53,433 INFO   dwReserved: 0
…

很容易了解到本机WIFI基本状态,以上面列出的最后显示的“A Hide Network ”为例。这是一个隐藏的WIFI网络,即电脑或手机上无法显示WIFI名称的那种,“A Hide Network”是我为了查询的需要而在日志输出语句中加的名称。网络类型是普通WIFI网络(区别于ad-hoc网络),网络中只有一个无线接入点(有可能是无线路由器、手机WIFI、电脑WIFI等),MAC为80:3f:5d:5e:24:e3,信号较弱,2.4G频段;接入点的WIFI物理组件使用802.11n协议,有安全机制,认证采用提前共享密钥的RSNA算法而加密的是CCMP算法,可被其他设备连接(因为是隐藏网络,所以要手动连接)。

别问我是怎么分析地这么头头是道,绝对属于我的是分析,而分析的内容有些是临时查WIN32 API、看无线网络安全基础、以及对一些关键词查字典式地查出的。

同时,也用Qtdesigner做了一个界面(只是引入UI,还没有加入wifi操作代码):

class MainGUI(QtWidgets.QMainWindow,Ui_MainWindow):
    def __init__(self, parent = None, flags = Qt.WindowFlags()):
        super().__init__(parent, flags)
        self.setupUi(self)
    def WiFipreconnect(self):pass
    def WiFiscan(self,INobj):pass
    def WiFiconnect(self,INobj):pass
if __name__=="__main__":
    app=QtWidgets.QApplication(sys.argv)
    window=MainGUI()
    window.show()
    try:
     sys.exit(app.exec_())
    except: pass

如果你也用过pywifi,可能会奇怪之前列出的wifi数据,原程序输出不是这样的,那是因为我已经做了我想要的改动。

在看代码期间,发现一个有趣的问题,原来代码:

片段一:

class Interface:
    """Interface provides methods for manipulating wifi devices."""
    """
    For encapsulating OS dependent behavior, we declare _raw_obj here for
    storing some common attribute (e.g. name) and os attributes (e.g. dbus
    objects for linux)
    """
    _raw_obj = {}
    _wifi_ctrl = {}
    _logger = None
    def __init__(self, raw_obj):
        self._raw_obj = raw_obj
        self._wifi_ctrl = wifiutil.WifiUtil()
    self._logger = logging.getLogger('pywifi')

片段二:

class PyWiFi:
    """PyWiFi provides operations to manipulate wifi devices."""
    _ifaces = []
    _logger = None
    def __init__(self):
        self._logger = logging.getLogger('pywifi')
    def interfaces(self):
        """Collect the available wlan interfaces."""
        self._ifaces = []
        wifi_ctrl = wifiutil.WifiUtil()
        for interface in wifi_ctrl.interfaces():
            iface = Interface(interface)
            self._ifaces.append(iface)
            self._logger.info("Get interface: %s", iface.name())
        if not self._ifaces:
            self._logger.error("Can't get wifi interface")
        return self._ifaces

我不知道pywifi包的发布者本来用意,但很明显,这两个片段都存在类属性与实例属性的设计矛盾。这实际上很容易发现。

这样的矛盾在现有pywifi包的设计架构上,或者说在几百行以内的python代码中不会导致什么很大的漏洞。如果要基于pywifi做进一步开发,则要警惕。

2、群中的一次讨论

2.1 类与子类

2.1.1 从现象说起1:属性成员

与这个设计缺陷相对应的,是那天在群里的一个讨论。讨论的内容简要还原如下(我用的是python 3.6.5):

>>> class parent:
	_d={}
	def __init__(self):
		self._d['a']=0
		self._d['b']=0
		self._d['c']=0		
>>> class A(parent):
	def init(self):
		self._d['a']=1
		self._d['b']=1
		self._d['c']=1
>>> class B(parent):
	def init(self):
		self._d['a']=2
		self._d['b']=2
		self._d['c']=2	
>>> a=A()
>>> b=B()
>>> a.init()
>>> l=[b,a]
>>> for i in l:print(i._d)
{'a': 1, 'b': 1, 'c': 1}
{'a': 1, 'b': 1, 'c': 1}

有群友提问,为什么输出结果都一样?

原因很简单:子类实例对象a和b所谓“继承”来的_d实际上是同一个“外部”(不拥有但可见可访问)对象。

请仔细观察下面经我粗暴标示的属性和方法:

>>> A.__dict__

mappingproxy({'__module__': '__main__', 'init': <function A.init at 0x0000024BF7073D08>, '__doc__': None})

>>> dir(A)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d', 'init']

>>> a.__dict__

{}

>>> dir(a)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d', 'init']

>>> B.__dict__

mappingproxy({'__module__': '__main__', 'init': <function B.init at 0x0000024BF7073D90>, '__doc__': None})

>>> dir(B)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d', 'init']

>>> b.__dict__

{}

>>> dir(b)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d', 'init']

>>> parent.__dict__

mappingproxy({'__module__': '__main__', '_d': {'a': 1, 'b': 1, 'c': 1}, '__init__': <function parent.__init__ at 0x0000024BF7073C80>, '__dict__': <attribute '__dict__' of 'parent' objects>, '__weakref__': <attribute '__weakref__' of 'parent' objects>, '__doc__': None})

>>> dir(parent)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d']

>>> parent().__dict__

{}

>>> dir(parent())

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d']

>>> id(a._d)

2525290427088

>>> id(b._d)

2525290427088

id()在不同条件、不同时间调用的结果并不一样,但不妨碍对相同对象的确定。

是否有发现?规律简单明了:__dict__确定拥有,dir()确定可见,通过拥有与可见共同“形成”python的“继承现象”。其中,“可见”含一种简单的“先实例再类再父类序列”的名称查找机制(更准确地,是对象确定机制),于是,就很容易理解最终输出结果为什么是相同的(后面有更具体的分析)。

改正方案很多,其中一种是:

>>> class parent:
	def __init__(self):
		self._d={}
		self._d['a']=0
		self._d['b']=0
		self._d['c']=0
>>> class A(parent):
	def init(self):
		self._d['a']=1
		self._d['b']=1
		self._d['c']=1		
>>> class B(parent):
	def init(self):
		self._d['a']=2
		self._d['b']=2
		self._d['c']=2		
>>> a=A()
>>> b=B()
>>> a.init()
>>> l=[b,a]
>>> for i in l:print(i._d)
{'a': 0, 'b': 0, 'c': 0}
{'a': 1, 'b': 1, 'c': 1}

这次,不妨用一个表格来看看(只考查显式定义成员):

对象

__dict__

dir()

parent

parent

__init__

__init__

parent()

{'_d': {'a': 0, 'b': 0, 'c': 0}}

__init__,_d

A

A

init

init

a

{'_d': {'a': 1, 'b': 1, 'c': 1}}

init,_d

B

B

init

init

b

{'_d': {'a': 0, 'b': 0, 'c': 0}}

init,_d

改正前后输出结果的对比总结如下(注意:python中类也是对象,__init__后面专门会探讨):

先说“可见”dir():类可见的属性,其实例一定可见;类不可见类实例属性;类可见的属性由自身拥有的属性和父类序列拥有的属性共同确定;可见则可访问。

再说“拥有”__dict__:类拥有的类属性,实例(无同名实例属性时)、子类(只可见父类的类属性,不拥有该属性,不可见父类实例属性)、子类实例都对其可见;类实例拥有的属性,类不可见,并被子类实例拥有(继承),子类不可见;谁拥有则对谁可见。

第一种情况分析:_dparent类属性

a,b实例化时,分别调用A,B的__init__(因为无显式定义__init__,通过名称查找找到parent.__init__,此时的参数self是实例a,b)进行初始化。

任何对象必须先建立,再操作。因为不是对象创建语句,所以接着,python对parent.__init__中self._d的_d的查找过程为:首先在a.__dict__中找_d,没有,则在A. __dict__中找_d,也没有,进一步在父类parent.__dict__中找_d,此时可以找到类属性_d。a,b合起来两次实例化,但初始化方法中的self._d都是parent._d,所以parent._d中三个键的值就被重复二次初始化为0。

同样,在a.init(普通方法,非__init__)中,通过self._d更新parent._d的值,而a,b两实例对象访问到的_d是同一个父类属性,所以输出相同结果。

第二种情况分析:_dparent实例属性

a,b实例化时,同上,通过parent.__init__初始化。但此时self._d={}是对象创建语句,所以这种语法上的_d在语义上是名为self的对象(更精确点,是self引用的对象)的属性。由于parent.__init__是实例方法,调用时参数self将是某个实例对象,所以_d为实例属性。那么_d是哪个实例对象的属性?实际上,self是谁,就是谁的属性,并不限于类parent的实例。所以a,b将各自拥有独立的属性_d,分别在a.__dict__和b.__dict__中。那么输出的结果不同就很容易理解了。

“可见”,决定一个对象在使用一个名称时,会不会抛出AttributeError异常;“拥有”,意味着这个名称代表谁。

如果类有同名的类属性和实例属性呢?以下面这种情形为例:

>>> class pd:
	d=2
	print(d,"=id:{}".format(id(d)))
	def __init__(self):
		self.d=9
		print(self.d,"=id:{}".format(id(self.d)))
2 =id:1611754592
>>> class dd(pd):
	def __init__(self):
		print(self.d,"=id:{}".format(id(self.d)))
		print(self.__class__.d,"=id:{}".format(id(self.__class__.d)))
>>> pd.d
2
>>> pd().d
9 =id:1611754816
9
>>> d=dd()
2 =id:1611754592
2 =id:1611754592

很明显,类访问到的是类属性,类实例访问到的是类实例属性。子类无论通过哪种方式,访问到的都是父类类属性。

如何确定操作的名称是哪个对象?

一句话总结:

先在实例中(即名称空间中,后同)找名称,再到类中找,最后按__mro__到父类序列中找;在哪一个环节找到,则停止查找并确定该对象。如果找不到,则抛出异常。注意:在父类中找的时候会找父类的类属性和类中包含的方法,不找父类的实例属性。即便找到名称,也还要考查名称的引用对象是否与另一个对象相同(同一个“值”对象)。

>>> class pd:
	def __init__(self):
		self.d=9
		print(self.d,"=id:{}".format(id(self.d)))
>>> class dd(pd):
	def __init__(self):
		print(self.d,"=id:{}".format(id(self.d)))
		print(self.__class__.d,"=id:{}".format(id(self.__class__.d)))
>>> d=dd()		     
AttributeError: 'dd' object has no attribute 'd'
>>> dd.d
AttributeError: type object 'dd' has no attribute 'd'

__init__可以是一个类方法,此时的初始化将创建类属性(同样,谁调用就创建谁的类属性)。

>>> class A:
	@classmethod
	def __init__(cls):
		cls.a=90
>>> a=A()
>>> A.__dict__
mappingproxy({'__module__': '__main__', '__init__': <classmethod object at 0x000001C1DF3124A8>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None, 'a': 90})
>>> a.__dict__
{}
>>> class B(A):pass
>>> b=B()
>>> B.__dict__
mappingproxy({'__module__': '__main__', '__doc__': None, 'a': 90})
>>>

 “到父类中找”涉及MRO。而且,名称查找还要考虑类的描述符,其实也比较简单,只是一个优先次序问题,暂不探讨。

对名称查找进一步理解还可以参考__getattribute__、__getattr__、__get__三种方法的使用。

不同的类实例访问同名的类属性是同一个对象,不同的类实例访问同名的实例属性是不同对象。

类中出现同名类属性和实例属性,类访问类属性,类实例访问类实例属性。子类与子类实例只能访问类属性。

如果是多继承呢?规律是一样的,只是多一个按MRO查找且就近决定原则:

>>> class pdp:
	d=90
	def __init__(self):
		      self.d=900
>>> class pd(pdp):
	d=2
	def __init__(self):
		self.d=9
>>> class pd2(pdp):
	d=22
	def __init__(self):
		self.d=99
>>> class dd(pd,pd2):
	def __init__(self):
		print(self.d)
		print(self.__class__.d)
>>> dd.__mro__		      
(<class '__main__.dd'>, <class '__main__.pd'>, <class '__main__.pd2'>, <class '__main__.pdp'>, <class 'object'>)
>>> d=dd()		      
2
2

可见即可访。

这里的“可见”不指我们可见,是我们创建的对象(类或类实例)可见。这种总结无法分析对象的身份,作为一个助记词就好。

2.1.2 从现象说起2:方法成员

类中的方法(不仅仅是@classmethod装饰的方法),即类中定义的方法,被类拥有。

>>> class parent:
	def __init__(self):
		self._d={}
		self._d['a']=0
		self._d['b']=0
		self._d['c']=0
	@classmethod
	def clamy(cls):
		      print("clamy--")
	@staticmethod
	def stamy():
		      print("stamy--")
	def __primy(self):
		      print("primy--")
	def _promy(self):
		      print("promy--")
>>> class A(parent):
	def init(self):
		self._d['a']=1
		self._d['b']=1
		self._d['c']=1
>>> class B(parent):
	def init(self):
		self._d['a']=2
		self._d['b']=2
		self._d['c']=2
>>> l=['parent.__dict__','dir(parent)','parent().__dict__','dir(parent())','A.__dict__','dir(A)','A().__dict__','dir(A())','B.__dict__','dir(B)','B().__dict__','dir(B())']
>>> a=A()
>>> b=B()
>>> for i in l:
print(i);
eval(i);
print()
parent.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function parent.__init__ at 0x00000184A280E378>, 'clamy': <classmethod object at 0x00000184A27F5F98>, 'stamy': <staticmethod object at 0x00000184A2C11198>, '_parent__primy': <function parent.__primy at 0x00000184A280E510>, '_promy': <function parent._promy at 0x00000184A280E598>, '__dict__': <attribute '__dict__' of 'parent' objects>, '__weakref__': <attribute '__weakref__' of 'parent' objects>, '__doc__': None})
dir(parent)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_parent__primy', '_promy', 'clamy', 'stamy']
parent().__dict__
{'_d': {'a': 0, 'b': 0, 'c': 0}}
dir(parent())
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d', '_parent__primy', '_promy', 'clamy', 'stamy']
A.__dict__
mappingproxy({'__module__': '__main__', 'init': <function A.init at 0x00000184A280E620>, '__doc__': None})
dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_parent__primy', '_promy', 'clamy', 'init', 'stamy']
A().__dict__
{'_d': {'a': 0, 'b': 0, 'c': 0}}
dir(A())
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d', '_parent__primy', '_promy', 'clamy', 'init', 'stamy']
B.__dict__
mappingproxy({'__module__': '__main__', 'init': <function B.init at 0x00000184A280E6A8>, '__doc__': None})
dir(B)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_parent__primy', '_promy', 'clamy', 'init', 'stamy']
B().__dict__
{'_d': {'a': 0, 'b': 0, 'c': 0}}
dir(B())
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_d', '_parent__primy', '_promy', 'clamy', 'init', 'stamy']

首先,如前所说,类中出现的方法都是类拥有的方法(包括实例方法,注意区别@classmethod装饰的类方法),因而,类比类的属性很容易理解上面输出结果:对于类中的方法(显式定义的),类实例、子类、子类实例都可见。同样,类实例属性对类不可见,对子类及子类实例不可见。

其次,区分方法的性质。

不管是实例方法、类方法、静态方法、保护实例方法、私有实例方法……都是类中的方法,被类所拥有,且不同性质的方法有调用形式上的区别和限制(由隐式参数传递和名称可见决定)。

要注意的是私有方法__primy和保护方法_promy,特别是私有方法,如果用定义时的名称来调用,则会得到一种“私有”的“表象”(访问时抛出异常效果,这也是之前为什么说可见是对象可见,而不是我们可见的原因),在后面会围绕python的“duck typing”来说明。

再来说一说__init__。虽然__init__对所有的类、类实例可见,但是,尽管名称相同,而拥有者却不同,即虽然都是__init__,但却是不同对象

>>> s='parent,parent(),A,a,B,b'

>>> l=s.split(',')

>>> for i in l:

      sta='id('+i+'.__init__)'

      print(sta)

      eval(sta)

      print()

id(parent.__init__)

1669173666680

id(parent().__init__)

1669172229128

id(A.__init__)

1669173666680

id(a.__init__)

1669172229128

id(B.__init__)

1669173666680

id(b.__init__)

1669172229128

1669173666680是什么?很简单,验证一下:

>>> A.__init__             

<function parent.__init__ at 0x00000184A280E378>

>>> int('0x00000184A280E378',16)               

1669173666680

原来是parent.__init__的内存地址。

parent中定义一个方法__init__,即拥有__init__,所以对子类A,B可见(即被继承,通过名称查找,A__init__、B.__init__指向parent.__init__),按常规来说,__init__这个初始化方法应该只有类才能拥有(用于初始化实例),但是,因为def __init__(self)也是一个实例方法,和其他普通实例方法一样,在使用上应该可以被类实例对象调用。

那么parent().__init__、a.__init__和b.__init__又是谁呢?

>>> parent().__init__
<bound method parent.__init__ of <__main__.parent object at 0x000001EA460FA6D8>>
>>> a.__init__
<bound method parent.__init__ of <__main__.A object at 0x000001EA4609F748>>
>>> b.__init__
<bound method parent.__init__ of <__main__.B object at 0x000001EA460FA6A0>>

可以看出,都是一个绑定到parent.__init__的方法:

>>> a._d		      
{'a': 0, 'b': 0, 'c': 0}
>>> b._d		      
{'a': 0, 'b': 0, 'c': 0}
>>> a._d=12345		      
>>> a.__init__()		      
>>> a._d		      
{'a': 0, 'b': 0, 'c': 0}
>>> a.__init__.__qualname__		      
'parent.__init__'
>>> a.__init__.__init__.__qualname__
'object.__init__'
>>> a.__init__.__init__.__init__.__qualname__
'object.__init__'
>>> a.__init__.__func__		      
<function parent.__init__ at 0x00000184A280E378>
>>> id(a.__init__.__func__)		      
1669173666680

绑定方法a.__init__可看作是一个中间方法,类似于对parent.__init__的封装。

如果子类重新定义属性对象或方法对象呢?

>>> class parent:
	def __init__(self):
		self._d={}
		self._d['a']=0
		self._d['b']=0
		self._d['c']=0
	@classmethod
	def clamy(cls):
		      print("clamy--")
	@staticmethod
	def stamy():
		      print("stamy--")
	def __primy(self):
		      print("primy--")
	def _promy(self):
		      print("promy--")
>>> class A(parent):
	def init(self):
		self._d['a']=1
		self._d['b']=1
		self._d['c']=1
>>> class B(parent):
	def __init__(self):
		self._d={'a':-1,'b':-1,'c':-1}
	def init(self):
		self._d['a']=2
		self._d['b']=2
		self._d['c']=2
>>> a=A()
>>> b=B()
>>> a._d
{'a': 0, 'b': 0, 'c': 0}
>>> b._d
{'a': -1, 'b': -1, 'c': -1}
>>>

对于子类A,见到的__init__是parent.__init__,而子类B见到的则是自己拥有的__init__,仍满足就近原则。

2.1.3 小结

在类中,实例属性和类属性有创建上的区别:位置区别和方式区别;

实例方法的拥有者是类,类方法的拥有者也是类,实例方法和类方法的区别在于定义形式、调用时参数传递方式;

创建实例的时候,会调用类的__init__进行初始化(__new__相当于构造函数)。__init__可以是类方法,一般是实例方法。

如果类没有显式定义__init__,__init__这个对象来自哪里?或显式或隐式继承自父类。注意,这里的继承仅是于我们从程序表现结果上感知的“继承”。

不管父类__init__是类方法(第一个隐式参数是类,不妨用cls表示),还是实例方法(第一个隐式参数是实例对象,不妨用self表示),python不会在调用它时进行实际参数的类型检查和相容类型间的类型转换,而是在通过cls、self操作属性时进行有还是没有该属性、类型与操作是否匹配的检查。除了在初始化会自动调用__init__外,任何时候可以像调用其它方法成员一样调用它;实例对象可以调用类方法(将隐式传递实例的类为第一个参数),类对象可以调用实例方法(此时要显式传递实例对象作为第一个参数,在实例对象调用实例方法时会对第一个参数进行隐式参数传递,因此对于没有参数的实例方法只能通过类来调用);继承父类__init__主要是为类提供(非拥有)类属性和创建类实例属性。

如果显式定义了__init__,那么实例初始化时调用的就是类拥有的__init__。可以在类拥有的__init__中显式调用父类__init__,从而在继承的基础上完成新特性的创建。

实例成员(属性或不是类拥有的方法)只对实例自身可见,无论何时创建何处创建;类拥有的成员(定义时创建的和后来使用中创建的,如果是方法则要区别于@classmethod修饰的类方法,包括类中显式定义的任何类型方法;类方法与实例方法的区别在于调用时隐式传参机制,被@static修饰的静态方法不会隐式传递参数)对所有实例有效且可见。

对于属性,无论是类的还是实例的,还要区分引用/指向的对象是否是同一个值对象,特别是在“=”语句中对非字面值对象操作时。

对于一个成员(包括类成员和实例成员,成员可以是属性,也可以是方法,并可以具备静态、保护、私有等性质)的确定,从内在来说是名称查找机制,从外在来说则是“可见”+“拥有”。

2.1.4 拓展:可见与拥有的关系

通过“可见”“拥有”两个维度很容易分析出已存在的一个名称引用的对象是谁。那么这两个维度之间又是什么关系呢?是可见即拥有,还是通过查找从而确定可见与拥有?因为之前我说的拥有是和__dict__关联的,可见是和dir()关联的,__dict__是类与实例在生成(这是一个很大的主题,以后再介绍)时就已创建的,而dir():

>>> help(dir)

Help on built-in function dir in module builtins:

dir(...)

    dir([object]) -> list of strings   

    If called without an argument, return the names in the current scope.

    Else, return an alphabetized list of names comprising (some of) the attributes of the given object, and of attributes reachable from it.

    If the object supplies a method named __dir__, it will be used; otherwise the default dir() logic is used and returns:

    for a module object: the module's attributes.

    for a class object:  its attributes, and recursively the attributes of its bases.

    for any other object: its attributes, its class's attributes, and recursively the attributes of its class's base classes.

我一直以来说的“属性”“方法”都属于帮助文档中的attributes

很明显,对于某条语句中的obj.name,python不是立即通过name引用的对象确定它的拥有者,而是先查找,再确定name引用的对象。

2.2 python类体系:一切皆对象,对象的访问可通过“可见+拥有”分析

明白父类、子类之间上述这些内在逻辑后,不妨再拓展到所有类:class定义的类或显式或隐式、或直接或间接地继承自object,那么对象(子类与子类实例)能够见到的那些或显式定义或隐式继承、或来自object或来自中间父类的属性、方法就容易理解了。

要区分子类与父类、实例与类、对象与对象类型、类与类型;要区分父类__base__和类型type;要理解父类、子类、父类实例、子类实例、值、类型都是对象。

当然不应止步于此,不妨再拓展到整个python。

>>> import keyword
>>> keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

python(3.6.5)关键字中,与名称可见直接相关的有as、del、from、global、from、import、nonlocal、class、def、lambda,但是可用于定义对象、与“拥有”(全局范围则是拥有者,其他语句块范围则是被拥有者)直接相关的只有class(定义类),def、lambda(定义class function的对象),实际上,一些特定位置也应该包含进来:比如“=”语句(可创建值对象)。

至于方法/函数的参数、方法/函数的返回对象、迭代器/生成器的返回对象、各种语句块中的对象,会根据对象所在位置和对对象的不同操作影响其可见和拥有。

通过追踪任意对象的__class__(即type)和__base__,最终都会进入python类体系,从而可以用“可见+拥有”去分析。

>>> class A:pass
>>> def f():pass
>>> x=lambda x:x+1
>>> a=9
>>> b=A()
>>> type(b)
<class '__main__.A'>
>>> type(A)
<class 'type'>
>>> type(f)
<class 'function'>
>>> type(x)
<class 'function'>
>>> type(function)
NameError: name 'function' is not defined
>>> type(type(f))
<class 'type'>
>>> type(a)
<class 'int'>
>>> type(int)
<class 'type'>
>>>
>>> def f(fobj):
	  print(fobj,"-->type:{}  id:{}".format(type(fobj),id(fobj)))
	  return fobj
>>> a=9
>>> id(a)
1529507136
>>> id(f(a))
9 -->type:<class 'int'>  id:1529507136
1529507136
>>> id(9)
1529507136
>>> id(f(9))
9 -->type:<class 'int'>  id:1529507136
1529507136
>>>
>>> dir(9)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> 9.__dict__
SyntaxError: invalid syntax
>>> a.__dict__
AttributeError: 'int' object has no attribute '__dict__'
>>>

在分析的时候,有时会看到一些限制,如不允许实例对象自己拥有非类指定的对象。这种“不允许”是怎么实现的呢?常见做法设置是__slots__(只能约束实例属性,但会导致实例对象自己不能拥有新的方法成员),并有语法(对应SyntaxError)、名称可见性(对应AttributeError或NameError)、对象与操作间的一致性(对应TypeError或ValueError)等机制保障其实现。

>>> class A:
	Aname='A'
	Aage=9
	Ano=2
	__slots__=("age",)
	def __init__(self):
		self.age=9
		self.name='A'
		self.no=2		
>>> a=A()
AttributeError: 'A' object has no attribute 'name'
>>> class A:
	Aname='A'
	Aage=9
	Ano=2
	__slots__=("age","name","no","say")
	def __init__(self):
		self.age=9
		self.name='A'
		self.no=2
	def say(self):print("Hello",self.name)
ValueError: 'say' in __slots__ conflicts with class variable
>>> class A:
	Aname='A'
	Aage=9
	Ano=2
	__slots__=("age","name","no")
	def __init__(self):
		self.age=9
		self.name='A'
		self.no=2

>>> a=A()

>>> a.age

9

>>> a.__dict__

AttributeError: 'A' object has no attribute '__dict__'

>>> a.addage=12

AttributeError: 'A' object has no attribute 'addage'

>>> a.f=lambda x:x+1

AttributeError: 'A' object has no attribute 'f'

>>>

>>> a=A()

>>> dir(a)

['Aage', 'Aname', 'Ano', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'age', 'name', 'no']

>>> dir(A)

['Aage', 'Aname', 'Ano', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'age', 'name', 'no']

>>> A.__dict__

mappingproxy({'__module__': '__main__', 'Aname': 'A', 'Aage': 9, 'Ano': 2, '__slots__': ('age', 'name', 'no'), '__init__': <function A.__init__ at 0x0000023B89583950>, 'age': <member 'age' of 'A' objects>, 'name': <member 'name' of 'A' objects>, 'no': <member 'no' of 'A' objects>, '__doc__': None})

>>> id(A.Aname)

2105699924824

>>> id(a.name)

2105699924824

>>> a.name='aaa'

>>> A.Aname

'A'

>>>

从可见与拥有去分析某个类在定义__slots__后的内在机制并基于该机制分析上面输出的AttributeError原因你get到了吗?这里考验一下你的概括水平。__slots__的具体使用这里就不介绍了,最后看看两大终极BOSS

>>> class A:pass
>>> A.__base__
<class 'object'>
>>> object.__base__
>>> type(A)
<class 'type'>
>>> type(type)
<class 'type'>
>>> type(object)
<class 'type'>
>>> type.__base__
<class 'object'>
>>>

探究这么久,到底在说啥?

大道至简:可以访问(操作)谁,是在对谁访问(操作)。

2.3 这是一个彩蛋:什么是知识

首先,没有知识,就没有相应结果。行为(有意识或无意识的)的前提是经验性或理论性知识。在同一个维度上,观念不对,行为结果就不对。这是毫无疑问的。

其次,借用一句经典,道可道,非常道,名可名,非常名,被人谈论的世界是具有客观虚拟性的(什么是客观虚拟性?看看我置顶的那篇关于自我调节的文字)。知识的内容远远超越客观世界本身,可能与客观世界及其规律一一对应,也可能仅仅是一种意识对象层面交流、改造又基于正确逻辑推理的结果。逻辑推理和客观规律不是一一对应的。

我们可以做个简单又可行的思想实验:向小学生教高中数学内容,比如函数概念。

高中课本中函数概念定义如下:设A,B是非空的数集,如果按照某种确定的对应关系f,使对于集合A中的任意一个数x,在集合B中都有唯一确定的数f(x)和它对应,那么就称f:A->B为从集合A到集合B的一个函数。

对于五、六年级学生来说,对“集合”“数集”可能有模糊印象,但是对于“设A,B是非空数集”“对应关系f”“任意一个数x”“在集合B中都有唯一确定的数f(x)”“对应”其中任意一个词或短语,经我初步估计都会是一团浆糊,有的学生甚至浆糊都没有,一片虚空。那么,怎么教会小学生?

简单,在属于函数概念范畴这道底线的前提下,不妨定一个适合小学生的教学目标,教学内容上只取部分外延,并在理解上“可视化”。

方程版:对于某个含有未知数等式的一般形式x+3=5,如果用另一个字母y代替5,形成的等式就是一个y和x之间的函数;

活动版:小明跟妈妈去商店买5支铅笔,妈妈和商店老板进行了讨价还价,小明发现,随着妈妈砍价,当铅笔单价变化时,总价也跟着变化。此时,“单价”和“总价”之间形成一个“函数”关系;

公式版:我们已经学过一些公式,比如路程=速度×时间、圆柱体积=底面积×高……这些公式中,等号“=”左边的量和右边的确定量构成一个函数。

……

为什么函数概念只有一个,却可以有多种不同呈现?

再比如,最初类属性值被改写的例子,既可以从“可见+拥有”解释与指导程序编写,也可以从“继承”解释与指导程序编写,还可以从名称查找机制解释与指导程序编写,为什么特定条件下的同一种现象可以有多种解释与指导方式?

再简单点,我们三个人在一口水井前,我戴的是无色眼镜,你戴的是黑色眼镜,他戴的是绿色眼镜,虽然三人都陶醉于自己看到的水井而对其他二位欣赏到的美景大加排斥,但并不影响口渴的时候三人都能正确地做出从井中取水解渴的行为。

再实际一些,考试中有时会用到解题技巧,如我从网上搜出的数学选择题十大速解方法:排除法、增加条件法、以小见大法、极限法、关键点法、对称法、小结论法、归纳法、感觉法、分析选项法。我们不妨来个简单又夸张的例子。

已知函数y=f(x)存在反函数y=g(x),若f(3)=-1,则函数y=g(x-1)的图象:()

A、过点(0,3)   B、book      C、唐伯虎点秋香    D、print(“Hello world”)

解法一:排除法,属于数学答案的只有A,选A;

解法二:

∵y=f(x)的反函数是y=g(x)且f(3)=-1

∴g(-1)=3

∵当x-1=-1的时候,g(x-1)=3,此时x=0,g(x)=3

∴y=g(x-1)的图象必过点(0,3)

故选A.

解法三:

∵y=f(x)的反函数是y=g(x)且f(3)=-1

∴g(-1)=3,即y=g(x)过点(-1,3)

∵y=g(x-1)是y=g(x)向右平移一个单位

∴y=g(x-1)的图象必过点(0,3)

故选A.

附加一个话题。一周前学习sklearn时,注意到模型生成时对过拟合的处理。归根结底,本来,程序生成的是一比一反映训练数据某种语义结构的模型,由于样本不够全,为能使模型代表的规则更抽象,以反映数据的更深层含义,能够对新数据推理且合理,便产生过拟合处理、基于概率而非具体数据生成模型、丢失数据的填充(有时候仅仅为了在逻辑思维上能够正常推理下去、算法得以正常进行而引入辅助量或填充数据的做法是理论、模型偏离实际规律的开端)等多种辅助方法。也就是说,在样本不全的情况下,试图生成适合样本总体的模型。

结合机器学习,请仔细探究下面这段文字:

昨天,我带小明去动物园游玩。一进门,一股大风吹来,我顿时感到天旋地转,一时没站稳狠狠地摔倒下去。愰惚中,门口旁巨大宣传牌也在大风中裂成二半,一半掉落在地,一半辟到游客身上。这时,风中传来巨响:“啊噢——”。

请快速阅读:

昨天,我带小明动物园游玩。一进门,一股大吹来,我顿时到天旋地转,一时没站稳狠狠地摔倒下去。愰惚中,门口旁大传牌也风中裂成二半,一掉落在地,一半辟到游客身上。这时,风中传来响:“啊噢——”。

请更快些:

昨天,我带小明去动物园游玩。一进门,一股大风吹来,我顿时波坡的啊无转,一时没站稳狠狠地摔倒下去。愰惚中,门口旁巨大宣传牌也在吧是得闻裂成二半,一半掉落在鸡旗西之,一半辟到游客身上。这时,风中传来巨响:“啊噢——”。

为什么在信息丢失或不全时,我们还是能正确理解?

再回头去看看“客观虚拟性”,“知识的内容远远超越客观世界本身,可能与客观规律一一对应,也可能仅仅是一种意识对象层面交流、改造又基于正确逻辑推理的结果。”

3、C++和python在实现继承上的几点比较

3.1 “继承”概念

很显然,如果程序语言能够解决客观世界的问题,那么无论其语法表达如何,语义中一定要有与客观世界中的对象一一对应起来之物,比如说,继承”,在客观世界中反映特性的传递,在程序设计上反映代码重用,在程序语言语义上反映客观世界规律变化:特性的传递

维基百科:

继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”,也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即子类的行为在编译期就已经决定,无法在运行期扩展。

从这段文字中,我们能了解到什么?

父类、子类概念;

继承的实质:子类具有父类的属性与方法,不需要二次编写相同代码;子类既可以覆盖父类的方法,完成自身传承与独立,也可以新增属性和方法,实现对新情境的适应与自身发展。

静态:类型(类)的定义在编译时确定,一经确定无法更改。

动态:非编译期间构建对象(类型)。

概括水平较高的非专业人员眼中的继承可能会就此打住,但对于专业人员来说,在继承的实现上还有许多细节要处理,如继承的深度(从祖先类到当前类)与广度(单个父类与多个父类),哪些特性需要被继承,实例创建与销毁,属性与方法的身份确认(对于以存储区域适用名称的语言如C++,父类属性包含在子类存储空间中,且名称与存储空间关系密切,这种问题不大;而对于以名称适用存储区域的语言如python,名称仅仅是对值对象的引用,名称本身无任何意义,仅提供一种“可见”,这种问题比较突出),新属性与新方法的延后出现(我们出生时只会哭,上学后会写课文中心思想)对应的实现方式等。

3.2 C++中的继承实现

一种程序设计语言围绕继承及具体实现继承特性的机制比较复杂,我已经很久没有看C++,没法展开。这里主要集中讨论两点:属性(数据成员)和方法(函数成员)的访问(3.3节同此)。

看一个很简单的例子。

#include<iostream>
using namespace std;
class A
{
public:
	int a = 1;
	int b = 2;
public:
	int add(int a, int b)
	{
		return a + b;
	}
};
class B :public A
{
public:
	int c = 0;
};
typedef int (A::*fp)(int, int);
int main()
{
	A a;
	B b;
	fp f = &A::add,f2=&B::add;
	cout<<sizeof(A)<<"  "<<sizeof(B)<<endl;
	cout << &a.a << "  " << &b.a << endl;
	cout << f << "  " << f2 << endl;
	system("pause");
}

运行结果:

A、B两个类在内存中的布局:

C++中,子类对父类数据成员的继承是开辟存储区域的。

定义一个类的时候,到底发生了什么?下面是一段经过我整理的资料(可搜索关键字“C++类的底层机理”):

依据C++语言的定义,定义/声明一个C++类实际上是声明或定义以下几类内容:

1.声明一个数据结构,存放类中的非静态数据成员、代码中看不到但若有虚函数就会自动生成的虚表入口地址指针。

2.声明并定义一些函数,它们第一个参数都是一个指向这个数据结构的指针。这些函数实际上就是类中那些非静态成员函数(包含虚函数),它们尽管写在类中一对大括号内部,但实际上没有产生任何被添加到第1条所说的内部数据结构中的相关内容。

实际上这种声明仅仅是为这些函数添加两个属性:函数名标识符的作用域为所在的类;函数第一个参数是this,调用时省略不写。

3.声明并定义另一些函数。它们看上去就是一些普通函数,与这个类几乎没有关系。这些函数实际上就是类中那些静态函数。它们也不会在第1条所说的内部数据结构中添加相关内容,仅仅是函数名标识符的作用域被限制在类中。

4.声明并定义一些全局变量,这里指类中的静态数据成员,不存放在类的数据结构,但名称标识符的作用域被限制在类中,所有对象共享各静态数据成员。

一个样例:

class MyClass
{
public:
    int x;
    int y;
    void Foo();
    void Bar(int newX, int newY);
    virtual void VFoo();
    virtual void VBar(int newX, int newY) = 0;
    static void SFoo();
    static void SBar(int newX, int newY);
    static int sx;
    static int sy;
};

再看之前的例子(稍作变动):

class A
{
public:
	int a;
private:
	static int b;
public:
	int add(int a, int b)
	{
		return a + b;
	}
	virtual int sub(int a, int b);
};
class B :public A
{
public:
	int c = 0;
	int sub(int a, int b)
	{
		return a - b;
	}
};
int A::b = 2;
typedef int (A::*fp)(int, int);

结论:

C++继承体系中,类的数据成员是被子类继承的(子类内部数据结构中开辟存储空间),静态数据成员因为不在类数据结构所以例外,但仍被所有类对象和子类对象共享,只要可被访问。而对于函数(方法)成员,和类外部定义的普通函数没太多区别,主要区别在于第一个参数是否是类数据结构指针和函数名称标识符被限制在类中,只要允许,子类的对象或函数成员都可访问。

对于一个名称标识符所代表的对象的确认,编译期间(也即事先)便已完成,使用时名称即对象(实例对象、数据成员、函数成员),而不是先查找再确认再使用。

3.3 python中的继承实现

为什么我时常觉得python的继承不是一种标准意义上的继承?

>>> class A:
	cls=9
	def __init__(self):
		self.obj=10
	def say():print("Hello")	
>>> class B(A):
	def __init__(self):
		super(B,self).__init__()
		self.obj2=20		

先从表面现象来分析这个例子中的“继承”概念。

A有两个属性,两个方法,那么子类B都继承到了什么?

>>> a=B()
>>> a.cls
9
>>> a.obj
10
>>> a.obj2
20
>>> a.__class__.say()
Hello
>>>

类A的属性cls、obj和方法say被子类继承,方法__init__被子类覆盖。根据之前的探究可知,在没有同名的情况下,cls相当于C++类的静态数据成员,被所有子类实例对象共享。

听起来,这不错嘛,继承不就是这样吗?

可是刚刚为什么要说“表面现象”?

python并没有像C++那样有一套比较接近标准继承概念的机制和相应处理方式,而是提供一套自己的“积木”,我们必须用这些积木搭建出“继承”效果:

>>> class A:
	cls=9
	def __init__(self):
		self.obj=10
	def say():print("Hello")	
>>> class B(A):
	def __init__(self):pass	
>>> a=B()
>>> a.cls
9
>>> a.obj
AttributeError: 'B' object has no attribute 'obj'

同样是class B(A),为什么会没有继承到A的obj?

python中,实际上是否实现“继承”,不在于形式(类的继承语法),而在于我们怎样运用这些“积木”。那么,继承语法class B(A)的作用是什么呢?我个人认为,这只是从名称搜索机制上拓展了“可见与可访”范围,能起到不重复代码的作用,但这种作用更多的是着眼于链式组合,而不是对“继承”的完整保证。

或许可以这么来看,python语言本身着眼于简单编程,简明易懂,所以一些太过灵活运用的语句,很多时候能超出概念上的预期和Guido van Rossum最初的那种美好憧憬。

>>> class A:
	cls=9
	def __init__(self):
		self.obj=10
	def say():print("Hello")	
>>> class B(A):
	def __init__(self):
		super(B,self).__init__()
		self.obj2=10		
>>> a=A()
>>> b=B()
>>> id(a.obj)
1618373984
>>> id(b.obj)
1618373984
>>> id(b.obj2)
1618373984
>>>

从输出的内存地址可知,实例对象a的属性obj、实例对象b的属性obj、obj2三者竟然是同一个对象。这也太让人大跌眼镜。

在python中,对象名称和实际对象是一种引用关系而不是代表关系(这绝对让C/C++毁三观),类似C++中指针和所指内存区域的关系。所以,上面输出的都是同一个值对象的地址,而不是名称本身地址。

结论:

python中的继承是一种通过一系列内在机制呈现的“看起来是”式的继承,子类实例不是一个真正意义上的继承对象,是与不是最主要的区分标志应是:拥有自己的存储区域,幷包含父类中可被继承的存储结构。

视野大小是思维的刚性BUG。坐井观天只能探究天的深度,无法探究天的广度。所以,此时此刻,我知道我的结论并不是绝对完美。

欢迎任何真诚且理性又专业的批评指教。

3.4 网络小品:duck typing

资料(经过整理)来源:https://www.jb51.net/article/92464.htm

维基百科:

在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象的“有效语义”,不是由继承自特定的类或实现特定的接口决定,而是由方法和属性的集合决定。这个概念来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

在鸭子类型中,关注的不是对象的类型本身,而是它是如何被使用的。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它只能接受一个类型为鸭子的对象,并于其中调用它的走方法和叫方法。而在使用鸭子类型的语言中,可调用走方法和叫方法的函数可以接受任意类型的对象。如果被调用的方法(如走方法、叫方法)不存在,那么将引发一个运行时错误。“任何拥有正确的走方法和叫方法的对象都可被函数接受”的这种行为引出了以上表述,这种决定类型的方式因此得名。

>>> class CollectionClass():
	lists = [1,2,3,4]
	def __getitem__(self, index):
		return self.lists[index]	
>>> class Another_iterAbleClass():
	lists=[1,2,3,4]
	list_position = -1
	def __iter__(self):
		return self
	def __next__(self):
		self.list_position += 1
		if self.list_position >3:
			raise StopIteration
		return self.lists[self.list_position]	
>>> iter_able_object = CollectionClass()
>>> another_iterable_object=Another_iterAbleClass()
>>> print(iter_able_object[1])
2
>>> print(iter_able_object[1:3])
[2, 3]
>>> another_iterable_object[2]
TypeError: 'Another_iterAbleClass' object does not support indexing
>>> print(next(another_iterable_object))
1
>>> print(next(another_iterable_object))
2
>>> print(next(iter_able_object))
TypeError: 'CollectionClass' object is not an iterator
>>>

第一个类实现__getitem__方法,那么python的解释器就会把它当做一个collection,就可以在这个类的对象上使用切片、获取子项等方法。第二个类实现__iter____next__方法,python就会认为它是一个iterator,就可以在这个类的对象上通过循环来获取各个子项。一个类可以实现它有能力实现的方法,并只能被用于在它有意义的情况下。

python鸭子类型的灵活性在于它关注的是调用的对象是如何被使用的,而没有关注对象类型的本身是什么。所以在python中使用isinstance来判断传入参数的类型是不提倡的,更pythonic的方法是直接使用传入的参数,通过tryexcept来处理传入参数不符合要求的情况。我们应该基于传入对象的能力而不是传入对象的类型来使用该对象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章