Можете ли вы использовать методы патчей обезьян для основных типов в python?

Ruby может добавлять методы к классу Number и другим основным типам, чтобы получить такие эффекты, как:

1.should_equal(1)

Но похоже, что python не может этого сделать. Это правда? И если да, то почему? Имеет ли это отношение к тому, что тип не может быть изменен?

Обновление: вместо того, чтобы говорить о различных определениях патчей обезьян, я хотел бы просто сосредоточиться на приведенном выше примере. Я уже пришел к выводу, что это невозможно сделать, поскольку некоторые из вас ответили. Но я хотел бы получить более подробное объяснение, почему это невозможно сделать, и, может быть, какая функция, если доступна в python, позволит это.

Чтобы ответить на некоторые из вас: причина, по которой я могу сделать это, - просто эстетика/удобочитаемость.

item.price.should_equal(19.99)

читается больше как на английском языке и четко указывает, что является тестируемым значением и которое является ожидаемым значением, как предполагается:

should_equal(item.price, 19.99)

Эта концепция - это Rspec, а некоторые другие структуры Ruby основаны на.

Ответ 1

Что именно вы подразумеваете под патчем Monkey? Есть несколько несколько разных определений.

Если вы имеете в виду, "можете ли вы изменить методы класса во время выполнения?", тогда ответ будет решительно да:

class Foo:
  pass # dummy class

Foo.bar = lambda self: 42

x = Foo()
print x.bar()

Если вы имеете в виду: "Можете ли вы изменить методы класса во время выполнения и сделать все экземпляры этого класса изменены после?" тогда и ответ да. Просто слегка измените порядок:

class Foo:
  pass # dummy class

x = Foo()

Foo.bar = lambda self: 42

print x.bar()

Но вы не можете сделать это для определенных встроенных классов, например int или float. Эти методы классов реализованы в C, и есть некоторые абстракции, принесенные в жертву, чтобы сделать реализацию проще и эффективнее.

Я не совсем понимаю, почему почему вы хотите изменить поведение встроенных числовых классов. Если вам нужно изменить их поведение, подклассируйте их!

Ответ 2

Нет, вы не можете. В Python все данные (классы, методы, функции и т.д.), Определенные в модулях расширения C (включая встроенные), неизменяемы. Это связано с тем, что C-модули совместно используются несколькими интерпретаторами в одном и том же процессе, поэтому monkeypatching them также влияет на несвязанные интерпретаторы в том же процессе.

Однако классы, определенные в коде Python, могут быть обезврежены, поскольку они являются локальными для этого интерпретатора.

Ответ 3

def should_equal_def(self, value):
    if self != value:
        raise ValueError, "%r should equal %r" % (self, value)

class MyPatchedInt(int):
    should_equal=should_equal_def

class MyPatchedStr(str):
    should_equal=should_equal_def

import __builtin__
__builtin__.str = MyPatchedStr
__builtin__.int = MyPatchedInt

int(1).should_equal(1)
str("44").should_equal("44")

Удачи;)

Ответ 4

Вы можете сделать это, но это требует немного взлома. К счастью, теперь есть модуль, называемый "Forbidden Fruit", который дает вам возможность исправлять методы встроенных типов очень просто. Вы можете найти его на

http://clarete.github.io/forbiddenfruit/?goback=.gde_50788_member_228887816

или

https://pypi.python.org/pypi/forbiddenfruit/0.1.0

С исходным вопросом вопроса, после того как вы напишете функцию "should_equal", вы просто выполните

from forbiddenfruit import curse
curse(int, "should_equal", should_equal)

и вам хорошо идти! Также существует функция "обратного" удаления исправленного метода.

Ответ 5

Типы ядра Python неизменяемы по дизайну, как указывают другие пользователи:

>>> int.frobnicate = lambda self: whatever()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'int'

Конечно, вы могли бы добиться эффекта, который вы описываете, создавая подкласс, поскольку пользовательские типы в Python по умолчанию изменяются.

>>> class MyInt(int):
...   def frobnicate(self):
...     print 'frobnicating %r' % self
... 
>>> five = MyInt(5)
>>> five.frobnicate()
frobnicating 5
>>> five + 8
13

Нет необходимости публиковать подкласс MyInt public; можно точно определить его непосредственно в функции или методе, который создает экземпляр.

Есть, конечно, несколько ситуаций, когда программисты Python, свободно владеющие идиомой, считают, что этот класс подкласса правилен. Например, os.stat() возвращает подкласс tuple, который добавляет именованные члены, точно для того, чтобы обратиться к вопросу о степени удобочитаемости, о котором вы говорите в своем примере.

>>> import os
>>> st = os.stat('.')
>>> st
(16877, 34996226, 65024L, 69, 1000, 1000, 4096, 1223697425, 1223699268, 1223699268)
>>> st[6]
4096
>>> st.st_size
4096

Тем не менее, в конкретном примере, который вы даете, я не считаю, что подклассификация float в item.price (или в другом месте) будет очень вероятна, чтобы считаться питонической задачей. Я легко могу представить, что кто-то решил добавить метод price_should_equal() к item, если это был основной случай использования; если бы кто-то искал что-то более общее, возможно, было бы разумнее использовать именованные аргументы, чтобы сделать предполагаемое значение более ясным, как в

should_equal(observed=item.price, expected=19.99)

или что-то в этом роде. Это немного многословно, но, без сомнения, его можно было бы улучшить. Возможным преимуществом такого подхода по сравнению с рубиновым обезьян-патчем является то, что should_equal() может легко выполнить сравнение по любому типу, а не только int или float. Но, возможно, я слишком зациклен на деталях конкретного примера, который вам предоставили.

Ответ 6

Вы не можете изменять основные типы в python. Однако вы можете использовать трубу для написания более понятного для человека кода:

from pipe import *

@Pipe
def should_equal(obj, val):
    if obj==val: return True
    return False

class dummy: pass
item=dummy()
item.value=19.99

print item.value | should_equal(19.99)

Ответ 7

Вот пример реализации item.price.should_equal, хотя я бы использовал Decimal вместо float в реальной программе:

class Price(float):
    def __init__(self, val=None):
        float.__init__(self)
        if val is not None:
            self = val

    def should_equal(self, val):
        assert self == val, (self, val)

class Item(object):
    def __init__(self, name, price=None):
        self.name = name
        self.price = Price(price)

item = Item("spam", 3.99)
item.price.should_equal(3.99)

Ответ 8

Если вы действительно действительно хотите сделать патч обезьяны в Python, вы можете сделать (sortof) взломать с помощью метода "import foo as bar".

Если у вас есть класс, такой как TelnetConnection, и вы хотите его расширить, подклассируйте его в отдельный файл и назовите его чем-то вроде TelnetConnectionExtended.

Затем в верхней части вашего кода, где вы обычно говорите:

import TelnetConnection

измените это значение:

import TelnetConnectionExtended as TelnetConnection

а затем всюду в вашем коде, который вы ссылаетесь на TelnetConnection, действительно будет ссылаться на TelnetConnectionExtended.

К сожалению, это предполагает, что у вас есть доступ к этому классу, а "как" работает только в этом конкретном файле (это не глобальное переименование), но я нашел его полезным время от времени.

Ответ 9

Нет, к сожалению, вы не можете расширять типы, реализованные в C во время выполнения.

Вы можете подклассировать int, хотя это нетривиально, вам может потребоваться переопределить __new__.

У вас также есть проблема с синтаксисом:

1.somemethod()  # invalid

Однако

(1).__eq__(1)  # valid

Ответ 10

Нет, вы не можете сделать это на Python. Я считаю, что это хорошо.

Ответ 11

Нет, но у вас есть UserDict UserString и UserList, которые были сделаны именно с учетом этого.

Если вы google, вы найдете примеры для других типов, но это встроенные функции.

В общем паттинге обезьян менее используется в Python, чем в Ruby.

Ответ 12

Что делает should_equal? Это логическое возвращение True или False? В этом случае он пишется:

item.price == 19.99

Там нет учета вкуса, но никакой обычный разработчик python не сказал бы, что он менее читабельный, чем ваша версия.

Использует ли should_equal какой-то валидатор? (почему валидатор должен быть ограничен одним значением? Почему бы просто не установить значение и не обновить его после этого?) Если вы хотите валидатор, это никогда не сможет работать в любом случае, так как вы предлагаете изменить либо определенное целое число, либо все целые числа. (Валидатор, который требует 18.99 равным 19.99, всегда терпит неудачу.) Вместо этого вы можете записать его следующим образом:

item.price_should_equal(19.99)

или это:

item.should_equal('price', 19.99)

и определить соответствующие методы для класса или суперклассов.

Ответ 13

Кажется, что вы действительно хотели написать:

assert item.price == 19.99

(Конечно, сравнение float для равенства или использование float для цен - плохая идея, поэтому вы пишете assert item.price == Decimal(19.99) или любой цифровой класс, который вы использовали для цены.)

Вы также можете использовать платформу тестирования, например py.test, чтобы получить дополнительную информацию о неудачных утверждениях в тестах.

Ответ 14

Здесь, как я достигаю поведения .should_something...:

result = calculate_result('blah') # some method defined somewhere else

the(result).should.equal(42)

или

the(result).should_NOT.equal(41)

Я включил метод декоратора для расширения этого поведения во время выполнения автономным способом:

@should_expectation
def be_42(self)
    self._assert(
        action=lambda: self._value == 42,
        report=lambda: "'{0}' should equal '5'.".format(self._value)
    )

result = 42

the(result).should.be_42()

Вы должны знать немного о внутренностях, но он работает.

Здесь источник:

https://github.com/mdwhatcott/pyspecs

Это также на PyPI под pyspecs.