從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來處理傳入參數不符合要求的情況。我們應該基於傳入對象的能力而不是傳入對象的類型來使用該對象。

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