โมดูลสามารถมีคุณสมบัติเหมือนกับที่ออบเจ็กต์ทำได้หรือไม่


102

ด้วยคุณสมบัติของ python ฉันสามารถทำให้เป็นเช่นนั้นได้

obj.y 

เรียกใช้ฟังก์ชันแทนที่จะส่งคืนค่า

มีวิธีดำเนินการกับโมดูลหรือไม่? ฉันมีกรณีที่ฉันต้องการ

module.y 

เพื่อเรียกใช้ฟังก์ชันแทนที่จะส่งคืนค่าที่เก็บไว้ที่นั่น


3
ดู__getattr__โมดูลสำหรับโซลูชันที่ทันสมัยยิ่งขึ้น
wim

คำตอบ:


55

เฉพาะอินสแตนซ์ของคลาสสไตล์ใหม่เท่านั้นที่สามารถมีคุณสมบัติได้ คุณสามารถทำให้งูใหญ่เชื่อเช่นตัวอย่างเป็นโมดูลโดย stashing sys.modules[thename] = theinstanceใน ตัวอย่างเช่นไฟล์โมดูล m.py ของคุณอาจเป็น:

import sys

class _M(object):
    def __init__(self):
        self.c = 0
    def afunction(self):
        self.c += 1
        return self.c
    y = property(afunction)

sys.modules[__name__] = _M()

2
มีใครลองทำแบบนี้บ้างไหม? เมื่อฉันใส่รหัสนี้ในไฟล์ x.py หนึ่งไฟล์และนำเข้าจากอีกไฟล์หนึ่งจากนั้นเรียกผลลัพธ์ xy ใน AttributeError: วัตถุ 'NoneType' ไม่มีแอตทริบิวต์ 'c' เนื่องจาก _M มีค่าไม่มี ...
Stephan202

3
อันที่จริงรหัสทำงานบนล่าม แต่เมื่อฉันใส่ลงในไฟล์ (พูดว่า bowwow.py) และฉันนำเข้าจากไฟล์อื่น (otherfile.py) มันก็ใช้งานไม่ได้อีกต่อไป ...
Stephan202

4
ถาม: จะมีข้อได้เปรียบเป็นพิเศษในการได้รับคลาสของอินสแตนซ์จากtypes.ModuleTypeที่แสดงในคำตอบที่คล้ายกันมากของ @ Unknown หรือไม่
martineau

12
เฉพาะอินสแตนซ์ของคลาสสไตล์ใหม่เท่านั้นที่สามารถมีคุณสมบัติได้ นี่ไม่ใช่เหตุผล: โมดูลเป็นอินสแตนซ์ของคลาสรูปแบบใหม่ซึ่งเป็นอินสแตนซ์builtins.moduleซึ่งตัวเองเป็นอินสแตนซ์ของtype(ซึ่งเป็นคำจำกัดความของคลาสสไตล์ใหม่) ปัญหาคือว่าคุณสมบัติจะต้องอยู่ในระดับที่ไม่อินสแตนซ์: ถ้าคุณทำf = Foo(), f.some_property = property(...)ก็จะล้มเหลวในทางเดียวกันเช่นถ้าคุณอย่างไร้เดียงสาวางไว้ในโมดูล วิธีแก้ปัญหาคือใส่ไว้ในคลาส แต่เนื่องจากคุณไม่ต้องการให้โมดูลทั้งหมดมีคุณสมบัติคุณจึงเป็นคลาสย่อย (ดูคำตอบของ Unknown)
Thanatos

3
@ โจการเปลี่ยนแปลงของglobals()(ทำให้คีย์ยังคงอยู่ แต่รีเซ็ตค่าเป็นNone) เมื่อการผูกชื่อใหม่sys.modulesเป็นปัญหาของ Python 2 - Python 3.4 ทำงานได้ตามที่ตั้งใจไว้ หากคุณต้องการเข้าถึงคลาสอ็อบเจ็กต์ใน Py2 ให้เพิ่มเช่น_M._cls = _Mทันทีหลังclassคำสั่ง (หรือซ่อนไว้ในเนมสเปซอื่น ๆ ) และเข้าถึงได้ตามself._clsวิธีการที่ต้องการ ( type(self)อาจใช้ได้ แต่ไม่ใช่ถ้าคุณทำคลาสย่อยด้วย_M) .
Alex Martelli

54

ฉันจะทำสิ่งนี้เพื่อสืบทอดคุณสมบัติทั้งหมดของโมดูลอย่างถูกต้องและถูกระบุอย่างถูกต้องโดย isinstance ()

import types

class MyModule(types.ModuleType):
    @property
    def y(self):
        return 5


>>> a=MyModule("test")
>>> a
<module 'test' (built-in)>
>>> a.y
5

จากนั้นคุณสามารถแทรกสิ่งนี้ลงใน sys.modules:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class

ดูเหมือนว่าจะใช้ได้กับกรณีที่ง่ายที่สุดเท่านั้น ปัญหาที่เป็นไปได้คือ: (1) ผู้ช่วยในการนำเข้าบางตัวอาจคาดหวังแอตทริบิวต์อื่น ๆ__file__ที่ต้องกำหนดด้วยตนเอง (2) การนำเข้าที่ทำในโมดูลที่มีคลาสจะไม่สามารถ "มองเห็นได้" ในระหว่างรันไทม์ ฯลฯ ...
tutuDajuju

1
มันไม่จำเป็นที่จะต้องได้รับมาจากคลาสย่อยtypes.ModuleType, ใด ๆ (แบบใหม่) ชั้นจะทำ คุณลักษณะของโมดูลพิเศษที่คุณหวังจะสืบทอดคืออะไร?
martineau

จะเกิดอะไรขึ้นถ้าโมดูลดั้งเดิมเป็นแพ็คเกจและฉันต้องการเข้าถึงโมดูลที่อยู่ใต้โมดูลดั้งเดิม?
kawing-chiu

2
@martineau คุณจะมี Repr โมดูลคุณสามารถระบุชื่อโมดูลเมื่ออินสแตนซ์และคุณจะได้รับพฤติกรรมที่ถูกต้องเมื่อมีการใช้__init__ isinstance
Wim

@wim: คะแนนที่ได้รับแม้ว่าตรงไปตรงมาดูเหมือนจะไม่มี IMO ที่สำคัญทั้งหมด
martineau

35

เนื่องจากPEP 562ถูกนำไปใช้ใน Python> = 3.7 ตอนนี้เราสามารถทำได้

ไฟล์: module.py

def __getattr__(name):
    if name == 'y':
        return 3
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

other = 4

การใช้งาน:

>>> import module
>>> module.y
3
>>> module.other
4
>>> module.nosuch
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "module.py", line 4, in __getattr__
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
AttributeError: module 'module' has no attribute 'nosuch'

โปรดทราบว่าถ้าคุณละเว้นraise AttributeErrorใน__getattr__ฟังก์ชั่นที่มันหมายถึงฟังก์ชั่นนี้จบลงด้วยreturn Noneแล้วจะได้รับค่าของmodule.nosuchNone


2
จากนี้ฉันได้เพิ่มคำตอบอื่น: stackoverflow.com/a/58526852/2124834

3
นี่เป็นเพียงครึ่งหนึ่งของทรัพย์สิน ไม่มีตัวตั้งค่า
Wim

น่าเสียดายที่ดูเหมือนว่าจะไม่ยากที่จะทำให้เครื่องมือตระหนักถึงคุณลักษณะดังกล่าว (?) ( getattrจะถูกเรียกก็ต่อเมื่อไม่พบสมาชิกทั่วไป)
olejorgenb

9

จากคำตอบของ John Lin :

def module_property(func):
    """Decorator to turn module functions into properties.
    Function names must be prefixed with an underscore."""
    module = sys.modules[func.__module__]

    def base_getattr(name):
        raise AttributeError(
            f"module '{module.__name__}' has no attribute '{name}'")

    old_getattr = getattr(module, '__getattr__', base_getattr)

    def new_getattr(name):
        if f'_{name}' == func.__name__:
            return func()
        else:
            return old_getattr(name)

    module.__getattr__ = new_getattr
    return func

การใช้งาน (สังเกตขีดล่างนำหน้า) ในthe_module.py:

@module_property
def _thing():
    return 'hello'

จากนั้น:

import the_module

print(the_module.thing)  # prints 'hello'

ขีดล่างนำหน้าเป็นสิ่งที่จำเป็นในการแยกความแตกต่างของฟังก์ชัน ized คุณสมบัติจากฟังก์ชันเดิม ฉันคิดวิธีกำหนดตัวระบุใหม่ไม่ได้เนื่องจากในช่วงเวลาของการดำเนินการมัณฑนากรยังไม่ได้รับมอบหมาย

โปรดทราบว่า IDE จะไม่ทราบว่ามีคุณสมบัติอยู่และจะแสดงสีแดง wavies


เยี่ยมมาก! เมื่อเทียบกับคุณสมบัติชั้นเรียน@property def x(self): return self._xแล้วฉันคิดว่าการdef thing()ไม่มีขีดล่างเป็นเรื่องธรรมดากว่า และคุณสามารถสร้างมัณฑนากร "module property setter" ในคำตอบของคุณได้หรือไม่?
John Lin

2
@JohnLin ฉันพยายามทำตามdef thing()คำแนะนำของคุณ ปัญหาคือว่า__getattr__เพียงได้รับการเรียกร้องให้มีคุณลักษณะที่ขาดหายไป แต่หลังจาก@module_property def thing(): …รันthe_module.thingถูกกำหนดดังนั้นgetattrจะไม่ถูกเรียก เราจำเป็นต้องลงทะเบียนthingในมัณฑนากรแล้วจึงลบออกจากเนมสเปซของโมดูล ฉันพยายามคืนNoneจากมัณฑนากร แต่thingถูกกำหนดให้เป็นNoneไฟล์. อาจทำได้@module_property def thing(): … del thingแต่ฉันพบว่าแย่กว่าการใช้thing()เป็นฟังก์ชัน
Ben Mares

ตกลงฉันไม่เห็น "ตัวตั้งค่าคุณสมบัติโมดูล" หรือ "โมดูล__getattribute__" ขอบคุณ.
John Lin

5

กรณีการใช้งานทั่วไปคือ: การเสริมสร้างโมดูลที่มีอยู่ (ขนาดใหญ่) ด้วยคุณลักษณะแบบไดนามิกบางอย่าง (น้อย) โดยไม่ต้องเปลี่ยนโมดูลทั้งหมดให้เป็นเค้าโครงคลาส น่าเสียดายที่แพทช์คลาสโมดูลที่เรียบง่ายที่สุดเช่นsys.modules[__name__].__class__ = MyPropertyModuleล้มเหลวด้วยTypeError: __class__ assignment: only for heap types. ดังนั้นการสร้างโมดูลจึงจำเป็นต้องเดินสายใหม่

วิธีนี้ทำได้โดยไม่ต้องใช้ตะขอนำเข้า Python เพียงแค่มี prolog อยู่ด้านบนของโค้ดโมดูล:

# propertymodule.py
""" Module property example """

if '__orgmod__' not in globals():

    # constant prolog for having module properties / supports reload()

    print "PropertyModule stub execution", __name__
    import sys, types
    class PropertyModule(types.ModuleType):
        def __str__(self):
            return "<PropertyModule %r from %r>" % (self.__name__, self.__file__)
    modnew = PropertyModule(__name__, __doc__)
    modnew.__modclass__ = PropertyModule        
    modnew.__file__ = __file__
    modnew.__orgmod__ = sys.modules[__name__]
    sys.modules[__name__] = modnew
    exec sys._getframe().f_code in modnew.__dict__

else:

    # normal module code (usually vast) ..

    print "regular module execution"
    a = 7

    def get_dynval(module):
        return "property function returns %s in module %r" % (a * 4, module.__name__)    
    __modclass__.dynval = property(get_dynval)

การใช้งาน:

>>> import propertymodule
PropertyModule stub execution propertymodule
regular module execution
>>> propertymodule.dynval
"property function returns 28 in module 'propertymodule'"
>>> reload(propertymodule)   # AFTER EDITS
regular module execution
<module 'propertymodule' from 'propertymodule.pyc'>
>>> propertymodule.dynval
"property function returns 36 in module 'propertymodule'"

หมายเหตุ: สิ่งที่ต้องการfrom propertymodule import dynvalจะสร้างสำเนาของหลักสูตรที่ถูกแช่แข็งซึ่งสอดคล้องกับdynval = someobject.dynval


1

คำตอบสั้น ๆ : ใช้ proxy_tools

proxy_toolsพยายามแพคเกจเพื่อให้@module_propertyการทำงาน

มันติดตั้งด้วย

pip install proxy_tools

ใช้การปรับเปลี่ยนตัวอย่างเล็กน้อยของ @ Marein ในthe_module.pyเรา

from proxy_tools import module_property

@module_property
def thing():
    print(". ", end='')  # Prints ". " on each invocation
    return 'hello'

ตอนนี้จากสคริปต์อื่นฉันสามารถทำได้

import the_module

print(the_module.thing)
# . hello

พฤติกรรมที่ไม่คาดคิด

วิธีนี้ไม่ได้โดยไม่มีข้อแม้ กล่าวคือthe_module.thingเป็นไม่สตริง ! เป็นproxy_tools.Proxyวัตถุที่มีการลบล้างเมธอดพิเศษเพื่อให้เลียนแบบสตริง นี่คือการทดสอบพื้นฐานบางส่วนที่แสดงให้เห็นประเด็น:

res = the_module.thing
# [No output!!! Evaluation doesn't occur yet.]

print(type(res))
# <class 'proxy_tools.Proxy'>

print(isinstance(res, str))
# False

print(res)
# . hello

print(res + " there")
# . hello there

print(isinstance(res + "", str))
# . True

print(res.split('e'))
# . ['h', 'llo']

ภายในฟังก์ชันดั้งเดิมจะถูกเก็บไว้ที่ the_module.thing._Proxy__local:

print(res._Proxy__local)
# <function thing at 0x7f729c3bf680>

ความคิดเพิ่มเติม

จริงๆแล้วฉันงงงันว่าทำไมโมดูลถึงไม่มีฟังก์ชันนี้ในตัวฉันคิดว่าประเด็นสำคัญของเรื่องนี้the_moduleคือตัวอย่างของtypes.ModuleTypeคลาส การตั้งค่า "คุณสมบัติโมดูล" เป็นการตั้งค่าคุณสมบัติบนอินสแตนซ์ของคลาสนี้แทนที่จะเป็นค่าtypes.ModuleTypeคลาสเอง สำหรับรายละเอียดเพิ่มเติมโปรดดูคำตอบนี้นี้

เราสามารถใช้คุณสมบัติtypes.ModuleTypeดังต่อไปนี้ได้จริงแม้ว่าผลลัพธ์จะไม่ดี เราไม่สามารถแก้ไขประเภทในตัวได้โดยตรง แต่เราสามารถสาปแช่งได้:

# python -m pip install forbiddenfruit
from forbiddenfruit import curse
from types import ModuleType
# curse has the same signature as setattr.
curse(ModuleType, "thing2", property(lambda module: f'hi from {module.__name__}'))

สิ่งนี้ทำให้เรามีคุณสมบัติที่มีอยู่ในโมดูลทั้งหมด มันค่อนข้างไม่เข้าใจเนื่องจากเราทำลายพฤติกรรมการตั้งค่าในทุกโมดูล:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute

1
วิธีนี้ดีกว่าแค่ทำให้โมดูลเป็นตัวอย่างของคลาสจริงดังที่แสดงในคำตอบของ @Alex Martelli
martineau

1
คุณพูดอย่างอื่นที่ไม่สมเหตุสมผลกับฉัน ดำเนินธุรกิจเกี่ยวกับการมี@module_propertyมัณฑนากรนี้ โดยทั่วไป@propertyมัณฑนากรในตัวจะใช้เมื่อมีการกำหนดคลาสไม่ใช่หลังจากสร้างอินสแตนซ์แล้วดังนั้นฉันคิดว่าสิ่งเดียวกันนี้จะเป็นจริงสำหรับคุณสมบัติโมดูลและเป็นไปตามคำตอบของอเล็กซ์ - จำคำถามนี้ได้ "โมดูลสามารถมีคุณสมบัติแบบเดียวกับออบเจ็กต์ได้หรือไม่" แต่มันเป็นไปได้ที่จะเพิ่มพวกเขาต่อไปและผมได้แก้ไขก่อนหน้านี้ของฉันข้อมูลโค้ดแสดงให้เห็นถึงวิธีการที่สามารถทำได้
martineau

1
Ben: หลังจากดูโค้ดในตัวอย่างที่เป็นรูปธรรมแล้วฉันคิดว่าฉันเข้าใจสิ่งที่คุณได้รับในตอนนี้ ฉันยังคิดว่าเมื่อเร็ว ๆ นี้ฉันสะดุดกับเทคนิคในการใช้บางสิ่งที่คล้ายกับคุณสมบัติของโมดูลที่ไม่ต้องการให้โมดูลถูกแทนที่ด้วยอินสแตนซ์คลาสเหมือนในคำตอบของ Alex แม้ว่าฉันจะไม่แน่ใจในตอนนี้ว่ามีวิธีทำ ผ่านมัณฑนากร - จะติดต่อกลับหากมีความคืบหน้า
martineau

1
ตกลงนี่คือลิงค์ไปยังคำตอบของคำถามอื่นซึ่งมีแนวคิดหลัก
martineau

1
อย่างน้อยก็ในกรณีของ a cached_module_propertyความจริงที่__getattr__()จะไม่ถูกเรียกอีกต่อไปหากแอตทริบิวต์ได้รับการกำหนดนั้นมีประโยชน์ (คล้ายกับสิ่งที่functools.cached_propertyทำสำเร็จ)
martineau
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.