2010年12月22日 星期三
將Roaming-Ralph.p3d嵌至網頁上的測試
以上是將Roaming-Ralph.p3d嵌至網頁上的測試。
請先安裝panda3d runtime程式。
接受執行授權後按下綠色開始鈕。因為沒有申請正式的認證,所以只用測試的認證方式,沒有毒,請安心使用。
操作:方向鍵左右,方向鍵上。
2010年12月19日 星期日
發佈panda3d應用程式的獨立執行檔
Panda3D-1.7.0提供一種讓panda3d應用程式獨立執行的方式:
panda3d runtime甚至能執行嵌在網頁上的panda3d應用程式。例如官方網站的Airblade。
p3d檔的包裝方式可參考網頁。不過,在Panda3D-1.7.0中,首先得從此下載缺失的檔案packp3d.p3d,放在panda3d的bin資料夾內(參考討論串)。並且必須安裝panda3d runtime程式。
用前一個範例Roaming-Ralph做p3d包裝測試。照參考討論串所描述,在命令模式下鍵入:
指定執行起始檔名: -m mystart.py,沒指定則預設為main.py。
等一段時間後,packp3d即將目標資料夾包裝成獨立的p3d檔案。(下載Roaming-Ralph.p3d)
點擊此檔即可開啟panda3d應用程式。
發佈時僅需要此獨立的p3d檔案,並且在執行環境中安裝panda3d runtime。
以下是將Roaming-Ralph.p3d嵌至網頁上的測試。接受執行授權後按下綠色開始鈕。請先安裝panda3d runtime程式。
將應用程式包裝成p3d檔案,可在有安裝panda3d runtime環境下執行。
panda3d runtime甚至能執行嵌在網頁上的panda3d應用程式。例如官方網站的Airblade。
p3d檔的包裝方式可參考網頁。不過,在Panda3D-1.7.0中,首先得從此下載缺失的檔案packp3d.p3d,放在panda3d的bin資料夾內(參考討論串)。並且必須安裝panda3d runtime程式。
用前一個範例Roaming-Ralph做p3d包裝測試。照參考討論串所描述,在命令模式下鍵入:
D:\Panda3D-1.7.0\bin\packp3d.exe -o Roaming-Ralph.p3d -d D:\Panda3D-1.7.0\samples\A_Roaming-Ralph_OO(packp3d -o 包裝輸出檔名.p3d -d 包裝來源資料夾路徑)
指定執行起始檔名: -m mystart.py,沒指定則預設為main.py。
等一段時間後,packp3d即將目標資料夾包裝成獨立的p3d檔案。(下載Roaming-Ralph.p3d)
點擊此檔即可開啟panda3d應用程式。
發佈時僅需要此獨立的p3d檔案,並且在執行環境中安裝panda3d runtime。
以下是將Roaming-Ralph.p3d嵌至網頁上的測試。接受執行授權後按下綠色開始鈕。請先安裝panda3d runtime程式。
2010年12月18日 星期六
Case Study:Roaming-Ralph
簡介
Roaming-Ralph演示一個在場景(地形)中行走的角色,以第三視角觀看。我嘗試將原本的範例程式改寫得更容易理解。這裡傾向表達清楚,並沒有對效能優化多做調整。原始檔可由此下載。
操作方式:方向鍵左右表示左右轉,方向鍵上表示前進。
此範例將會用到:
- 起始panda3d
- 接收鍵盤按鍵事件
- 3d模型的位置移動與面向設定
- 碰撞偵測的設定
- 碰裝物件的擷取
- 攝影機位置與面向設定
在這個範例中,共有四種物件。
1. 控制器 -- 接收鍵盤事件的的控制器。
2. 角色 -- 根據鍵盤命令移動的角色。
3. 場景 -- 場景提供角色與地面的碰撞偵測,讓角色能走在地面上,並且不能進入障礙物如樹,石頭中。
4. 攝影機 -- 跟著角色移動的攝影機。
最後用一個世界物件,將上述四種物件裝起來。
因此我將檔案分成--
main.py -- 執行起始。
_define.py -- 一些定義,主要是控制器的命令定義。
World.py -- 世界類別。
Controller.py -- 控制器類別。
Avatar.py -- 角色類別。
Environment.py -- 場景類別。
CameraController.py -- 攝影機類別。
以下簡單描述各檔案的內容。
世界類別
main.py檔案很單純的僅是產生一個世界類別,然後開始panda3d的main loop。
世界類別,World.py。先載入角色與場景的3d模型(函數_model_load中,呼叫_avatar_model_get與 _environment_model_get函數),並賦予給角色物件與場景物件。設定好控制器(_controller),角色(_avatar),場景(_environment),攝影機(_camera_ctrl)等物件後。將每個frame更新的函數(_main_loop)掛上 taskMgr。
每個frame的更新函數(_main_loop)會依序呼叫角色的更新函數,場景的更新函數,最後是攝影機更新函數。
角色更新函數主要是從控制器物件獲取目前命令狀態,並依此做角色的3d模型位置的y值與面向更新。
場景更新函數主要是讓角色能站在地面上,依據碰撞偵測的反應更新角色模型位置的z值。
最後,攝影機取得角色的位置,並依此更新攝影機的位置與面向。
角色更新與場景更新函數可這麼看:角色更新函數依據輸入命令與自身狀態,給出理論的角色行為。場景更新函數依據角色與場景間的關係,修正理論的角色行為,得到實際上的角色行為。如此範例中,角色更新函數依據輸入命令前進,但場景更新函數因為角色將會進入障礙物而不讓角色前進,修正角色的位置為改變前的位置。
控制器類別
控制器類別,Controller.py。內含目前命令狀態(key_state)。在接收鍵盤事件後,更新目前命令狀態值(函數setKey)。在這個範例中僅有三種命令--左轉,右轉,與前進。
鍵盤事件僅包含按鍵按下與按鍵放開,例如"arrow_left"表示左箭頭按鍵按下的事件,"arrow_left-up"表示左箭頭按鍵放開的事件。因此,按鍵持續按著的這個動作就由key_state保存,在按下後設定對應的key_state命令狀態為1,與放開後設定對應的key_state命令狀態為0。
角色類別
角色類別,Avatar.py。在每個frame時,從控制器取得目前命令狀態(函數_cmd_get),藉此更新角色3d模型的y值與面向(函數_pos_update),並更新角色3d模型的動畫狀態(函數_animation_update)。函數pos_recover用來恢復角色3d模型改變前的位置,於之後的場景類別中使用。
場景類別
場景類別,Environment.py。設定角色的碰撞(函數_collision_setup),於每個frame更新時檢查碰撞事件(函數 _collision_object_list_get),並藉此更新角色3d模型位置的z值(函數_collision_handle),讓角色能隨著地形起伏改變高度,達到"站在地上"的視覺效果。當角色3d模型"進入"場景中的石頭或樹木等障礙物時,恢復到角色移動前的位置(角色類別的函數 pos_recover),如此角色就不會穿越障礙物了。
攝影機類別
攝影機類別,CameraController.py。這裡只是很簡單的設定攝影機的位置與面向,讓角色能以第三人視角表現。
Roaming-Ralph演示一個在場景(地形)中行走的角色,以第三視角觀看。我嘗試將原本的範例程式改寫得更容易理解。這裡傾向表達清楚,並沒有對效能優化多做調整。原始檔可由此下載。
操作方式:方向鍵左右表示左右轉,方向鍵上表示前進。
此範例將會用到:
- 起始panda3d
- 接收鍵盤按鍵事件
- 3d模型的位置移動與面向設定
- 碰撞偵測的設定
- 碰裝物件的擷取
- 攝影機位置與面向設定
在這個範例中,共有四種物件。
1. 控制器 -- 接收鍵盤事件的的控制器。
2. 角色 -- 根據鍵盤命令移動的角色。
3. 場景 -- 場景提供角色與地面的碰撞偵測,讓角色能走在地面上,並且不能進入障礙物如樹,石頭中。
4. 攝影機 -- 跟著角色移動的攝影機。
最後用一個世界物件,將上述四種物件裝起來。
因此我將檔案分成--
main.py -- 執行起始。
_define.py -- 一些定義,主要是控制器的命令定義。
World.py -- 世界類別。
Controller.py -- 控制器類別。
Avatar.py -- 角色類別。
Environment.py -- 場景類別。
CameraController.py -- 攝影機類別。
以下簡單描述各檔案的內容。
世界類別
main.py檔案很單純的僅是產生一個世界類別,然後開始panda3d的main loop。
import direct.directbase.DirectStart from World import World w = World() run()
世界類別,World.py。先載入角色與場景的3d模型(函數_model_load中,呼叫_avatar_model_get與 _environment_model_get函數),並賦予給角色物件與場景物件。設定好控制器(_controller),角色(_avatar),場景(_environment),攝影機(_camera_ctrl)等物件後。將每個frame更新的函數(_main_loop)掛上 taskMgr。
每個frame的更新函數(_main_loop)會依序呼叫角色的更新函數,場景的更新函數,最後是攝影機更新函數。
角色更新函數主要是從控制器物件獲取目前命令狀態,並依此做角色的3d模型位置的y值與面向更新。
場景更新函數主要是讓角色能站在地面上,依據碰撞偵測的反應更新角色模型位置的z值。
最後,攝影機取得角色的位置,並依此更新攝影機的位置與面向。
角色更新與場景更新函數可這麼看:角色更新函數依據輸入命令與自身狀態,給出理論的角色行為。場景更新函數依據角色與場景間的關係,修正理論的角色行為,得到實際上的角色行為。如此範例中,角色更新函數依據輸入命令前進,但場景更新函數因為角色將會進入障礙物而不讓角色前進,修正角色的位置為改變前的位置。
from direct.actor.Actor import Actor from Controller import Controller from Avatar import Avatar from Environment import Environment from CameraController import CameraController def _environment_model_get(): environment = loader.loadModel("models/world") environment.reparentTo(render) environment.setPos(0,0,0) return environment def _avatar_model_get(): avatar = Actor("models/ralph", {"run":"models/ralph-run", "walk":"models/ralph-walk"}) avatar.reparentTo(render) avatar.setScale(.2) return avatar class World: def __init__(self): self._model_load() self._controller = Controller() self._avatar = Avatar(self._avatar_model, self._controller) self._environment = Environment(self._environment_model, self._avatar) self._camera_ctrl = CameraController(self._avatar) self._main_loop_start() def _model_load(self): self._environment_model = _environment_model_get() self._avatar_model = _avatar_model_get() avatar_start_pos = self._environment_model.find("**/start_point").getPos() self._avatar_model.setPos(avatar_start_pos) def _main_loop_start(self): taskMgr.add(self._main_loop,"Main Loop") def _main_loop(self, task): self._avatar.update() self._environment.update() self._camera_ctrl.update() return task.cont
控制器類別
控制器類別,Controller.py。內含目前命令狀態(key_state)。在接收鍵盤事件後,更新目前命令狀態值(函數setKey)。在這個範例中僅有三種命令--左轉,右轉,與前進。
鍵盤事件僅包含按鍵按下與按鍵放開,例如"arrow_left"表示左箭頭按鍵按下的事件,"arrow_left-up"表示左箭頭按鍵放開的事件。因此,按鍵持續按著的這個動作就由key_state保存,在按下後設定對應的key_state命令狀態為1,與放開後設定對應的key_state命令狀態為0。
from direct.showbase.DirectObject import DirectObject from _define import _LEFT, _RIGHT, _FORWARD class Controller(DirectObject): def __init__(self): self.key_state = {_LEFT:0, _RIGHT:0, _FORWARD:0} self._keyborad_event_setup() def setKey(self, key, value): self.key_state[key] = value def _keyborad_event_setup(self): self.accept("arrow_left", self.setKey, [_LEFT,1]) self.accept("arrow_right", self.setKey, [_RIGHT,1]) self.accept("arrow_up", self.setKey, [_FORWARD,1]) self.accept("arrow_left-up", self.setKey, [_LEFT,0]) self.accept("arrow_right-up", self.setKey, [_RIGHT,0]) self.accept("arrow_up-up", self.setKey, [_FORWARD,0]) def state_get(self): ret = self.key_state return ret
角色類別
角色類別,Avatar.py。在每個frame時,從控制器取得目前命令狀態(函數_cmd_get),藉此更新角色3d模型的y值與面向(函數_pos_update),並更新角色3d模型的動畫狀態(函數_animation_update)。函數pos_recover用來恢復角色3d模型改變前的位置,於之後的場景類別中使用。
from _define import _LEFT, _RIGHT, _FORWARD class Avatar: def __init__(self, model, controller): self.model = model self._isMoving = False self._cmd_get = controller.state_get def update(self): cmd_state = self._cmd_get() cmd_forward = cmd_state[_FORWARD] cmd_left = cmd_state[_LEFT] - cmd_state[_RIGHT] self._pos_update(cmd_forward, cmd_left) self._animation_update(cmd_forward, cmd_left) def _pos_update(self, cmd_forward, cmd_left): self._old_pos = self.model.getPos() if cmd_forward: self.model.setY(self.model, -25 * globalClock.getDt()) if cmd_left: self.model.setH(self.model.getH() + (cmd_left * 300 * globalClock.getDt())) def _animation_update(self, cmd_forward, cmd_left): if (cmd_forward or cmd_left): if (not self._isMoving): self.model.loop("run") self._isMoving = True else: if self._isMoving: self.model.stop() self.model.pose("walk",5) self._isMoving = False def pos_recover(self): self.model.setPos(self._old_pos)
場景類別
場景類別,Environment.py。設定角色的碰撞(函數_collision_setup),於每個frame更新時檢查碰撞事件(函數 _collision_object_list_get),並藉此更新角色3d模型位置的z值(函數_collision_handle),讓角色能隨著地形起伏改變高度,達到"站在地上"的視覺效果。當角色3d模型"進入"場景中的石頭或樹木等障礙物時,恢復到角色移動前的位置(角色類別的函數 pos_recover),如此角色就不會穿越障礙物了。
from panda3d.core import CollisionTraverser,CollisionNode from panda3d.core import CollisionHandlerQueue,CollisionRay from panda3d.core import BitMask32 class Environment: def __init__(self, model, avatar): self.model = model self._avatar = avatar self._avatar_model = avatar.model base.win.setClearColor((0,0,0,1)) self._collision_setup() def _collision_setup(self): self.cTrav = CollisionTraverser() self._avatar_GroundRay = CollisionRay() self._avatar_GroundRay.setOrigin(0,0,1000) self._avatar_GroundRay.setDirection(0,0,-1) self._avatar_GroundCol = CollisionNode('_avatar_Ray') self._avatar_GroundCol.addSolid(self._avatar_GroundRay) self._avatar_GroundCol.setFromCollideMask(BitMask32.bit(0)) self._avatar_GroundCol.setIntoCollideMask(BitMask32.allOff()) self._avatar_GroundColNp = self._avatar_model.attachNewNode(self._avatar_GroundCol) self._avatar_GroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self._avatar_GroundColNp, self._avatar_GroundHandler) def update(self): entries = self._collision_object_list_get() self._collision_handle(entries) def _collision_object_list_get(self): self.cTrav.traverse(render) entries = [self._avatar_GroundHandler.getEntry(i) for i in range(self._avatar_GroundHandler.getNumEntries())] entries.sort(key = lambda x: x.getSurfacePoint(render).getZ()) return entries def _collision_handle(self, obj_list): if (len(obj_list)>0) and (obj_list[0].getIntoNode().getName() == "terrain"): self._avatar_model.setZ(obj_list[0].getSurfacePoint(render).getZ()) else: self._avatar.pos_recover()
攝影機類別
攝影機類別,CameraController.py。這裡只是很簡單的設定攝影機的位置與面向,讓角色能以第三人視角表現。
class CameraController: def __init__(self, avatar): self._avatar_model = avatar.model base.disableMouse() def update(self): base.camera.setPos(self._avatar_model.getX(), self._avatar_model.getY()+10, self._avatar_model.getZ()+2) base.camera.lookAt(self._avatar_model)
2010年12月12日 星期日
Q&A -- 使用__dict__["__setattr__"]取代現有的類別函數
Q:
操作物件內的__dict__字典相當於操作物件內的屬性存取. 雖然仍有些不同.
obj.__dict__[name] = value # obj.name = value
然而以 obj.__dict__["__setattr__"] = new_fn 來取代現有的 __setattr__ 時
遇到一個問題(使用python2.6):
aKlass內含兩個函數, a_fn, __setattr__.
另準備兩個對應的函數new_a_fn, new_setattr_fn
希望使用aKlass.__dict__[ ]的方式取代 --
執行結果:
aKlass.a_fn
new_a_fn
aKlass.__setattr__ var 1
aKlass.__setattr__ var 1
aKlass.__dict__["a_fn"] = new_a_fn 的確用new_a_fn取代了原先的a_fn.
但似乎 aKlass.__dict__["__setattr__"] = new_setattr_fn 沒有達到預期的效果.
不知原因為何?
(雖然使用 setattr(aKlass,"__setattr__",new_setattr_fn) 就可以取代原函數了.)
A:
你測試的兩個 case 並不算是全等的,如果讓兩者在做法更接近,應該是:
接下來回到為什麼修改 aKlass.__dict__ 後,在如此的 statement:
aObj.var = 1
上看不到作用?
1. 這只會發生在 old-style class。
(new-style class 的 __dict__ 則是 immutable,實際上是個 proxy)
2. aKlass object(不是指 aObj)除了 __dict__ 裡有 reference 指涉原本在
constructing aKlass 過程中所建立的 __setattr__ function(這個 function
是指 aKlass.__setattr__.im_func),aKlass object 本身也有一個欄位存著
該 function 的位址。(可以看看 Python 安裝後一併附的 classobject.h)
我把 CPython 2.6.5 的 classobject.h 部分內容節錄在此:
aKlass 在記憶體裡的結構會分別有 __getattr__/__setattr__/__delattr__
的位址(if any)。
當執行過此 statement:
aKlass.__dict__['__setattr__'] = new_setattr
只變更了 cl_dict 所指涉的 dict 的狀態,並沒有變更 cl_setattr 指向另一個
function,而 aObj.var = 1 statement 會直接使用 cl_setattr 的值(一種優化,
省了 dictionary lookup 動作),所以 aObj.var = 1 還是調用了原來的
__setattr__ function。
如果你使用下列的 statement(任一)來變更 __setattr__ 屬性:
aKlass.__setattr__ = new_setattr_fn
setattr(aKlass, '__setattr__', new_setattr_fn)
則會變更 cl_setattr 欄位指向 new_setattr_fn,所以 aObj.var = 1
有預期中的表現。
接下來我要透過一些手法來變更 aKlass 在記憶體中其 cl_setattr 欄位的值,
看看有什麼效果。
* 有心一試的人請在 console mode 執行 python REPL(interpreter),不要
使用 IDLE 或是 PythonWin Editor 等 IDE 環境。
沿用 aKlass/new_a_fn/new_setattr_fn 的定義。
接下來 import ctypes 套件(已內建於 Python 2.5+)
執行完以上的程式碼後,大概可以看到類似如下的 output:
12325616
0 3
1 505362952
2 11210800
3 11416608
4 12313696
5 0
6 12325616
7 0
8 7
9 505362336
這表示 p[6] 是 cl_setattr 欄位(p[5] 是 cl_getattr,p[7] 是 cl_delattr,
兩者皆是 0,因為 aKlass 沒有定義 __getattr__, __delattr__)。
以上幾個 statement 的輸出應該如下:
new_setattr_fn var 1
aKlass.__setattr__ var 2
操作物件內的__dict__字典相當於操作物件內的屬性存取. 雖然仍有些不同.
obj.__dict__[name] = value # obj.name = value
然而以 obj.__dict__["__setattr__"] = new_fn 來取代現有的 __setattr__ 時
遇到一個問題(使用python2.6):
class aKlass: var = 0 def a_fn(self): print "aKlass.a_fn" def __setattr__(self, name, value): print "aKlass.__setattr__",name,value def new_a_fn(self): print "new_a_fn" def new_setattr_fn(self,name,value): print "new_setattr_fn",name,value
aKlass內含兩個函數, a_fn, __setattr__.
另準備兩個對應的函數new_a_fn, new_setattr_fn
希望使用aKlass.__dict__[ ]的方式取代 --
aObj = aKlass() aObj.a_fn() aKlass.__dict__["a_fn"] = new_a_fn aObj.a_fn() aObj.var = 1 aKlass.__dict__["__setattr__"] = new_setattr_fn aObj.var = 1
執行結果:
aKlass.a_fn
new_a_fn
aKlass.__setattr__ var 1
aKlass.__setattr__ var 1
aKlass.__dict__["a_fn"] = new_a_fn 的確用new_a_fn取代了原先的a_fn.
但似乎 aKlass.__dict__["__setattr__"] = new_setattr_fn 沒有達到預期的效果.
不知原因為何?
(雖然使用 setattr(aKlass,"__setattr__",new_setattr_fn) 就可以取代原函數了.)
A:
你測試的兩個 case 並不算是全等的,如果讓兩者在做法更接近,應該是:
aKlass.__dict__["a_fn"] = new_a_fn aObj.a_fn() aKlass.__dict__["__setattr__"] = new_setattr_fn aObj.__setattr__('var', 1)
接下來回到為什麼修改 aKlass.__dict__ 後,在如此的 statement:
aObj.var = 1
上看不到作用?
1. 這只會發生在 old-style class。
(new-style class 的 __dict__ 則是 immutable,實際上是個 proxy)
2. aKlass object(不是指 aObj)除了 __dict__ 裡有 reference 指涉原本在
constructing aKlass 過程中所建立的 __setattr__ function(這個 function
是指 aKlass.__setattr__.im_func),aKlass object 本身也有一個欄位存著
該 function 的位址。(可以看看 Python 安裝後一併附的 classobject.h)
我把 CPython 2.6.5 的 classobject.h 部分內容節錄在此:
typedef struct { PyObject_HEAD PyObject *cl_bases; /* A tuple of class objects */ PyObject *cl_dict; /* A dictionary */ PyObject *cl_name; /* A string */ /* The following three are functions or NULL */ PyObject *cl_getattr; PyObject *cl_setattr; PyObject *cl_delattr; } PyClassObject;
aKlass 在記憶體裡的結構會分別有 __getattr__/__setattr__/__delattr__
的位址(if any)。
當執行過此 statement:
aKlass.__dict__['__setattr__'] = new_setattr
只變更了 cl_dict 所指涉的 dict 的狀態,並沒有變更 cl_setattr 指向另一個
function,而 aObj.var = 1 statement 會直接使用 cl_setattr 的值(一種優化,
省了 dictionary lookup 動作),所以 aObj.var = 1 還是調用了原來的
__setattr__ function。
如果你使用下列的 statement(任一)來變更 __setattr__ 屬性:
aKlass.__setattr__ = new_setattr_fn
setattr(aKlass, '__setattr__', new_setattr_fn)
則會變更 cl_setattr 欄位指向 new_setattr_fn,所以 aObj.var = 1
有預期中的表現。
接下來我要透過一些手法來變更 aKlass 在記憶體中其 cl_setattr 欄位的值,
看看有什麼效果。
* 有心一試的人請在 console mode 執行 python REPL(interpreter),不要
使用 IDLE 或是 PythonWin Editor 等 IDE 環境。
沿用 aKlass/new_a_fn/new_setattr_fn 的定義。
接下來 import ctypes 套件(已內建於 Python 2.5+)
from ctypes import * a = aKlass() wrapper = py_object(aKlass) p = cast(addressof(wrapper), POINTER(POINTER(c_uint32))).contents print id(aKlass.__setattr__.im_func) # address of original __setattr__ # function for x in xrange(10): print x, p[x]
執行完以上的程式碼後,大概可以看到類似如下的 output:
12325616
0 3
1 505362952
2 11210800
3 11416608
4 12313696
5 0
6 12325616
7 0
8 7
9 505362336
這表示 p[6] 是 cl_setattr 欄位(p[5] 是 cl_getattr,p[7] 是 cl_delattr,
兩者皆是 0,因為 aKlass 沒有定義 __getattr__, __delattr__)。
old_setattr_addr = p[6] # 記下原 __setattr__ 的位址 p[6] = id(new_setattr_fn) a.var = 1 # see output p[6] = old_setattr_addr a.var = 2 # see output
以上幾個 statement 的輸出應該如下:
new_setattr_fn var 1
aKlass.__setattr__ var 2
Q&A -- 如何用程式判斷某函數是否含有yield?
Q:
如何能在不執行某函數的狀況下, 以程式判斷此函數含有yield?
(即此函數可以成為iterator)
A:
inspect.isgeneratorfunction
Example:
Result:
True
False
如何能在不執行某函數的狀況下, 以程式判斷此函數含有yield?
(即此函數可以成為iterator)
A:
inspect.isgeneratorfunction
Example:
def a_iter_fn(): yield 1 def a_fn(): return 1 from inspect import isgeneratorfunction print isgeneratorfunction(a_iter_fn) print isgeneratorfunction(a_fn)
Result:
True
False
訂閱:
文章 (Atom)