ฉันต้องการวิธีการทำงานในการรับคลาสทั้งหมดที่สืบทอดจากคลาสพื้นฐานใน Python
ฉันต้องการวิธีการทำงานในการรับคลาสทั้งหมดที่สืบทอดจากคลาสพื้นฐานใน Python
คำตอบ:
คลาสสไตล์ใหม่ (เช่นคลาสย่อยจากobject
ซึ่งเป็นค่าเริ่มต้นใน Python 3) มี__subclasses__
เมธอดที่ส่งคืนคลาสย่อย:
class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass
นี่คือชื่อของคลาสย่อย:
print([cls.__name__ for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']
นี่คือคลาสย่อยเอง:
print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]
การยืนยันว่าคลาสย่อยนั้นแสดงFoo
เป็นฐานแน่นอน:
for cls in Foo.__subclasses__():
print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>
หมายเหตุถ้าคุณต้องการ sububclasses คุณจะต้องเรียกคืน:
def all_subclasses(cls):
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
print(all_subclasses(Foo))
# {<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>}
โปรดทราบว่าหากคำจำกัดความของคลาสของคลาสย่อยนั้นยังไม่ได้ดำเนินการตัวอย่างเช่นหากโมดูลของคลาสย่อยนั้นยังไม่ได้นำเข้า - ดังนั้นคลาสย่อยนั้นจะยังไม่ปรากฏและ__subclasses__
จะไม่พบมัน
คุณพูดถึง "ตั้งชื่อ" เนื่องจากคลาส Python เป็นออบเจ็กต์ชั้นหนึ่งคุณไม่จำเป็นต้องใช้สตริงที่มีชื่อคลาสแทนตำแหน่งหรืออะไรแบบนั้น คุณสามารถใช้คลาสได้โดยตรงและคุณควร
หากคุณมีสตริงที่แสดงชื่อของคลาสและคุณต้องการค้นหาคลาสย่อยของคลาสนั้นมีสองขั้นตอน: ค้นหาคลาสที่ให้ชื่อจากนั้นค้นหาคลาสย่อย__subclasses__
ตามข้างบน
วิธีค้นหาชั้นเรียนจากชื่อขึ้นอยู่กับที่คุณคาดหวังว่าจะพบ หากคุณคาดหวังว่าจะพบมันในโมดูลเดียวกับรหัสที่พยายามค้นหาชั้นเรียน
cls = globals()[name]
จะทำงานหรือในกรณีที่ไม่น่าเป็นไปได้ที่คุณคาดว่าจะพบมันในท้องถิ่น
cls = locals()[name]
ถ้าชั้นจะเป็นในโมดูลใด ๆ แล้วสตริงชื่อของคุณควรมีชื่อที่มีคุณสมบัติครบถ้วน - สิ่งที่ต้องการแทนเพียง'pkg.module.Foo'
'Foo'
ใช้importlib
เพื่อโหลดโมดูลของคลาสจากนั้นดึงแอตทริบิวต์ที่เกี่ยวข้อง:
import importlib
modname, _, clsname = name.rpartition('.')
mod = importlib.import_module(modname)
cls = getattr(mod, clsname)
อย่างไรก็ตามคุณพบคลาสcls.__subclasses__()
จากนั้นส่งคืนรายการของคลาสย่อย
หากคุณต้องการคลาสย่อยโดยตรงก็ใช้.__subclasses__()
งานได้ดี หากคุณต้องการให้คลาสย่อยทั้งหมดเป็นคลาสย่อยของคลาสย่อยและอื่น ๆ คุณจะต้องมีฟังก์ชันที่จะทำสิ่งนั้นให้คุณ
นี่คือฟังก์ชั่นที่ง่ายและอ่านได้ซึ่งจะค้นหาคลาสย่อยทั้งหมดของคลาสที่กำหนดซ้ำ:
def get_all_subclasses(cls):
all_subclasses = []
for subclass in cls.__subclasses__():
all_subclasses.append(subclass)
all_subclasses.extend(get_all_subclasses(subclass))
return all_subclasses
all_subclasses
เป็นการset
กำจัดรายการที่ซ้ำกันหรือ
A(object)
, B(A)
, และC(A)
. D(B, C)
get_all_subclasses(A) == [B, C, D, D]
ทางออกที่ง่ายที่สุดในรูปแบบทั่วไป:
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from get_subclasses(subclass)
yield subclass
และวิธีการเรียนในกรณีที่คุณมีชั้นเดียวที่คุณสืบทอดมาจาก:
@classmethod
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from subclass.get_subclasses()
yield subclass
__init_subclass__
ตามคำตอบอื่น ๆ ที่กล่าวถึงคุณสามารถตรวจสอบ__subclasses__
แอตทริบิวต์เพื่อรับรายการของคลาสย่อยเนื่องจาก python 3.6 คุณสามารถแก้ไขการสร้างแอททริบิวต์นี้ได้โดยการแทนที่__init_subclass__
เมธอด
class PluginBase:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
class Plugin1(PluginBase):
pass
class Plugin2(PluginBase):
pass
ด้วยวิธีนี้ถ้าคุณรู้ว่าคุณกำลังทำอะไรคุณสามารถลบล้างพฤติกรรมของ__subclasses__
และละเว้น / เพิ่มคลาสย่อยจากรายการนี้
__init_subclass
ในคลาสของผู้ปกครอง
หมายเหตุ: ฉันเห็นว่ามีคน (ไม่ใช่ @unutbu) เปลี่ยนคำตอบที่อ้างอิงเพื่อไม่ให้ใช้อีกต่อไปvars()['Foo']
ดังนั้นจุดหลักของโพสต์ของฉันจะไม่ถูกนำไปใช้อีกต่อไป
FWIW นี่คือสิ่งที่ฉันต้องการเกี่ยวกับคำตอบของ @ unutbu การทำงานกับคลาสที่กำหนดไว้ในเครื่องเท่านั้น - และการใช้eval()
แทนvars()
จะทำให้มันทำงานกับคลาสที่เข้าถึงได้ใด ๆ ไม่ใช่เฉพาะที่กำหนดไว้ในขอบเขตปัจจุบัน
สำหรับผู้ที่ไม่ชอบใช้eval()
วิธีการก็จะถูกแสดงเพื่อหลีกเลี่ยง
ครั้งแรกที่นี่เป็นตัวอย่างที่เป็นรูปธรรมแสดงให้เห็นถึงปัญหาที่อาจเกิดขึ้นกับการใช้vars()
:
class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass
# unutbu's approach
def all_subclasses(cls):
return cls.__subclasses__() + [g for s in cls.__subclasses__()
for g in all_subclasses(s)]
print(all_subclasses(vars()['Foo'])) # Fine because Foo is in scope
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]
def func(): # won't work because Foo class is not locally defined
print(all_subclasses(vars()['Foo']))
try:
func() # not OK because Foo is not local to func()
except Exception as e:
print('calling func() raised exception: {!r}'.format(e))
# -> calling func() raised exception: KeyError('Foo',)
print(all_subclasses(eval('Foo'))) # OK
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]
# using eval('xxx') instead of vars()['xxx']
def func2():
print(all_subclasses(eval('Foo')))
func2() # Works
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]
สิ่งนี้สามารถปรับปรุงได้โดยการย้ายeval('ClassName')
ลงไปในฟังก์ชั่นที่กำหนดไว้ซึ่งทำให้การใช้งานง่ายขึ้นโดยไม่สูญเสียความสามารถทั่วไปเพิ่มเติมที่ได้รับจากการใช้eval()
ซึ่งไม่เหมือนvars()
บริบทที่ไม่สำคัญ:
# easier to use version
def all_subclasses2(classname):
direct_subclasses = eval(classname).__subclasses__()
return direct_subclasses + [g for s in direct_subclasses
for g in all_subclasses2(s.__name__)]
# pass 'xxx' instead of eval('xxx')
def func_ez():
print(all_subclasses2('Foo')) # simpler
func_ez()
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]
สุดท้ายเป็นไปได้และอาจสำคัญในบางกรณีเพื่อหลีกเลี่ยงการใช้eval()
ด้วยเหตุผลด้านความปลอดภัยดังนั้นนี่คือรุ่นที่ไม่มี:
def get_all_subclasses(cls):
""" Generator of all a class's subclasses. """
try:
for subclass in cls.__subclasses__():
yield subclass
for subclass in get_all_subclasses(subclass):
yield subclass
except TypeError:
return
def all_subclasses3(classname):
for cls in get_all_subclasses(object): # object is base of all new-style classes.
if cls.__name__.split('.')[-1] == classname:
break
else:
raise ValueError('class %s not found' % classname)
direct_subclasses = cls.__subclasses__()
return direct_subclasses + [g for s in direct_subclasses
for g in all_subclasses3(s.__name__)]
# no eval('xxx')
def func3():
print(all_subclasses3('Foo'))
func3() # Also works
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]
eval()
- ดีกว่าตอนนี้หรือไม่
เวอร์ชันที่สั้นกว่ามากสำหรับรับรายการของคลาสย่อยทั้งหมด:
from itertools import chain
def subclasses(cls):
return list(
chain.from_iterable(
[list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
)
)
ฉันจะค้นหาคลาสย่อยทั้งหมดของคลาสที่ให้ชื่อได้อย่างไร?
แน่นอนว่าเราสามารถทำสิ่งนี้ได้อย่างง่ายดายหากได้รับอนุญาตให้เข้าถึงตัววัตถุเองได้
เพียงแค่ให้ชื่อมันเป็นความคิดที่ไม่ดีเนื่องจากอาจมีหลายชั้นในชื่อเดียวกันแม้จะกำหนดไว้ในโมดูลเดียวกัน
ฉันสร้างการนำไปใช้สำหรับคำตอบอื่นและเนื่องจากมันตอบคำถามนี้และมันก็ดูดีกว่าโซลูชันอื่น ๆ ที่นี่เล็กน้อยนี่คือ:
def get_subclasses(cls):
"""returns all subclasses of argument, cls"""
if issubclass(cls, type):
subclasses = cls.__subclasses__(cls)
else:
subclasses = cls.__subclasses__()
for subclass in subclasses:
subclasses.extend(get_subclasses(subclass))
return subclasses
การใช้งาน:
>>> import pprint
>>> list_of_classes = get_subclasses(int)
>>> pprint.pprint(list_of_classes)
[<class 'bool'>,
<enum 'IntEnum'>,
<enum 'IntFlag'>,
<class 'sre_constants._NamedIntConstant'>,
<class 'subprocess.Handle'>,
<enum '_ParameterKind'>,
<enum 'Signals'>,
<enum 'Handlers'>,
<enum 'RegexFlag'>]
นี่ไม่ใช่คำตอบที่ดีเท่ากับการใช้__subclasses__()
วิธีการเรียนในตัวแบบพิเศษที่ @unutbu กล่าวถึงดังนั้นฉันจึงนำเสนอเป็นแบบฝึกหัดเท่านั้น subclasses()
ฟังก์ชั่นที่กำหนดผลตอบแทนพจนานุกรมซึ่งแผนที่ทั้งหมดชื่อประเภทรองเพื่อคลาสย่อยตัวเอง
def traced_subclass(baseclass):
class _SubclassTracer(type):
def __new__(cls, classname, bases, classdict):
obj = type(classname, bases, classdict)
if baseclass in bases: # sanity check
attrname = '_%s__derived' % baseclass.__name__
derived = getattr(baseclass, attrname, {})
derived.update( {classname:obj} )
setattr(baseclass, attrname, derived)
return obj
return _SubclassTracer
def subclasses(baseclass):
attrname = '_%s__derived' % baseclass.__name__
return getattr(baseclass, attrname, None)
class BaseClass(object):
pass
class SubclassA(BaseClass):
__metaclass__ = traced_subclass(BaseClass)
class SubclassB(BaseClass):
__metaclass__ = traced_subclass(BaseClass)
print subclasses(BaseClass)
เอาท์พุท:
{'SubclassB': <class '__main__.SubclassB'>,
'SubclassA': <class '__main__.SubclassA'>}
นี่คือรุ่นที่ไม่มีการเรียกซ้ำ:
def get_subclasses_gen(cls):
def _subclasses(classes, seen):
while True:
subclasses = sum((x.__subclasses__() for x in classes), [])
yield from classes
yield from seen
found = []
if not subclasses:
return
classes = subclasses
seen = found
return _subclasses([cls], [])
สิ่งนี้แตกต่างจากการใช้งานอื่น ๆ ที่มันส่งคืนคลาสเดิม นี่เป็นเพราะมันทำให้รหัสง่ายขึ้นและ:
class Ham(object):
pass
assert(issubclass(Ham, Ham)) # True
ถ้า get_subclasses_gen ดูแปลก ๆ นั่นเป็นเพราะมันถูกสร้างขึ้นโดยการแปลงการใช้งานแบบเรียกซ้ำหางเป็นตัวกำเนิดลูป:
def get_subclasses(cls):
def _subclasses(classes, seen):
subclasses = sum(*(frozenset(x.__subclasses__()) for x in classes))
found = classes + seen
if not subclasses:
return found
return _subclasses(subclasses, found)
return _subclasses([cls], [])